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