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 }