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 }