1 // Copyright 2022 Garrett D'Amore 2 // 3 // Distributed under the Boost Software License, Version 1.0. 4 // (See accompanying file LICENSE or https://www.boost.org/LICENSE_1_0.txt) 5 6 /** 7 * This module implements a command to extra terminfo data from the system, 8 * and build a database of Termcap data for use by this library. 9 */ 10 module mkinfo; 11 12 import std.algorithm : findSplit; 13 import std.stdio; 14 import std.process : execute; 15 import std.string; 16 import std.conv : to; 17 import std.outbuffer; 18 import std.traits; 19 import std.conv; 20 21 import dcell.termcap; 22 import dcell.database; 23 24 /** 25 * Caps represents a "parsed" terminfo entry, before it is converted into 26 * a Termcap structure. 27 */ 28 private struct Caps 29 { 30 string name; 31 string desc; 32 string[] aliases; 33 bool[string] bools; 34 int[string] ints; 35 string[string] strs; 36 37 int getInt(string s) 38 { 39 return (s in ints ? ints[s] : 0); 40 } 41 42 bool getBool(string s) 43 { 44 return (s in bools) !is null; 45 } 46 47 string getStr(string s) 48 { 49 return (s in strs ? strs[s] : ""); 50 } 51 } 52 53 /** 54 * Unescape string data emitted by infocmp(1) into binary representations 55 * suitable for use by this library. This understands C style escape 56 * sequences such as \n, but also octal sequences. A lone \0 is understood 57 * to represent a special form of NULL. See terminfo(5) for more information. 58 */ 59 private string unescape(string s) 60 { 61 enum escape 62 { 63 none, 64 ctrl, 65 esc 66 } 67 68 string result; 69 70 escape state = escape.none; 71 72 while (s.length > 0) 73 { 74 auto c = s[0]; 75 s = s[1 .. $]; 76 final switch (state) 77 { 78 case escape.none: 79 switch (c) 80 { 81 case '\\': 82 state = escape.esc; 83 break; 84 case '^': 85 state = escape.ctrl; 86 break; 87 default: 88 result ~= c; 89 break; 90 } 91 break; 92 case escape.ctrl: 93 result ~= (c ^ (1 << 6)); // flip bit six 94 state = escape.none; 95 break; 96 case escape.esc: 97 switch (c) 98 { 99 case 'E', 'e': 100 result ~= '\x1b'; 101 break; 102 case '0', '1', '2', '3', '4', '5', '6', '7': 103 if (s.length >= 2 && s[0] >= '0' && s[0] <= '7' && s[1] >= '0' && s[1] <= '7') 104 { 105 result ~= ((c - '0') << 6) + ((s[0] - '0') << 3) + (s[1] - '0'); 106 s = s[2 .. $]; 107 } 108 else if (c == '0') 109 { 110 result ~= '\200'; 111 } 112 break; 113 case 'n': 114 result ~= '\n'; 115 break; 116 case 'r': 117 result ~= '\r'; 118 break; 119 case 't': 120 result ~= '\t'; 121 break; 122 case 'b': 123 result ~= '\b'; 124 break; 125 case 'f': 126 result ~= '\f'; 127 break; 128 case 's': 129 result ~= ' '; 130 break; 131 case 'l': 132 result ~= '\n'; 133 break; 134 default: 135 result ~= c; 136 break; 137 } 138 state = escape.none; 139 break; 140 } 141 } 142 return result; 143 } 144 145 unittest 146 { 147 assert(unescape("123") == "123"); 148 assert(unescape(`1\n2`) == "1\n2"); 149 assert(unescape("a^Gb") == "a\007b"); 150 assert(unescape("1\\_\\007") == "1_\007"); 151 assert(unescape(`\,\:\0`) == ",:\200"); 152 assert(unescape(`\e\E`) == "\x1b\x1b"); 153 assert(unescape(`\r\s\f\l\t\b`) == "\r \f\n\t\b"); 154 } 155 156 /** 157 * Load capabilities (parsed from infocmp -1 -x). 158 * 159 * Params: info = output from infocmp -1 -x 160 * Returns: 161 * parsed capabilities on success, null otherwise 162 */ 163 Caps* parseCaps(string info) 164 { 165 auto cap = new Caps; 166 auto first = true; 167 foreach (line; splitLines(info)) 168 { 169 // skip empty lines and comments 170 if (line.length == 0 || line[0] == '#') 171 { 172 continue; 173 } 174 if (first) 175 { 176 // first line is name|alias|alias...|description 177 auto parts = split(line, '|'); 178 cap.name = parts[0]; 179 if (parts.length > 1) 180 { 181 cap.desc = parts[$ - 1]; 182 cap.aliases = parts[1 .. $ - 1]; 183 } 184 first = false; 185 continue; 186 } 187 if (line[0] != '\t' || line[$ - 1] != ',') 188 { 189 // this is malformed, but ignore it 190 continue; 191 } 192 line = line[1 .. $ - 1]; 193 194 // we can try to split the string across an equals sign. 195 // this is guaranteed to be safe even if there are escaped 196 // equals signs, because those can only appear *after* a bare 197 // one (for a string capability) 198 auto nvp = findSplit(line, "="); 199 if (nvp[1] == "=") 200 { 201 // this is a string capability 202 cap.strs[nvp[0]] = unescape(nvp[2]); 203 continue; 204 205 } 206 nvp = findSplit(line, "#"); 207 if (nvp[1] == "#") 208 { 209 // numeric capability 210 cap.ints[nvp[0]] = to!int(nvp[2]); 211 continue; 212 } 213 // boolean capability 214 cap.bools[nvp[0]] = true; 215 } 216 if (cap.name == "") 217 { 218 return null; 219 } 220 return cap; 221 } 222 223 unittest 224 { 225 assert(parseCaps("\n") is null); 226 assert(parseCaps("#junk") is null); 227 auto c = parseCaps("myterm|something\n\tam,\n\tcup=123\\t345,\n\tcolor#4,\n\n"); 228 assert(c !is null); 229 assert(c.bools["am"] == true); 230 assert(c.ints["color"] == 4); 231 assert(c.strs["cup"] == "123\t345"); 232 } 233 234 Caps* loadCaps(string name) 235 { 236 auto info = execute(["infocmp", "-x", "-1", name]); 237 if (info.status != 0) 238 { 239 return null; 240 } 241 return (parseCaps(info.output)); 242 } 243 244 private string escape(string s) 245 { 246 string result = ""; 247 foreach (char c; s) 248 { 249 switch (c) 250 { 251 case '\t': 252 result ~= `\t`; 253 break; 254 case '\n': 255 result ~= `\n`; 256 break; 257 case '\r': 258 result ~= `\r`; 259 break; 260 case '\'', '"', '\\': 261 result ~= "\\"; 262 result ~= c; 263 break; 264 default: 265 if (c < ' ') 266 { 267 result ~= format("\\x%02x", c); 268 } 269 else 270 { 271 result ~= c; 272 } 273 break; 274 } 275 } 276 return result; 277 } 278 279 unittest 280 { 281 assert(escape(`a'b`) == `a\'b`); 282 assert(escape(`a\b`) == `a\\b`); 283 assert(escape(`a"b`) == `a\"b`); 284 assert(escape("a\nb") == `a\nb`); 285 assert(escape("a\tb") == `a\tb`); 286 assert(escape("a\rb") == `a\rb`); 287 assert(escape("a\x1bb") == `a\x1bb`); 288 } 289 290 Termcap* getTermcap(string name) 291 { 292 auto caps = loadCaps(name); 293 if (caps == null) 294 { 295 return null; 296 } 297 return convertCaps(caps); 298 } 299 300 private Termcap* convertCaps(Caps* caps) 301 { 302 auto tc = new Termcap; 303 tc.name = caps.name; 304 tc.aliases = cast(immutable(string)[])caps.aliases; 305 tc.colors = caps.getInt("colors"); 306 tc.columns = caps.getInt("columns"); 307 tc.lines = caps.getInt("lines"); 308 tc.bell = caps.getStr("bel"); 309 tc.clear = caps.getStr("clear"); 310 tc.enterCA = caps.getStr("smcup"); 311 tc.exitCA = caps.getStr("rmcup"); 312 313 tc.showCursor = caps.getStr("cnorm"); 314 tc.hideCursor = caps.getStr("civis"); 315 tc.attrOff = caps.getStr("sgr0"); 316 tc.underline = caps.getStr("smul"); 317 tc.bold = caps.getStr("bold"); 318 tc.blink = caps.getStr("blink"); 319 tc.dim = caps.getStr("dim"); 320 tc.italic = caps.getStr("sitm"); 321 tc.reverse = caps.getStr("rev"); 322 tc.enterKeypad = caps.getStr("smkx"); 323 tc.exitKeypad = caps.getStr("rmkx"); 324 tc.setFg = caps.getStr("setaf"); 325 tc.setBg = caps.getStr("setab"); 326 tc.resetColors = caps.getStr("op"); 327 tc.setCursor = caps.getStr("cup"); 328 tc.cursorBack1 = caps.getStr("cub1"); 329 tc.cursorUp1 = caps.getStr("cuu1"); 330 tc.insertChar = caps.getStr("ich1"); 331 tc.automargin = caps.getBool("am"); 332 tc.keyF1 = caps.getStr("kf1"); 333 tc.keyF2 = caps.getStr("kf2"); 334 tc.keyF3 = caps.getStr("kf3"); 335 tc.keyF4 = caps.getStr("kf4"); 336 tc.keyF5 = caps.getStr("kf5"); 337 tc.keyF6 = caps.getStr("kf6"); 338 tc.keyF7 = caps.getStr("kf7"); 339 tc.keyF8 = caps.getStr("kf8"); 340 tc.keyF9 = caps.getStr("kf9"); 341 tc.keyF10 = caps.getStr("kf10"); 342 tc.keyF11 = caps.getStr("kf11"); 343 tc.keyF12 = caps.getStr("kf12"); 344 tc.keyInsert = caps.getStr("kich1"); 345 tc.keyDelete = caps.getStr("kdch1"); 346 tc.keyBackspace = caps.getStr("kbs"); 347 tc.keyHome = caps.getStr("khome"); 348 tc.keyEnd = caps.getStr("kend"); 349 tc.keyUp = caps.getStr("kcuu1"); 350 tc.keyDown = caps.getStr("kcud1"); 351 tc.keyRight = caps.getStr("kcuf1"); 352 tc.keyLeft = caps.getStr("kcub1"); 353 tc.keyPgDn = caps.getStr("knp"); 354 tc.keyPgUp = caps.getStr("kpp"); 355 tc.keyBacktab = caps.getStr("kcbt"); 356 tc.keyExit = caps.getStr("kext"); 357 tc.keyCancel = caps.getStr("kcan"); 358 tc.keyPrint = caps.getStr("kprt"); 359 tc.keyHelp = caps.getStr("khlp"); 360 tc.keyClear = caps.getStr("kclr"); 361 tc.altChars = caps.getStr("acsc"); 362 tc.enterACS = caps.getStr("smacs"); 363 tc.exitACS = caps.getStr("rmacs"); 364 tc.enableACS = caps.getStr("enacs"); 365 tc.strikethrough = caps.getStr("smxx"); 366 tc.mouse = caps.getStr("kmous"); 367 368 // Lookup high level function keys. 369 tc.keyShfInsert = caps.getStr("kIC"); 370 tc.keyShfDelete = caps.getStr("kDC"); 371 tc.keyShfRight = caps.getStr("kRIT"); 372 tc.keyShfLeft = caps.getStr("kLFT"); 373 tc.keyShfHome = caps.getStr("kHOM"); 374 tc.keyShfEnd = caps.getStr("kEND"); 375 tc.keyF13 = caps.getStr("kf13"); 376 tc.keyF14 = caps.getStr("kf14"); 377 tc.keyF15 = caps.getStr("kf15"); 378 tc.keyF16 = caps.getStr("kf16"); 379 tc.keyF17 = caps.getStr("kf17"); 380 tc.keyF18 = caps.getStr("kf18"); 381 tc.keyF19 = caps.getStr("kf19"); 382 tc.keyF20 = caps.getStr("kf20"); 383 tc.keyF21 = caps.getStr("kf21"); 384 tc.keyF22 = caps.getStr("kf22"); 385 tc.keyF23 = caps.getStr("kf23"); 386 tc.keyF24 = caps.getStr("kf24"); 387 tc.keyF25 = caps.getStr("kf25"); 388 tc.keyF26 = caps.getStr("kf26"); 389 tc.keyF27 = caps.getStr("kf27"); 390 tc.keyF28 = caps.getStr("kf28"); 391 tc.keyF29 = caps.getStr("kf29"); 392 tc.keyF30 = caps.getStr("kf30"); 393 tc.keyF31 = caps.getStr("kf31"); 394 tc.keyF32 = caps.getStr("kf32"); 395 tc.keyF33 = caps.getStr("kf33"); 396 tc.keyF34 = caps.getStr("kf34"); 397 tc.keyF35 = caps.getStr("kf35"); 398 tc.keyF36 = caps.getStr("kf36"); 399 tc.keyF37 = caps.getStr("kf37"); 400 tc.keyF38 = caps.getStr("kf38"); 401 tc.keyF39 = caps.getStr("kf39"); 402 tc.keyF40 = caps.getStr("kf40"); 403 tc.keyF41 = caps.getStr("kf41"); 404 tc.keyF42 = caps.getStr("kf42"); 405 tc.keyF43 = caps.getStr("kf43"); 406 tc.keyF44 = caps.getStr("kf44"); 407 tc.keyF45 = caps.getStr("kf45"); 408 tc.keyF46 = caps.getStr("kf46"); 409 tc.keyF47 = caps.getStr("kf47"); 410 tc.keyF48 = caps.getStr("kf48"); 411 tc.keyF49 = caps.getStr("kf49"); 412 tc.keyF50 = caps.getStr("kf50"); 413 tc.keyF51 = caps.getStr("kf51"); 414 tc.keyF52 = caps.getStr("kf52"); 415 tc.keyF53 = caps.getStr("kf53"); 416 tc.keyF54 = caps.getStr("kf54"); 417 tc.keyF55 = caps.getStr("kf55"); 418 tc.keyF56 = caps.getStr("kf56"); 419 tc.keyF57 = caps.getStr("kf57"); 420 tc.keyF58 = caps.getStr("kf58"); 421 tc.keyF59 = caps.getStr("kf59"); 422 tc.keyF60 = caps.getStr("kf60"); 423 tc.keyF61 = caps.getStr("kf61"); 424 tc.keyF62 = caps.getStr("kf62"); 425 tc.keyF63 = caps.getStr("kf63"); 426 tc.keyF64 = caps.getStr("kf64"); 427 428 // And the same thing for rxvt. 429 // It seems that urxvt at least send ESC as ALT prefix for these, 430 // although some places seem to indicate a separate ALT key sequence. 431 // Users are encouraged to update to an emulator that more closely 432 // matches xterm for better functionality. 433 if (tc.keyShfRight == "\x1b[c" && tc.keyShfLeft == "\x1b[d") 434 { 435 tc.keyShfUp = "\x1b[a"; 436 tc.keyShfDown = "\x1b[b"; 437 tc.keyCtrlUp = "\x1b[Oa"; 438 tc.keyCtrlDown = "\x1b[Ob"; 439 tc.keyCtrlRight = "\x1b[Oc"; 440 tc.keyCtrlLeft = "\x1b[Od"; 441 } 442 if (tc.keyShfHome == "\x1b[7$" && tc.keyShfEnd == "\x1b[8$") 443 { 444 tc.keyCtrlHome = "\x1b[7^"; 445 tc.keyCtrlEnd = "\x1b[8^"; 446 } 447 448 // Technically the RGB flag that is provided for xterm-direct is not 449 // quite right. The problem is that the -direct flag that was introduced 450 // with ncurses 6.1 requires a parsing for the parameters that we lack. 451 // For this case we'll just assume it's XTerm compatible. Someday this 452 // may be incorrect, but right now it is correct, and nobody uses it 453 // anyway. 454 if (caps.getBool("Tc")) 455 { 456 // This presumes XTerm 24-bit true color. 457 tc.colors = 1 << 24; 458 } 459 else if (caps.getBool("RGB")) 460 { 461 // This is for xterm-direct, which uses a different scheme entirely. 462 // (ncurses went a very different direction from everyone else, and 463 // so it's unlikely anything is using this definition.) 464 tc.colors = 1 < 24; 465 tc.setBg = "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m"; 466 tc.setFg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m"; 467 } 468 469 // We only support colors in ANSI 8 or 256 color mode. 470 if (tc.colors < 8 || tc.setFg == "") 471 { 472 tc.colors = 0; 473 } 474 if (tc.setCursor == "") 475 { 476 return null; // terminal is not addressable 477 } 478 // For padding, we lookup the pad char. If that isn't present, 479 // and npc is *not* set, then we assume a null byte. 480 tc.padChar = caps.getStr("pad"); 481 if (tc.padChar == "") 482 { 483 if (!caps.getBool("npc")) 484 { 485 tc.padChar = "\u0000"; 486 } 487 } 488 489 return tc; 490 } 491 492 unittest 493 { 494 assert(getTermcap("nosuch") is null); 495 auto tc = getTermcap("xterm-256color"); 496 assert(tc !is null); 497 tc = getTermcap("vt100"); 498 tc = getTermcap("rxvt"); 499 } 500 501 // there might be better ways to do this 502 503 OutBuffer mkTermSource(Termcap*[] tcs, string modname) 504 { 505 auto ob = new OutBuffer; 506 507 ob.writefln("// Generated automatically. DO NOT HAND-EDIT."); 508 ob.writefln(""); 509 ob.writefln("module %s;", modname); 510 ob.writefln(""); 511 ob.writefln("import dcell.database;"); 512 513 void addInt(string n, int i) 514 { 515 if (i != 0) 516 { 517 ob.writefln(" %s: %d,", n, i); 518 } 519 } 520 521 void addStr(string n, string s) 522 { 523 if (s != "") 524 { 525 ob.writefln(" %s: \"%s\",", n, escape(s)); 526 } 527 } 528 529 void addBool(string n, bool b) 530 { 531 if (b) 532 { 533 ob.writefln(" %s: true,", n); 534 } 535 } 536 537 void addArr(string n, string[] a) 538 { 539 if (a.length > 0) 540 { 541 ob.writef(" %s: [", n); 542 foreach (i, string s; a) 543 { 544 if (i > 0) 545 { 546 ob.writef(", "); 547 } 548 ob.writef(`"%s"`, escape(s)); 549 } 550 ob.writefln("],"); 551 } 552 } 553 554 foreach (num, Termcap *tc; tcs) 555 { 556 ob.writefln(""); 557 ob.writefln("// %s", tc.name); 558 ob.writefln("static immutable Termcap term%d = {", num); 559 560 auto names = FieldNameTuple!Termcap; 561 foreach (int i, ref x; tc.tupleof) 562 { 563 auto n = names[i]; 564 565 static if (is(typeof(x) == int)) 566 { 567 addInt(n, x); 568 } 569 static if (is(typeof(x) == string)) 570 { 571 addStr(n, x); 572 } 573 static if (is(typeof(x) == bool)) 574 { 575 addBool(n, x); 576 } 577 static if (is(typeof(x) == string[])) 578 { 579 addArr(n, x); 580 } 581 } 582 583 ob.writefln("};"); 584 } 585 ob.writefln(""); 586 ob.writefln("static this()"); 587 ob.writefln("{"); 588 foreach (num, _; tcs) 589 { 590 ob.writefln(" Database.put(&term%d);", num); 591 } 592 ob.writefln("}"); 593 return ob; 594 } 595 596 unittest 597 { 598 assert(getTermcap("nosuch") is null); 599 auto tc = getTermcap("xterm-256color"); 600 assert(tc !is null); 601 auto ob = mkTermSource([tc], "dcell.terminfo.xterm256color"); 602 } 603 604 void main(string[] args) 605 { 606 import core.stdc.stdlib; 607 import std.getopt; 608 import std.path; 609 import std.process; 610 611 string[] terms; 612 string directory = "."; 613 614 auto help = getopt(args, "directory", &directory); 615 if (help.helpWanted) 616 { 617 defaultGetoptFormatter(stderr.lockingTextWriter(), 618 "Emit terminal database", help.options); 619 exit(1); 620 } 621 args = args[1 .. $]; 622 if (args.length != 0) 623 { 624 terms = args; 625 } 626 else 627 { 628 terms ~= environment.get("TERM", "ansi"); 629 } 630 foreach (index, string name; terms) 631 { 632 Termcap*[] tcs; 633 auto tc = getTermcap(name); 634 if (tc is null) 635 { 636 throw new Exception("failed to get term for " ~ name); 637 } 638 tcs ~= tc; 639 640 // look for common variants 641 foreach (unused, suffix; [ 642 "16color", "88color", "256color", "truecolor", "direct" 643 ]) 644 { 645 tc = getTermcap(name ~ "-" ~ suffix); 646 if (tc !is null) 647 { 648 tcs ~= tc; 649 } 650 } 651 652 string pkg; 653 pkg = replace(name, "-", ""); 654 pkg = replace(pkg, ".", ""); 655 auto ob = mkTermSource(tcs, "dcell.terminfo." ~ pkg); 656 import std.file; 657 658 auto autof = chainPath(directory, pkg ~ ".d"); 659 write(autof, ob.toString()); 660 } 661 }