dte test coverage


Directory: ./
File: src/terminal/feature.c
Date: 2025-09-07 23:01:39
Exec Total Coverage
Lines: 68 72 94.4%
Functions: 4 4 100.0%
Branches: 32 44 72.7%

Line Branch Exec Source
1 #include <stdint.h>
2 #include <string.h>
3 #include "feature.h"
4 #include "util/array.h"
5 #include "util/bsearch.h"
6 #include "util/debug.h"
7 #include "util/log.h"
8 #include "util/macros.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 enum {
20 // Short aliases for TermFeatureFlags:
21 BCE = TFLAG_BACK_COLOR_ERASE,
22 REP = TFLAG_ECMA48_REPEAT,
23 TITLE = TFLAG_SET_WINDOW_TITLE,
24 RXVT = TFLAG_RXVT,
25 LINUX = TFLAG_LINUX,
26 OSC52 = TFLAG_OSC52_COPY,
27 KITTYKBD = TFLAG_KITTY_KEYBOARD,
28 MOKEYS = TFLAG_MODIFY_OTHER_KEYS,
29 SYNC = TFLAG_SYNC,
30 NOQUERY1 = TFLAG_NO_QUERY_L1,
31 NOQUERY3 = TFLAG_NO_QUERY_L3,
32 BSCTRL = TFLAG_BS_CTRL_BACKSPACE, // Only useful if not superseded by KITTYKBD
33 DELCTRL = TFLAG_DEL_CTRL_BACKSPACE, // Only useful if not superseded by KITTYKBD
34 C8 = TFLAG_8_COLOR,
35 C16 = TFLAG_16_COLOR | C8,
36 C256 = TFLAG_256_COLOR | C16,
37 TC = TFLAG_TRUE_COLOR | C256,
38 NCVUL = TFLAG_NCV_UNDERLINE,
39 NCVREV = TFLAG_NCV_REVERSE,
40 NCVDIM = TFLAG_NCV_DIM,
41 };
42
43 #define t(tname, feat) { \
44 .name = tname, \
45 .name_len = STRLEN(tname), \
46 .features = (TermFeatureFlags)feat, \
47 }
48
49 static const TermEntry terms[] = {
50 t("Eterm", C8 | BCE),
51 t("alacritty", TC | BCE | REP | OSC52 | SYNC),
52 t("ansi", C8 | NCVUL),
53 t("ansiterm", 0),
54 t("aterm", C8 | BCE),
55 t("contour", TC | BCE | REP | TITLE | OSC52 | SYNC),
56 t("cx", C8),
57 t("cx100", C8),
58 t("cygwin", C8),
59 t("cygwinB19", C8 | NCVUL),
60 t("cygwinDBG", C8 | NCVUL),
61 t("decansi", C8),
62 t("domterm", C8 | BCE),
63 t("dtterm", C8),
64 t("dvtm", C8 | BSCTRL),
65 t("fbterm", C256 | BCE | NCVUL | NCVDIM),
66 t("foot", TC | BCE | REP | TITLE | OSC52 | KITTYKBD | SYNC),
67 t("ghostty", TC | BCE | REP | TITLE | OSC52 | KITTYKBD | SYNC),
68 t("hurd", C8 | BCE | NCVUL | NCVDIM),
69 t("iTerm.app", C256 | BCE),
70 t("iTerm2.app", C256 | BCE | TITLE | OSC52 | SYNC),
71 t("iterm", C256 | BCE),
72 t("iterm2", C256 | BCE | TITLE | OSC52 | SYNC),
73 t("jfbterm", C8 | BCE | NCVUL | NCVDIM),
74 t("kitty", TC | TITLE | OSC52 | KITTYKBD | SYNC),
75 t("kon", C8 | BCE | NCVUL | NCVDIM),
76 t("kon2", C8 | BCE | NCVUL | NCVDIM),
77 t("konsole", C8 | BCE),
78 t("kterm", C8),
79 t("linux", C8 | LINUX | BCE | NCVUL | NCVDIM),
80 t("mgt", C8 | BCE),
81 t("mintty", C8 | BCE | REP | TITLE | OSC52 | SYNC),
82 t("mlterm", C8 | TITLE),
83 t("mlterm2", C8 | TITLE),
84 t("mlterm3", C8 | TITLE),
85 t("mrxvt", C8 | RXVT | BCE | TITLE | OSC52),
86 t("pcansi", C8 | NCVUL),
87 t("putty", C8 | BCE | NCVUL | NCVDIM | NCVREV), // TODO: BSCTRL?
88 t("rio", TC | BCE | REP | OSC52 | SYNC),
89 t("rxvt", C8 | RXVT | BCE | TITLE | OSC52 | BSCTRL),
90 t("screen", C8 | TITLE | OSC52),
91 t("st", C8 | BCE | OSC52 | BSCTRL),
92 t("stterm", C8 | BCE | OSC52),
93 t("teken", C8 | BCE | NCVDIM | NCVREV),
94 t("terminator", C256 | BCE | TITLE | BSCTRL),
95 t("termite", C8 | TITLE),
96 t("tmux", C8 | TITLE | OSC52 | NOQUERY3 | BSCTRL), // See also: parse_dcs_query_reply()
97 t("vt220", NOQUERY1), // Used by cu(1) and picocom(1), which wrongly handle queries
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 447 static int term_name_compare(const void *key, const void *elem)
120 {
121 447 const StringView *prefix = key;
122 447 const TermEntry *entry = elem;
123 447 size_t cmplen = MIN(prefix->length, entry->name_len);
124 447 int r = memcmp(prefix->data, entry->name, cmplen);
125
2/2
✓ Branch 0 (2→3) taken 107 times.
✓ Branch 1 (2→4) taken 340 times.
447 return r ? r : (int)prefix->length - entry->name_len;
126 }
127
128 24 UNITTEST {
129 24 CHECK_BSEARCH_ARRAY(terms, name);
130 24 CHECK_STRUCT_ARRAY(color_suffixes, suffix);
131
132 // NOLINTBEGIN(bugprone-assert-side-effect)
133 24 StringView k = STRING_VIEW("xtermz");
134 24 BUG_ON(BSEARCH(&k, terms, term_name_compare));
135 24 k.length--;
136 24 BUG_ON(!BSEARCH(&k, terms, term_name_compare));
137 24 k.length--;
138 24 BUG_ON(BSEARCH(&k, terms, term_name_compare));
139 // NOLINTEND(bugprone-assert-side-effect)
140
141
2/2
✓ Branch 0 (19→13) taken 1248 times.
✓ Branch 1 (19→23) taken 24 times.
1272 for (size_t i = 0; i < ARRAYLEN(terms); i++) {
142 1248 const char *name = terms[i].name;
143 1248 size_t len = strlen(name);
144 1248 BUG_ON(terms[i].name_len != len);
145 1248 TermFeatureFlags imode_flags = KITTYKBD | BSCTRL | DELCTRL;
146 1248 TermFeatureFlags masked = terms[i].features & imode_flags;
147
3/4
✓ Branch 0 (15→16) taken 216 times.
✓ Branch 1 (15→18) taken 1032 times.
✗ Branch 2 (16→17) not taken.
✓ Branch 3 (16→18) taken 216 times.
1248 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 120 times.
✓ Branch 1 (23→24) taken 24 times.
144 for (size_t i = 0; i < ARRAYLEN(color_suffixes); i++) {
153 120 size_t len = strlen(color_suffixes[i].suffix);
154 120 BUG_ON(color_suffixes[i].suffix_len != len);
155 }
156 24 }
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 7 static StringView term_extract_name(const char *name, size_t len, size_t *pos)
161 {
162 7 StringView root = get_delim(name, pos, len, '-');
163
4/4
✓ Branch 0 (3→4) taken 4 times.
✓ Branch 1 (3→6) taken 3 times.
✓ Branch 2 (5→6) taken 1 times.
✓ Branch 3 (5→7) taken 3 times.
7 if (*pos >= len || !strview_equal_cstring(root, "xterm")) {
164 4 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 7 TermFeatureFlags term_get_features(const char *name, const char *colorterm)
183 {
184 7 TermFeatureFlags features = TFLAG_8_COLOR;
185
2/4
✓ Branch 0 (2→3) taken 7 times.
✗ Branch 1 (2→4) not taken.
✗ Branch 2 (3→4) not taken.
✓ Branch 3 (3→6) taken 7 times.
7 if (!name || name[0] == '\0') {
186 LOG_NOTICE("$TERM unset; skipping terminal info lookup");
187 return features;
188 }
189
190 7 LOG_INFO("TERM=%s", name);
191
192 7 size_t pos = 0;
193 7 size_t name_len = strlen(name);
194 7 StringView root_name = term_extract_name(name, name_len, &pos);
195
196 // Look up the root name in the list of known terminals
197 7 const TermEntry *entry = BSEARCH(&root_name, terms, term_name_compare);
198
1/2
✓ Branch 0 (9→10) taken 7 times.
✗ Branch 1 (9→12) not taken.
7 if (entry) {
199 7 LOG_INFO("using built-in terminal info for '%s'", entry->name);
200 7 features = entry->features;
201 }
202
203
2/2
✓ Branch 0 (12→13) taken 1 times.
✓ Branch 1 (12→18) taken 6 times.
7 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 5 times.
7 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 2 times.
5 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(str, string_view(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