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 module dcell.termcap; 7 8 import core.thread; 9 import std.conv; 10 import std.algorithm; 11 import std.functional; 12 import std.range; 13 import std.stdio; 14 import std.string; 15 16 /** 17 * Represents the actual capabilities - this is an entry in a terminfo 18 * database. 19 */ 20 struct Termcap 21 { 22 string name; /// primary name for terminal, e.g. "xterm" 23 immutable(string)[] aliases; /// alternate names for terminal 24 int columns; /// `cols`, the number of columns present 25 int lines; /// `lines`, the number lines (rows) present 26 int colors; // `colors`, the number of colors supported 27 string bell; /// `bell`, the sequence to ring a bell 28 string clear; /// `clear`, the sequence to clear the screen 29 string enterCA; /// `smcup`, sequence to enter cursor addressing mode 30 string exitCA; /// `rmcup`, sequence to exit cursor addressing mode 31 string showCursor; /// `cnorm`, should display the normal cursor 32 string hideCursor; /// `civis`, mark the cursor invisible 33 string attrOff; /// `sgr0`, turn off all text attributes and colors 34 string underline; /// `smul`, starts underlining 35 string bold; /// `bold`, starts bold (maybe intense or double-strike) 36 string blink; /// `blink`, starts blinking text 37 string reverse; /// `rev`, inverts the foreground and background colors 38 string dim; /// `dim`, reduces the intensity of text 39 string italic; /// `sitm`, starts italics mode (not widely supported) 40 string enterKeypad; /// `smkx`, enables keypad mode 41 string exitKeypad; /// `rmkx`, leaves keypad mode 42 string setFg; /// `setaf`, sets foreground text color (indexed) 43 string setBg; /// `setab`, sets background text color (indexed) 44 string resetColors; /// `op`, sets foreground and background to default 45 string setCursor; /// `cup`, sets cursor location to row and column 46 string cursorBack1; /// `cub1`, move cursor backwards one 47 string cursorUp1; /// `cuu1`, mover cursor up one line 48 string padChar; /// `pad`, padding character, if non-empty enables padding delays 49 string insertChar; /// `ich1`, insert a character, used for inserting at bottom right for automargin terminals 50 string keyBackspace; /// `kbs`, backspace key 51 string keyF1; // kf1 52 string keyF2; // kf2 53 string keyF3; // kf3 54 string keyF4; // kf4 55 string keyF5; // kf5 56 string keyF6; // kf6 57 string keyF7; // kf7 58 string keyF8; // kf8 59 string keyF9; // kf9 60 string keyF10; // kf10 61 string keyF11; // kf11 62 string keyF12; // kf12 63 string keyF13; // kf13 64 string keyF14; // kf14 65 string keyF15; // kf15 66 string keyF16; // kf16 67 string keyF17; // kf17 68 string keyF18; // kf18 69 string keyF19; // kf19 70 string keyF20; // kf20 71 string keyF21; // kf21 72 string keyF22; // kf22 73 string keyF23; // kf23 74 string keyF24; // kf24 75 string keyF25; // kf25 76 string keyF26; // kf26 77 string keyF27; // kf27 78 string keyF28; // kf28 79 string keyF29; // kf29 80 string keyF30; // kf30 81 string keyF31; // kf31 82 string keyF32; // kf32 83 string keyF33; // kf33 84 string keyF34; // kf34 85 string keyF35; // kf35 86 string keyF36; // kf36 87 string keyF37; // kf37 88 string keyF38; // kf38 89 string keyF39; // kf39 90 string keyF40; // kf40 91 string keyF41; // kf41 92 string keyF42; // kf42 93 string keyF43; // kf43 94 string keyF44; // kf44 95 string keyF45; // kf45 96 string keyF46; // kf46 97 string keyF47; // kf47 98 string keyF48; // kf48 99 string keyF49; // kf49 100 string keyF50; // kf50 101 string keyF51; // kf51 102 string keyF52; // kf52 103 string keyF53; // kf53 104 string keyF54; // kf54 105 string keyF55; // kf55 106 string keyF56; // kf56 107 string keyF57; // kf57 108 string keyF58; // kf58 109 string keyF59; // kf59 110 string keyF60; // kf60 111 string keyF61; // kf61 112 string keyF62; // kf62 113 string keyF63; // kf63 114 string keyF64; // kf64 115 string keyInsert; // kich1 116 string keyDelete; // kdch1 117 string keyHome; // khome 118 string keyEnd; // kend 119 string keyHelp; // khlp 120 string keyPgUp; // kpp 121 string keyPgDn; // knp 122 string keyUp; // kcuu1 123 string keyDown; // kcud1 124 string keyLeft; // kcub1 125 string keyRight; // kcuf1 126 string keyBacktab; // kcbt 127 string keyExit; // kext 128 string keyClear; // kclr 129 string keyPrint; // kprt 130 string keyCancel; // kcan 131 string mouse; /// `kmouse`, indicates support for mouse mode - XTerm style sequences are assumed 132 string altChars; /// `acsc`, alternate characters, used for non-ASCII characters with certain legacy terminals 133 string enterACS; /// `smacs`, sequence to switch to alternate character set 134 string exitACS; /// `rmacs`, sequence to return to normal character set 135 string enableACS; /// `enacs`, sequence to enable alternate character set support 136 string keyShfRight; // kRIT 137 string keyShfLeft; // kLFT 138 string keyShfHome; // kHOM 139 string keyShfEnd; // kEND 140 string keyShfInsert; // kIC 141 string keyShfDelete; // kDC 142 bool automargin; /// `am`, if true cursor wraps and advances to next row after last column 143 144 // Non-standard additions to terminfo. YMMV. 145 string strikethrough; // smxx 146 string setFgBg; /// sequence to set both foreground and background together, using indexed colors 147 string setFgBgRGB; /// sequence to set both foreground and background together, using RGB colors 148 string setFgRGB; /// sequence to set foreground color to RGB value 149 string setBgRGB; /// sequence to set background color RGB value 150 string keyShfUp; 151 string keyShfDown; 152 string keyShfPgUp; 153 string keyShfPgDn; 154 string keyCtrlUp; 155 string keyCtrlDown; 156 string keyCtrlRight; 157 string keyCtrlLeft; 158 string keyMetaUp; 159 string keyMetaDown; 160 string keyMetaRight; 161 string keyMetaLeft; 162 string keyAltUp; 163 string keyAltDown; 164 string keyAltRight; 165 string keyAltLeft; 166 string keyCtrlHome; 167 string keyCtrlEnd; 168 string keyMetaHome; 169 string keyMetaEnd; 170 string keyAltHome; 171 string keyAltEnd; 172 string keyAltShfUp; 173 string keyAltShfDown; 174 string keyAltShfLeft; 175 string keyAltShfRight; 176 string keyMetaShfUp; 177 string keyMetaShfDown; 178 string keyMetaShfLeft; 179 string keyMetaShfRight; 180 string keyCtrlShfUp; 181 string keyCtrlShfDown; 182 string keyCtrlShfLeft; 183 string keyCtrlShfRight; 184 string keyCtrlShfHome; 185 string keyCtrlShfEnd; 186 string keyAltShfHome; 187 string keyAltShfEnd; 188 string keyMetaShfHome; 189 string keyMetaShfEnd; 190 string enablePaste; /// sequence to enable delimited paste mode 191 string disablePaste; /// sequence to disable delimited paste mode 192 string pasteStart; /// sequence sent by terminal to indicate start of a paste buffer 193 string pasteEnd; /// sequence sent by terminal to indicated end of a paste buffer 194 string cursorReset; /// sequence to reset the cursor shape to default 195 string cursorBlock; /// sequence to change the cursor to a solid block 196 string cursorUnderline; /// sequence to change the cursor to a steady underscore 197 string cursorBar; /// sequence to change the cursor to a steady vertical bar 198 string cursorBlinkingBlock; /// sequence to change the cursor to a blinking block 199 string cursorBlinkingUnderline; /// sequence to change the cursor to a blinking underscore 200 string cursorBlinkingBar; /// sequence to change the cursor to a blinking vertical bar 201 string enterURL; /// sequence to start making text a clickable link 202 string exitURL; /// sequence to stop making text clickable link 203 string setWindowSize; /// sequence to resize the window (rarely supported) 204 205 /** Permits a constant value to be assigned to a mutable value. */ 206 void opAssign(scope const(Termcap)* other) @trusted 207 { 208 foreach (i, ref v; this.tupleof) 209 { 210 v = other.tupleof[i]; 211 } 212 } 213 214 /** 215 * Put a string to an out range, which is normally a file 216 * to an interactive terminal like /dev/tty or stdin, while 217 * interpretreting embedded delay sequences of the form 218 * $<DELAY> (where DELAY is given in milliseconds, and must 219 * be a postive rational number of millseconds). When these 220 * are encountered, the flush delegate is called (if not null), 221 * and the function sleeps for the indicated amount of time. 222 */ 223 static void puts(R)(R output, string s, void delegate() flush = null) 224 if (isOutputRange!(R, ubyte)) 225 { 226 while (s.length > 0) 227 { 228 auto beg = indexOf(s, "$<"); 229 if (beg == -1) 230 { 231 cast(void) copy(s, output); 232 return; 233 } 234 cast(void) copy(s[0 .. beg], output); 235 s = s[beg .. $]; 236 auto end = indexOf(s, ">"); 237 if (end < 0) 238 { 239 // unterminated escape, emit it as is 240 cast(void) copy(s, output); 241 return; 242 } 243 auto val = s[2 .. end]; 244 s = s[end + 1 .. $]; 245 int usec = 0; 246 int mult = 1000; // 1 ms 247 bool dot = false; 248 bool valid = true; 249 250 while (valid && val.length > 0) 251 { 252 switch (val[0]) 253 { 254 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 255 usec *= 10; 256 usec += val[0] - '0'; 257 if (dot && mult > 1) 258 { 259 mult /= 10; 260 } 261 break; 262 263 case '.': 264 if (!dot) 265 { 266 dot = true; 267 } 268 else 269 { 270 valid = false; 271 } 272 break; 273 274 default: 275 valid = false; 276 break; 277 } 278 val = val[1 .. $]; 279 } 280 281 if (valid) 282 { 283 if (flush !is null) 284 { 285 flush(); 286 } 287 Thread.sleep(usecs(usec * mult)); 288 } 289 } 290 } 291 292 unittest 293 { 294 import std.datetime : Clock; 295 import core.time : seconds; 296 import std.outbuffer; 297 298 auto now = Clock.currTime(); 299 auto ob = new OutBuffer(); 300 301 puts(ob, "AB$<1000>C"); 302 puts(ob, "DEF$<100.5>\n"); 303 auto end = Clock.currTime(); 304 assert(end > now); 305 assert(now + seconds(1) <= end); 306 assert(now + seconds(2) > end); 307 308 assert(ob.toString() == "ABCDEF\n"); 309 // negative tests -- we don't care what's in the file (UB), but it must not panic 310 puts(ob, "Z$<123..0123>"); // malformed dots 311 puts(ob, "LMN$<12X>"); // invalid number 312 puts(ob, "GHI$<123JKL"); // unterminated delay 313 } 314 315 unittest 316 { 317 import std.datetime; 318 import std.outbuffer; 319 320 auto now = Clock.currTime(); 321 auto ob = new OutBuffer(); 322 323 puts(ob, "AB$<100>C"); 324 puts(ob, "DEF$<100.5>\n"); 325 auto end = Clock.currTime(); 326 assert(end > now); 327 assert(now + msecs(200) <= end); 328 assert(now + msecs(300) > end); 329 330 assert(ob.toString() == "ABCDEF\n"); 331 } 332 333 unittest 334 { 335 import std.outbuffer; 336 import std.datetime; 337 338 class Flusher : OutBuffer 339 { 340 private int flushes = 0; 341 void flush() 342 { 343 flushes++; 344 } 345 } 346 347 auto f = new Flusher(); 348 auto now = Clock.currTime(); 349 puts(f, "ABC$<100>DEF", &f.flush); 350 auto end = Clock.currTime(); 351 assert(end > now); 352 assert(now + msecs(100) <= end); 353 assert(f.flushes == 1); 354 assert(f.toString() == "ABCDEF"); 355 } 356 357 unittest 358 { 359 import std.range; 360 361 auto o = nullSink(); 362 puts(o, "Z$<123..0123>"); // malformed dots 363 puts(o, "LMN$<12X>"); // invalid number 364 puts(o, "GHI$<123JKL"); // unterminated delay 365 } 366 367 private struct Parameter 368 { 369 int i; 370 string s; 371 } 372 373 /** 374 * Evaluates a terminal capability string and expands it, using the supplied integer parameters. 375 * 376 * Params: 377 * s = A terminal capability string. The actual string, not the name of the capability. 378 * args = A list of parameters for the capability. 379 * 380 * Returns: 381 * The evaluated capability with parameters applied. 382 */ 383 384 static string param(string s, int[] args...) pure @safe 385 { 386 Parameter[] ps = new Parameter[args.length]; 387 foreach (i, val; args) 388 { 389 ps[i].i = val; 390 } 391 return paramInner(s, ps); 392 } 393 394 static string param(string s, string[] args...) pure @safe 395 { 396 Parameter[] ps = new Parameter[args.length]; 397 foreach (i, val; args) 398 { 399 ps[i].s = val; 400 } 401 return paramInner(s, ps); 402 } 403 404 static string param(string s) pure @safe 405 { 406 return paramInner(s, []); 407 } 408 409 private static paramInner(string s, Parameter[] params) pure @safe 410 { 411 enum Skip 412 { 413 emit, 414 toElse, 415 toEnd 416 } 417 418 char[] input; 419 char[] output; 420 Parameter[byte] saved; 421 Parameter[] stack; 422 Skip skip; 423 424 input = s.dup; 425 426 void push(Parameter p) 427 { 428 stack ~= p; 429 } 430 431 void pushInt(int i) 432 { 433 Parameter p; 434 p.i = i; 435 push(p); 436 } 437 438 // pop a parameter from the stack. 439 // If the stack is empty, returns a zero value Parameter. 440 Parameter pop() 441 { 442 Parameter p; 443 if (stack.length > 0) 444 { 445 p = stack[$ - 1]; 446 stack = stack[0 .. $ - 1]; 447 } 448 return p; 449 } 450 451 int popInt() 452 { 453 return pop().i; 454 } 455 456 string popStr() 457 { 458 return pop().s; 459 } 460 461 char nextCh() 462 { 463 char ch; 464 if (input.length > 0) 465 { 466 ch = input[0]; 467 input = input[1 .. $]; 468 } 469 return (ch); 470 } 471 472 // We do not currently support the printf style formats. 473 // We are not aware of any use by such formats in any real-world 474 // terminfo descriptions. 475 476 while (input.length > 0) 477 { 478 Parameter p; 479 int i1, i2; 480 481 // In some cases we need to pop both parameters 482 // into local variables before evaluating. This is required 483 // to ensure both pops are evaluated in a specific order. 484 // In some cases the order of a binary operation is not important 485 // and then we can write it as push(pop op pop). Also, it turns out 486 // that the right most parameter is pushed first, and the left most last. 487 // So for example, the divisor is pushed before the numerator. This 488 // seems somewhat counterintuitive, but it is the current behavior of ncurses. 489 490 auto ch = nextCh(); 491 492 if (ch != '%') 493 { 494 if (skip == Skip.emit) 495 { 496 output ~= ch; 497 } 498 continue; 499 } 500 501 ch = nextCh(); 502 503 if (skip == skip.toElse) 504 { 505 if (ch == 'e' || ch == ';') 506 { 507 skip = Skip.emit; 508 } 509 continue; 510 } 511 else if (skip == skip.toEnd) 512 { 513 if (ch == ';') 514 { 515 skip = Skip.emit; 516 } 517 continue; 518 } 519 520 switch (ch) 521 { 522 case '%': // literal % 523 output ~= ch; 524 break; 525 526 case 'i': // increment both parameters (ANSI cup support) 527 if (params.length >= 2) 528 { 529 params[0].i++; 530 params[1].i++; 531 } 532 break; 533 534 case 'c': // integer as character 535 output ~= cast(byte) popInt(); 536 break; 537 538 case 's': // character or string 539 output ~= popStr(); 540 break; 541 542 case 'd': // decimal value 543 output ~= to!string(popInt()); 544 break; 545 546 case 'p': // push i'th parameter (could be string or integer) 547 ch = nextCh(); 548 if (ch >= '1' && (ch - '1') < params.length) 549 { 550 push(params[ch - '1']); 551 } 552 else 553 { 554 pushInt(0); 555 } 556 break; 557 558 case 'P': // pop and store 559 ch = nextCh(); 560 if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) 561 { 562 saved[ch] = pop(); 563 } 564 break; 565 566 case 'g': // recall and push 567 ch = nextCh(); 568 if (ch in saved) 569 { 570 push(saved[ch]); 571 } 572 else 573 { 574 pushInt(0); 575 } 576 break; 577 578 case '\'': // push a character (will be of form %'c') 579 ch = nextCh(); 580 pushInt(cast(byte) ch); 581 nextCh(); // consume the closing ' 582 break; 583 584 case '{': // push integer, terminated by '}' 585 i1 = 0; 586 ch = nextCh(); 587 while ((ch >= '0') && (ch <= '9')) 588 { 589 i1 *= 10; 590 i1 += ch - '0'; 591 ch = nextCh(); 592 } 593 pushInt(i1); 594 break; 595 596 case 'l': // push(strlen(pop)) 597 pushInt(cast(int) popStr().length); 598 break; 599 600 case '+': // pop two parameters, add the result 601 pushInt(popInt() + popInt()); 602 break; 603 604 case '-': 605 i1 = popInt(); 606 i2 = popInt(); 607 pushInt(i2 - i1); 608 break; 609 610 case '*': 611 pushInt(popInt() * popInt()); 612 break; 613 614 case '/': 615 i1 = popInt(); 616 i2 = popInt(); 617 if (i1 != 0) 618 { 619 pushInt(i2 / i1); 620 } 621 else 622 { 623 pushInt(0); 624 } 625 break; 626 627 case 'm': // modulo 628 i1 = popInt(); 629 i2 = popInt(); 630 if (i1 != 0) 631 { 632 pushInt(i2 % i1); 633 } 634 else 635 { 636 pushInt(0); 637 } 638 break; 639 640 case '&': // bitwise AND 641 pushInt(popInt() & popInt()); 642 break; 643 644 case '|': // bitwise OR 645 pushInt(popInt() | popInt()); 646 break; 647 648 case '^': // bitwise XOR 649 pushInt(popInt() ^ popInt()); 650 break; 651 652 case '~': // bit complement 653 pushInt(~popInt()); 654 break; 655 656 case '!': // NOT 657 pushInt(!popInt()); 658 break; 659 660 case 'A': // logical AND 661 i1 = popInt(); // pop both (no short circuit evaluation) 662 i2 = popInt(); 663 pushInt(i1 && i2); 664 break; 665 666 case 'O': // logical OR 667 i1 = popInt(); // pop both 668 i2 = popInt(); 669 pushInt(i1 || i2); 670 break; 671 672 case '=': // numeric compare 673 pushInt(popInt() == popInt()); 674 break; 675 676 case '<': 677 i1 = popInt(); 678 i2 = popInt(); 679 pushInt(i2 < i1); 680 break; 681 682 case '>': 683 i1 = popInt(); 684 i2 = popInt(); 685 pushInt(i2 > i1); 686 break; 687 688 case '?': // start of conditional 689 break; 690 691 case ';': 692 break; 693 694 case 't': // then 695 if (!popInt()) 696 { 697 skip = Skip.toElse; 698 } 699 break; 700 701 case 'e': 702 // We've already processed the true branch of the conditional. 703 // We won't process anything more for the rest of the conditional, 704 // including any other branches. 705 skip = Skip.toEnd; 706 break; 707 708 default: 709 // Unrecognized sequence, so just emit it. 710 output ~= '%'; 711 output ~= ch; 712 break; 713 } 714 } 715 716 return to!string(output); 717 } 718 719 @safe unittest 720 { 721 assert(param("%i%p1%d;%p2%d", 2, 3) == "3;4"); // increment (goto) 722 assert(param("%{50}%{3}%-%d") == "47"); // subtraction 723 assert(param("%{50}%{5}%/%d") == "10"); // division 724 assert(param("%p1%p2%/%d", 50, 3) == "16"); // division with truncation 725 assert(param("%p1%p2%/%d", 5, 0) == "0"); // division by zero 726 assert(param("%{50}%{3}%m%d") == "2"); // modulo 727 assert(param("%p1%p2%m%d", 5, 0) == "0"); // modulo (division by zero) 728 assert(param("%{4}%{25}%*%d") == "100"); // multiplication 729 assert(param("%p1%l%d", "four") == "4"); // strlen 730 assert(param("%p1%p2%=%d", 2, 2) == "1"); // equal 731 assert(param("%p1%p2%=%d", 2, 3) == "0"); // equal (false) 732 assert(param("%?%p1%p2%<%ttrue%efalse%;", 7, 8) == "true"); // NB: push/pop reverses order 733 assert(param("%?%p1%p2%>%ttrue%efalse%;", 7, 8) == "false"); // NB: push/pop reverses order 734 assert(param("x%p1%cx", 65) == "xAx"); // emit using %c, 'A' == 65 (ASCII) 735 assert(param("x%'a'%p1%+%cx", 1) == "xbx"); // literal character encodes ASCII value 736 assert(param("x%%x") == "x%x"); // literal % character 737 assert(param("%_") == "%_"); // unrecognized sequence 738 assert(param("%p2%d") == "0"); // invalid parameter, evaluates to zero (undefined behavior) 739 assert(param("%p1%Pgx%gg%gg%dx%c", 65) == "x65xA"); // saved variables (dynamic) 740 assert(param("%p1%PZx%gZ%d%gZ%dx", 789) == "x789789x"); // saved variables (static) 741 assert(param("%gB%d") == "0"); // saved values are zero if not changed 742 assert(param("%p1%Ph%p2%gh%+%Ph%p3%gh%*%d", 1, 2, 5) == "15"); // overwrite saved values 743 assert(param("%p1%p2%&%d", 3, 10) == "2"); 744 assert(param("%p1%p2%|%d", 3, 10) == "11"); 745 assert(param("%p1%p2%^%d", 3, 10) == "9"); 746 assert(param("%p1%~%p2%&%d", 2, 0xff) == "253"); // bit complement, but mask it down 747 assert(param("%p1%p2%<%p2%p3%<%A%d", 1, 2, 3) == "1"); // AND 748 assert(param("%p1%p2%<%p2%p3%<%A%d", 1, 3, 2) == "0"); // AND false 749 assert(param("%p1%p2%<%p2%p3%<%!%A%d", 1, 3, 2) == "1"); // NOT (and) 750 assert(param("%p1%p2%<%p1%p2%=%O%d", 1, 1) == "1"); // OR 751 assert(param("%p1[%s]", "garrett") == "[garrett]"); // string parameteter 752 } 753 }