Skip to content

Consume unhandled CSI escape sequences instead of printing them literally - #106

Open
amanfcp wants to merge 1 commit into
jesseduffield:masterfrom
amanfcp:fix/csi-unhandled-sequences-literal-escape-codes
Open

Consume unhandled CSI escape sequences instead of printing them literally#106
amanfcp wants to merge 1 commit into
jesseduffield:masterfrom
amanfcp:fix/csi-unhandled-sequences-literal-escape-codes

Conversation

@amanfcp

@amanfcp amanfcp commented Jul 17, 2026

Copy link
Copy Markdown

Summary

escapeInterpreter.parseOne (escape.go) only recognizes digits, SGR (m), and erase-in-line (K) as valid CSI bytes. Any other valid CSI sequence , cursor hide/show (DEC private mode ?25l/?25h), cursor position (1;1H), cursor movement (1A), clear screen (2J), save/restore cursor (s/u) , falls into the parse-error path, and View.parseInput turns a parse error into the raw escape bytes being printed as literal text instead of being consumed.

Real-world colorized CLI output frequently mixes SGR color with these other sequences (progress bars, spinners, dev-server reloaders all commonly emit cursor-hide/show or cursor-position sequences alongside color), so consumers of gocui Views see garbage interleaved with otherwise-correct color. This is the root cause of the "container log colors" issue reported against lazydocker, which pipes docker logs output straight into a gocui View with no ANSI handling of its own: jesseduffield/lazydocker#525.

This PR makes the CSI state machine treat any CSI final byte (0x40-0x7e per ECMA-48) it doesn't specifically implement as a valid sequence to consume silently (matching how a real terminal emulator would swallow an unsupported-but-well-formed control sequence), and accepts the private-marker parameter prefix (0x3c-0x3f, e.g. ? in \x1b[?25l) so DEC private-mode sequences parse instead of erroring on their very first byte.

Root cause investigation

Confirmed via a headless-gocui repro (gocui.NewGuiOpts{Headless: true}, write raw ANSI strings to a View, read back View.Buffer()):

Input Before After
\x1b[32m... (SGR color) renders correctly renders correctly
\x1b[38;5;208m... (256-color) renders correctly renders correctly
\x1b[?25l (cursor hide) printed literally consumed
\x1b[1;1H (cursor position) printed literally consumed
\x1b[1A (cursor up) printed literally consumed
\x1b[2J (clear screen) printed literally consumed
\x1b[s / \x1b[u (save/restore cursor) printed literally consumed

Test plan

  • Added TestParseOneUnhandledCSISequencesAreConsumed in escape_test.go, watched it fail with errCSIParseError before the fix (RED), passes after (GREEN)
  • go test ./... , full suite passes
  • go vet ./... clean
  • gofmt -l . clean
  • go mod tidy , no diff (matches this repo's CI check)

…ally

escapeInterpreter.parseOne only recognized digits, SGR (m), and
erase-in-line (K) as valid CSI bytes. Any other valid CSI sequence -
cursor hide/show (DEC private mode), cursor position/movement, clear
screen, save/restore cursor - fell into the parse-error path, which
View.parseInput turns into literal escape bytes printed as text.

Real-world colorized output frequently mixes SGR color with these other
sequences (progress bars, spinners, dev-server reloaders), so users see
garbage interleaved with otherwise-correct color: the "container log
colors" symptom reported against lazydocker
(jesseduffield/lazydocker#525), which pipes docker logs straight into a
gocui View with no ANSI handling of its own.

Treat any CSI final byte (0x40-0x7e per ECMA-48) we don't specifically
implement as a valid sequence to consume silently, and accept the
private-marker parameter prefix (0x3c-0x3f, e.g. '?' in "\x1b[?25l") so
DEC private-mode sequences parse instead of erroring on their first
byte.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant