package app import ( "testing" "github.com/hjbdev/patterm/internal/preset" ) func newTestPalette() *paletteState { return newPalette(nil, "", preset.Set{}) } func TestPaletteIgnoresKittyReleaseEvent(t *testing.T) { // A kitty key-release for Ctrl-K. With the legacy handler this looked // like ESC followed by `[`, which fell through to cancel. p := newTestPalette() chunk := []byte("\x1b[107;5:3u") action, done, adv := p.handleInput(chunk, 0) if done { t.Fatalf("release event closed palette: action=%+v", action) } if adv != len(chunk) { t.Fatalf("advance %d, want %d", adv, len(chunk)) } } func TestPaletteEscViaKittyCancels(t *testing.T) { p := newTestPalette() chunk := []byte("\x1b[27u") action, done, adv := p.handleInput(chunk, 0) if !done || action.kind != "cancel" { t.Fatalf("Esc via CSI u didn't cancel: action=%+v done=%v", action, done) } if adv != len(chunk) { t.Fatalf("advance %d, want %d", adv, len(chunk)) } } func TestPaletteBareEscCancels(t *testing.T) { p := newTestPalette() action, done, adv := p.handleInput([]byte{0x1b}, 0) if !done || action.kind != "cancel" { t.Fatalf("bare ESC didn't cancel: action=%+v done=%v", action, done) } if adv != 1 { t.Fatalf("advance %d, want 1", adv) } } func TestPaletteKittyArrowsNavigate(t *testing.T) { pr := []*preset.Preset{{Name: "a"}, {Name: "b"}, {Name: "c"}} p := newPalette(nil, "", preset.Set{Agents: pr}) if p.cursor != 0 { t.Fatalf("initial cursor %d", p.cursor) } // Kitty functional Down arrow. _, _, adv := p.handleInput([]byte("\x1b[57353u"), 0) if adv != 8 { t.Fatalf("advance %d", adv) } if p.cursor != 1 { t.Fatalf("cursor %d after Down, want 1", p.cursor) } // Kitty functional Up arrow. _, _, _ = p.handleInput([]byte("\x1b[57352u"), 0) if p.cursor != 0 { t.Fatalf("cursor %d after Up, want 0", p.cursor) } } func TestPaletteLegacyArrowsStillWork(t *testing.T) { pr := []*preset.Preset{{Name: "a"}, {Name: "b"}} p := newPalette(nil, "", preset.Set{Agents: pr}) _, _, adv := p.handleInput([]byte("\x1b[B"), 0) if adv != 3 { t.Fatalf("advance %d", adv) } if p.cursor != 1 { t.Fatalf("cursor %d, want 1", p.cursor) } } func TestPaletteKittyEnterAccepts(t *testing.T) { pr := []*preset.Preset{{Name: "x"}} p := newPalette(nil, "", preset.Set{Agents: pr}) action, done, _ := p.handleInput([]byte("\x1b[13u"), 0) if !done || action.kind != "spawn-agent" { t.Fatalf("Enter via CSI u didn't accept: action=%+v done=%v", action, done) } } func TestPaletteKittyBackspace(t *testing.T) { p := newTestPalette() p.query = []rune("hello") _, _, _ = p.handleInput([]byte("\x1b[127u"), 0) if string(p.query) != "hell" { t.Fatalf("query %q after backspace", string(p.query)) } } func TestPaletteLegacyPrintableTypes(t *testing.T) { p := newTestPalette() _, _, _ = p.handleInput([]byte("a"), 0) _, _, _ = p.handleInput([]byte("b"), 0) if string(p.query) != "ab" { t.Fatalf("query %q", string(p.query)) } } // peekArrowEvent powers the chunk-level dedupe in processStdin. The // scenarios below cover the patterns we've actually seen terminals // emit for one physical Down press: a kitty press event, a legacy CSI // arrow, and the pair of the two adjacent. We assert classification // here so processStdin can rely on it. func TestPeekArrowEventClassifies(t *testing.T) { cases := []struct { name string in []byte wantNav byte wantLen int }{ {"legacy down", []byte("\x1b[B"), 'D', 3}, {"legacy up", []byte("\x1b[A"), 'U', 3}, {"kitty down press", []byte("\x1b[57353u"), 'D', 8}, {"kitty up press", []byte("\x1b[57352u"), 'U', 8}, {"kitty down release", []byte("\x1b[57353;1:3u"), 0, 12}, {"kitty enter", []byte("\x1b[13u"), 0, 0}, {"not a CSI", []byte("a"), 0, 0}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { nav, adv := peekArrowEvent(tc.in, 0) if nav != tc.wantNav || adv != tc.wantLen { t.Fatalf("got nav=%q len=%d, want nav=%q len=%d", nav, adv, tc.wantNav, tc.wantLen) } }) } }