dte test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 50.0% high: ≥ 85.0%
Coverage Exec / Excl / Total
Lines: 69.7% 69 / 2 / 101
Functions: 80.0% 4 / 0 / 5
Branches: 46.4% 32 / 10 / 79

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