dte test coverage


Directory: ./
File: src/terminal/terminal.c
Date: 2025-05-08 15:05:54
Exec Total Coverage
Lines: 85 117 72.6%
Functions: 6 8 75.0%
Branches: 37 68 54.4%

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