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 }