fix(keys): pass a legacy Escape before the kitty switch and keep lock bits on CSI u keys
Hold a lone trailing ESC only once a CSI u key event proved the terminal encodes Escape as CSI 27 u, stop normalizing when the flag push fails, and keep lock bits on every key sent as an escape code.
- commit
- 83ef12902012f505d98906534641c43e6a734ee4
- author
- brice · · 2026-10-15 03:35 UTC
- parent
- a9ec34a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +27 -9
M internal/usecase/client/supervisor.go +1 -0
| @@ -1726,6 +1726,7 @@ func (s *Supervisor) detectTerminalCapabilities(ctx context.Context, pump *termi | |||
| 1726 | 1726 | // Normalize before the terminal switches, so no read escapes it. | |
| 1727 | 1727 | pump.keys.enable() | |
| 1728 | 1728 | if err := s.cfg.Terminal.EnableKittyKeyboard(kittykey.RequestFlags); err != nil { | |
| 1729 | + | pump.keys.disable() | |
| 1729 | 1730 | s.logger.Warn("enabling kitty keyboard protocol failed", "err", err) | |
| 1730 | 1731 | caps.KittyKeyboard = false | |
| 1731 | 1732 | } | |
| @@ -1726,6 +1726,7 @@ func (s *Supervisor) detectTerminalCapabilities(ctx context.Context, pump *termi | |||||
| 1726 | // Normalize before the terminal switches, so no read escapes it. | 1726 | // Normalize before the terminal switches, so no read escapes it. | ||
| 1727 | pump.keys.enable() | 1727 | pump.keys.enable() | ||
| 1728 | if err := s.cfg.Terminal.EnableKittyKeyboard(kittykey.RequestFlags); err != nil { | 1728 | if err := s.cfg.Terminal.EnableKittyKeyboard(kittykey.RequestFlags); err != nil { | ||
| 1729 | + | pump.keys.disable() | |||
| 1729 | s.logger.Warn("enabling kitty keyboard protocol failed", "err", err) | 1730 | s.logger.Warn("enabling kitty keyboard protocol failed", "err", err) | ||
| 1730 | caps.KittyKeyboard = false | 1731 | caps.KittyKeyboard = false | ||
| 1731 | } | 1732 | } | ||
M internal/usecase/client/terminal_keys.go +3 -0
| @@ -39,6 +39,9 @@ func newTerminalKeys() *terminalKeys { | |||
| 39 | 39 | // kittykey.RequestFlags; input still in the old encoding passes unchanged. | |
| 40 | 40 | func (k *terminalKeys) enable() { k.enabled.Store(true) } | |
| 41 | 41 | ||
| 42 | + | // disable stops normalizing, for a terminal that refused the flags. | |
| 43 | + | func (k *terminalKeys) disable() { k.enabled.Store(false) } | |
| 44 | + | ||
| 42 | 45 | // normalize rewrites one read. focus is the terminal focus after the read: | |
| 43 | 46 | // a window that lost focus may never report the Ctrl release. | |
| 44 | 47 | func (k *terminalKeys) normalize(data []byte, focus domain.TerminalFocus) []byte { | |
| @@ -39,6 +39,9 @@ func newTerminalKeys() *terminalKeys { | |||||
| 39 | // kittykey.RequestFlags; input still in the old encoding passes unchanged. | 39 | // kittykey.RequestFlags; input still in the old encoding passes unchanged. | ||
| 40 | func (k *terminalKeys) enable() { k.enabled.Store(true) } | 40 | func (k *terminalKeys) enable() { k.enabled.Store(true) } | ||
| 41 | 41 | ||||
| 42 | + | // disable stops normalizing, for a terminal that refused the flags. | |||
| 43 | + | func (k *terminalKeys) disable() { k.enabled.Store(false) } | |||
| 44 | + | ||||
| 42 | // normalize rewrites one read. focus is the terminal focus after the read: | 45 | // normalize rewrites one read. focus is the terminal focus after the read: | ||
| 43 | // a window that lost focus may never report the Ctrl release. | 46 | // a window that lost focus may never report the Ctrl release. | ||
| 44 | func (k *terminalKeys) normalize(data []byte, focus domain.TerminalFocus) []byte { | 47 | func (k *terminalKeys) normalize(data []byte, focus domain.TerminalFocus) []byte { | ||
M internal/usecase/keys/kittykey/normalize.go +13 -6
| @@ -28,14 +28,17 @@ const ( | |||
| 28 | 28 | // | |
| 29 | 29 | // Input already in the OuterFlags or legacy encoding passes through | |
| 30 | 30 | // unchanged, so enabling a Normalizer before the terminal switches flags is | |
| 31 | - | // safe. A trailing incomplete escape sequence, a lone ESC included, is held | |
| 32 | - | // until the next call: with FlagDisambiguate the Escape key itself arrives as | |
| 33 | - | // CSI 27 u, so a lone ESC only ends a read split inside a sequence. | |
| 31 | + | // safe. A trailing incomplete escape sequence is held until the next call. A | |
| 32 | + | // lone trailing ESC is held only once a CSI u key event proved the terminal | |
| 33 | + | // reports Escape as CSI 27 u; before that it may be a real Escape press. | |
| 34 | 34 | // A Normalizer is not safe for concurrent use. | |
| 35 | 35 | type Normalizer struct { | |
| 36 | 36 | pending []byte | |
| 37 | 37 | paste bool | |
| 38 | 38 | ctrl bool | |
| 39 | + | // kitty records that a CSI u key event arrived, so the terminal encodes | |
| 40 | + | // Escape itself as one and a lone ESC can only start a split sequence. | |
| 41 | + | kitty bool | |
| 39 | 42 | } | |
| 40 | 43 | ||
| 41 | 44 | // CtrlHeld reports whether Ctrl is held on its own: a Ctrl key is down and no | |
| @@ -82,6 +85,10 @@ func (n *Normalizer) Normalize(data []byte) []byte { | |||
| 82 | 85 | continue | |
| 83 | 86 | } | |
| 84 | 87 | params, final, size, partial := scanCSI(rest) | |
| 88 | + | if partial && len(rest) == 1 && !n.kitty { | |
| 89 | + | out = append(out, rest...) | |
| 90 | + | return out | |
| 91 | + | } | |
| 85 | 92 | if partial { | |
| 86 | 93 | n.hold(rest) | |
| 87 | 94 | return out | |
| @@ -148,6 +155,7 @@ func (n *Normalizer) rewrite(params []byte, final byte) ([]byte, bool) { | |||
| 148 | 155 | if !ok { | |
| 149 | 156 | return nil, false | |
| 150 | 157 | } | |
| 158 | + | n.kitty = true | |
| 151 | 159 | n.track(key.ev, true) | |
| 152 | 160 | return encodeOuter(key), true | |
| 153 | 161 | case '~', 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'S': | |
| @@ -225,10 +233,9 @@ func encodeOuter(key fullKey) []byte { | |||
| 225 | 233 | return []byte{byte(ev.Code)} | |
| 226 | 234 | case ev.Mods&^ModShift == 0 && isTextKey(ev.Code): | |
| 227 | 235 | return legacyText(ev) | |
| 228 | - | case !isTextKey(ev.Code): | |
| 229 | - | // Lock bits are reported for keys that do not produce text. | |
| 230 | - | ev.Mods |= key.locks | |
| 231 | 236 | } | |
| 237 | + | // A key sent as an escape code reports the lock bits too. | |
| 238 | + | ev.Mods |= key.locks | |
| 232 | 239 | return encodeCSIu(ev, OuterFlags) | |
| 233 | 240 | } | |
| 234 | 241 | ||
| @@ -28,14 +28,17 @@ const ( | |||||
| 28 | // | 28 | // | ||
| 29 | // Input already in the OuterFlags or legacy encoding passes through | 29 | // Input already in the OuterFlags or legacy encoding passes through | ||
| 30 | // unchanged, so enabling a Normalizer before the terminal switches flags is | 30 | // unchanged, so enabling a Normalizer before the terminal switches flags is | ||
| 31 | - | // safe. A trailing incomplete escape sequence, a lone ESC included, is held | 31 | + | // safe. A trailing incomplete escape sequence is held until the next call. A |
| 32 | - | // until the next call: with FlagDisambiguate the Escape key itself arrives as | 32 | + | // lone trailing ESC is held only once a CSI u key event proved the terminal |
| 33 | - | // CSI 27 u, so a lone ESC only ends a read split inside a sequence. | 33 | + | // reports Escape as CSI 27 u; before that it may be a real Escape press. |
| 34 | // A Normalizer is not safe for concurrent use. | 34 | // A Normalizer is not safe for concurrent use. | ||
| 35 | type Normalizer struct { | 35 | type Normalizer struct { | ||
| 36 | pending []byte | 36 | pending []byte | ||
| 37 | paste bool | 37 | paste bool | ||
| 38 | ctrl bool | 38 | ctrl bool | ||
| 39 | + | // kitty records that a CSI u key event arrived, so the terminal encodes | |||
| 40 | + | // Escape itself as one and a lone ESC can only start a split sequence. | |||
| 41 | + | kitty bool | |||
| 39 | } | 42 | } | ||
| 40 | 43 | ||||
| 41 | // CtrlHeld reports whether Ctrl is held on its own: a Ctrl key is down and no | 44 | // CtrlHeld reports whether Ctrl is held on its own: a Ctrl key is down and no | ||
| @@ -82,6 +85,10 @@ func (n *Normalizer) Normalize(data []byte) []byte { | |||||
| 82 | continue | 85 | continue | ||
| 83 | } | 86 | } | ||
| 84 | params, final, size, partial := scanCSI(rest) | 87 | params, final, size, partial := scanCSI(rest) | ||
| 88 | + | if partial && len(rest) == 1 && !n.kitty { | |||
| 89 | + | out = append(out, rest...) | |||
| 90 | + | return out | |||
| 91 | + | } | |||
| 85 | if partial { | 92 | if partial { | ||
| 86 | n.hold(rest) | 93 | n.hold(rest) | ||
| 87 | return out | 94 | return out | ||
| @@ -148,6 +155,7 @@ func (n *Normalizer) rewrite(params []byte, final byte) ([]byte, bool) { | |||||
| 148 | if !ok { | 155 | if !ok { | ||
| 149 | return nil, false | 156 | return nil, false | ||
| 150 | } | 157 | } | ||
| 158 | + | n.kitty = true | |||
| 151 | n.track(key.ev, true) | 159 | n.track(key.ev, true) | ||
| 152 | return encodeOuter(key), true | 160 | return encodeOuter(key), true | ||
| 153 | case '~', 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'S': | 161 | case '~', 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'S': | ||
| @@ -225,10 +233,9 @@ func encodeOuter(key fullKey) []byte { | |||||
| 225 | return []byte{byte(ev.Code)} | 233 | return []byte{byte(ev.Code)} | ||
| 226 | case ev.Mods&^ModShift == 0 && isTextKey(ev.Code): | 234 | case ev.Mods&^ModShift == 0 && isTextKey(ev.Code): | ||
| 227 | return legacyText(ev) | 235 | return legacyText(ev) | ||
| 228 | - | case !isTextKey(ev.Code): | |||
| 229 | - | // Lock bits are reported for keys that do not produce text. | |||
| 230 | - | ev.Mods |= key.locks | |||
| 231 | } | 236 | } | ||
| 237 | + | // A key sent as an escape code reports the lock bits too. | |||
| 238 | + | ev.Mods |= key.locks | |||
| 232 | return encodeCSIu(ev, OuterFlags) | 239 | return encodeCSIu(ev, OuterFlags) | ||
| 233 | } | 240 | } | ||
| 234 | 241 | ||||
M internal/usecase/keys/kittykey/normalize_test.go +10 -3
| @@ -36,7 +36,8 @@ func TestNormalizerRewritesRequestFlagsToOuterFlags(t *testing.T) { | |||
| 36 | 36 | {name: "altgr text wins over modifiers", in: "\x1b[113;7;64u", want: "@"}, | |
| 37 | 37 | {name: "long ime commit", in: "\x1b[0;;" + strings.Repeat("19968:", 11) + "19968u", want: strings.Repeat("一", 12)}, | |
| 38 | 38 | {name: "caps lock dropped from text key", in: "\x1b[97;65;65u", want: "A"}, | |
| 39 | - | {name: "caps lock kept on ctrl letter", in: "\x1b[97;69u", want: "\x1b[97;5u"}, | |
| 39 | + | {name: "caps lock kept on ctrl letter", in: "\x1b[97;69u", want: "\x1b[97;69u"}, | |
| 40 | + | {name: "both locks kept on alt letter", in: "\x1b[97;195u", want: "\x1b[97;195u"}, | |
| 40 | 41 | {name: "num lock kept on escape", in: "\x1b[27;129u", want: "\x1b[27;129u"}, | |
| 41 | 42 | {name: "caps lock kept on arrow", in: "\x1b[1;65A", want: "\x1b[1;65A"}, | |
| 42 | 43 | {name: "caps lock arrow repeat", in: "\x1b[1;65:2A", want: "\x1b[1;65A"}, | |
| @@ -57,7 +58,8 @@ func TestNormalizerRewritesRequestFlagsToOuterFlags(t *testing.T) { | |||
| 57 | 58 | {name: "keyboard reply untouched", in: "\x1b[?5u", want: "\x1b[?5u"}, | |
| 58 | 59 | {name: "cursor report untouched", in: "\x1b[12;40R", want: "\x1b[12;40R"}, | |
| 59 | 60 | {name: "paste body untouched", in: "\x1b[200~\x1b[97;1:3u\x1b[201~\x1b[97;1:3u", want: "\x1b[200~\x1b[97;1:3u\x1b[201~"}, | |
| 60 | - | {name: "lone esc held", in: "x\x1b", want: "x"}, | |
| 61 | + | {name: "lone esc passes before any kitty key", in: "x\x1b", want: "x\x1b"}, | |
| 62 | + | {name: "lone esc held after a kitty key", in: "\x1b[97;;97u\x1b", want: "a"}, | |
| 61 | 63 | {name: "trailing garbage", in: "\x1b[97;;97u\x1b[zz", want: "a\x1b[zz"}, | |
| 62 | 64 | {name: "bad event type kept", in: "\x1b[97;1:4u", want: "\x1b[97;1:4u"}, | |
| 63 | 65 | } | |
| @@ -78,7 +80,8 @@ func TestNormalizerHoldsSplitSequences(t *testing.T) { | |||
| 78 | 80 | {name: "split csi u", reads: []string{"a\x1b[97;", ";98u"}, want: []string{"a", "b"}}, | |
| 79 | 81 | {name: "split csi prefix", reads: []string{"\x1b[", "57442;1:3u"}, want: []string{"", ""}}, | |
| 80 | 82 | {name: "split paste close", reads: []string{"\x1b[200~x\x1b[20", "1~\x1b[97;;97u"}, want: []string{"\x1b[200~x", "\x1b[201~a"}}, | |
| 81 | - | {name: "split after esc", reads: []string{"\x1b", "[57442;5u", "\x1b", "[97;;97u"}, want: []string{"", "", "", "a"}}, | |
| 83 | + | {name: "split after esc", reads: []string{"\x1b[27u\x1b", "[57442;5u", "\x1b", "[97;;97u"}, want: []string{"\x1b[27u", "", "", "a"}}, | |
| 84 | + | {name: "legacy escape before the switch", reads: []string{"\x1b", "\x1b[97;;97u"}, want: []string{"\x1b", "a"}}, | |
| 82 | 85 | {name: "split paste open", reads: []string{"\x1b[20", "0~\x1b[97;1:3u\x1b[201~"}, want: []string{"", "\x1b[200~\x1b[97;1:3u\x1b[201~"}}, | |
| 83 | 86 | } | |
| 84 | 87 | for _, tt := range tests { | |
| @@ -94,6 +97,8 @@ func TestNormalizerHoldsSplitSequences(t *testing.T) { | |||
| 94 | 97 | // TestNormalizerEverySplitMatchesOneRead feeds each input split at every byte | |
| 95 | 98 | // and requires the same output and Ctrl state as one read. | |
| 96 | 99 | func TestNormalizerEverySplitMatchesOneRead(t *testing.T) { | |
| 100 | + | // Each input starts with a kitty key, as a terminal in RequestFlags mode | |
| 101 | + | // sends before any split can matter. | |
| 97 | 102 | inputs := []string{ | |
| 98 | 103 | "\x1b[57442;5u\x1b[49;5u\x1b[49;5:3u", | |
| 99 | 104 | "a\x1b[97;;97u\x1b[57442;1:3u", | |
| @@ -102,9 +107,11 @@ func TestNormalizerEverySplitMatchesOneRead(t *testing.T) { | |||
| 102 | 107 | } | |
| 103 | 108 | for _, in := range inputs { | |
| 104 | 109 | var whole Normalizer | |
| 110 | + | whole.Normalize([]byte("\x1b[27u")) | |
| 105 | 111 | want := string(whole.Normalize([]byte(in))) | |
| 106 | 112 | for cut := 1; cut < len(in); cut++ { | |
| 107 | 113 | var n Normalizer | |
| 114 | + | n.Normalize([]byte("\x1b[27u")) | |
| 108 | 115 | got := string(n.Normalize([]byte(in[:cut]))) + string(n.Normalize([]byte(in[cut:]))) | |
| 109 | 116 | require.Equal(t, want, got, "input %q cut %d", in, cut) | |
| 110 | 117 | require.Equal(t, whole.CtrlHeld(), n.CtrlHeld(), "input %q cut %d", in, cut) | |
| @@ -36,7 +36,8 @@ func TestNormalizerRewritesRequestFlagsToOuterFlags(t *testing.T) { | |||||
| 36 | {name: "altgr text wins over modifiers", in: "\x1b[113;7;64u", want: "@"}, | 36 | {name: "altgr text wins over modifiers", in: "\x1b[113;7;64u", want: "@"}, | ||
| 37 | {name: "long ime commit", in: "\x1b[0;;" + strings.Repeat("19968:", 11) + "19968u", want: strings.Repeat("一", 12)}, | 37 | {name: "long ime commit", in: "\x1b[0;;" + strings.Repeat("19968:", 11) + "19968u", want: strings.Repeat("一", 12)}, | ||
| 38 | {name: "caps lock dropped from text key", in: "\x1b[97;65;65u", want: "A"}, | 38 | {name: "caps lock dropped from text key", in: "\x1b[97;65;65u", want: "A"}, | ||
| 39 | - | {name: "caps lock kept on ctrl letter", in: "\x1b[97;69u", want: "\x1b[97;5u"}, | 39 | + | {name: "caps lock kept on ctrl letter", in: "\x1b[97;69u", want: "\x1b[97;69u"}, |
| 40 | + | {name: "both locks kept on alt letter", in: "\x1b[97;195u", want: "\x1b[97;195u"}, | |||
| 40 | {name: "num lock kept on escape", in: "\x1b[27;129u", want: "\x1b[27;129u"}, | 41 | {name: "num lock kept on escape", in: "\x1b[27;129u", want: "\x1b[27;129u"}, | ||
| 41 | {name: "caps lock kept on arrow", in: "\x1b[1;65A", want: "\x1b[1;65A"}, | 42 | {name: "caps lock kept on arrow", in: "\x1b[1;65A", want: "\x1b[1;65A"}, | ||
| 42 | {name: "caps lock arrow repeat", in: "\x1b[1;65:2A", want: "\x1b[1;65A"}, | 43 | {name: "caps lock arrow repeat", in: "\x1b[1;65:2A", want: "\x1b[1;65A"}, | ||
| @@ -57,7 +58,8 @@ func TestNormalizerRewritesRequestFlagsToOuterFlags(t *testing.T) { | |||||
| 57 | {name: "keyboard reply untouched", in: "\x1b[?5u", want: "\x1b[?5u"}, | 58 | {name: "keyboard reply untouched", in: "\x1b[?5u", want: "\x1b[?5u"}, | ||
| 58 | {name: "cursor report untouched", in: "\x1b[12;40R", want: "\x1b[12;40R"}, | 59 | {name: "cursor report untouched", in: "\x1b[12;40R", want: "\x1b[12;40R"}, | ||
| 59 | {name: "paste body untouched", in: "\x1b[200~\x1b[97;1:3u\x1b[201~\x1b[97;1:3u", want: "\x1b[200~\x1b[97;1:3u\x1b[201~"}, | 60 | {name: "paste body untouched", in: "\x1b[200~\x1b[97;1:3u\x1b[201~\x1b[97;1:3u", want: "\x1b[200~\x1b[97;1:3u\x1b[201~"}, | ||
| 60 | - | {name: "lone esc held", in: "x\x1b", want: "x"}, | 61 | + | {name: "lone esc passes before any kitty key", in: "x\x1b", want: "x\x1b"}, |
| 62 | + | {name: "lone esc held after a kitty key", in: "\x1b[97;;97u\x1b", want: "a"}, | |||
| 61 | {name: "trailing garbage", in: "\x1b[97;;97u\x1b[zz", want: "a\x1b[zz"}, | 63 | {name: "trailing garbage", in: "\x1b[97;;97u\x1b[zz", want: "a\x1b[zz"}, | ||
| 62 | {name: "bad event type kept", in: "\x1b[97;1:4u", want: "\x1b[97;1:4u"}, | 64 | {name: "bad event type kept", in: "\x1b[97;1:4u", want: "\x1b[97;1:4u"}, | ||
| 63 | } | 65 | } | ||
| @@ -78,7 +80,8 @@ func TestNormalizerHoldsSplitSequences(t *testing.T) { | |||||
| 78 | {name: "split csi u", reads: []string{"a\x1b[97;", ";98u"}, want: []string{"a", "b"}}, | 80 | {name: "split csi u", reads: []string{"a\x1b[97;", ";98u"}, want: []string{"a", "b"}}, | ||
| 79 | {name: "split csi prefix", reads: []string{"\x1b[", "57442;1:3u"}, want: []string{"", ""}}, | 81 | {name: "split csi prefix", reads: []string{"\x1b[", "57442;1:3u"}, want: []string{"", ""}}, | ||
| 80 | {name: "split paste close", reads: []string{"\x1b[200~x\x1b[20", "1~\x1b[97;;97u"}, want: []string{"\x1b[200~x", "\x1b[201~a"}}, | 82 | {name: "split paste close", reads: []string{"\x1b[200~x\x1b[20", "1~\x1b[97;;97u"}, want: []string{"\x1b[200~x", "\x1b[201~a"}}, | ||
| 81 | - | {name: "split after esc", reads: []string{"\x1b", "[57442;5u", "\x1b", "[97;;97u"}, want: []string{"", "", "", "a"}}, | 83 | + | {name: "split after esc", reads: []string{"\x1b[27u\x1b", "[57442;5u", "\x1b", "[97;;97u"}, want: []string{"\x1b[27u", "", "", "a"}}, |
| 84 | + | {name: "legacy escape before the switch", reads: []string{"\x1b", "\x1b[97;;97u"}, want: []string{"\x1b", "a"}}, | |||
| 82 | {name: "split paste open", reads: []string{"\x1b[20", "0~\x1b[97;1:3u\x1b[201~"}, want: []string{"", "\x1b[200~\x1b[97;1:3u\x1b[201~"}}, | 85 | {name: "split paste open", reads: []string{"\x1b[20", "0~\x1b[97;1:3u\x1b[201~"}, want: []string{"", "\x1b[200~\x1b[97;1:3u\x1b[201~"}}, | ||
| 83 | } | 86 | } | ||
| 84 | for _, tt := range tests { | 87 | for _, tt := range tests { | ||
| @@ -94,6 +97,8 @@ func TestNormalizerHoldsSplitSequences(t *testing.T) { | |||||
| 94 | // TestNormalizerEverySplitMatchesOneRead feeds each input split at every byte | 97 | // TestNormalizerEverySplitMatchesOneRead feeds each input split at every byte | ||
| 95 | // and requires the same output and Ctrl state as one read. | 98 | // and requires the same output and Ctrl state as one read. | ||
| 96 | func TestNormalizerEverySplitMatchesOneRead(t *testing.T) { | 99 | func TestNormalizerEverySplitMatchesOneRead(t *testing.T) { | ||
| 100 | + | // Each input starts with a kitty key, as a terminal in RequestFlags mode | |||
| 101 | + | // sends before any split can matter. | |||
| 97 | inputs := []string{ | 102 | inputs := []string{ | ||
| 98 | "\x1b[57442;5u\x1b[49;5u\x1b[49;5:3u", | 103 | "\x1b[57442;5u\x1b[49;5u\x1b[49;5:3u", | ||
| 99 | "a\x1b[97;;97u\x1b[57442;1:3u", | 104 | "a\x1b[97;;97u\x1b[57442;1:3u", | ||
| @@ -102,9 +107,11 @@ func TestNormalizerEverySplitMatchesOneRead(t *testing.T) { | |||||
| 102 | } | 107 | } | ||
| 103 | for _, in := range inputs { | 108 | for _, in := range inputs { | ||
| 104 | var whole Normalizer | 109 | var whole Normalizer | ||
| 110 | + | whole.Normalize([]byte("\x1b[27u")) | |||
| 105 | want := string(whole.Normalize([]byte(in))) | 111 | want := string(whole.Normalize([]byte(in))) | ||
| 106 | for cut := 1; cut < len(in); cut++ { | 112 | for cut := 1; cut < len(in); cut++ { | ||
| 107 | var n Normalizer | 113 | var n Normalizer | ||
| 114 | + | n.Normalize([]byte("\x1b[27u")) | |||
| 108 | got := string(n.Normalize([]byte(in[:cut]))) + string(n.Normalize([]byte(in[cut:]))) | 115 | got := string(n.Normalize([]byte(in[:cut]))) + string(n.Normalize([]byte(in[cut:]))) | ||
| 109 | require.Equal(t, want, got, "input %q cut %d", in, cut) | 116 | require.Equal(t, want, got, "input %q cut %d", in, cut) | ||
| 110 | require.Equal(t, whole.CtrlHeld(), n.CtrlHeld(), "input %q cut %d", in, cut) | 117 | require.Equal(t, whole.CtrlHeld(), n.CtrlHeld(), "input %q cut %d", in, cut) | ||