dte test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 50.0% high: ≥ 85.0%
Coverage Exec / Excl / Total
Lines: 69.6% 71 / 2 / 104
Functions: 80.0% 4 / 0 / 5
Branches: 44.0% 33 / 10 / 85

src/terminal/feature.c
Line Branch Exec Source
1 #include <stdint.h>
2 #include <string.h>
3 #include "feature.h"
4 #include "util/array.h"
5 #include "util/bit.h"
6 #include "util/bsearch.h"
7 #include "util/debug.h"
8 #include "util/log.h"
9 #include "util/str-util.h"
10 #include "util/string-view.h"
11 #include "util/xstring.h"
12
13 typedef struct {
14 const char name[11];
15 uint8_t name_len;
16 TermFeatureFlags features;
17 } TermEntry;
18
19 // Short aliases for TermFeatureFlags.
20 // See also: term_feature_to_str() and the UNITTEST{} block below.
21 enum {
22 BCE = TFLAG_BACK_COLOR_ERASE,
23 REP = TFLAG_ECMA48_REPEAT,
24 TITLE = TFLAG_SET_WINDOW_TITLE,
25 RXVT = TFLAG_RXVT, // Mutually exclusive with LINUX and KITTYKBD
26 LINUX = TFLAG_LINUX, // Mutually exclusive with RXVT and KITTYKBD
27 OSC52 = TFLAG_OSC52_COPY,
28 KITTYKBD = TFLAG_KITTY_KEYBOARD, // Mutually exclusive with RXVT, LINUX, DELCTRL and BSCTRL
29 SYNC = TFLAG_SYNC,
30 NOQUERY1 = TFLAG_NO_QUERY_L1, // Mutually exclusive with NOQUERY3
31 NOQUERY3 = TFLAG_NO_QUERY_L3, // Mutually exclusive with NOQUERY1
32 C8 = TFLAG_8_COLOR,
33 C16 = TFLAG_16_COLOR | C8,
34 C256 = TFLAG_256_COLOR | C16,
35 TC = TFLAG_TRUE_COLOR | C256,
36 DELCTRL = TFLAG_DEL_CTRL_BACKSPACE, // Mutually exclusive with BSCTRL and KITTYKBD
37 BSCTRL = TFLAG_BS_CTRL_BACKSPACE, // Mutually exclusive with DELCTRL and KITTYKBD
38 NCVUL = TFLAG_NCV_UNDERLINE,
39 NCVDIM = TFLAG_NCV_DIM,
40 NCVREV = TFLAG_NCV_REVERSE,
41
42 // Query-only flags (not used in terms[] entries)
43 METAESC = TFLAG_META_ESC,
44 ALTESC = TFLAG_ALT_ESC,
45 QUERY2 = TFLAG_QUERY_L2,
46 QUERY3 = TFLAG_QUERY_L3,
47 MOKEYS = TFLAG_MODIFY_OTHER_KEYS,
48 QUERY_ONLY_FFLAGS = METAESC | ALTESC | QUERY2 | QUERY3 | MOKEYS,
49 };
50
51 const char *term_feature_to_str(TermFeatureFlags flag)
52 {
53 // This function only handles single flags. Values with multiple
54 // bits set should be handled as appropriate, by the caller.
55 WARN_ON(!IS_POWER_OF_2(flag));
56
57 switch ((unsigned int)flag) {
58 case TFLAG_BACK_COLOR_ERASE: return "BCE";
59 case TFLAG_ECMA48_REPEAT: return "REP";
60 case TFLAG_SET_WINDOW_TITLE: return "TITLE";
61 case TFLAG_RXVT: return "RXVT";
62 case TFLAG_LINUX: return "LINUX";
63 case TFLAG_OSC52_COPY: return "OSC52";
64 case TFLAG_META_ESC: return "METAESC";
65 case TFLAG_ALT_ESC: return "ALTESC";
66 case TFLAG_KITTY_KEYBOARD: return "KITTYKBD";
67 case TFLAG_SYNC: return "SYNC";
68 case TFLAG_QUERY_L2: return "QUERY2";
69 case TFLAG_QUERY_L3: return "QUERY3";
70 case TFLAG_NO_QUERY_L1: return "NOQUERY1";
71 case TFLAG_NO_QUERY_L3: return "NOQUERY3";
72 case TFLAG_8_COLOR: return "C8";
73 case TFLAG_16_COLOR: return "C16";
74 case TFLAG_256_COLOR: return "C256";
75 case TFLAG_TRUE_COLOR: return "TC";
76 case TFLAG_MODIFY_OTHER_KEYS: return "MOKEYS";
77 case TFLAG_DEL_CTRL_BACKSPACE: return "DELCTRL";
78 case TFLAG_BS_CTRL_BACKSPACE: return "BSCTRL";
79 case TFLAG_NCV_UNDERLINE: return "NCVUL";
80 case TFLAG_NCV_DIM: return "NCVDIM";
81 case TFLAG_NCV_REVERSE: return "NCVREV";
82 }
83
84 return "??";
85 }
86
87 #define t(tname, feat) { \
88 .name = tname, \
89 .name_len = STRLEN(tname), \
90 .features = (TermFeatureFlags)feat, \
91 }
92
93 static const TermEntry terms[] = {
94 t("Eterm", C8 | BCE),
95 t("alacritty", TC | BCE | REP | OSC52 | SYNC),
96 t("ansi", C8 | NCVUL),
97 t("ansiterm", 0),
98 t("aterm", C8 | BCE),
99 t("contour", TC | BCE | REP | TITLE | OSC52 | SYNC),
100 t("cx", C8),
101 t("cx100", C8),
102 t("cygwin", C8),
103 t("cygwinB19", C8 | NCVUL),
104 t("cygwinDBG", C8 | NCVUL),
105 t("decansi", C8),
106 t("domterm", C8 | BCE),
107 t("dtterm", C8),
108 t("dvtm", C8 | BSCTRL),
109 t("fbterm", C256 | BCE | NCVUL | NCVDIM),
110 t("foot", TC | BCE | REP | TITLE | OSC52 | KITTYKBD | SYNC),
111 t("ghostty", TC | BCE | REP | TITLE | OSC52 | KITTYKBD | SYNC),
112 t("hurd", C8 | BCE | NCVUL | NCVDIM),
113 t("iTerm.app", C256 | BCE),
114 t("iTerm2.app", C256 | BCE | TITLE | OSC52 | SYNC),
115 t("iterm", C256 | BCE),
116 t("iterm2", C256 | BCE | TITLE | OSC52 | SYNC),
117 t("jfbterm", C8 | BCE | NCVUL | NCVDIM),
118 t("kitty", TC | TITLE | OSC52 | KITTYKBD | SYNC),
119 t("kon", C8 | BCE | NCVUL | NCVDIM),
120 t("kon2", C8 | BCE | NCVUL | NCVDIM),
121 t("konsole", C8 | BCE),
122 t("kterm", C8),
123 t("linux", C8 | LINUX | BCE | NCVUL | NCVDIM),
124 t("mgt", C8 | BCE),
125 t("mintty", C8 | BCE | REP | TITLE | OSC52 | SYNC),
126 t("mlterm", C8 | TITLE),
127 t("mlterm2", C8 | TITLE),
128 t("mlterm3", C8 | TITLE),
129 t("mrxvt", C8 | RXVT | BCE | TITLE | OSC52),
130 t("pcansi", C8 | NCVUL),
131 t("putty", C8 | BCE | NCVUL | NCVDIM | NCVREV), // TODO: BSCTRL?
132 t("rio", TC | BCE | REP | OSC52 | SYNC),
133 t("rxvt", C8 | RXVT | BCE | TITLE | OSC52 | BSCTRL),
134 t("screen", C8 | TITLE | OSC52),
135 t("st", C8 | BCE | OSC52 | BSCTRL),
136 t("stterm", C8 | BCE | OSC52),
137 t("teken", C8 | BCE | NCVDIM | NCVREV),
138 t("terminator", C256 | BCE | TITLE | BSCTRL),
139 t("termite", C8 | TITLE),
140 t("tmux", C8 | TITLE | OSC52 | NOQUERY3 | BSCTRL), // See also: parse_xtversion_reply()
141 t("vt220", NOQUERY1), // Used by cu(1) and picocom(1), which wrongly handle queries
142 t("wezterm", TC | BCE | REP | TITLE | OSC52 | SYNC | BSCTRL), // See also: parse_xtversion_reply()
143 t("xfce", C8 | BCE | TITLE),
144 // The real xterm supports ECMA-48 REP, but TERM=xterm* is used by too
145 // many other terminals to safely add it here.
146 // See also: parse_xtgettcap_reply()
147 t("xterm", C8 | BCE | TITLE | OSC52),
148 t("xterm.js", C8 | BCE),
149 };
150
151 static const struct {
152 const char suffix[9];
153 uint8_t suffix_len;
154 unsigned int flags; // TermFeatureFlags
155 } color_suffixes[] = {
156 {"direct", 6, TC},
157 {"256color", 8, C256},
158 {"16color", 7, C16},
159 {"mono", 4, 0},
160 {"m", 1, 0},
161 };
162
163 447 static int term_name_compare(const void *key, const void *elem)
164 {
165 447 const StringView *prefix = key;
166 447 const TermEntry *entry = elem;
167 447 size_t cmplen = MIN(prefix->length, entry->name_len);
168 447 int r = memcmp(prefix->data, entry->name, cmplen);
169
2/2
✓ Branch 2 → 3 taken 107 times.
✓ Branch 2 → 4 taken 340 times.
447 return r ? r : (int)prefix->length - entry->name_len;
170 }
171
172 24 UNITTEST {
173 24 CHECK_BSEARCH_ARRAY(terms, name);
174 24 CHECK_STRUCT_ARRAY(color_suffixes, suffix);
175
176 // NOLINTBEGIN(bugprone-assert-side-effect)
177 24 StringView k = STRING_VIEW("xtermz");
178 24 BUG_ON(BSEARCH(&k, terms, term_name_compare));
179 24 k.length--;
180 24 BUG_ON(!BSEARCH(&k, terms, term_name_compare));
181 24 k.length--;
182 24 BUG_ON(BSEARCH(&k, terms, term_name_compare));
183 // NOLINTEND(bugprone-assert-side-effect)
184
185 // Each terms[] entry should have at most 1 flag in any of these sets
186 static const TermFeatureFlags mutually_exclusive_flags[] = {
187 (KITTYKBD | BSCTRL | DELCTRL),
188 (KITTYKBD | LINUX | RXVT),
189 (NOQUERY1 | NOQUERY3),
190 };
191
192
2/2
✓ Branch 22 → 13 taken 1248 times.
✓ Branch 22 → 26 taken 24 times.
1272 for (size_t i = 0; i < ARRAYLEN(terms); i++) {
193 1248 const char *name = terms[i].name;
194 1248 size_t len = strlen(name);
195 1248 BUG_ON(terms[i].name_len != len);
196
197 1248 TermFeatureFlags features = terms[i].features;
198
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 20 taken 1248 times.
1248 if (features & QUERY_ONLY_FFLAGS) {
199 BUG("TermEntry '%s' has query-only flags", name);
200 }
201
202
2/2
✓ Branch 20 → 17 taken 3744 times.
✓ Branch 20 → 21 taken 1248 times.
4992 for (size_t j = 0; j < ARRAYLEN(mutually_exclusive_flags); j++) {
203 3744 TermFeatureFlags flag_union = features & mutually_exclusive_flags[j];
204 3744 unsigned int count = u32_popcount(flag_union);
205
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 3744 times.
3744 if (count > 1) {
206 BUG (
207 "TermEntry '%s' has %u mutually exclusive flags: 0x%x",
208 name, count, (unsigned int)flag_union
209 );
210 }
211 }
212 }
213
214
2/2
✓ Branch 26 → 23 taken 120 times.
✓ Branch 26 → 27 taken 24 times.
144 for (size_t i = 0; i < ARRAYLEN(color_suffixes); i++) {
215 120 size_t len = strlen(color_suffixes[i].suffix);
216 120 BUG_ON(color_suffixes[i].suffix_len != len);
217 }
218 24 }
219
220 // Extract the "root name" from $TERM, as defined by terminfo(5).
221 // This is the initial part of the string up to the first hyphen.
222 7 static StringView term_extract_name(const char *name, size_t len, size_t *pos)
223 {
224 7 StringView root = get_delim(name, pos, len, '-');
225
4/4
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 6 taken 3 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 3 times.
7 if (*pos >= len || !strview_equal_cstring(root, "xterm")) {
226 4 return root;
227 }
228
229 // Skip past phony "xterm-" prefix used by certain terminals
230 3 size_t tmp = *pos;
231 3 StringView word2 = get_delim(name, &tmp, len, '-');
232
2/2
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 14 taken 1 time.
3 if (
233 3 strview_equal_cstring(word2, "kitty")
234
1/2
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 14 not taken.
2 || strview_equal_cstring(word2, "termite")
235
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 2 times.
2 || strview_equal_cstring(word2, "ghostty")
236 ) {
237 1 *pos = tmp;
238 1 return word2;
239 }
240
241 2 return root;
242 }
243
244 7 TermFeatureFlags term_get_features(const char *name, const char *colorterm)
245 {
246 7 TermFeatureFlags features = TFLAG_8_COLOR;
247
2/4
✓ Branch 2 → 3 taken 7 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 7 times.
7 if (!name || name[0] == '\0') {
248 LOG_NOTICE("$TERM unset; skipping terminal info lookup");
249 return features;
250 }
251
252 7 LOG_INFO("TERM=%s", name);
253
254 7 size_t pos = 0;
255 7 size_t name_len = strlen(name);
256 7 StringView root_name = term_extract_name(name, name_len, &pos);
257
258 // Look up the root name in the list of known terminals
259 7 const TermEntry *entry = BSEARCH(&root_name, terms, term_name_compare);
260
1/2
✓ Branch 9 → 10 taken 7 times.
✗ Branch 9 → 12 not taken.
7 if (entry) {
261 7 LOG_INFO("using built-in terminal info for '%s'", entry->name);
262 7 features = entry->features;
263 }
264
265
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 18 taken 6 times.
7 if (colorterm) {
266
1/4
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 1 time.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
1 if (streq(colorterm, "truecolor") || streq(colorterm, "24bit")) {
267 1 features |= TC;
268 1 LOG_INFO("24-bit color support detected (COLORTERM=%s)", colorterm);
269 } else if (colorterm[0] != '\0') {
270 LOG_WARNING("unknown $COLORTERM value: '%s'", colorterm);
271 }
272 }
273
274
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 32 taken 5 times.
7 if (features & TFLAG_TRUE_COLOR) {
275 2 return features;
276 }
277
278
2/2
✓ Branch 32 → 20 taken 3 times.
✓ Branch 32 → 33 taken 2 times.
5 while (pos < name_len) {
279 3 const StringView str = get_delim(name, &pos, name_len, '-');
280
1/2
✓ Branch 30 → 22 taken 9 times.
✗ Branch 30 → 31 not taken.
9 for (size_t i = 0; i < ARRAYLEN(color_suffixes); i++) {
281 9 const char *suffix = color_suffixes[i].suffix;
282 9 size_t suffix_len = color_suffixes[i].suffix_len;
283
2/2
✓ Branch 23 → 24 taken 3 times.
✓ Branch 23 → 29 taken 6 times.
9 if (strview_equal(str, string_view(suffix, suffix_len))) {
284 3 TermFeatureFlags color_features = color_suffixes[i].flags;
285
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 2 times.
3 if (color_features == 0) {
286 1 features &= ~(TC | C256 | C16 | C8 | NCVUL | NCVREV | NCVDIM);
287 } else {
288 2 features |= color_features;
289 }
290 3 LOG_INFO("color type detected from $TERM suffix '-%s'", suffix);
291 3 return features;
292 }
293 }
294 }
295
296 return features;
297 }
298