fix(screen): restore charsets only on 1049 and end REP after fragmented Kitty APCs
- commit
- 62093707819230e52a827c950cefcb12d3295eab
- author
- brice · · 2026-10-15 02:38 UTC
- parent
- 1589bdf
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +23 -5
M screen.go +3 -0
| @@ -184,6 +184,8 @@ func (s *Screen) Write(data []byte) { | |||
| 184 | 184 | if len(s.escapeBuf) == 0 { | |
| 185 | 185 | s.trimEscapeBuffers() | |
| 186 | 186 | } else if isKittyEscapeContinuation(s.escapeBuf, data) { | |
| 187 | + | // A Kitty APC sits between any earlier graphic and what follows. | |
| 188 | + | s.lastPrinted = 0 | |
| 187 | 189 | prefix := s.escapeBuf | |
| 188 | 190 | s.escapeBuf = nil | |
| 189 | 191 | data = s.continueKittyEscape(prefix, data) | |
| @@ -218,6 +220,7 @@ func (s *Screen) Write(data []byte) { | |||
| 218 | 220 | } | |
| 219 | 221 | } | |
| 220 | 222 | if s.kittyDiscard { | |
| 223 | + | s.lastPrinted = 0 | |
| 221 | 224 | consumed := s.consumeKittyDiscard(data) | |
| 222 | 225 | data = data[consumed:] | |
| 223 | 226 | if s.kittyDiscard { | |
| @@ -184,6 +184,8 @@ func (s *Screen) Write(data []byte) { | |||||
| 184 | if len(s.escapeBuf) == 0 { | 184 | if len(s.escapeBuf) == 0 { | ||
| 185 | s.trimEscapeBuffers() | 185 | s.trimEscapeBuffers() | ||
| 186 | } else if isKittyEscapeContinuation(s.escapeBuf, data) { | 186 | } else if isKittyEscapeContinuation(s.escapeBuf, data) { | ||
| 187 | + | // A Kitty APC sits between any earlier graphic and what follows. | |||
| 188 | + | s.lastPrinted = 0 | |||
| 187 | prefix := s.escapeBuf | 189 | prefix := s.escapeBuf | ||
| 188 | s.escapeBuf = nil | 190 | s.escapeBuf = nil | ||
| 189 | data = s.continueKittyEscape(prefix, data) | 191 | data = s.continueKittyEscape(prefix, data) | ||
| @@ -218,6 +220,7 @@ func (s *Screen) Write(data []byte) { | |||||
| 218 | } | 220 | } | ||
| 219 | } | 221 | } | ||
| 220 | if s.kittyDiscard { | 222 | if s.kittyDiscard { | ||
| 223 | + | s.lastPrinted = 0 | |||
| 221 | consumed := s.consumeKittyDiscard(data) | 224 | consumed := s.consumeKittyDiscard(data) | ||
| 222 | data = data[consumed:] | 225 | data = data[consumed:] | ||
| 223 | if s.kittyDiscard { | 226 | if s.kittyDiscard { | ||
M screen_charset_test.go +13 -1
| @@ -25,7 +25,9 @@ func TestCharsetsAndRepeat(t *testing.T) { | |||
| 25 | 25 | {name: "REP after another sequence is ignored", input: "a\x1b[1m\x1b[2b", want: "a"}, | |
| 26 | 26 | {name: "consecutive REP repeats once", input: "a\x1b[b\x1b[b", want: "aa"}, | |
| 27 | 27 | {name: "DECSC and DECRC restore SO invocation", input: "\x1b)0\x0e\x1b7\x0f\x1b8q", want: "─"}, | |
| 28 | - | {name: "leaving the alternate screen restores charsets", input: "\x1b(0\x1b[?1049h\x1b(B\x1b[?1049lq", want: "─"}, | |
| 28 | + | {name: "leaving 1049 restores charsets", input: "\x1b(0\x1b[?1049h\x1b(B\x1b[?1049lq", want: "─"}, | |
| 29 | + | {name: "leaving 47 keeps current charsets", input: "\x1b(0\x1b[?47h\x1b(B\x1b[?47lq", want: "q"}, | |
| 30 | + | {name: "leaving 1047 keeps current charsets", input: "\x1b(0\x1b[?1047h\x1b(B\x1b[?1047lq", want: "q"}, | |
| 29 | 31 | {name: "DECSTR saved cursor restores home", input: "\x1b[1;5Hx\x1b[!p\x1b8y", want: "y x"}, | |
| 30 | 32 | {name: "partial charset escape across writes", input: "\x1b(", want: ""}, | |
| 31 | 33 | } | |
| @@ -66,6 +68,16 @@ func TestRepeatSplitAcrossWrites(t *testing.T) { | |||
| 66 | 68 | } | |
| 67 | 69 | } | |
| 68 | 70 | ||
| 71 | + | func TestRepeatAfterFragmentedKittyAPCIsIgnored(t *testing.T) { | |
| 72 | + | s := NewScreen(10, 1) | |
| 73 | + | s.Write([]byte("a\x1b_G")) | |
| 74 | + | s.Write([]byte("a=q,i=1;\x1b\\")) | |
| 75 | + | s.Write([]byte("\x1b[2b")) | |
| 76 | + | if got := strings.TrimRight(lineText(s, 0), " "); got != "a" { | |
| 77 | + | t.Fatalf("line = %q, want %q", got, "a") | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | ||
| 69 | 81 | func TestRepeatIsBounded(t *testing.T) { | |
| 70 | 82 | s := NewScreen(4, 2) | |
| 71 | 83 | s.Write([]byte("x\x1b[999999999b")) | |
| @@ -25,7 +25,9 @@ func TestCharsetsAndRepeat(t *testing.T) { | |||||
| 25 | {name: "REP after another sequence is ignored", input: "a\x1b[1m\x1b[2b", want: "a"}, | 25 | {name: "REP after another sequence is ignored", input: "a\x1b[1m\x1b[2b", want: "a"}, | ||
| 26 | {name: "consecutive REP repeats once", input: "a\x1b[b\x1b[b", want: "aa"}, | 26 | {name: "consecutive REP repeats once", input: "a\x1b[b\x1b[b", want: "aa"}, | ||
| 27 | {name: "DECSC and DECRC restore SO invocation", input: "\x1b)0\x0e\x1b7\x0f\x1b8q", want: "─"}, | 27 | {name: "DECSC and DECRC restore SO invocation", input: "\x1b)0\x0e\x1b7\x0f\x1b8q", want: "─"}, | ||
| 28 | - | {name: "leaving the alternate screen restores charsets", input: "\x1b(0\x1b[?1049h\x1b(B\x1b[?1049lq", want: "─"}, | 28 | + | {name: "leaving 1049 restores charsets", input: "\x1b(0\x1b[?1049h\x1b(B\x1b[?1049lq", want: "─"}, |
| 29 | + | {name: "leaving 47 keeps current charsets", input: "\x1b(0\x1b[?47h\x1b(B\x1b[?47lq", want: "q"}, | |||
| 30 | + | {name: "leaving 1047 keeps current charsets", input: "\x1b(0\x1b[?1047h\x1b(B\x1b[?1047lq", want: "q"}, | |||
| 29 | {name: "DECSTR saved cursor restores home", input: "\x1b[1;5Hx\x1b[!p\x1b8y", want: "y x"}, | 31 | {name: "DECSTR saved cursor restores home", input: "\x1b[1;5Hx\x1b[!p\x1b8y", want: "y x"}, | ||
| 30 | {name: "partial charset escape across writes", input: "\x1b(", want: ""}, | 32 | {name: "partial charset escape across writes", input: "\x1b(", want: ""}, | ||
| 31 | } | 33 | } | ||
| @@ -66,6 +68,16 @@ func TestRepeatSplitAcrossWrites(t *testing.T) { | |||||
| 66 | } | 68 | } | ||
| 67 | } | 69 | } | ||
| 68 | 70 | ||||
| 71 | + | func TestRepeatAfterFragmentedKittyAPCIsIgnored(t *testing.T) { | |||
| 72 | + | s := NewScreen(10, 1) | |||
| 73 | + | s.Write([]byte("a\x1b_G")) | |||
| 74 | + | s.Write([]byte("a=q,i=1;\x1b\\")) | |||
| 75 | + | s.Write([]byte("\x1b[2b")) | |||
| 76 | + | if got := strings.TrimRight(lineText(s, 0), " "); got != "a" { | |||
| 77 | + | t.Fatalf("line = %q, want %q", got, "a") | |||
| 78 | + | } | |||
| 79 | + | } | |||
| 80 | + | ||||
| 69 | func TestRepeatIsBounded(t *testing.T) { | 81 | func TestRepeatIsBounded(t *testing.T) { | ||
| 70 | s := NewScreen(4, 2) | 82 | s := NewScreen(4, 2) | ||
| 71 | s.Write([]byte("x\x1b[999999999b")) | 83 | s.Write([]byte("x\x1b[999999999b")) | ||
M screen_modes.go +7 -4
| @@ -211,7 +211,8 @@ func (s *Screen) setMode(private bool, parts []int, enabled bool) { | |||
| 211 | 211 | if enabled { | |
| 212 | 212 | s.enterAlternateScreen() | |
| 213 | 213 | } else { | |
| 214 | - | s.exitAlternateScreen() | |
| 214 | + | // Only 1049 saves and restores cursor state, which includes charsets. | |
| 215 | + | s.exitAlternateScreen(mode == 1049) | |
| 215 | 216 | } | |
| 216 | 217 | case 2026: | |
| 217 | 218 | s.syncUpdateActive = enabled | |
| @@ -269,7 +270,7 @@ func (s *Screen) enterAlternateScreen() { | |||
| 269 | 270 | s.fullRedraw() | |
| 270 | 271 | } | |
| 271 | 272 | ||
| 272 | - | func (s *Screen) exitAlternateScreen() { | |
| 273 | + | func (s *Screen) exitAlternateScreen(restoreCharsets bool) { | |
| 273 | 274 | if s.alternate == nil { | |
| 274 | 275 | return | |
| 275 | 276 | } | |
| @@ -287,8 +288,10 @@ func (s *Screen) exitAlternateScreen() { | |||
| 287 | 288 | s.originMode = state.originMode | |
| 288 | 289 | s.insertMode = state.insertMode | |
| 289 | 290 | s.kittyKeyboard = state.kittyKeyboard | |
| 290 | - | s.charsets = state.charsets | |
| 291 | - | s.shiftOut = state.shiftOut | |
| 291 | + | if restoreCharsets { | |
| 292 | + | s.charsets = state.charsets | |
| 293 | + | s.shiftOut = state.shiftOut | |
| 294 | + | } | |
| 292 | 295 | s.alternate = nil | |
| 293 | 296 | s.fullRedraw() | |
| 294 | 297 | } | |
| @@ -211,7 +211,8 @@ func (s *Screen) setMode(private bool, parts []int, enabled bool) { | |||||
| 211 | if enabled { | 211 | if enabled { | ||
| 212 | s.enterAlternateScreen() | 212 | s.enterAlternateScreen() | ||
| 213 | } else { | 213 | } else { | ||
| 214 | - | s.exitAlternateScreen() | 214 | + | // Only 1049 saves and restores cursor state, which includes charsets. |
| 215 | + | s.exitAlternateScreen(mode == 1049) | |||
| 215 | } | 216 | } | ||
| 216 | case 2026: | 217 | case 2026: | ||
| 217 | s.syncUpdateActive = enabled | 218 | s.syncUpdateActive = enabled | ||
| @@ -269,7 +270,7 @@ func (s *Screen) enterAlternateScreen() { | |||||
| 269 | s.fullRedraw() | 270 | s.fullRedraw() | ||
| 270 | } | 271 | } | ||
| 271 | 272 | ||||
| 272 | - | func (s *Screen) exitAlternateScreen() { | 273 | + | func (s *Screen) exitAlternateScreen(restoreCharsets bool) { |
| 273 | if s.alternate == nil { | 274 | if s.alternate == nil { | ||
| 274 | return | 275 | return | ||
| 275 | } | 276 | } | ||
| @@ -287,8 +288,10 @@ func (s *Screen) exitAlternateScreen() { | |||||
| 287 | s.originMode = state.originMode | 288 | s.originMode = state.originMode | ||
| 288 | s.insertMode = state.insertMode | 289 | s.insertMode = state.insertMode | ||
| 289 | s.kittyKeyboard = state.kittyKeyboard | 290 | s.kittyKeyboard = state.kittyKeyboard | ||
| 290 | - | s.charsets = state.charsets | 291 | + | if restoreCharsets { |
| 291 | - | s.shiftOut = state.shiftOut | 292 | + | s.charsets = state.charsets |
| 293 | + | s.shiftOut = state.shiftOut | |||
| 294 | + | } | |||
| 292 | s.alternate = nil | 295 | s.alternate = nil | ||
| 293 | s.fullRedraw() | 296 | s.fullRedraw() | ||
| 294 | } | 297 | } | ||