dte test coverage


Directory: ./
File: src/terminal/feature.c
Date: 2026-01-09 16:07:09
Coverage Exec Excl Total
Lines: 94.7% 71 2 77
Functions: 100.0% 4 0 4
Branches: 71.7% 33 0 46

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