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