1package daemon
2
3import (
4 "testing"
5
6 vt "github.com/bnema/vev-vt"
7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/usecase/ui"
9 "github.com/stretchr/testify/require"
10)
11
12func TestOverlayInputCursor(t *testing.T) {
13 inner := domain.Rect{X: 3, Y: 2, Width: 10, Height: 4}
14 withCaret := func(col int) capturedModal {
15 return capturedModal{active: true, presentation: ui.Presentation{Inner: inner}, caretCol: col, hasCaret: true}
16 }
17 notices := capturedModal{active: true, presentation: ui.Presentation{Inner: inner}}
18 bar := capturedCursorInputs{style: 6, hasStyle: true, visible: true, renderable: true}
19 for _, tt := range []struct {
20 name string
21 overlays capturedOverlayRenderState
22 pane capturedCursorInputs
23 want cursorOut
24 }{
25 {
26 name: "palette caret owns the cursor and keeps the pane shape",
27 overlays: capturedOverlayRenderState{palette: withCaret(4)},
28 pane: bar,
29 want: cursorOut{valid: true, row: 2, col: 7, style: 6, hasStyle: true},
30 },
31 {
32 name: "hidden pane cursor keeps its app-set shape",
33 overlays: capturedOverlayRenderState{palette: withCaret(4)},
34 pane: capturedCursorInputs{style: 6, hasStyle: true},
35 want: cursorOut{valid: true, row: 2, col: 7, style: 6, hasStyle: true},
36 },
37 {
38 name: "unstyled pane cursor falls back to the default shape",
39 overlays: capturedOverlayRenderState{palette: withCaret(4)},
40 pane: capturedCursorInputs{visible: true, renderable: true},
41 want: cursorOut{valid: true, row: 2, col: 7, style: 1, hasStyle: true},
42 },
43 {
44 name: "prompt above palette owns the cursor",
45 overlays: capturedOverlayRenderState{palette: withCaret(4), prompt: withCaret(6)},
46 pane: bar,
47 want: cursorOut{valid: true, row: 2, col: 9, style: 6, hasStyle: true},
48 },
49 {
50 name: "prompt above copy search owns the cursor",
51 overlays: capturedOverlayRenderState{copySearch: withCaret(1), prompt: withCaret(6)},
52 pane: bar,
53 want: cursorOut{valid: true, row: 2, col: 9, style: 6, hasStyle: true},
54 },
55 {
56 name: "palette above notices owns the cursor",
57 overlays: capturedOverlayRenderState{palette: withCaret(4), noticesOverlay: notices},
58 pane: bar,
59 want: cursorOut{valid: true, row: 2, col: 7, style: 6, hasStyle: true},
60 },
61 {
62 name: "copy search caret owns the cursor",
63 overlays: capturedOverlayRenderState{copySearch: withCaret(1)},
64 pane: bar,
65 want: cursorOut{valid: true, row: 2, col: 4, style: 6, hasStyle: true},
66 },
67 {
68 name: "notices above copy search hide the cursor",
69 overlays: capturedOverlayRenderState{copySearch: withCaret(1), noticesOverlay: notices},
70 pane: bar,
71 want: cursorOut{hidden: true},
72 },
73 {
74 name: "modal without caret hides the cursor",
75 overlays: capturedOverlayRenderState{palette: capturedModal{active: true, presentation: ui.Presentation{Inner: inner}, caretCol: 4}},
76 pane: bar,
77 want: cursorOut{hidden: true},
78 },
79 {
80 name: "caret outside the frame hides the cursor",
81 overlays: capturedOverlayRenderState{palette: capturedModal{active: true, presentation: ui.Presentation{Inner: domain.Rect{X: 18, Y: 2, Width: 10, Height: 1}}, hasCaret: true, caretCol: 4}},
82 pane: bar,
83 want: cursorOut{hidden: true},
84 },
85 {
86 name: "copy mode without a modal hides the cursor",
87 overlays: capturedOverlayRenderState{copyActive: true},
88 pane: bar,
89 want: cursorOut{hidden: true},
90 },
91 } {
92 t.Run(tt.name, func(t *testing.T) {
93 require.Equal(t, tt.want, overlayInputCursor(tt.overlays, tt.pane, 20, 10))
94 })
95 }
96}
97
98// TestPaletteInputLineOwnsTerminalCursor drives the production render path:
99// the open palette shows the real terminal cursor after its query, which is
100// what lets client predictive echo type ahead in it.
101func TestPaletteInputLineOwnsTerminalCursor(t *testing.T) {
102 p, release := newBlockingPTY(t)
103 defer release()
104 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
105 client := vt.NewScreen(80, 25)
106 d.paint(sess, ac, true, nil)
107 mustApplyOutput(t, client, awaitFrame(t, sends, "Output"))
108
109 d.handleInput(sess, ac, []byte("\x1b "))
110 mustApplyOutput(t, client, awaitFrame(t, sends, "Output"))
111 require.True(t, client.CursorVisible(), "open palette shows the real cursor")
112 row, col := client.CursorRow(), client.CursorCol()
113 require.Equal(t, '>', client.Cell(col-2, row).Rune, "cursor sits after the query prompt")
114
115 d.handleInput(sess, ac, []byte("ab"))
116 mustApplyOutput(t, client, awaitFrame(t, sends, "Output"))
117 require.True(t, client.CursorVisible())
118 require.Equal(t, row, client.CursorRow())
119 require.Equal(t, col+2, client.CursorCol(), "cursor follows typed query")
120 require.Equal(t, 'b', client.Cell(col+1, row).Rune)
121}