diff --git a/src/basic/string-util.c b/src/basic/string-util.c index 1715d62a39..171a368059 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -698,7 +698,8 @@ char* strip_tab_ansi(char **ibuf, size_t *_isz, size_t highlight[2]) { STATE_OTHER, STATE_ESCAPE, STATE_CSI, - STATE_CSO, + STATE_OSC, + STATE_OSC_CLOSING, } state = STATE_OTHER; _cleanup_(memstream_done) MemStream m = {}; size_t isz, shift[2] = {}, n_carriage_returns = 0; @@ -711,7 +712,7 @@ char* strip_tab_ansi(char **ibuf, size_t *_isz, size_t highlight[2]) { * * 1. Replaces TABs by 8 spaces * 2. Strips ANSI color sequences (a subset of CSI), i.e. ESC '[' … 'm' sequences - * 3. Strips ANSI operating system sequences (CSO), i.e. ESC ']' … BEL sequences + * 3. Strips ANSI operating system sequences (OSC), i.e. ESC ']' … ST sequences * 4. Strip trailing \r characters (since they would "move the cursor", but have no * other effect). * @@ -719,7 +720,7 @@ char* strip_tab_ansi(char **ibuf, size_t *_isz, size_t highlight[2]) { * are any other special characters. Truncated ANSI sequences are left-as is too. This call is * supposed to suppress the most basic formatting noise, but nothing else. * - * Why care for CSO sequences? Well, to undo what terminal_urlify() and friends generate. */ + * Why care for OSC sequences? Well, to undo what terminal_urlify() and friends generate. */ isz = _isz ? *_isz : strlen(*ibuf); @@ -766,8 +767,8 @@ char* strip_tab_ansi(char **ibuf, size_t *_isz, size_t highlight[2]) { } else if (*i == '[') { /* ANSI CSI */ state = STATE_CSI; begin = i + 1; - } else if (*i == ']') { /* ANSI CSO */ - state = STATE_CSO; + } else if (*i == ']') { /* ANSI OSC */ + state = STATE_OSC; begin = i + 1; } else { fputc('\x1B', f); @@ -793,17 +794,35 @@ char* strip_tab_ansi(char **ibuf, size_t *_isz, size_t highlight[2]) { break; - case STATE_CSO: + case STATE_OSC: assert(n_carriage_returns == 0); + /* There are three kinds of OSC terminators: \x07, \x1b\x5c or \x9c. We only support + * the first two, because the last one is a valid UTF-8 codepoint and hence creates + * an ambiguity (many Terminal emulators refuse to support it as well). */ if (i >= *ibuf + isz || /* EOT … */ - (*i != '\a' && (uint8_t) *i < 32U) || (uint8_t) *i > 126U) { /* … or invalid chars in sequence */ + (!IN_SET(*i, '\x07', '\x1b') && (uint8_t) *i < 32U) || (uint8_t) *i > 126U) { /* … or invalid chars in sequence */ fputc('\x1B', f); fputc(']', f); advance_offsets(i - *ibuf, highlight, shift, 2); state = STATE_OTHER; i = begin-1; - } else if (*i == '\a') + } else if (*i == '\x07') /* Single character ST */ + state = STATE_OTHER; + else if (*i == '\x1B') + state = STATE_OSC_CLOSING; + + break; + + case STATE_OSC_CLOSING: + if (i >= *ibuf + isz || /* EOT … */ + *i != '\x5c') { /* … or incomplete two-byte ST in sequence */ + fputc('\x1B', f); + fputc(']', f); + advance_offsets(i - *ibuf, highlight, shift, 2); + state = STATE_OTHER; + i = begin-1; + } else if (*i == '\x5c') state = STATE_OTHER; break; diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 8ded46f493..81c02d85a3 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -1923,12 +1923,12 @@ int get_default_background_color(double *ret_red, double *ret_green, double *ret if (tcsetattr(STDIN_FILENO, TCSADRAIN, &new_termios) < 0) return -errno; - r = loop_write(STDOUT_FILENO, "\x1B]11;?\x07", SIZE_MAX); + r = loop_write(STDOUT_FILENO, ANSI_OSC "11;?" ANSI_ST, SIZE_MAX); if (r < 0) goto finish; usec_t end = usec_add(now(CLOCK_MONOTONIC), 333 * USEC_PER_MSEC); - char buf[STRLEN("\x1B]11;rgb:0/0/0\x07")]; /* shortest possible reply */ + char buf[STRLEN(ANSI_OSC "11;rgb:0/0/0" ANSI_ST)]; /* shortest possible reply */ size_t buf_full = 0; BackgroundColorContext context = {}; diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index b446e547d6..08d8cbac25 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -27,6 +27,16 @@ #define ANSI_WINDOW_TITLE_PUSH "\x1b[22;2t" #define ANSI_WINDOW_TITLE_POP "\x1b[23;2t" +/* ANSI "string terminator" character ("ST"). Terminal emulators typically allow three different ones: 0x07, + * 0x9c, and 0x1B 0x5C. We'll avoid 0x07 (BEL, aka ^G) since it might trigger unexpected TTY signal + * handling. And we'll avoid 0x9c since that's also valid regular codepoint in UTF-8 and elsewhere, and + * creates ambiguities. Because of that some terminal emulators explicitly choose not to support it. Hence we + * use 0x1B 0x5c */ +#define ANSI_ST "\e\\" + +/* The "operating system command" ("OSC") start sequence */ +#define ANSI_OSC "\e]" + bool isatty_safe(int fd); int terminal_reset_defensive(int fd, bool switch_to_text); diff --git a/src/shared/pretty-print.c b/src/shared/pretty-print.c index df1c20fa24..4e38e60775 100644 --- a/src/shared/pretty-print.c +++ b/src/shared/pretty-print.c @@ -88,7 +88,9 @@ int terminal_urlify(const char *url, const char *text, char **ret) { text = url; if (urlify_enabled()) - n = strjoin("\x1B]8;;", url, "\a", text, "\x1B]8;;\a"); + n = strjoin(ANSI_OSC "8;;", url, ANSI_ST, + text, + ANSI_OSC "8;;" ANSI_ST); else n = strdup(text); if (!n) @@ -475,7 +477,7 @@ void draw_progress_bar_unbuffered(const char *prefix, double percentage) { * https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC * https://github.com/microsoft/terminal/pull/8055 */ - fprintf(stderr, "\x1B]9;4;1;%u\a", (unsigned) ceil(percentage)); + fprintf(stderr, ANSI_OSC "9;4;1;%u" ANSI_ST, (unsigned) ceil(percentage)); size_t cols = columns(); size_t prefix_width = utf8_console_width(prefix) + 1 /* space */; @@ -534,7 +536,7 @@ void clear_progress_bar_unbuffered(const char *prefix) { stderr); else /* Undo Windows Terminal progress indication again. */ - fputs("\x1B]9;4;0;;\a" + fputs(ANSI_OSC "9;4;0;;" ANSI_ST ANSI_ERASE_TO_END_OF_LINE, stderr); fputc('\r', stderr); diff --git a/src/shared/ptyfwd.c b/src/shared/ptyfwd.c index c06c279d87..c1e3b7180b 100644 --- a/src/shared/ptyfwd.c +++ b/src/shared/ptyfwd.c @@ -397,7 +397,7 @@ static int insert_window_title_fix(PTYForward *f, size_t offset) { if (!t) return 0; - _cleanup_free_ char *joined = strjoin("\x1b]0;", f->title_prefix, t, "\a"); + _cleanup_free_ char *joined = strjoin(ANSI_OSC "0;", f->title_prefix, t, ANSI_ST); if (!joined) return -ENOMEM; @@ -507,11 +507,16 @@ static int pty_forward_ansi_process(PTYForward *f, size_t offset) { } else { /* Otherwise, the OSC sequence is over * - * There are two allowed ways to end an OSC sequence: - * BEL '\x07' - * String Terminator (ST): \ - "\x1b\x5c" - * since we cannot lookahead to see if the Esc is followed by a \ - * we cut a corner here and assume it will be \. */ + * There are three documented ways to end an OSC sequence: + * 1. BEL aka ^G aka \x07 + * 2. \x9c + * 3. \x1b\x5c + * since we cannot look ahead to see if the Esc is followed by a "\" + * we cut a corner here and assume it will be "\"e. + * + * Note that we do not support \x9c here, because that's also a valid UTF8 + * codepoint, and that would create ambiguity. Various terminal emulators + * similar do not support it. */ if (IN_SET(c, '\x07', '\x1b')) { r = insert_window_title_fix(f, i+1); @@ -567,7 +572,7 @@ static int do_shovel(PTYForward *f) { if (f->title) { if (!strextend(&f->out_buffer, ANSI_WINDOW_TITLE_PUSH - "\x1b]2;", f->title, "\a")) + ANSI_OSC "2;", f->title, ANSI_ST)) return log_oom(); } diff --git a/src/test/test-strip-tab-ansi.c b/src/test/test-strip-tab-ansi.c index 3ad0fcd90e..00bd7cdaf1 100644 --- a/src/test/test-strip-tab-ansi.c +++ b/src/test/test-strip-tab-ansi.c @@ -67,6 +67,15 @@ TEST(strip_tab_ansi) { assert_se(strip_tab_ansi(&q, NULL, NULL)); ASSERT_STREQ(q, qq); } + + /* Test that both kinds of ST are recognized after OSC */ + assert_se(p = strdup("before" ANSI_OSC "inside1" ANSI_ST + "between1" ANSI_OSC "inside2\a" + "between2" ANSI_OSC "inside3\x1b\x5c" + "after")); + assert_se(strip_tab_ansi(&p, NULL, NULL)); + ASSERT_STREQ(p, "beforebetween1between2after"); + free(p); } DEFINE_TEST_MAIN(LOG_INFO);