dte test coverage


Directory: ./
File: src/terminal/terminal.c
Date: 2025-02-14 16:55:22
Exec Total Coverage
Lines: 85 119 71.4%
Functions: 6 8 75.0%
Branches: 37 70 52.9%

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