mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Silently consume unrecognized escape sequences
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>
This commit is contained in:
parent
8a8dacca14
commit
31ed34a421
|
|
@ -151,6 +151,12 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
|
|||
characterEquals(ch, '+'):
|
||||
ei.state = stateCharacterSetDesignation
|
||||
return true, nil
|
||||
case len(ch) == 1 && ch[0] >= 0x30 && ch[0] <= 0x7E:
|
||||
// Single-byte ESC sequence (e.g. ESC c = RIS). We don't
|
||||
// interpret these, but we must consume them so they don't
|
||||
// leak into the view as literal text.
|
||||
ei.state = stateNone
|
||||
return true, nil
|
||||
default:
|
||||
return false, errNotCSI
|
||||
}
|
||||
|
|
@ -166,6 +172,19 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
|
|||
ei.csiParam = append(ei.csiParam, "0")
|
||||
case characterEquals(ch, 'K'):
|
||||
// fall through
|
||||
case len(ch) == 1 && ch[0] >= 0x3C && ch[0] <= 0x3F:
|
||||
// Private-mode prefix byte (<, =, >, ?). We don't interpret
|
||||
// DEC private-mode sequences, but must consume them so they
|
||||
// don't leak into the view as literal text. Seed an empty
|
||||
// param so the subsequent digits land on a valid slot.
|
||||
ei.csiParam = append(ei.csiParam, "")
|
||||
ei.state = stateParams
|
||||
return true, nil
|
||||
case len(ch) == 1 && ch[0] >= 0x40 && ch[0] <= 0x7E:
|
||||
// Valid CSI final byte we don't implement — swallow.
|
||||
ei.state = stateNone
|
||||
ei.csiParam = nil
|
||||
return true, nil
|
||||
default:
|
||||
return false, errCSIParseError
|
||||
}
|
||||
|
|
@ -203,6 +222,12 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
|
|||
ei.instruction = noInstruction{}
|
||||
}
|
||||
|
||||
ei.state = stateNone
|
||||
ei.csiParam = nil
|
||||
return true, nil
|
||||
case len(ch) == 1 && ch[0] >= 0x40 && ch[0] <= 0x7E:
|
||||
// Valid CSI final byte we don't implement — swallow the
|
||||
// whole sequence rather than printing it as text.
|
||||
ei.state = stateNone
|
||||
ei.csiParam = nil
|
||||
return true, nil
|
||||
|
|
|
|||
|
|
@ -166,10 +166,7 @@ func TestParseOneIgnoresUnknownSequences(t *testing.T) {
|
|||
|
||||
for _, input := range scenarios {
|
||||
ei := newEscapeInterpreter(OutputNormal)
|
||||
/* EXPECTED:
|
||||
parseEscRunes(t, ei, input)
|
||||
ACTUAL: */
|
||||
parseEscRunesExpectingError(t, ei, input)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,13 +178,3 @@ func parseEscRunes(t *testing.T, ei *escapeInterpreter, runes string) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func parseEscRunesExpectingError(t *testing.T, ei *escapeInterpreter, runes string) {
|
||||
t.Helper()
|
||||
for _, b := range []byte(runes) {
|
||||
if _, err := ei.parseOne([]byte{b}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("expected a parse error for %q, got none", runes)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue