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