dte test coverage


Directory: ./
File: src/terminal/output.c
Date: 2025-09-07 23:01:39
Exec Total Coverage
Lines: 265 360 73.6%
Functions: 28 38 73.7%
Branches: 86 154 55.8%

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