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 }