ConPTY compresses runs of default-colored spaces into ECH + CUF
(\x1b[NX\x1b[NC) instead of emitting them literally. ECH is still a
no-op for us — our buffer is built sequentially and has nothing to
erase — but CUF has to materialize as N visible space cells so the
gap actually appears, otherwise content the child wrote with leading
indentation slides left against the preceding cell.
The view's cursorForward branch reuses the same machinery as tab
expansion: substitute the trigger byte for a space and let the
repeatCount path emit the cells under the parser-tracked SGR. The
existing notifyCellsWritten plumbing then advances screenCol over
the gap, keeping subsequent CUP targets aligned.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ConPTY presents its child's output as a screen buffer and uses CUP /
CUD / CNL / VPA to skip over blank rows rather than emitting LFs. The
previous behaviour swallowed all of those and the visible content
collapsed together. Now the escape parser tracks the screen-relative
cursor row, and any CSI that moves the cursor past the current row
emits a cursorDown instruction that the view turns into the matching
number of empty lines.
Column tracking is deliberately omitted: doing it correctly would mean
duplicating the view's grapheme-cluster width math in the parser, and
ConPTY in practice positions to column 1 after a CR-equivalent, which
the existing wx-reset path already handles. ConPTY-internal scrolling
needs no special handling either: it only emits cursor-positioning
escapes within the first, un-scrolled screenful — once its screen
scrolls it switches to plain linefeeds, which the view advances on
directly regardless of the tracked cursor.
Backward cursor moves are silently dropped — the view's buffer is
append-style and can't undo earlier writes. The exception is cursor-home
(CUP to row 1): ConPTY emits it at the start of every screen, so rather
than drop it we re-anchor the row tracking to the current write position.
Without that, a view not rewound in lockstep with ConPTY's screen (the
command log, which streams pty output without a rewind) accumulates
drift, and every later absolute CUP becomes a dropped backward move that
collapses the rows ConPTY positioned with.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the previous commit, the escape interpreter still had five paths
that returned an error from parseOne, which view.go handles by rendering
whatever bytes it had accumulated as literal cells. Each of these is a
case where silently consuming the sequence is strictly better than
leaking garbage.
- ';' as the first CSI byte: '\x1b[;5H' is a valid sequence (row
defaults to 1) but we errored on the leading ';'.
- Intermediate bytes in CSI ('\x1b[0 q' = DECSCUSR): the sequence ends
in a final byte we don't implement, so consume and drop.
- Malformed SGR params (empty slot like '\x1b[1;;m'): if outputCSI
fails mid-parse, reset state instead of re-emitting the sequence.
- OSC 8 that isn't actually OSC 8 ('\x1b]8x...'): treat as an OSC we
don't understand and skip to its terminator rather than error-
resetting mid-sequence, which used to leave the rest of the OSC body
to be printed as text.
- The sanity-check overflow paths (too many params, param too long)
now switch to a 'discard until final byte' state rather than
returning the accumulated bytes.
A new stateCSIDiscard centralizes the 'consume bytes until the CSI
final' behavior used by both the intermediate-byte and overflow paths.
errCSITooLong and errOSCParseError are gone with their only callers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A text-mode escape interpreter can't do anything meaningful with cursor
positioning, DEC private modes, or terminal resets — but it must still
consume them, not print them as literal text. Before this change, any
sequence outside SGR / EL / OSC-8 errored out of parseOne, and view.go
rendered the unparsed bytes as visible cells. On Windows this would show
up as junk at the start of main-panel output once we add PTY support
using ConPTY, because ConPTY's session-init stream is full of such
sequences.
Three additions to the state machine:
- stateEscape: a single byte in 0x30–0x7E after ESC (e.g. ESC c = RIS)
is a complete Fs/Fp sequence per ECMA-48; consume and reset.
- stateCSI: accept the DEC private-mode prefix bytes (<, =, >, ?), and
accept a CSI final byte (0x40–0x7E) immediately after [ as the end of
a zero-param sequence.
- stateParams: accept any CSI final byte we don't implement as the end
of the sequence rather than a parse error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The escape interpreter errors on anything outside the handful of
sequences it understands (SGR, EL, OSC 8 hyperlinks), and view.go then
renders the unparsed bytes as text cells.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
I copied all files except dot files (.github and .gitignore), the _examples
folder, and go.mod/go.sum.
At some point we may want to copy the files back to the gocui repo when other
clients (e.g. lazydocker) want to use the newer versions of them.