Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion cmd/agent-deck/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,6 @@ func main() {
homeModel,
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
tea.WithInput(ui.NewCSIuReader(os.Stdin)),
)

// Start maintenance worker (background goroutine, respects config toggle)
Expand Down
8 changes: 4 additions & 4 deletions internal/ui/keyboard_compat.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,11 @@ func (c *csiuReader) translate(final bool) []byte {
continue
}

// We have ESC '['. Scan forward for the terminator.
// We have ESC '['. Scan forward for the CSI final byte.
// CSI final bytes are in the range 0x40-0x7E (@ through ~).
// Only 'u' and '~' get special handling; everything else passes through.
j := i + 2
for j < len(c.inBuf) && c.inBuf[j] != 'u' && c.inBuf[j] != 'A' &&
c.inBuf[j] != 'B' && c.inBuf[j] != 'C' && c.inBuf[j] != 'D' &&
c.inBuf[j] != 'H' && c.inBuf[j] != 'F' && c.inBuf[j] != '~' {
for j < len(c.inBuf) && (c.inBuf[j] < 0x40 || c.inBuf[j] > 0x7E) {
j++
}

Expand Down
45 changes: 45 additions & 0 deletions internal/ui/keyboard_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,51 @@ func TestCSIuReaderPassesStandardEscapeSequences(t *testing.T) {
}
}

// TestCSIuReaderPassesSGRMouseEvents verifies that SGR mouse sequences
// (final byte 'M'/'m') pass through correctly and don't eat subsequent input.
// Regression test: the original terminator set omitted 'M' and 'm', causing
// mouse events to consume following arrow key sequences.
func TestCSIuReaderPassesSGRMouseEvents(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "mouse press then arrow up",
input: "\x1b[<0;10;20M\x1b[A",
want: "\x1b[<0;10;20M\x1b[A",
},
{
name: "mouse release then arrow down",
input: "\x1b[<0;10;20m\x1b[B",
want: "\x1b[<0;10;20m\x1b[B",
},
{
name: "mouse move then keystroke",
input: "\x1b[<35;5;15M" + "j",
want: "\x1b[<35;5;15M" + "j",
},
{
name: "mouse between two arrows",
input: "\x1b[A\x1b[<0;1;1M\x1b[B",
want: "\x1b[A\x1b[<0;1;1M\x1b[B",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := NewCSIuReader(bytes.NewReader([]byte(tt.input)))
out, err := io.ReadAll(r)
if err != nil {
t.Fatalf("ReadAll error: %v", err)
}
if string(out) != tt.want {
t.Errorf("got %q, want %q", string(out), tt.want)
}
})
}
}

// TestCSIuReaderMixedInput verifies mixed input is correctly handled.
func TestCSIuReaderMixedInput(t *testing.T) {
// "a" + shift+r CSI u + "b"
Expand Down