Repository navigation
fix: hide tput charset escapes, recognize colon-form colors, print tabs as one space - #781
Merged
sirmalloc merged 7 commits intoNov 5, 2026
Merged
Conversation
With Preserve Colors off, Custom Command reduces its output to visible text with getVisibleText before the renderer's sanitizer sees it. The escape parser behind it read ESC and any character other than [ or ] as a complete two-character sequence. That is right for ESC 7 or ESC c, but not for ESC ( B, a character-set selection with an intermediate byte, which `tput sgr0` prints before ESC [ m on xterm-256color. Its final "B" was left as text, so a script printing "$(tput setaf 2)OK$(tput sgr0) done" showed "OKB done". DCS, SOS, PM and APC strings (ESC P ... ESC \) lost only their first two characters, so their payload printed as text. By the time the sanitizer ran, these were ordinary characters it had no reason to remove. Read these forms the way terminal-sanitize.ts already does: ESC, intermediate bytes (0x20-0x2F) and a final byte as one sequence, and a DCS, SOS, PM or APC string up to ST or BEL, stopping before any other ESC so an unterminated string doesn't hide the text after it. Visible width goes through the same parser, so with Preserve Colors on these sequences no longer count toward Max Width either.
SGR parameters can be separated with colons, the ITU T.416 form: ESC[38:5:196m, ESC[38:2::255:0:0m, or ESC[4:3m for a curly underline, which `tput Smulx 3` prints on xterm-256color. The terminal sanitizer keeps these, but the color pattern in ansi.ts only allowed digits and semicolons, so everything downstream treated them as unknown: - a Preserve Colors Custom Command cut by Max Width never got its closing reset, so its color or underline ran into the separators and widgets after it - a global foreground override left the command's own colon-form color in place, where the semicolon form is recolored - in Powerline mode, a colon-form reset such as ESC[0;38:5:196m didn't get the segment background re-applied after it Allow colons in the pattern. When reading a sequence for a background reset, an extended color written with colons holds its arguments in its own parameter, so the following parameters are no longer skipped as if they were its arguments: ESC[48:5:1;0m ends with the background cleared, and ESC[0;38:2::0:0:0;41m keeps its red background.
Widget output kept its tabs: the terminal sanitizer let them through,
and width measurement gave them no columns, as string-width does for
control characters. A terminal moves a tab on to the next tab stop, up
to 8 columns, so a line holding one printed wider than it was measured.
A Custom Command such as `git rev-list --left-right --count HEAD...@{u}`,
which prints "0<tab>3", beside a flex separator filled the line to its
width by count and then ran past it on screen, where it wraps or gets
cut off. Max Width and the line's own truncation let the extra columns
through the same way.
How far a tab moves depends on the column it lands in. That isn't known
while a widget is measured, before the line is laid out, and it changes
with where the host draws the line, so measuring tabs at their real
width can't be done reliably. Instead, the sanitizer prints each tab as
one space, and width measurement counts a tab as one column, so text
measured before it is sanitized, as Custom Command's Max Width is,
agrees with what is printed. A single space reads as the field
separator a tab usually is in one-line output, without spending up to
8 columns of the status line on it.
An escape with intermediate bytes (ESC ( B) was only tested with its final byte present. Pin down what happens when it is missing: at the end of the text the escape is hidden up to where it stops, and a character that cannot end it, here an emoji, stays visible after it.
…yte range The scan for ESC, intermediate bytes and a final byte compared raw UTF-16 units and spelled the final-byte range inline in the slice call. It now reads code points, as the grapheme-cluster scan in this file does, and tests the final byte with isEscapeFinalByte next to isIntermediateByte, so the return reads as "one past the final byte if there is one". The parse is the same for every input: both ranges are ASCII, and code points only differ from code units at a surrogate pair (a value of 0x10000 or more instead of 0xD800 to 0xDBFF) and past the end of the text (undefined instead of NaN), which neither range accepts either way.
# Conflicts: # docs/DEVELOPMENT.md # docs/USAGE.md
# Conflicts: # docs/USAGE.md # src/utils/ansi.ts
Contributor
|
Reproduced on current Red: 17 failing / 166 total — matching the "17 of the 21 new cases fail on main" claim, across all three symptom families: Green: with the full PR head checked out, the same files pass 166 / 0.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Three fixes to how widget output's escape sequences and tabs are parsed and measured, mostly visible with Custom Command:
tputno longer shows a strayBafter each reset, and DCS/SOS/PM/APC strings no longer print their payload.ESC[38:5:196m,ESC[38:2::255:0:0m,ESC[4:3m) are recognized. Max Width closes them after a cut, a global foreground override replaces them, and Powerline re-applies the segment background after a colon-form reset.Why (evidence)
On xterm-256color,
tput sgr0printsESC ( B ESC [ m. With preserve colors off, Custom Command reduces its output to visible text withgetVisibleTextbefore the renderer's sanitizer runs. Its parser read ESC plus any character other than[or]as a two-character sequence. SogetVisibleText('\x1b[32mOK\x1b(B\x1b[m done')returnedOKB done, and'a\x1bPq#0;2;0;0;0\x1b\\b'returnedaq#0;2;0;0;0b.terminal-sanitize.tsalready parses both forms, but by the time it ran they were ordinary text.terminal-sanitize.tskeeps SGR matching[0-9;:]*m, and its tests keepESC[38:2::255:128:0m. The color pattern inansi.tsonly allowed[0-9;]*m, so:printf '\033[38:5:196mhello world long text\033[0m'with Max Width 8 renderedESC[38:5:196mhello... | …with no closing reset, and the red ran into the separator. The semicolon form gets the reset.ESC[0;38:5:196m.Colon form isn't exotic:
tput Smulx 3(curly underline) printsESC[4:3m.The sanitizer kept tabs, and
getVisibleWidth('3\t5')was 2: string-width gives control characters no width, but a terminal moves to the next tab stop. AtCCSTATUSLINE_WIDTH=40,[Custom Command printf '3\t5', flex separator, "END"]printed 35 characters spanning 41 columns, against a 34-column target. A seven-field tabbed line measured 27 columns, spanned 51, and was never truncated. Everyday commands print tabs:git rev-list --left-right --count HEAD...@{u}prints0\t3, anddu -sh .prints1.2G\t..How
parseEscapeSequenceuses the same rules asterminal-sanitize.ts::. When reading a sequence for a background reset, a colon-form extended color holds its own arguments, so the parameters after it are no longer skipped as if they were its arguments.ESC[48:5:1;0mnow ends with the background cleared, andESC[0;38:2::0:0:0;41mkeeps its red background.Demo
Piped renders of Custom Command widgets, before (
main) and after.tputreset with preserve colors off. Line 1 has preserve colors off; line 2 has it on, for reference. Before: "build okB, 3 warningsB".Powerline
Before:
After:
Plain
Before:
After:
Tabs beside a flex separator. The line is rendered at the terminal's width, and the dashes mark where flex fills it to. Line 1 prints
ahead<TAB>3<TAB>behind<TAB>0; line 2 prints the same with spaces. Before, line 1 runs past the terminal and wraps. After, it ends with line 2.Powerline
Before:
After:
Plain
Before:
After:
Colon-form colors under a foreground override, cut by Max Width. The global foreground override is white, and both commands are cut to 12 columns. Line 1 writes its underline and color with colons; line 2 writes them with semicolons. Before, line 1 keeps its pink, and its curly underline runs into the separator and Context Length.
Powerline
Before:
After:
Plain
Before:
After:
Testing
mainas of fix: use env override and settings for usable context ratio calculation #407 (71e8b42): merges cleanly,bun run lintis clean, andbun testgives 3959 pass, 0 fail. The built CLI under Bun 1.4.4 and Node 26.13.0 matchesmainin plain and Powerline mode with the baseline configs, and the TUI opens and exits cleanly. It adds no JavaScript or Node API newer than Node 14, the build's target.bun run lintis clean.bun testpasses 3189 tests, 0 failing, across 178 files. The five touched test files go from 122 to 143 tests. 17 of the 21 new cases fail onmain. Of the other 4, 1 pins down an escape whose final byte is missing and 3 keep the background after a colon-form color. One of those 3 (0;38:2::0:0:0;41) fails if only the pattern is changed; the other two guard against regressions.main. The same 2 tests fail on both because they spy onos.homedir, which Node's ESM doesn't allow. All new tests pass.dist/under Bun 1.4.2 and Node v26.10.0:main. The TUI opens and exits cleanly under both.mainas shown above.