dte test coverage


Directory: ./
File: src/terminal/terminal.c
Date: 2024-12-21 16:03:22
Exec Total Coverage
Lines: 80 112 71.4%
Functions: 6 8 75.0%
Branches: 31 62 50.0%

Line Branch Exec Source
1 #include <stdint.h>
2 #include <string.h>
3 #include "terminal.h"
4 #include "color.h"
5 #include "linux.h"
6 #include "output.h"
7 #include "parse.h"
8 #include "rxvt.h"
9 #include "util/array.h"
10 #include "util/bsearch.h"
11 #include "util/debug.h"
12 #include "util/log.h"
13 #include "util/str-util.h"
14
15 typedef struct {
16 unsigned int ncv_attrs : 5; // TermStyle attributes (see "ncv" in terminfo(5))
17 unsigned int features : 27; // TermFeatureFlags
18 } TermInfo;
19
20 typedef struct {
21 const char name[11];
22 uint8_t name_len;
23 TermInfo info;
24 } TermEntry;
25
26 enum {
27 // Short aliases for TermFeatureFlags:
28 BCE = TFLAG_BACK_COLOR_ERASE,
29 REP = TFLAG_ECMA48_REPEAT,
30 TITLE = TFLAG_SET_WINDOW_TITLE,
31 RXVT = TFLAG_RXVT,
32 LINUX = TFLAG_LINUX,
33 OSC52 = TFLAG_OSC52_COPY,
34 KITTYKBD = TFLAG_KITTY_KEYBOARD,
35 MOKEYS = TFLAG_MODIFY_OTHER_KEYS,
36 ITERM2 = TFLAG_ITERM2,
37 SYNC = TFLAG_SYNC,
38 NOQUERY3 = TFLAG_NO_QUERY_L3,
39 C8 = TFLAG_8_COLOR,
40 C16 = TFLAG_16_COLOR | C8,
41 C256 = TFLAG_256_COLOR | C16,
42 TC = TFLAG_TRUE_COLOR | C256,
43 };
44
45 enum {
46 // Short aliases for TermStyle attributes:
47 UL = ATTR_UNDERLINE,
48 REV = ATTR_REVERSE,
49 DIM = ATTR_DIM,
50 };
51
52 #define t(tname, ncv, feat) { \
53 .name = tname, \
54 .name_len = STRLEN(tname), \
55 .info = { \
56 .ncv_attrs = ncv, \
57 .features = feat, \
58 }, \
59 }
60
61 static const TermEntry terms[] = {
62 t("Eterm", 0, C8 | BCE),
63 t("alacritty", 0, TC | BCE | REP | OSC52 | SYNC),
64 t("ansi", UL, C8),
65 t("ansiterm", 0, 0),
66 t("aterm", 0, C8 | BCE),
67 t("contour", 0, TC | BCE | REP | TITLE | OSC52 | SYNC),
68 t("cx", 0, C8),
69 t("cx100", 0, C8),
70 t("cygwin", 0, C8),
71 t("cygwinB19", UL, C8),
72 t("cygwinDBG", UL, C8),
73 t("decansi", 0, C8),
74 t("domterm", 0, C8 | BCE),
75 t("dtterm", 0, C8),
76 t("dvtm", 0, C8),
77 t("fbterm", DIM | UL, C256 | BCE),
78 t("foot", 0, TC | BCE | REP | TITLE | OSC52 | KITTYKBD | SYNC),
79 t("ghostty", 0, TC | BCE | REP | TITLE | OSC52 | KITTYKBD | SYNC),
80 t("hurd", DIM | UL, C8 | BCE),
81 t("iTerm.app", 0, C256 | BCE),
82 t("iTerm2.app", 0, C256 | BCE | TITLE | OSC52 | ITERM2 | SYNC),
83 t("iterm", 0, C256 | BCE),
84 t("iterm2", 0, C256 | BCE | TITLE | OSC52 | ITERM2 | SYNC),
85 t("jfbterm", DIM | UL, C8 | BCE),
86 t("kitty", 0, TC | TITLE | OSC52 | KITTYKBD | SYNC),
87 t("kon", DIM | UL, C8 | BCE),
88 t("kon2", DIM | UL, C8 | BCE),
89 t("konsole", 0, C8 | BCE),
90 t("kterm", 0, C8),
91 t("linux", DIM | UL, C8 | LINUX | BCE),
92 t("mgt", 0, C8 | BCE),
93 t("mintty", 0, C8 | BCE | REP | TITLE | OSC52 | SYNC),
94 t("mlterm", 0, C8 | TITLE),
95 t("mlterm2", 0, C8 | TITLE),
96 t("mlterm3", 0, C8 | TITLE),
97 t("mrxvt", 0, C8 | RXVT | BCE | TITLE | OSC52),
98 t("pcansi", UL, C8),
99 t("putty", DIM | REV | UL, C8 | BCE),
100 t("rio", 0, TC | BCE | REP | OSC52 | SYNC),
101 t("rxvt", 0, C8 | RXVT | BCE | TITLE | OSC52),
102 t("screen", 0, C8 | TITLE | OSC52),
103 t("st", 0, C8 | BCE | OSC52),
104 t("stterm", 0, C8 | BCE | OSC52),
105 t("teken", DIM | REV, C8 | BCE),
106 t("terminator", 0, C256 | BCE | TITLE),
107 t("termite", 0, C8 | TITLE),
108 t("tmux", 0, C8 | TITLE | OSC52 | NOQUERY3), // See also: parse_dcs_query_reply()
109 t("wezterm", 0, TC | BCE | REP | TITLE | OSC52 | SYNC),
110 t("xfce", 0, C8 | BCE | TITLE),
111 // The real xterm supports ECMA-48 REP, but TERM=xterm* is used by too
112 // many other terminals to safely add it here.
113 // See also: parse_xtgettcap_reply()
114 t("xterm", 0, C8 | BCE | TITLE | OSC52),
115 t("xterm.js", 0, C8 | BCE),
116 };
117
118 static const struct {
119 const char suffix[9];
120 uint8_t suffix_len;
121 unsigned int flags; // TermFeatureFlags
122 } color_suffixes[] = {
123 {"direct", 6, TC},
124 {"256color", 8, C256},
125 {"16color", 7, C16},
126 {"mono", 4, 0},
127 {"m", 1, 0},
128 };
129
130 341 static int term_name_compare(const void *key, const void *elem)
131 {
132 341 const StringView *prefix = key;
133 341 const TermEntry *entry = elem;
134 341 size_t cmplen = MIN(prefix->length, entry->name_len);
135 341 int r = memcmp(prefix->data, entry->name, cmplen);
136
2/2
✓ Branch 0 taken 82 times.
✓ Branch 1 taken 259 times.
341 return r ? r : (int)prefix->length - entry->name_len;
137 }
138
139 18 UNITTEST {
140 18 CHECK_BSEARCH_ARRAY(terms, name, strcmp);
141 18 CHECK_STRUCT_ARRAY(color_suffixes, suffix);
142
143 // NOLINTBEGIN(bugprone-assert-side-effect)
144 18 StringView k = STRING_VIEW("xtermz");
145 18 BUG_ON(BSEARCH(&k, terms, term_name_compare));
146 18 k.length--;
147 18 BUG_ON(!BSEARCH(&k, terms, term_name_compare));
148 18 k.length--;
149 18 BUG_ON(BSEARCH(&k, terms, term_name_compare));
150 // NOLINTEND(bugprone-assert-side-effect)
151
152
2/2
✓ Branch 0 taken 918 times.
✓ Branch 1 taken 18 times.
936 for (size_t i = 0; i < ARRAYLEN(terms); i++) {
153 918 size_t len = strlen(terms[i].name);
154 918 BUG_ON(terms[i].name_len != len);
155 }
156
157
2/2
✓ Branch 0 taken 90 times.
✓ Branch 1 taken 18 times.
108 for (size_t i = 0; i < ARRAYLEN(color_suffixes); i++) {
158 90 size_t len = strlen(color_suffixes[i].suffix);
159 90 BUG_ON(color_suffixes[i].suffix_len != len);
160 }
161 18 }
162
163 // Extract the "root name" from $TERM, as defined by terminfo(5).
164 // This is the initial part of the string up to the first hyphen.
165 6 static StringView term_extract_name(const char *name, size_t len, size_t *pos)
166 {
167 6 StringView root = get_delim(name, pos, len, '-');
168
4/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
6 if (*pos >= len || !strview_equal_cstring(&root, "xterm")) {
169 3 return root;
170 }
171
172 // Skip past phony "xterm-" prefix used by certain terminals
173 3 size_t tmp = *pos;
174 3 StringView word2 = get_delim(name, &tmp, len, '-');
175
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (
176 3 strview_equal_cstring(&word2, "kitty")
177
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 || strview_equal_cstring(&word2, "termite")
178
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 || strview_equal_cstring(&word2, "ghostty")
179 ) {
180 1 *pos = tmp;
181 1 return word2;
182 }
183
184 2 return root;
185 }
186
187 6 static TermInfo term_get_info(const char *name, const char *colorterm)
188 {
189 6 TermInfo info = {
190 .features = TFLAG_8_COLOR,
191 .ncv_attrs = 0,
192 };
193
194
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
6 if (!name || name[0] == '\0') {
195 LOG_NOTICE("$TERM unset; skipping terminal info lookup");
196 return info;
197 }
198
199 6 LOG_INFO("TERM=%s", name);
200
201 6 size_t pos = 0;
202 6 size_t name_len = strlen(name);
203 6 StringView root_name = term_extract_name(name, name_len, &pos);
204
205 // Look up the root name in the list of known terminals
206 6 const TermEntry *entry = BSEARCH(&root_name, terms, term_name_compare);
207
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (entry) {
208 6 LOG_INFO("using built-in terminal info for '%s'", entry->name);
209 6 info = entry->info;
210 }
211
212
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 5 times.
6 if (colorterm) {
213
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1 if (streq(colorterm, "truecolor") || streq(colorterm, "24bit")) {
214 1 info.features |= TC;
215 1 LOG_INFO("24-bit color support detected (COLORTERM=%s)", colorterm);
216 } else if (colorterm[0] != '\0') {
217 LOG_WARNING("unknown $COLORTERM value: '%s'", colorterm);
218 }
219 }
220
221
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
6 if (info.features & TFLAG_TRUE_COLOR) {
222 2 return info;
223 }
224
225
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
4 while (pos < name_len) {
226 3 const StringView str = get_delim(name, &pos, name_len, '-');
227
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 for (size_t i = 0; i < ARRAYLEN(color_suffixes); i++) {
228 9 const char *suffix = color_suffixes[i].suffix;
229 9 size_t suffix_len = color_suffixes[i].suffix_len;
230
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 6 times.
9 if (strview_equal_strn(&str, suffix, suffix_len)) {
231 3 TermFeatureFlags flags = color_suffixes[i].flags;
232
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
3 if (flags == 0) {
233 1 info.features &= ~TC;
234 1 info.ncv_attrs = 0;
235 } else {
236 2 info.features |= flags;
237 }
238 3 LOG_INFO("color type detected from $TERM suffix '-%s'", suffix);
239 3 return info;
240 }
241 }
242 }
243
244 1 return info;
245 }
246
247 6 void term_init(Terminal *term, const char *name, const char *colorterm)
248 {
249 6 TermInfo info = term_get_info(name, colorterm);
250 6 term->features = info.features;
251 6 term->width = 80;
252 6 term->height = 24;
253 6 term->ncv_attributes = info.ncv_attrs;
254
255
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (info.features & RXVT) {
256 term->parse_input = rxvt_parse_key;
257
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 } else if (info.features & LINUX) {
258 term->parse_input = linux_parse_key;
259 } else {
260 6 term->parse_input = term_parse_sequence;
261 }
262 6 }
263
264 void term_enable_private_modes(Terminal *term)
265 {
266 TermOutputBuffer *obuf = &term->obuf;
267 TermFeatureFlags features = term->features;
268
269 // Note that changes to some of the sequences below may require
270 // corresponding updates to handle_query_reply()
271
272 if (features & TFLAG_META_ESC) {
273 term_put_literal(obuf, "\033[?1036h"); // DECSET 1036 (metaSendsEscape)
274 }
275 if (features & TFLAG_ALT_ESC) {
276 term_put_literal(obuf, "\033[?1039h"); // DECSET 1039 (altSendsEscape)
277 }
278
279 if (features & KITTYKBD) {
280 // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
281 term_put_literal(obuf, "\033[>5u");
282 } else if (features & ITERM2) {
283 // https://gitlab.com/craigbarnes/dte/-/issues/130#note_864453071
284 term_put_literal(obuf, "\033[>1u");
285 } else if (features & MOKEYS) {
286 // Try to use "modifyOtherKeys" mode (level 2 or 1)
287 term_put_literal(obuf, "\033[>4;1m\033[>4;2m");
288 }
289
290 // Try to enable bracketed paste mode. This is done unconditionally,
291 // since it should be ignored by terminals that don't recognize it
292 // and we really want to enable it for terminals that support it but
293 // are spoofing $TERM for whatever reason.
294
295 // TODO: fix term_read_bracketed_paste() to handle end delimiters
296 // that get split between 2 reads before re-enabling this
297 //term_put_literal(obuf, "\033[?2004s\033[?2004h");
298 }
299
300 void term_restore_private_modes(Terminal *term)
301 {
302 TermOutputBuffer *obuf = &term->obuf;
303 TermFeatureFlags features = term->features;
304 if (features & TFLAG_META_ESC) {
305 term_put_literal(obuf, "\033[?1036l"); // DECRST 1036 (metaSendsEscape)
306 }
307 if (features & TFLAG_ALT_ESC) {
308 term_put_literal(obuf, "\033[?1039l"); // DECRST 1039 (altSendsEscape)
309 }
310 if (features & (KITTYKBD | ITERM2)) {
311 term_put_literal(obuf, "\033[<u");
312 } else if (features & MOKEYS) {
313 term_put_literal(obuf, "\033[>4m");
314 }
315 //term_put_literal(obuf, "\033[?2004l\033[?2004r");
316 }
317
318 1 void term_restore_cursor_style(Terminal *term)
319 {
320 // TODO: Query the cursor style at startup (using DECRQSS DECSCUSR)
321 // and restore the value provided in the reply (if any), instead
322 // of using CURSOR_DEFAULT (which basically amounts to using the
323 // so-called "DECSCUSR 0 hack")
324 1 static const TermCursorStyle reset = {
325 .type = CURSOR_DEFAULT,
326 .color = COLOR_DEFAULT,
327 };
328
329 1 term_set_cursor_style(term, reset);
330 1 }
331