1package vt
2
3import (
4 "bytes"
5 "testing"
6
7 "github.com/bnema/vev/pkg/renderer"
8)
9
10func TestScreenQueryResponses(t *testing.T) {
11 cases := []struct {
12 name string
13 input string
14 want string
15 }{
16 {name: "primary DA bare", input: "\x1b[c", want: "\x1b[?6c"},
17 {name: "primary DA with 0", input: "\x1b[0c", want: "\x1b[?6c"},
18 {name: "secondary DA", input: "\x1b[>c", want: "\x1b[>0;0;0c"},
19 {name: "secondary DA with 0", input: "\x1b[>0c", want: "\x1b[>0;0;0c"},
20 {name: "DSR status report", input: "\x1b[5n", want: "\x1b[0n"},
21 {name: "CPR at home", input: "\x1b[6n", want: "\x1b[1;1R"},
22 {name: "CPR after CUP", input: "\x1b[3;7H\x1b[6n", want: "\x1b[3;7R"},
23 {name: "private CPR", input: "\x1b[3;7H\x1b[?6n", want: "\x1b[?3;7R"},
24 {name: "CPR in DECOM reports origin-relative row", input: "\x1b[2;4r\x1b[?6h\x1b[6n", want: "\x1b[1;1R"},
25 {name: "DECRQM DECOM reset", input: "\x1b[?6$p", want: "\x1b[?6;2$y"},
26 {name: "DECRQM DECOM set", input: "\x1b[?6h\x1b[?6$p", want: "\x1b[?6;1$y"},
27 {name: "DECRQM IRM reset", input: "\x1b[4$p", want: "\x1b[4;2$y"},
28 {name: "DECRQM IRM set", input: "\x1b[4h\x1b[4$p", want: "\x1b[4;1$y"},
29 {name: "DECRQM 2026 reset", input: "\x1b[?2026$p", want: "\x1b[?2026;2$y"},
30 {name: "DECRQM 2026 set", input: "\x1b[?2026h\x1b[?2026$p", want: "\x1b[?2026;1$y"},
31 {name: "DECRQM 2031 reset", input: "\x1b[?2031$p", want: "\x1b[?2031;2$y"},
32 {name: "DECRQM 2031 set", input: "\x1b[?2031h\x1b[?2031$p", want: "\x1b[?2031;1$y"},
33 {name: "color scheme DSR dark", input: "\x1b[?996n", want: ColorSchemeReportDark},
34 {name: "color scheme DSR light", input: "\x1b[?996n", want: ColorSchemeReportLight},
35 {name: "DECRQM unknown mode", input: "\x1b[?1337$p", want: "\x1b[?1337;0$y"},
36 {name: "kitty keyboard query unanswered", input: "\x1b[?u", want: ""},
37 {name: "XTVERSION unanswered", input: "\x1b[>0q", want: ""},
38 {name: "DA split across writes", input: "", want: "\x1b[?6c"},
39 }
40 for _, tc := range cases {
41 t.Run(tc.name, func(t *testing.T) {
42 s := NewScreen(80, 24)
43 var got bytes.Buffer
44 s.OnResponse = func(b []byte) { got.Write(b) }
45 switch tc.name {
46 case "DA split across writes":
47 s.Write([]byte("\x1b["))
48 s.Write([]byte("0c"))
49 case "color scheme DSR light":
50 s.SetColorScheme(true)
51 s.Write([]byte(tc.input))
52 case "color scheme DSR dark":
53 s.SetColorScheme(false)
54 s.Write([]byte(tc.input))
55 default:
56 s.Write([]byte(tc.input))
57 }
58 if got.String() != tc.want {
59 t.Fatalf("response = %q, want %q", got.String(), tc.want)
60 }
61 })
62 }
63}
64
65func TestScreenOSCDefaultColorQueries(t *testing.T) {
66 cases := []struct {
67 name string
68 input string
69 want string
70 }{
71 {name: "foreground BEL", input: "\x1b]10;?\a", want: "\x1b]10;rgb:1212/3434/5656\a"},
72 {name: "background BEL", input: "\x1b]11;?\a", want: "\x1b]11;rgb:7878/9a9a/bcbc\a"},
73 {name: "foreground ST", input: "\x1b]10;?\x1b\\", want: "\x1b]10;rgb:1212/3434/5656\x1b\\"},
74 {name: "background ST", input: "\x1b]11;?\x1b\\", want: "\x1b]11;rgb:7878/9a9a/bcbc\x1b\\"},
75 }
76 for _, tc := range cases {
77 t.Run(tc.name, func(t *testing.T) {
78 s := NewScreen(80, 24)
79 s.SetDefaultColors(
80 renderer.RGB{R: 0x12, G: 0x34, B: 0x56},
81 renderer.RGB{R: 0x78, G: 0x9a, B: 0xbc},
82 true,
83 )
84 var got bytes.Buffer
85 s.OnResponse = func(b []byte) { got.Write(b) }
86 s.Write([]byte(tc.input))
87 if got.String() != tc.want {
88 t.Fatalf("response = %q, want %q", got.String(), tc.want)
89 }
90 })
91 }
92}
93
94func TestScreenOSCDefaultColorQueriesUnknownAreSilent(t *testing.T) {
95 s := NewScreen(80, 24)
96 s.SetDefaultColors(renderer.RGB{R: 1, G: 2, B: 3}, renderer.RGB{R: 4, G: 5, B: 6}, false)
97 var got bytes.Buffer
98 s.OnResponse = func(b []byte) { got.Write(b) }
99 s.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
100 if got.Len() != 0 {
101 t.Fatalf("response = %q, want silence", got.String())
102 }
103}
104
105func TestScreenOSCDefaultColorQuerySplitAcrossWrites(t *testing.T) {
106 s := NewScreen(80, 24)
107 s.SetDefaultColors(renderer.RGB{R: 0x01, G: 0x02, B: 0x03}, renderer.RGB{R: 0x04, G: 0x05, B: 0x06}, true)
108 var got bytes.Buffer
109 s.OnResponse = func(b []byte) { got.Write(b) }
110 s.Write([]byte("\x1b]10"))
111 s.Write([]byte(";?\a"))
112 if got.String() != "\x1b]10;rgb:0101/0202/0303\a" {
113 t.Fatalf("response = %q", got.String())
114 }
115}
116
117func TestScreenQueriesWithNilResponderDoNotPanic(t *testing.T) {
118 s := NewScreen(80, 24)
119 s.Write([]byte("\x1b[c\x1b[6n\x1b[?2026$p"))
120 s.SetDefaultColors(renderer.RGB{R: 1, G: 2, B: 3}, renderer.RGB{R: 4, G: 5, B: 6}, true)
121 s.Write([]byte("\x1b]10;?\a\x1b]11;?\x1b\\"))
122}
123
124func TestCSIuDispatch(t *testing.T) {
125 cases := []struct {
126 name string
127 input string
128 wantRow int
129 wantCol int
130 }{
131 {name: "bare u restores cursor", input: "\x1b[5;10H\x1b[s\x1b[2;2H\x1b[u", wantRow: 4, wantCol: 9},
132 {name: "kitty query leaves cursor", input: "\x1b[5;10H\x1b[s\x1b[2;2H\x1b[?u", wantRow: 1, wantCol: 1},
133 {name: "kitty push leaves cursor", input: "\x1b[5;10H\x1b[s\x1b[2;2H\x1b[>1u", wantRow: 1, wantCol: 1},
134 {name: "kitty pop leaves cursor", input: "\x1b[5;10H\x1b[s\x1b[2;2H\x1b[<u", wantRow: 1, wantCol: 1},
135 }
136 for _, tc := range cases {
137 t.Run(tc.name, func(t *testing.T) {
138 s := NewScreen(80, 24)
139 s.Write([]byte(tc.input))
140 if s.Row != tc.wantRow || s.Col != tc.wantCol {
141 t.Fatalf("cursor = %d;%d, want %d;%d", s.Row, s.Col, tc.wantRow, tc.wantCol)
142 }
143 })
144 }
145}
146
147func TestScreenColorSchemeQueriesUnknownAreSilent(t *testing.T) {
148 s := NewScreen(80, 24)
149 s.SetColorScheme(true)
150 s.ClearColorScheme()
151 var got bytes.Buffer
152 s.OnResponse = func(b []byte) { got.Write(b) }
153 s.Write([]byte("\x1b[?996n"))
154 if got.Len() != 0 {
155 t.Fatalf("response = %q, want silence", got.String())
156 }
157}
158
159func TestScreenResetClearsColorSchemeModeSubscription(t *testing.T) {
160 s := NewScreen(80, 24)
161 var got bytes.Buffer
162 s.OnResponse = func(b []byte) { got.Write(b) }
163
164 s.Write([]byte("\x1b[?2031h"))
165 s.Write([]byte("\x1bc"))
166
167 s.SetColorScheme(true)
168 if got.Len() != 0 {
169 t.Fatalf("unsolicited response after reset = %q, want silence", got.String())
170 }
171
172 got.Reset()
173 s.Write([]byte("\x1b[?2031$p"))
174 if got.String() != "\x1b[?2031;2$y" {
175 t.Fatalf("DECRQM 2031 after reset = %q, want reset state", got.String())
176 }
177}
178
179func TestScreenColorSchemeUnsolicitedOnlySubscribedAndChanged(t *testing.T) {
180 s := NewScreen(80, 24)
181 var got bytes.Buffer
182 s.OnResponse = func(b []byte) { got.Write(b) }
183
184 s.SetColorScheme(true)
185 if got.Len() != 0 {
186 t.Fatalf("unsubscribed response = %q, want silence", got.String())
187 }
188
189 s.Write([]byte("\x1b[?2031h"))
190 if !s.ColorSchemeMode() {
191 t.Fatalf("ColorSchemeMode() = false, want true")
192 }
193 s.SetColorScheme(true)
194 if got.Len() != 0 {
195 t.Fatalf("unchanged response = %q, want silence", got.String())
196 }
197 s.SetColorScheme(false)
198 if got.String() != ColorSchemeReportDark {
199 t.Fatalf("changed response = %q, want dark notification", got.String())
200 }
201 got.Reset()
202 s.Write([]byte("\x1b[?2031l"))
203 s.SetColorScheme(true)
204 if got.Len() != 0 {
205 t.Fatalf("disabled response = %q, want silence", got.String())
206 }
207}