Skip to content

Commit 4d8e27a

Browse files
authored
Merge pull request #118 from trengrj/fix-cursor-probe-swallow
fix(core): don't drop user input coalesced with the cursor-position answer
2 parents d3aedcb + ffec2d4 commit 4d8e27a

2 files changed

Lines changed: 58 additions & 9 deletions

File tree

internal/core/keys_unix.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,22 +61,24 @@ func (k *Keys) GetCursorPos() (x, y int) {
6161
return disable()
6262
}
6363

64-
// Attempt to locate cursor response in it.
65-
match = rxRcvCursorPos.FindAllStringSubmatch(string(cursor), 1)
64+
// Split the chunk: the cursor answer out, everything else kept as
65+
// user input. A single read can contain both — a line typed while
66+
// the query was in flight coalesces with the response in the tty
67+
// buffer — so the remainder must be preserved, not dropped.
68+
cursorSeq, remain := k.extractCursorPos(cursor)
6669

67-
// If there is something but not cursor answer, its user input.
68-
if len(match) == 0 && len(cursor) > 0 {
70+
if len(remain) > 0 {
6971
k.mutex.RLock()
70-
k.buf = append(k.buf, cursor...)
72+
k.buf = append(k.buf, remain...)
7173
k.mutex.RUnlock()
74+
}
7275

76+
// No cursor answer yet: keep reading.
77+
if len(cursorSeq) == 0 {
7378
continue
7479
}
7580

76-
// And if empty, then we should abort.
77-
if len(match) == 0 {
78-
return disable()
79-
}
81+
match = rxRcvCursorPos.FindAllStringSubmatch(string(cursorSeq), 1)
8082

8183
break
8284
}

internal/core/keys_unix_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//go:build unix
2+
3+
package core
4+
5+
import (
6+
"os"
7+
"testing"
8+
9+
"github.com/creack/pty"
10+
11+
"github.com/reeflective/readline/internal/term"
12+
)
13+
14+
func TestGetCursorPosPreservesCoalescedInput(t *testing.T) {
15+
ptmx, tty, err := pty.Open()
16+
if err != nil {
17+
t.Fatalf("pty: %v", err)
18+
}
19+
defer ptmx.Close()
20+
defer tty.Close()
21+
22+
// Raw mode, as Readline always sets before the probe runs — in the
23+
// slave's default canonical mode a read would block waiting for \n.
24+
if _, err := term.MakeRaw(int(tty.Fd())); err != nil {
25+
t.Fatalf("raw mode: %v", err)
26+
}
27+
28+
origStdin := os.Stdin
29+
os.Stdin = tty
30+
defer func() { os.Stdin = origStdin }()
31+
32+
// The typed line and the cursor answer arrive as one chunk: written
33+
// before the probe reads, they sit together in the tty input queue.
34+
if _, err := ptmx.Write([]byte("echo hello\r\x1b[5;10R")); err != nil {
35+
t.Fatalf("write: %v", err)
36+
}
37+
38+
keys := &Keys{}
39+
40+
x, y := keys.GetCursorPos()
41+
if x != 10 || y != 5 {
42+
t.Errorf("cursor position = (%d, %d), want (10, 5)", x, y)
43+
}
44+
if got := string(keys.buf); got != "echo hello\r" {
45+
t.Errorf("pending input = %q, want %q (typed bytes must survive the probe)", got, "echo hello\r")
46+
}
47+
}

0 commit comments

Comments
 (0)