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