| 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 |