dte test coverage


Directory: ./
File: src/terminal/output.c
Date: 2026-01-27 12:16:02
Coverage Exec Excl Total
Lines: 64.7% 266 0 411
Functions: 70.0% 28 0 40
Branches: 45.3% 86 0 190

Line Branch Exec Source
1 // Terminal output and control functions.
2 // Copyright © Craig Barnes.
3 // Copyright © Timo Hirvonen.
4 // SPDX-License-Identifier: GPL-2.0-only
5 // See also:
6 // • ECMA-48 5th edition, §8.3 (CUP, ED, EL, REP, SGR):
7 // https://ecma-international.org/publications-and-standards/standards/ecma-48/
8 // • DEC Manual EK-VT510-RM, Chapter 5 (DECRQM, DECRQSS, DECSCUSR, DECTCEM):
9 // https://vt100.net/docs/vt510-rm/contents.html
10 // • XTerm's ctlseqs.html (XTWINOPS, XTQMODKEYS, XTGETTCAP, OSC 12, OSC 112):
11 // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
12 // • Kitty's keyboard protocol documentation (CSI ? u):
13 // https://sw.kovidgoyal.net/kitty/keyboard-protocol/
14
15 #include <limits.h>
16 #include <stdint.h>
17 #include <string.h>
18 #include <unistd.h>
19 #include "output.h"
20 #include "color.h"
21 #include "cursor.h"
22 #include "indent.h"
23 #include "options.h"
24 #include "util/ascii.h"
25 #include "util/bit.h"
26 #include "util/debug.h"
27 #include "util/log.h"
28 #include "util/numtostr.h"
29 #include "util/str-util.h"
30 #include "util/utf8.h"
31 #include "util/xreadwrite.h"
32
33 62 char *term_output_reserve_space(TermOutputBuffer *obuf, size_t count)
34 {
35 62 BUG_ON(count > TERM_OUTBUF_SIZE);
36 62 BUG_ON(obuf->count > TERM_OUTBUF_SIZE);
37
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 62 times.
62 if (unlikely(obuf_avail(obuf) < count)) {
38 term_output_flush(obuf);
39 }
40 62 return obuf->buf + obuf->count;
41 }
42
43 8 void term_output_reset(Terminal *term, size_t start_x, size_t width, size_t scroll_x)
44 {
45 8 TermOutputBuffer *obuf = &term->obuf;
46 8 obuf->x = 0;
47 8 obuf->width = width;
48 8 obuf->scroll_x = scroll_x;
49 8 obuf->tab_width = 8;
50 8 obuf->tab_mode = TAB_CONTROL;
51 8 obuf->can_clear = ((start_x + width) == term->width);
52 8 }
53
54 // Write directly to the terminal, as done when e.g. flushing the output buffer
55 static bool term_direct_write(const char *str, size_t count)
56 {
57 bool ok = (xwrite_all(STDOUT_FILENO, str, count) == count);
58 LOG_ERRNO_ON(!ok, "write");
59 return ok;
60 }
61
62 // NOTE: does not update `obuf.x`; see term_put_byte()
63 24 void term_put_bytes(TermOutputBuffer *obuf, const char *str, size_t count)
64 {
65
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 8 taken 24 times.
24 if (unlikely(count >= TERM_OUTBUF_SIZE)) {
66 term_output_flush(obuf);
67 if (term_direct_write(str, count)) {
68 LOG_INFO("writing %zu bytes directly to terminal", count);
69 }
70 return;
71 }
72
73 24 char *buf = term_output_reserve_space(obuf, count);
74 24 obuf->count += count;
75 24 memcpy(buf, str, count);
76 }
77
78 4 static void term_repeat_byte(TermOutputBuffer *obuf, char ch, size_t count)
79 {
80
1/2
✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 5 not taken.
4 if (likely(count <= TERM_OUTBUF_SIZE)) {
81 // Repeat count fits in buffer; reserve space and tail-call memset(3)
82 4 char *buf = term_output_reserve_space(obuf, count);
83 4 obuf->count += count;
84 4 memset(buf, ch, count);
85 4 return;
86 }
87
88 // Repeat count greater than buffer size; fill buffer with `ch` and call
89 // write() repeatedly until `count` reaches zero
90 term_output_flush(obuf);
91 memset(obuf->buf, ch, TERM_OUTBUF_SIZE);
92 while (count) {
93 size_t n = MIN(count, TERM_OUTBUF_SIZE);
94 count -= n;
95 term_direct_write(obuf->buf, n);
96 }
97 }
98
99 4 static bool ecma48_repeat_byte(TermOutputBuffer *obuf, char ch, size_t count)
100 {
101
4/4
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 2 times.
4 if (!ascii_isprint(ch) || count < ECMA48_REP_MIN || count > ECMA48_REP_MAX) {
102 2 term_repeat_byte(obuf, ch, count);
103 2 return false;
104 }
105
106 // ECMA-48 REP (CSI Pn b)
107 2 static_assert(ECMA48_REP_MAX == 30000);
108 2 const size_t maxlen = STRLEN("_E[30000b");
109 2 char *buf = term_output_reserve_space(obuf, maxlen);
110 2 size_t i = 0;
111 2 buf[i++] = ch;
112 2 buf[i++] = '\033';
113 2 buf[i++] = '[';
114 2 i += buf_uint_to_str(count - 1, buf + i);
115 2 buf[i++] = 'b';
116 2 BUG_ON(i > maxlen);
117 2 obuf->count += i;
118 2 return true;
119 }
120
121 6 TermSetBytesMethod term_set_bytes(Terminal *term, char ch, size_t count)
122 {
123 6 TermOutputBuffer *obuf = &term->obuf;
124
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 6 times.
6 if (obuf->x + count > obuf->scroll_x + obuf->width) {
125 count = obuf->scroll_x + obuf->width - obuf->x;
126 }
127
128 6 ssize_t skip = obuf->scroll_x - obuf->x;
129
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 6 times.
6 if (skip > 0) {
130 skip = MIN(skip, count);
131 obuf->x += skip;
132 count -= skip;
133 }
134
135 6 obuf->x += count;
136
2/2
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 9 taken 2 times.
6 if (term->features & TFLAG_ECMA48_REPEAT) {
137 4 bool used_rep = ecma48_repeat_byte(obuf, ch, count);
138 4 return used_rep ? TERM_SET_BYTES_REP : TERM_SET_BYTES_MEMSET;
139 }
140
141 2 term_repeat_byte(obuf, ch, count);
142 2 return TERM_SET_BYTES_MEMSET;
143 }
144
145 // Append a single byte to the buffer.
146 // NOTE: this does not update `obuf.x`, since it can be used to write
147 // bytes within escape sequences without advancing the cursor position.
148 void term_put_byte(TermOutputBuffer *obuf, char ch)
149 {
150 char *buf = term_output_reserve_space(obuf, 1);
151 buf[0] = ch;
152 obuf->count++;
153 }
154
155 2 void term_put_str(TermOutputBuffer *obuf, const char *str)
156 {
157 2 size_t i = 0;
158
2/2
✓ Branch 7 → 3 taken 14 times.
✓ Branch 7 → 8 taken 1 time.
15 while (str[i]) {
159
2/2
✓ Branch 5 → 6 taken 13 times.
✓ Branch 5 → 8 taken 1 time.
14 if (!term_put_char(obuf, u_str_get_char(str, &i))) {
160 break;
161 }
162 }
163 2 }
164
165 /*
166 * See also:
167 * • handle_query_reply()
168 * • TFLAG_QUERY_L2
169 * • parse_csi_query_reply()
170 * • parse_dcs_query_reply()
171 * • parse_xtwinops_query_reply()
172 */
173 1 static void term_put_level_2_queries(Terminal *term, bool emit_all)
174 {
175 1 static const char queries[] =
176 "\033[>0q" // XTVERSION (terminal name and version)
177 "\033[>c" // DA2 (Secondary Device Attributes)
178 "\033[?u" // Kitty keyboard protocol flags
179 "\033[?1036$p" // DECRQM 1036 (metaSendsEscape; must be after kitty query)
180 "\033[?1039$p" // DECRQM 1039 (altSendsEscape; must be after kitty query)
181 ;
182
183 1 static const char debug_queries[] =
184 "\033[?4m" // XTQMODKEYS 4 (xterm modifyOtherKeys mode)
185 "\033[?7$p" // DECRQM 7 (DECAWM; auto-wrap mode)
186 "\033[?25$p" // DECRQM 25 (DECTCEM; cursor visibility)
187 "\033[?45$p" // DECRQM 45 (XTREVWRAP; reverse-wraparound mode)
188 "\033[?67$p" // DECRQM 67 (DECBKM; backspace key sends BS)
189 "\033[?1049$p" // DECRQM 1049 (alternate screen buffer)
190 "\033[?2004$p" // DECRQM 2004 (bracketed paste)
191 "\033[18t" // XTWINOPS 18 (text area size in "characters"/cells)
192 ;
193
194 1 TermOutputBuffer *obuf = &term->obuf;
195 1 LOG_INFO("sending level 2 queries to terminal");
196 1 term_put_bytes(obuf, queries, sizeof(queries) - 1);
197
198
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
1 TermFeatureFlags features = emit_all ? 0 : term->features;
199 if (!(features & TFLAG_SYNC)) {
200 1 term_put_literal(obuf, "\033[?2026$p"); // DECRQM 2026
201 }
202
203 // Debug query responses are used purely for logging/informational purposes
204
1/4
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 1 time.
✗ Branch 9 → 10 not taken.
✗ Branch 9 → 11 not taken.
1 if (emit_all || log_level_debug_enabled()) {
205 1 term_put_bytes(obuf, debug_queries, sizeof(debug_queries) - 1);
206 }
207 1 }
208
209 /*
210 * Some terminals fail to parse DCS sequences in accordance with ECMA-48,
211 * so DCS queries are sent separately and only after probing for some
212 * known problem cases (e.g. PuTTY).
213 *
214 * See also:
215 * • handle_query_reply()
216 * • TFLAG_QUERY_L3
217 * • parse_dcs_query_reply()
218 * • parse_xtgettcap_reply()
219 * • handle_decrqss_sgr_reply()
220 */
221 1 static void term_put_level_3_queries(Terminal *term, bool emit_all)
222 {
223 // Note: the correct (according to ISO 8613-6) format for the SGR
224 // sequence here would be "\033[0;38:2::60:70:80;48:5:255m", but we
225 // use the standards-incorrect (but de facto more widely supported)
226 // format because that's what is actually used in term_set_style()
227 1 static const char sgr_query[] =
228 "\033[0;38;2;60;70;80;48;5;255m" // SGR with direct fg and indexed bg
229 "\033P$qm\033\\" // DECRQSS SGR (check support for SGR params above)
230 "\033[0m" // SGR 0
231 ;
232
233 1 static const char debug_queries[] =
234 "\033P+q71756572792d6f732d6e616d65\033\\" // XTGETTCAP "query-os-name"
235 "\033P$q q\033\\" // DECRQSS DECSCUSR (cursor style)
236 ;
237
238 1 TermOutputBuffer *obuf = &term->obuf;
239
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 TermFeatureFlags features = emit_all ? 0 : term->features;
240 1 LOG_INFO("sending level 3 queries to terminal");
241
242
1/2
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 7 not taken.
1 if (!(features & TFLAG_TRUE_COLOR)) {
243 1 term_put_bytes(obuf, sgr_query, sizeof(sgr_query) - 1);
244 }
245
246
1/2
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 9 not taken.
1 if (!(features & TFLAG_BACK_COLOR_ERASE)) {
247 1 term_put_literal(obuf, "\033P+q626365\033\\"); // XTGETTCAP "bce"
248 }
249
1/2
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 11 not taken.
1 if (!(features & TFLAG_ECMA48_REPEAT)) {
250 1 term_put_literal(obuf, "\033P+q726570\033\\"); // XTGETTCAP "rep"
251 }
252
1/2
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 13 not taken.
1 if (!(features & TFLAG_SET_WINDOW_TITLE)) {
253 1 term_put_literal(obuf, "\033P+q74736C\033\\"); // XTGETTCAP "tsl"
254 }
255
1/2
✓ Branch 13 → 14 taken 1 time.
✗ Branch 13 → 15 not taken.
1 if (!(features & TFLAG_OSC52_COPY)) {
256 1 term_put_literal(obuf, "\033P+q4D73\033\\"); // XTGETTCAP "Ms"
257 }
258
259 // Debug query responses are used purely for logging/informational purposes
260
1/4
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 18 taken 1 time.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
1 if (emit_all || log_level_debug_enabled()) {
261 1 term_put_bytes(obuf, debug_queries, sizeof(debug_queries) - 1);
262 }
263 1 }
264
265 /*
266 * See also:
267 * • handle_query_reply()
268 * • parse_csi_query_reply()
269 * • https://vt100.net/docs/vt510-rm/DA1.html
270 * • ECMA-48 §8.3.24
271 */
272 2 void term_put_initial_queries(Terminal *term, unsigned int level)
273 {
274
2/4
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 13 not taken.
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 13 not taken.
2 if (level < 1 || (term->features & TFLAG_NO_QUERY_L1)) {
275 return;
276 }
277
278 2 LOG_INFO("sending level 1 queries to terminal");
279 2 term_put_literal(&term->obuf, "\033[c"); // ECMA-48 DA (AKA "DA1")
280
281
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 13 taken 1 time.
2 if (level < 2) {
282 return;
283 }
284
285 // Level 6 or greater means emit all query levels and also emit even
286 // conditional queries, i.e. those that are usually omitted when the
287 // corresponding feature flag was already set by term_init()
288 1 bool emit_all = (level >= 6);
289
1/2
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 9 not taken.
1 if (emit_all) {
290 1 LOG_INFO("query level set to %u; unconditionally sending all queries", level);
291 }
292
293 1 term_put_level_2_queries(term, emit_all);
294 1 term->features |= TFLAG_QUERY_L2;
295
296
1/2
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 13 not taken.
1 if (level < 3) {
297 return;
298 }
299
300 1 term_put_level_3_queries(term, emit_all);
301 1 term->features |= TFLAG_QUERY_L3;
302 }
303
304 // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
305 1 void term_use_alt_screen_buffer(Terminal *term)
306 {
307 1 term_put_literal(&term->obuf, "\033[?1049h"); // DECSET 1049
308 1 }
309
310 1 void term_use_normal_screen_buffer(Terminal *term)
311 {
312 1 term_put_literal(&term->obuf, "\033[?1049l"); // DECRST 1049
313 1 }
314
315 1 void term_hide_cursor(Terminal *term)
316 {
317 1 term_put_literal(&term->obuf, "\033[?25l"); // DECRST 25 (DECTCEM)
318 1 }
319
320 1 void term_show_cursor(Terminal *term)
321 {
322 1 term_put_literal(&term->obuf, "\033[?25h"); // DECSET 25 (DECTCEM)
323 1 }
324
325 1 void term_begin_sync_update(Terminal *term)
326 {
327 1 TermOutputBuffer *obuf = &term->obuf;
328
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 WARN_ON(obuf->sync_pending);
329
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
1 if (term->features & TFLAG_SYNC) {
330 1 term_put_literal(obuf, "\033[?2026h"); // DECSET 2026
331 1 obuf->sync_pending = true;
332 }
333 1 }
334
335 1 void term_end_sync_update(Terminal *term)
336 {
337 1 TermOutputBuffer *obuf = &term->obuf;
338
2/4
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 6 not taken.
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 6 not taken.
1 if ((term->features & TFLAG_SYNC) && obuf->sync_pending) {
339 1 term_put_literal(obuf, "\033[?2026l"); // DECRST 2026
340 1 obuf->sync_pending = false;
341 }
342 1 }
343
344 2 void term_move_cursor(TermOutputBuffer *obuf, unsigned int x, unsigned int y)
345 {
346 // ECMA-48 CUP (CSI Pl ; Pc H)
347 2 const size_t maxlen = STRLEN("E[;H") + (2 * DECIMAL_STR_MAX(x));
348 2 char *buf = term_output_reserve_space(obuf, maxlen);
349 2 size_t i = copyliteral(buf, "\033[");
350 2 i += buf_uint_to_str(y + 1, buf + i);
351
352
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 8 taken 1 time.
2 if (x != 0) {
353 1 buf[i++] = ';';
354 1 i += buf_uint_to_str(x + 1, buf + i);
355 }
356
357 2 buf[i++] = 'H';
358 2 BUG_ON(i > maxlen);
359 2 obuf->count += i;
360 2 }
361
362 // Save (push) window title on XTWINOPS stack
363 // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#:~:text=Save%20xterm%20window%20title%20on%20stack
364 void term_save_title(Terminal *term)
365 {
366 if (term->features & TFLAG_SET_WINDOW_TITLE) {
367 term_put_literal(&term->obuf, "\033[22;2t");
368 }
369 }
370
371 // Restore (pop) window title from XTWINOPS stack
372 // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#:~:text=Restore%20xterm%20window%20title%20from%20stack
373 void term_restore_title(Terminal *term)
374 {
375 if (term->features & TFLAG_SET_WINDOW_TITLE) {
376 term_put_literal(&term->obuf, "\033[23;2t");
377 }
378 }
379
380 // Restore saved title as current title, then immediately save again.
381 // Used when yielding to and resuming from child processes or when the
382 // `set_window_title` option changes from true to false.
383 void term_restore_and_save_title(Terminal *term)
384 {
385 if (term->features & TFLAG_SET_WINDOW_TITLE) {
386 term_put_literal(&term->obuf, "\033[23;2t\033[22;2t");
387 }
388 }
389
390 5 bool term_can_clear_eol_with_el_sequence(const Terminal *term)
391 {
392 5 const TermOutputBuffer *obuf = &term->obuf;
393 5 bool bce = !!(term->features & TFLAG_BACK_COLOR_ERASE);
394 5 bool rev = !!(obuf->style.attr & ATTR_REVERSE);
395 5 bool bg = (obuf->style.bg >= COLOR_BLACK);
396
6/6
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 2 times.
5 return obuf->can_clear && (bce || !bg) && !rev;
397 }
398
399 6 int term_clear_eol(Terminal *term)
400 {
401 6 TermOutputBuffer *obuf = &term->obuf;
402 6 const size_t end = obuf->scroll_x + obuf->width;
403
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 15 taken 1 time.
6 if (obuf->x >= end) {
404 // Cursor already at EOL; nothing to clear
405 return 0;
406 }
407
408
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 6 taken 3 times.
5 if (term_can_clear_eol_with_el_sequence(term)) {
409 2 obuf->x = end;
410 2 term_put_literal(obuf, "\033[K"); // Erase to end of line (EL 0)
411 2 static_assert(ECMA48_REP_MIN > -TERM_CLEAR_EOL_USED_EL);
412 2 return TERM_CLEAR_EOL_USED_EL;
413 }
414
415 3 size_t count = end - obuf->x;
416 3 TermSetBytesMethod method = term_set_bytes(term, ' ', count);
417
418
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 12 taken 3 times.
3 if (unlikely(count > INT_MAX)) {
419 // This is basically impossible, given that POSIX requires INT_MAX
420 // to be at least 2³¹-1 and lines of that length simply aren't
421 // something that happens. In any case, the return value here is
422 // only ever used for logging purposes in update_status_line().
423 LOG_ERROR("repeat count in %s() too large for int return value", __func__);
424 return (method == TERM_SET_BYTES_REP) ? INT_MIN : INT_MAX;
425 }
426
427 // Return a negative count if an ECMA-48 REP sequence was used, or a
428 // positive count if space was emitted `count` times
429
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 2 times.
3 return (method == TERM_SET_BYTES_REP) ? -count : count;
430 }
431
432 void term_clear_screen(TermOutputBuffer *obuf)
433 {
434 term_put_literal (
435 obuf,
436 "\033[0m" // Reset colors and attributes (SGR 0)
437 "\033[H" // Move cursor to 1,1 (CUP; done only to mimic terminfo(5) "clear")
438 "\033[2J" // Clear whole screen (ED 2)
439 );
440 }
441
442 void term_output_flush(TermOutputBuffer *obuf)
443 {
444 size_t n = obuf->count;
445 if (n) {
446 BUG_ON(n > TERM_OUTBUF_SIZE);
447 obuf->count = 0;
448 term_direct_write(obuf->buf, n);
449 }
450 }
451
452 static const char *get_tab_str(TermTabOutputMode tab_mode)
453 {
454 static const char tabstr[][8] = {
455 [TAB_NORMAL] = " ",
456 [TAB_SPECIAL] = ">-------",
457 // TAB_CONTROL is printed with u_set_char() and is thus omitted
458 };
459 BUG_ON(tab_mode >= ARRAYLEN(tabstr));
460 return tabstr[tab_mode];
461 }
462
463 static void skipped_too_much(TermOutputBuffer *obuf, CodePoint u)
464 {
465 char *buf = term_output_reserve_space(obuf, 7);
466 size_t n = obuf->x - obuf->scroll_x;
467 BUG_ON(n == 0);
468
469 if (u == '\t' && obuf->tab_mode != TAB_CONTROL) {
470 static_assert(TAB_WIDTH_MAX == 8);
471 BUG_ON(n > 7);
472 memcpy(buf, get_tab_str(obuf->tab_mode) + 1, 7);
473 obuf->count += n;
474 return;
475 }
476
477 if (u < 0x20 || u == 0x7F) {
478 BUG_ON(n != 1);
479 buf[0] = (u + 64) & 0x7F;
480 obuf->count++;
481 return;
482 }
483
484 if (u_is_unprintable(u)) {
485 static_assert(U_SET_HEX_LEN == 4);
486 BUG_ON(n >= U_SET_HEX_LEN);
487 char tmp[2 * U_SET_HEX_LEN] = {'\0'};
488 u_set_hex(tmp, u);
489 memcpy(buf, tmp + U_SET_HEX_LEN - n, U_SET_HEX_LEN);
490 obuf->count += n;
491 return;
492 }
493
494 BUG_ON(n != 1);
495 buf[0] = '>';
496 obuf->count++;
497 }
498
499 static void buf_skip(TermOutputBuffer *obuf, CodePoint u)
500 {
501 if (u == '\t' && obuf->tab_mode != TAB_CONTROL) {
502 obuf->x = next_indent_width(obuf->x, obuf->tab_width);
503 } else {
504 obuf->x += u_char_width(u);
505 }
506
507 if (obuf->x > obuf->scroll_x) {
508 skipped_too_much(obuf, u);
509 }
510 }
511
512 15 bool term_put_char(TermOutputBuffer *obuf, CodePoint u)
513 {
514
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 15 times.
15 if (unlikely(obuf->x < obuf->scroll_x)) {
515 // Scrolled, char (at least partially) invisible
516 buf_skip(obuf, u);
517 return true;
518 }
519
520 15 const size_t space = obuf->scroll_x + obuf->width - obuf->x;
521
2/2
✓ Branch 5 → 6 taken 14 times.
✓ Branch 5 → 27 taken 1 time.
15 if (unlikely(!space)) {
522 return false;
523 }
524
525 14 static_assert(U_SET_CHAR_MAXLEN == 4);
526 14 static_assert(INDENT_WIDTH_MAX == 8);
527 14 const size_t nreserved = 8;
528 14 char *buf = term_output_reserve_space(obuf, nreserved);
529 14 size_t i = 0;
530
531
2/2
✓ Branch 7 → 8 taken 11 times.
✓ Branch 7 → 17 taken 3 times.
14 if (likely(u < 0x80)) {
532
2/2
✓ Branch 8 → 9 taken 8 times.
✓ Branch 8 → 10 taken 3 times.
11 if (likely(!ascii_iscntrl(u))) {
533 8 buf[i++] = u;
534
3/4
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 15 taken 1 time.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 15 taken 2 times.
3 } else if (u == '\t' && obuf->tab_mode != TAB_CONTROL) {
535 size_t width = next_indent_width(obuf->x, obuf->tab_width) - obuf->x;
536 // Fill all 8 reserved bytes and just set `i` as appropriate
537 memcpy(buf, get_tab_str(obuf->tab_mode), 8);
538 i += MIN(width, space);
539 } else {
540 // Use caret notation for control chars:
541 3 buf[i++] = '^';
542
1/2
✓ Branch 15 → 16 taken 3 times.
✗ Branch 15 → 26 not taken.
3 if (likely(space > 1)) {
543 3 buf[i++] = (u + 64) & 0x7F;
544 }
545 }
546 } else {
547 3 const size_t width = u_char_width(u);
548
1/2
✓ Branch 17 → 18 taken 3 times.
✗ Branch 17 → 20 not taken.
3 if (likely(width <= space)) {
549 // This is the only case where the additions to `x` and `count`
550 // aren't necessarily the same, so just set them here and return
551 3 obuf->x += width;
552 3 obuf->count += u_set_char(buf, u);
553 3 return true;
554 } else if (u_is_unprintable(u)) {
555 // <xx> would not fit.
556 // There's enough space in the buffer so render all 4 characters
557 // but increment position less.
558 u_set_hex(buf, u);
559 i += space;
560 } else {
561 buf[i++] = '>';
562 }
563 }
564
565 11 BUG_ON(i > nreserved);
566 11 obuf->count += i;
567 11 obuf->x += i;
568 11 return true;
569 }
570
571 12 static size_t set_color_suffix(char *buf, int32_t color)
572 {
573 12 BUG_ON(color < 0);
574
2/2
✓ Branch 4 → 5 taken 7 times.
✓ Branch 4 → 6 taken 5 times.
12 if (likely(color < 16)) {
575 7 buf[0] = '0' + (color & 7);
576 7 return 1;
577 }
578
579
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 12 taken 4 times.
5 if (!color_is_rgb(color)) {
580 1 BUG_ON(color > 255);
581 1 size_t i = copyliteral(buf, "8;5;");
582 1 return i + buf_u8_to_str(color, buf + i);
583 }
584
585 4 size_t i = copyliteral(buf, "8;2;");
586 4 i += buf_u8_to_str(color_r(color), buf + i);
587 4 buf[i++] = ';';
588 4 i += buf_u8_to_str(color_g(color), buf + i);
589 4 buf[i++] = ';';
590 4 i += buf_u8_to_str(color_b(color), buf + i);
591 4 return i;
592 }
593
594 8 static size_t set_fg_color(char *buf, int32_t color)
595 {
596
2/2
✓ Branch 2 → 3 taken 7 times.
✓ Branch 2 → 7 taken 1 time.
8 if (color < 0) {
597 return 0;
598 }
599
600 7 bool light = (color >= 8 && color <= 15);
601 7 buf[0] = ';';
602
2/2
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 5 taken 1 time.
7 buf[1] = light ? '9' : '3';
603 7 return 2 + set_color_suffix(buf + 2, color);
604 }
605
606 8 static size_t set_bg_color(char *buf, int32_t color)
607 {
608
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 9 taken 3 times.
8 if (color < 0) {
609 return 0;
610 }
611
612 5 bool light = (color >= 8 && color <= 15);
613 5 buf[0] = ';';
614
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 1 time.
5 buf[1] = light ? '1' : '4';
615 5 buf[2] = '0';
616
2/2
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 1 time.
5 size_t i = light ? 3 : 2;
617 5 return i + set_color_suffix(buf + i, color);
618 }
619
620 16 static int32_t color_normalize(int32_t color)
621 {
622 16 BUG_ON(!color_is_valid(color));
623 16 return (color <= COLOR_KEEP) ? COLOR_DEFAULT : color;
624 }
625
626 8 static void term_style_sanitize(TermStyle *style, unsigned int ncv_attrs)
627 {
628 // Replace COLOR_KEEP fg/bg colors with COLOR_DEFAULT, to normalize the
629 // values set in TermOutputBuffer::style
630 8 style->fg = color_normalize(style->fg);
631 8 style->bg = color_normalize(style->bg);
632
633 // Unset ATTR_KEEP, since it's meaningless at this stage (and shouldn't
634 // be set in TermOutputBuffer::style)
635 8 style->attr &= ~ATTR_KEEP;
636
637 // Unset ncv_attrs bits, if fg and/or bg color is non-default (see "ncv"
638 // in terminfo(5) man page)
639
3/4
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 7 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1 time.
8 bool have_color = (style->fg > COLOR_DEFAULT || style->bg > COLOR_DEFAULT);
640 7 style->attr &= (have_color ? ~ncv_attrs : ~0u);
641 8 }
642
643 8 void term_set_style(Terminal *term, TermStyle style)
644 {
645 8 static const struct {
646 char code;
647 unsigned int attr;
648 } attr_map[] = {
649 {'1', ATTR_BOLD},
650 {'2', ATTR_DIM},
651 {'3', ATTR_ITALIC},
652 {'4', ATTR_UNDERLINE},
653 {'5', ATTR_BLINK},
654 {'7', ATTR_REVERSE},
655 {'8', ATTR_INVIS},
656 {'9', ATTR_STRIKETHROUGH}
657 };
658
659 // TODO: take `TermOutputBuffer::style` into account and only emit
660 // the minimal set of parameters needed to update the terminal's
661 // current state (i.e. without using `0` to reset or emitting
662 // already active attributes/colors)
663
664 8 term_style_sanitize(&style, term->ncv_attributes);
665
666 8 const size_t maxcolor = STRLEN(";38;2;255;255;255");
667 8 const size_t maxlen = STRLEN("E[0m") + (2 * maxcolor) + (2 * ARRAYLEN(attr_map));
668 8 char *buf = term_output_reserve_space(&term->obuf, maxlen);
669 8 size_t pos = copyliteral(buf, "\033[0");
670
671
2/2
✓ Branch 9 → 6 taken 64 times.
✓ Branch 9 → 10 taken 8 times.
72 for (size_t i = 0; i < ARRAYLEN(attr_map); i++) {
672
2/2
✓ Branch 6 → 7 taken 16 times.
✓ Branch 6 → 8 taken 48 times.
64 if (style.attr & attr_map[i].attr) {
673 16 buf[pos++] = ';';
674 16 buf[pos++] = attr_map[i].code;
675 }
676 }
677
678 8 pos += set_fg_color(buf + pos, style.fg);
679 8 pos += set_bg_color(buf + pos, style.bg);
680 8 buf[pos++] = 'm';
681 8 BUG_ON(pos > maxlen);
682 8 term->obuf.count += pos;
683 8 term->obuf.style = style;
684 8 }
685
686 2 static void cursor_style_normalize(TermCursorStyle *s)
687 {
688 2 BUG_ON(!cursor_type_is_valid(s->type));
689 2 BUG_ON(!cursor_color_is_valid(s->color));
690
1/2
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 8 not taken.
2 s->type = (s->type == CURSOR_KEEP) ? CURSOR_DEFAULT : s->type;
691
1/2
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 10 not taken.
2 s->color = (s->color == COLOR_KEEP) ? COLOR_DEFAULT : s->color;
692 2 }
693
694 2 void term_set_cursor_style(Terminal *term, TermCursorStyle s)
695 {
696 2 TermOutputBuffer *obuf = &term->obuf;
697 2 cursor_style_normalize(&s);
698 2 obuf->cursor_style = s;
699
700 2 const size_t maxlen = STRLEN("E[7 qE]12;rgb:aa/bb/ccST");
701 2 char *buf = term_output_reserve_space(obuf, maxlen);
702
703 // Set shape with DECSCUSR
704 2 BUG_ON(s.type < 0 || s.type > 9);
705 2 const char seq[] = {'\033', '[', '0' + s.type, ' ', 'q'};
706 2 size_t i = copystrn(buf, seq, sizeof(seq));
707
708
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 10 taken 1 time.
2 if (s.color == COLOR_DEFAULT) {
709 // Reset color with OSC 112
710 1 i += copyliteral(buf + i, "\033]112");
711 } else {
712 // Set RGB color with OSC 12
713 1 i += copyliteral(buf + i, "\033]12;rgb:");
714 1 i += hex_encode_byte(buf + i, color_r(s.color));
715 1 buf[i++] = '/';
716 1 i += hex_encode_byte(buf + i, color_g(s.color));
717 1 buf[i++] = '/';
718 1 i += hex_encode_byte(buf + i, color_b(s.color));
719 }
720
721 2 i += copyliteral(buf + i, "\033\\"); // String Terminator (ST)
722 2 BUG_ON(i > maxlen);
723 2 obuf->count += i;
724 2 }
725
726 static COLD void log_detected_features (
727 TermFeatureFlags existing,
728 TermFeatureFlags detected
729 ) {
730 if (likely(!log_level_enabled(LOG_LEVEL_INFO))) {
731 return;
732 }
733
734 // Don't log QUERY flags more than once
735 const TermFeatureFlags query_flags = TFLAG_QUERY_L2 | TFLAG_QUERY_L3;
736 const TermFeatureFlags repeat_query_flags = query_flags & detected & existing;
737 detected &= ~repeat_query_flags;
738
739 while (detected) {
740 // Iterate through detected features, by finding the least
741 // significant set bit and then logging and unsetting it
742 TermFeatureFlags flag = u32_lsbit(detected);
743 const char *name = term_feature_to_str(flag);
744 const char *extra = (existing & flag) ? " (was already set)" : "";
745 LOG_INFO("terminal feature %s detected via query%s", name, extra);
746 detected &= ~flag;
747 }
748 }
749
750 KeyCode term_handle_query_reply(Terminal *term, TermFeatureFlags detected)
751 {
752 const TermFeatureFlags existing = term->features;
753 BUG_ON(detected & existing & KEYCODE_QUERY_REPLY_BIT);
754 term->features |= detected;
755 log_detected_features(existing, detected);
756
757 TermFeatureFlags escflags = TFLAG_META_ESC | TFLAG_ALT_ESC;
758 if ((detected & escflags) && (existing & TFLAG_KITTY_KEYBOARD)) {
759 const char *name = term_feature_to_str(detected);
760 const char *ovr = term_feature_to_str(TFLAG_KITTY_KEYBOARD);
761 LOG_INFO("terminal feature %s overridden by %s", name, ovr);
762 detected &= ~escflags;
763 }
764
765 if ((detected & TFLAG_QUERY_L3) && (existing & TFLAG_NO_QUERY_L3)) {
766 LOG_INFO("terminal feature QUERY3 overridden by NOQUERY3");
767 detected &= ~TFLAG_QUERY_L3;
768 }
769
770 // The subset of flags present in this query reply (`detected`) that
771 // weren't already present in the set of known features (`existing`)
772 // and/or overridden by the conditions above
773 const TermFeatureFlags newly_detected = ~existing & detected;
774
775 TermOutputBuffer *obuf = &term->obuf;
776 if (newly_detected & TFLAG_QUERY_L2) {
777 term_put_level_2_queries(term, false);
778 }
779 if (newly_detected & TFLAG_QUERY_L3) {
780 term_put_level_3_queries(term, false);
781 }
782
783 if (newly_detected & TFLAG_KITTY_KEYBOARD) {
784 if (existing & TFLAG_MODIFY_OTHER_KEYS) {
785 // Disable modifyOtherKeys mode, if previously enabled by
786 // main() → ui_first_start() → term_enable_private_modes()
787 // (i.e. due to feature flags set by term_init())
788 term_put_literal(obuf, "\033[>4m");
789 }
790 // Enable Kitty Keyboard Protocol bits 1 and 4 (1|4 == 5). See also:
791 // • https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#:~:text=CSI%20%3E%20Pp%20m
792 // • https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
793 term_put_literal(obuf, "\033[>5u");
794 }
795
796 bool using_kittykbd = (existing | detected) & TFLAG_KITTY_KEYBOARD;
797 bool enable_mokeys = (newly_detected & TFLAG_MODIFY_OTHER_KEYS) && !using_kittykbd;
798 if (enable_mokeys) {
799 term_put_literal(obuf, "\033[>4;1m\033[>4;2m"); // modifyOtherKeys=1/2
800 }
801
802 if (newly_detected & TFLAG_META_ESC) {
803 term_put_literal(obuf, "\033[?1036h"); // DECSET 1036 (metaSendsEscape)
804 }
805 if (newly_detected & TFLAG_ALT_ESC) {
806 term_put_literal(obuf, "\033[?1039h"); // DECSET 1039 (altSendsEscape)
807 }
808
809 if (newly_detected & TFLAG_SET_WINDOW_TITLE) {
810 BUG_ON(!(term->features & TFLAG_SET_WINDOW_TITLE));
811 term_save_title(term);
812 }
813
814 // This is the list of features that cause output to be emitted above
815 // (and thus require a flush of the output buffer)
816 const TermFeatureFlags features_producing_output =
817 TFLAG_QUERY_L2 | TFLAG_QUERY_L3 | TFLAG_KITTY_KEYBOARD
818 | TFLAG_META_ESC | TFLAG_ALT_ESC | TFLAG_SET_WINDOW_TITLE
819 ;
820
821 if ((newly_detected & features_producing_output) || enable_mokeys) {
822 term_output_flush(obuf);
823 }
824
825 return KEY_NONE;
826 }
827