From 8c19b729464e5e2078881db885c57c8309c1dcb3 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sun, 23 Aug 2026 18:55:08 +0100 Subject: [PATCH 1/2] [terminfo] support the %'c' character constant The terminfo parser handled %{nn} integer constants but had no case for %'c', so the operator fell through to the default branch and its bytes were copied into the output as literal text. xterm-256color's setaf uses %'\010' and %'\020' to test the colour index against 8 and 16, so the conditional never evaluated and tiparm_s() returned a malformed CSI sequence containing a literal BS: setaf(112) -> \E[\010'3112m instead of \E[38;5;112m 0x08 and ' are not valid in a CSI parameter string, so terminals abort the sequence and print the rest as text, which corrupts the display. This affects every terminfo entry whose setaf/setab uses %'c', including xterm-256color, xterm-16color, screen-256color, tmux-256color, alacritty, xterm-kitty, xterm-ghostty and vte-256color. Handle plain characters, octal escapes such as %'\010', and the common named escapes. Co-Authored-By: Claude Opus 5 (1M context) --- src/third-party/notcurses/src/lib/terminfo.c | 37 ++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/third-party/notcurses/src/lib/terminfo.c b/src/third-party/notcurses/src/lib/terminfo.c index 8843191242d..6d87422d907 100644 --- a/src/third-party/notcurses/src/lib/terminfo.c +++ b/src/third-party/notcurses/src/lib/terminfo.c @@ -487,6 +487,43 @@ tiparm_s(const char* fmt, int argc, TiparmValue* argv) argv[1].i++; p++; break; + case '\'': { + // %'c' pushes the literal character c. terminfo also + // allows a backslash escape here, e.g. %'\010' as used by + // xterm-256color's setaf/setab to test against 8 and 16. + p++; + int val = 0; + if (*p == '\\') { + p++; + if (*p >= '0' && *p <= '7') { + // up to three octal digits + for (int i = 0; i < 3 && *p >= '0' && *p <= '7'; i++) { + val = val * 8 + (*p++ - '0'); + } + } else { + switch (*p) { + case 'n': val = '\n'; break; + case 'r': val = '\r'; break; + case 't': val = '\t'; break; + case 'b': val = '\b'; break; + case 'f': val = '\f'; break; + case 'e': + case 'E': val = 0x1b; break; + default: val = (unsigned char) *p; break; + } + if (*p) { + p++; + } + } + } else if (*p) { + val = (unsigned char) *p++; + } + if (*p == '\'') { + p++; + } + push(&stack, (StackVal) {STK_INT, .i = val}); + break; + } case '{': { p++; int val = 0; From cf21cc9637c95690b18460bf4005bdb81aa8dae3 Mon Sep 17 00:00:00 2001 From: Timothy Stack Date: Sun, 23 Aug 2026 13:44:52 -0700 Subject: [PATCH 2/2] [terminfo] gate stack ops on exec and drop the %'c' escape branch tiparm_s consulted `exec` only when producing output, never when mutating the stack, so `%p`, `%{n}`, `%'c'` and the binary operators ran in both arms of a `%?...%t...%e...%;`. Entries that place `%d` after the `%;` then print whatever the untaken arm left behind: setaf=\E[%?%p1%{8}%<%t%p1%{30}%+%e%p1%'R'%+%;%dm For setaf(0) the live arm pushes 30, the dead arm pushes 0 and 'R' and adds them to 82, and `%d` pops 82. Adding the `%'c'` push made this reachable for the whole `*-16color` family: setaf(0..7) emitted SGR 82-89, which terminals ignore, and setab(0..7) emitted SGR 92-99, which are bright foreground colors, so a background request silently recolored the foreground instead. Gating every stack operation on `exec` brings xterm-16color and xterm-256color to zero mismatches against ncurses tparm. The backslash handling in `%'c'` is also removed. terminfo_load reads compiled entries, and tic resolves escapes at compile time, so a source `%'\010'` arrives as a raw 0x08 that the plain-character path already handles. The only byte the escape branch could actually encounter is a literal backslash from a source `%'\\'`, and it mis-parsed that one, consuming the closing quote as the escaped character and pushing 0x27 instead of 0x5C. Co-Authored-By: Claude Opus 5 --- src/third-party/notcurses/src/lib/terminfo.c | 81 ++++++++++++-------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/src/third-party/notcurses/src/lib/terminfo.c b/src/third-party/notcurses/src/lib/terminfo.c index 6d87422d907..403520b6de9 100644 --- a/src/third-party/notcurses/src/lib/terminfo.c +++ b/src/third-party/notcurses/src/lib/terminfo.c @@ -468,7 +468,7 @@ tiparm_s(const char* fmt, int argc, TiparmValue* argv) break; case 'p': { int idx = p[1] - '1'; - if (idx >= 0 && idx < argc) { + if (exec && idx >= 0 && idx < argc) { if (argv[idx].type == TIPARM_INT) { push(&stack, (StackVal) {STK_INT, .i = argv[idx].i}); @@ -488,40 +488,21 @@ tiparm_s(const char* fmt, int argc, TiparmValue* argv) p++; break; case '\'': { - // %'c' pushes the literal character c. terminfo also - // allows a backslash escape here, e.g. %'\010' as used by - // xterm-256color's setaf/setab to test against 8 and 16. + // %'c' pushes the literal character c. tic resolves + // backslash escapes when compiling the terminfo source, so + // the byte between the quotes is always the character + // itself, never an escape sequence. p++; int val = 0; - if (*p == '\\') { - p++; - if (*p >= '0' && *p <= '7') { - // up to three octal digits - for (int i = 0; i < 3 && *p >= '0' && *p <= '7'; i++) { - val = val * 8 + (*p++ - '0'); - } - } else { - switch (*p) { - case 'n': val = '\n'; break; - case 'r': val = '\r'; break; - case 't': val = '\t'; break; - case 'b': val = '\b'; break; - case 'f': val = '\f'; break; - case 'e': - case 'E': val = 0x1b; break; - default: val = (unsigned char) *p; break; - } - if (*p) { - p++; - } - } - } else if (*p) { + if (*p) { val = (unsigned char) *p++; } if (*p == '\'') { p++; } - push(&stack, (StackVal) {STK_INT, .i = val}); + if (exec) { + push(&stack, (StackVal) {STK_INT, .i = val}); + } break; } case '{': { @@ -537,10 +518,16 @@ tiparm_s(const char* fmt, int argc, TiparmValue* argv) } if (*p == '}') p++; - push(&stack, (StackVal) {STK_INT, .i = val * sign}); + if (exec) { + push(&stack, (StackVal) {STK_INT, .i = val * sign}); + } break; } case '+': { + if (!exec) { + p++; + break; + } StackVal b = pop(&stack), a = pop(&stack); if (a.type == STK_INT && b.type == STK_INT) push(&stack, (StackVal) {STK_INT, .i = a.i + b.i}); @@ -548,6 +535,10 @@ tiparm_s(const char* fmt, int argc, TiparmValue* argv) break; } case '-': { + if (!exec) { + p++; + break; + } StackVal b = pop(&stack), a = pop(&stack); if (a.type == STK_INT && b.type == STK_INT) push(&stack, (StackVal) {STK_INT, .i = a.i - b.i}); @@ -555,6 +546,10 @@ tiparm_s(const char* fmt, int argc, TiparmValue* argv) break; } case '=': { + if (!exec) { + p++; + break; + } StackVal b = pop(&stack), a = pop(&stack); if (a.type == STK_INT && b.type == STK_INT) push(&stack, (StackVal) {STK_INT, .i = (a.i == b.i)}); @@ -562,6 +557,10 @@ tiparm_s(const char* fmt, int argc, TiparmValue* argv) break; } case '<': { + if (!exec) { + p++; + break; + } StackVal b = pop(&stack), a = pop(&stack); if (a.type == STK_INT && b.type == STK_INT) push(&stack, (StackVal) {STK_INT, .i = (a.i < b.i)}); @@ -569,6 +568,10 @@ tiparm_s(const char* fmt, int argc, TiparmValue* argv) break; } case '>': { + if (!exec) { + p++; + break; + } StackVal b = pop(&stack), a = pop(&stack); if (a.type == STK_INT && b.type == STK_INT) push(&stack, (StackVal) {STK_INT, .i = (a.i > b.i)}); @@ -576,8 +579,12 @@ tiparm_s(const char* fmt, int argc, TiparmValue* argv) break; } case 'd': { + if (!exec) { + p++; + break; + } StackVal v = pop(&stack); - if (exec && v.type == STK_INT) { + if (v.type == STK_INT) { char numbuf[32]; snprintf(numbuf, sizeof(numbuf), "%d", v.i); size_t len = strlen(numbuf); @@ -592,8 +599,12 @@ tiparm_s(const char* fmt, int argc, TiparmValue* argv) break; } case 's': { + if (!exec) { + p++; + break; + } StackVal v = pop(&stack); - if (exec && v.type == STK_STR && v.s) { + if (v.type == STK_STR && v.s) { size_t len = strlen(v.s); if (out_len + len >= out_cap) { while (out_len + len >= out_cap) @@ -613,8 +624,14 @@ tiparm_s(const char* fmt, int argc, TiparmValue* argv) p++; break; case 't': { + // the condition was only evaluated, and so only pushed, if + // this branch is live; exec stays false either way + if (!exec) { + p++; + break; + } StackVal v = pop(&stack); - exec = exec && (v.type == STK_INT && v.i); + exec = (v.type == STK_INT && v.i); if (exec) { cond_execed = 1; }