From ec365401f010ee8a6e45b124991b3fae2035f7ce Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 3 Oct 2026 11:59:35 +0900 Subject: [PATCH] Take the second chance only for an unfinished sequence The check looked at the last ESC in the buffer, not at the sequence just parsed. A dropped \e[97;5u followed by CTRL-G and ALT-[ in one read blocked until the next key, holding back the abort. Require both to be unfinished: an unfinished sequence followed by other bytes cannot complete either. Reported by Copilot on #4926. --- src/tui/light.go | 16 ++++++++++++---- src/tui/light_csi_test.go | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index 0ecbe48f..b5557dc4 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -444,6 +444,14 @@ func stringReply(buffer []byte) bool { return false } +// stillArriving reports whether the sequence that consumed sz bytes of the +// buffer may still be completed by more input. Both it and the buffer have to +// end unfinished: a complete sequence followed by ALT-[ is not waiting for +// anything, and an unfinished one followed by other bytes cannot complete. +func stillArriving(buffer []byte, sz int) bool { + return incompleteEscape(buffer[:min(sz, len(buffer))]) && incompleteEscape(buffer) +} + // incompleteEscape reports whether the buffer ends in an escape sequence that // has not been terminated yet. The read loop keeps waiting while it does, so a // fragment reaches the parser only once that wait has run out. @@ -578,10 +586,10 @@ func (r *LightRenderer) GetChar(cancellable bool) Event { return Event{CtrlSlash, 0, nil} case Esc.Byte(): ev := r.escSequence(&sz) - // Second chance, but only while the buffer ends in an unfinished - // sequence. Re-reading otherwise blocks until the next keystroke, - // holding back whatever follows in the buffer. - if ev.Type == Invalid && incompleteEscape(r.buffer) { + // Second chance, but only for an unfinished sequence. Re-reading + // otherwise blocks until the next keystroke, holding back whatever + // follows in the buffer. + if ev.Type == Invalid && stillArriving(r.buffer, sz) { r.buffer, result, err = r.getBytes(true) if err != nil { return Event{Fatal, 0, nil} diff --git a/src/tui/light_csi_test.go b/src/tui/light_csi_test.go index b702bb29..3a7ccb86 100644 --- a/src/tui/light_csi_test.go +++ b/src/tui/light_csi_test.go @@ -127,6 +127,28 @@ func TestStringEnd(t *testing.T) { } } +// A dropped sequence followed by ALT-[ is not waiting for anything, so the +// bytes in between must not be held back until the next keystroke +func TestStillArriving(t *testing.T) { + for _, c := range []struct { + buffer string + sz int + want bool + }{ + {"\x1b[97;5u\a\x1b[", 7, false}, + {"\x1b[97;5u\x1b[", 7, false}, + {"\x1b[1;\a", 4, false}, // the BEL has already ended it + {"\x1b[1;", 4, true}, + {"\x1b[", 2, true}, + {"\x1bO1", 3, true}, + {"\x1b[<0;1", 3, true}, + } { + if got := stillArriving([]byte(c.buffer), c.sz); got != c.want { + t.Errorf("stillArriving(%q, %d) = %v, want %v", c.buffer, c.sz, got, c.want) + } + } +} + func TestStringReply(t *testing.T) { for _, c := range []struct { buffer string