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.database;
7 
8 import core.thread;
9 import std.algorithm;
10 import std.conv;
11 import std.process : environment;
12 import std.stdio;
13 import std.string;
14 
15 public import dcell.termcap;
16 
17 @safe:
18 
19 /**
20  * Represents a database of terminal entries, indexed by their name.
21  */
22 synchronized class Database
23 {
24     private static Termcap[string] terms;
25     private static const(Termcap)*[string] entries;
26 
27     /**
28      * Adds an entry to the database.
29      * This should be called by terminal descriptions.
30      *
31      * Params:
32      *   ti = terminal capabilities to add
33      */
34     static void put(immutable(Termcap)* tc) @safe
35     {
36         entries[tc.name] = tc;
37         foreach (name; tc.aliases)
38         {
39             entries[name] = tc;
40         }
41     }
42 
43     /**
44      * Looks up an entry in the database.
45      * The name is most likely to be taken from the $TERM environment variable.
46      * Some massaging of the entry is done to amend with capabilities and support
47      * reasonable fallbacks.
48      *
49      * Params:
50      *   name = name of the terminal (typically from $TERM)
51      *
52      * Returns:
53      *   terminal capabilities if known, `null` if not.
54      */
55     static const(Termcap)* get(string name, bool addTrueColor = false, bool add256Color = false) @safe
56     {
57         if (name !in entries)
58         {
59             // this handles fallbacks for each possible color terminal
60             // note that going from "-color" to non-color will wind up
61             // falling back to b&w.  we could possibly have a method to
62             // add 16 and 8 color fallbacks.
63             if (endsWith(name, "-truecolor"))
64             {
65                 return get(name[0 .. $ - "-truecolor".length] ~ "-256color", true, true);
66             }
67             if (endsWith(name, "-256color"))
68             {
69                 return get(name[0 .. $ - "-256color".length] ~ "-88color", addTrueColor, true);
70             }
71             if (endsWith(name, "-88color"))
72             {
73                 return get(name[0 .. $ - "-88color".length] ~ "-color", addTrueColor, add256Color);
74             }
75             if (endsWith(name, "-color"))
76             {
77                 return get(name[0 .. $ - "-color".length], addTrueColor, add256Color);
78             }
79             return null;
80         }
81 
82         string colorTerm;
83         if ("COLORTERM" in environment)
84         {
85             colorTerm = environment["COLORTERM"];
86         }
87         auto tc = new Termcap;
88 
89         // poor mans copy, but we have to bypass the const,
90         // although we're not going to change actual values.
91         // we promise not to modify the aliases array.
92         *tc = *(entries[name]);
93 
94         // For terminals that use "standard" SGR sequences, lets combine the
95         // foreground and background together. This saves one byte sent
96         // per screen cell.  Not huge, but it might be as much as 10%.
97         if (startsWith(tc.setFg, "\x1b[") &&
98             startsWith(tc.setBg, "\x1b[") &&
99             endsWith(tc.setFg, ";m") &&
100             endsWith(tc.setBg, ";m"))
101         {
102             tc.setFgBg = tc.setFg[0 .. $ - 1]; // drop m
103             tc.setFgBg ~= ';';
104             tc.setFgBg ~= replace(tc.setBg[2 .. $], "%p1", "%p2");
105         }
106 
107         if (tc.colors > 256 || canFind(colorTerm, "truecolor") ||
108             canFind(colorTerm, "24bit") || canFind(colorTerm, "24-bit"))
109         {
110             addTrueColor = true;
111         }
112 
113         if (addTrueColor && tc.setFgBgRGB == "" && tc.setFgRGB == "" && tc.setBgRGB == "")
114         {
115             // vanilla ISO 8613-6:1994 24-bit color (ala xterm)
116             tc.setFgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%dm";
117             tc.setBgRGB = "\x1b[48;2;%p1%d;%p2%d;%p3%dm";
118             tc.setFgBgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%d;48;2;%p4%d;%p5%d;%p6%dm";
119             if (tc.resetColors == "")
120             {
121                 tc.resetColors = "\x1b[39;49m;";
122             }
123             // assume we can also add 256 color
124             tc.colors = 1 << 24;
125         }
126 
127         if (add256Color || (tc.colors >= 256 && tc.setFg == ""))
128         {
129             if (tc.colors < 256)
130                 tc.colors = 256;
131             tc.setFg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m";
132             tc.setBg = "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m";
133             tc.setFgBg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;;" ~
134                 "%?%p2%{8}%<%t4%p2%d%e%p2%{16}%<%t10%p2%{8}%-%d%e48;5;%p2%d%;m";
135             tc.resetColors = "\x1b[39;49m";
136         }
137 
138         // if we have mouse support, we can try to assume that we
139         // can safely/reasonably add some other features, if they
140         // are not provided for explicitly in the database.
141         if (tc.mouse != "")
142         {
143             // changeable cursor shapes
144             if (tc.cursorReset == "")
145             {
146                 tc.cursorReset = "\x1b[0 q";
147                 tc.cursorBlinkingBlock = "\x1b[1 q";
148                 tc.cursorBlock = "\x1b[2 q";
149                 tc.cursorBlinkingUnderline = "\x1b[3 q";
150                 tc.cursorUnderline = "\x1b[4 q";
151                 tc.cursorBlinkingBar = "\x1b[5 q";
152                 tc.cursorBar = "\x1b[6 q";
153             }
154             // bracketed paste
155             if (tc.enablePaste == "")
156             {
157                 tc.enablePaste = "\x1b[?2004h";
158                 tc.disablePaste = "\x1b[?2004l";
159                 tc.pasteStart = "\x1b[200~";
160                 tc.pasteEnd = "\x1b[201~";
161             }
162             // OSC URL support
163             if (tc.enterURL == "")
164             {
165                 tc.enterURL = "\x1b]8;;%p1%s\x1b\\";
166                 tc.exitURL = "\x1b]8;;\x1b\\";
167             }
168             // OSC window size
169             if (tc.setWindowSize == "")
170             {
171                 tc.setWindowSize = "\x1b[8;%p1%p2%d;%dt";
172             }
173         }
174 
175         // Amend some sequences for URXVT.
176         // It seems that urxvt at least send ESC as ALT prefix for these,
177         // although some places seem to indicate a separate ALT key sequence.
178         // Users are encouraged to update to an emulator that more closely
179         // matches xterm for better functionality.
180         if (tc.keyShfRight == "\x1b[c" && tc.keyShfLeft == "\x1b[d")
181         {
182             tc.keyShfUp = "\x1b[a";
183             tc.keyShfDown = "\x1b[b";
184             tc.keyCtrlUp = "\x1b[Oa";
185             tc.keyCtrlDown = "\x1b[Ob";
186             tc.keyCtrlRight = "\x1b[Oc";
187             tc.keyCtrlLeft = "\x1b[Od";
188         }
189         if (tc.keyShfHome == "\x1b[7$" && tc.keyShfEnd == "\x1b[8$")
190         {
191             tc.keyCtrlHome = "\x1b[7^";
192             tc.keyCtrlEnd = "\x1b[8^";
193         }
194 
195         // likewise, if we have mouse support, let's try to add backeted
196         // paste support.
197         return tc;
198     }
199 }
200 
201 @safe unittest
202 {
203     static immutable Termcap caps = {name: "mytest", aliases: ["mytest-1", "mytest-2"]};
204     static immutable Termcap caps2 = {name: "ctest", mouse: ":mouse", colors: 1<<24};
205 
206     Database.put(&caps);
207     Database.put(&caps2);
208 
209     assert(Database.get("nosuch") is null);
210     auto tc = Database.get("mytest");
211     assert((tc !is null) && tc.name == "mytest");
212     assert(Database.get("mytest-1") !is null);
213     assert(Database.get("mytest-2") !is null);
214 
215     environment["COLORTERM"] = "truecolor";
216     tc = Database.get("mytest-truecolor");
217     assert(tc !is null);
218     assert(tc.colors == 1 << 24);
219     assert(tc.setFgBgRGB != "");
220     assert(tc.setFg != "");
221     assert(tc.resetColors != "");
222 
223     environment["COLORTERM"] = "";
224     tc = Database.get("mytest-256color");
225     assert(tc !is null);
226     assert(tc.colors == 256);
227     assert(tc.setFgBgRGB == "");
228     assert(tc.setFgBg != "");
229     assert(tc.setFg != "");
230     assert(tc.resetColors != "");
231 
232     tc = Database.get("ctest");
233     assert(tc !is null);
234     assert(tc.colors == 1 << 24);
235     assert(tc.setFgBgRGB != "");
236     assert(tc.setFg != "");
237     assert(tc.resetColors != "");
238     assert(tc.enablePaste != ""); // xterm like
239 }