| 🛟 Updated. 28d5985 k33g 16h ago | 1 | package terminal |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | |
| 7 | "github.com/gdamore/tcell/v2" |
| 8 | ) |
| 9 | |
| 10 | // feed writes a byte stream to a new parser and returns the screen it drew on. |
| 11 | func feed(width, height int, stream string) *Screen { |
| 12 | screen := NewScreen(width, height) |
| 13 | NewParser(screen).Write([]byte(stream)) //nolint:errcheck // Write never fails |
| 14 | return screen |
| 15 | } |
| 16 | |
| 17 | func TestPlainTextIsWritten(t *testing.T) { |
| 18 | s := feed(20, 3, "hello") |
| 19 | |
| 20 | wantLines(t, s, "hello") |
| 21 | } |
| 22 | |
| 23 | func TestWriteAlwaysConsumesEverything(t *testing.T) { |
| 24 | // A terminal has nothing useful to do with a byte it does not understand |
| 25 | // except carry on, so Write can neither fail nor stop short. |
| 26 | p := NewParser(NewScreen(10, 2)) |
| 27 | stream := []byte("a\x1b[1mb\x1b]0;title\x07c\xff") |
| 28 | |
| 29 | n, err := p.Write(stream) |
| 30 | |
| 31 | if err != nil { |
| 32 | t.Errorf("Write() error = %v, want nil", err) |
| 33 | } |
| 34 | if n != len(stream) { |
| 35 | t.Errorf("Write() consumed %d of %d bytes", n, len(stream)) |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | func TestControlCharacters(t *testing.T) { |
| 40 | tests := []struct { |
| 41 | name string |
| 42 | stream string |
| 43 | lines []string |
| 44 | }{ |
| 45 | {"carriage return", "abc\rX", []string{"Xbc"}}, |
| 46 | {"line feed", "a\nb", []string{"a", " b"}}, |
| 47 | {"backspace moves without erasing", "abc\bX", []string{"abX"}}, |
| 48 | {"tab", "a\tb", []string{"a b"}}, |
| 49 | {"vertical tab behaves as a line feed", "a\vb", []string{"a", " b"}}, |
| 50 | {"form feed behaves as a line feed", "a\fb", []string{"a", " b"}}, |
| 51 | {"delete prints nothing", "a\x7fb", []string{"ab"}}, |
| 52 | } |
| 53 | |
| 54 | for _, tc := range tests { |
| 55 | t.Run(tc.name, func(t *testing.T) { |
| 56 | wantLines(t, feed(20, 4, tc.stream), tc.lines...) |
| 57 | }) |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | func TestTheBellIsReportedOnceThenForgotten(t *testing.T) { |
| 62 | p := NewParser(NewScreen(10, 2)) |
| 63 | |
| 64 | p.Write([]byte("a\x07b")) //nolint:errcheck |
| 65 | |
| 66 | if !p.TakeBell() { |
| 67 | t.Error("TakeBell() = false after the bell rang") |
| 68 | } |
| 69 | if p.TakeBell() { |
| 70 | t.Error("TakeBell() = true a second time; it must be forgotten once taken") |
| 71 | } |
| 72 | wantLines(t, p.Screen(), "ab") |
| 73 | } |
| 74 | |
| 75 | func TestCursorMovement(t *testing.T) { |
| 76 | tests := []struct { |
| 77 | name string |
| 78 | stream string |
| 79 | want Cursor |
| 80 | }{ |
| 81 | {"CUP to a row and column", "\x1b[3;5H", Cursor{Row: 2, Col: 4}}, |
| 82 | {"CUP with no parameters goes home", "\x1b[H", Cursor{}}, |
| 83 | {"HVP is the same as CUP", "\x1b[2;3f", Cursor{Row: 1, Col: 2}}, |
| 84 | {"CUU up", "\x1b[5;5H\x1b[2A", Cursor{Row: 2, Col: 4}}, |
| 85 | {"CUD down", "\x1b[1;1H\x1b[2B", Cursor{Row: 2, Col: 0}}, |
| 86 | {"CUF forward", "\x1b[1;1H\x1b[3C", Cursor{Row: 0, Col: 3}}, |
| 87 | {"CUB back", "\x1b[1;6H\x1b[3D", Cursor{Row: 0, Col: 2}}, |
| 88 | {"a movement with no count moves one", "\x1b[3;3H\x1b[A", Cursor{Row: 1, Col: 2}}, |
| 89 | {"CHA to a column", "\x1b[3;3H\x1b[7G", Cursor{Row: 2, Col: 6}}, |
| 90 | {"VPA to a row", "\x1b[3;3H\x1b[6d", Cursor{Row: 5, Col: 2}}, |
| 91 | {"CNL down and home", "\x1b[1;5H\x1b[2E", Cursor{Row: 2, Col: 0}}, |
| 92 | {"CPL up and home", "\x1b[5;5H\x1b[2F", Cursor{Row: 2, Col: 0}}, |
| 93 | {"movement is clamped", "\x1b[99;99H", Cursor{Row: 7, Col: 19}}, |
| 94 | } |
| 95 | |
| 96 | for _, tc := range tests { |
| 97 | t.Run(tc.name, func(t *testing.T) { |
| 98 | if got := feed(20, 8, tc.stream).Cursor(); got != tc.want { |
| 99 | t.Errorf("Cursor() = %+v, want %+v", got, tc.want) |
| 100 | } |
| 101 | }) |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | func TestEraseSequences(t *testing.T) { |
| 106 | const filled = "\x1b[1;1Haaaaa\x1b[2;1Hbbbbb\x1b[3;1Hccccc\x1b[2;3H" |
| 107 | |
| 108 | tests := []struct { |
| 109 | name string |
| 110 | stream string |
| 111 | lines []string |
| 112 | }{ |
| 113 | {"ED to the end", filled + "\x1b[J", []string{"aaaaa", "bb", ""}}, |
| 114 | {"ED to the start", filled + "\x1b[1J", []string{"", " bb", "ccccc"}}, |
| 115 | {"ED all", filled + "\x1b[2J", []string{"", "", ""}}, |
| 116 | {"ED with scrollback cleared", filled + "\x1b[3J", []string{"", "", ""}}, |
| 117 | {"EL to the end", filled + "\x1b[K", []string{"aaaaa", "bb", "ccccc"}}, |
| 118 | {"EL to the start", filled + "\x1b[1K", []string{"aaaaa", " bb", "ccccc"}}, |
| 119 | {"EL all", filled + "\x1b[2K", []string{"aaaaa", "", "ccccc"}}, |
| 120 | {"ECH blanks in place", filled + "\x1b[2X", []string{"aaaaa", "bb b", "ccccc"}}, |
| 121 | } |
| 122 | |
| 123 | for _, tc := range tests { |
| 124 | t.Run(tc.name, func(t *testing.T) { |
| 125 | wantLines(t, feed(5, 3, tc.stream), tc.lines...) |
| 126 | }) |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | func TestLineAndCharacterEditing(t *testing.T) { |
| 131 | const filled = "\x1b[1;1Hone\x1b[2;1Htwo\x1b[3;1Hsix\x1b[2;1H" |
| 132 | |
| 133 | tests := []struct { |
| 134 | name string |
| 135 | stream string |
| 136 | lines []string |
| 137 | }{ |
| 138 | {"IL inserts a blank line", filled + "\x1b[L", []string{"one", "", "two"}}, |
| 139 | {"DL removes a line", filled + "\x1b[M", []string{"one", "six", ""}}, |
| 140 | {"ICH opens cells", filled + "\x1b[2@", []string{"one", " t"}}, |
| 141 | {"DCH closes cells", filled + "\x1b[1P", []string{"one", "wo"}}, |
| 142 | } |
| 143 | |
| 144 | for _, tc := range tests { |
| 145 | t.Run(tc.name, func(t *testing.T) { |
| 146 | wantLines(t, feed(3, 3, tc.stream), tc.lines...) |
| 147 | }) |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | func TestScrollSequences(t *testing.T) { |
| 152 | const filled = "\x1b[1;1Ha\x1b[2;1Hb\x1b[3;1Hc" |
| 153 | |
| 154 | wantLines(t, feed(3, 3, filled+"\x1b[S"), "b", "c", "") |
| 155 | wantLines(t, feed(3, 3, filled+"\x1b[T"), "", "a", "b") |
| 156 | wantLines(t, feed(3, 3, filled+"\x1b[2S"), "c", "", "") |
| 157 | } |
| 158 | |
| 159 | func TestScrollRegion(t *testing.T) { |
| 160 | // Rows 2 to 3 scroll; row 1 is a status line that must stay put. |
| 161 | s := feed(6, 4, "\x1b[1;1Hhead\x1b[2;4r\x1b[4;1H\x1b[2;1Ha\x1b[3;1Hb\x1b[4;1Hc\n") |
| 162 | |
| 163 | wantLines(t, s, "head", "b", "c", "") |
| 164 | } |
| 165 | |
| 166 | func TestScrollRegionWithNoParametersOpensBackUp(t *testing.T) { |
| 167 | s := feed(6, 4, "\x1b[2;3r\x1b[r\x1b[4;1H\n") |
| 168 | |
| 169 | // With the whole screen scrolling again, a line feed on the last row moves |
| 170 | // everything up rather than only rows 2 and 3. |
| 171 | if got := s.Cursor().Row; got != 3 { |
| 172 | t.Errorf("Cursor().Row = %d, want the last row", got) |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | func TestAnImpossibleScrollRegionOpensBackUp(t *testing.T) { |
| 177 | s := feed(6, 4, "\x1b[3;2r") |
| 178 | |
| 179 | s.MoveTo(3, 0) |
| 180 | s.LineFeed() |
| 181 | if got := s.Cursor().Row; got != 3 { |
| 182 | t.Errorf("Cursor().Row = %d, want the region to cover the whole screen", got) |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | func TestSaveAndRestoreCursorSequences(t *testing.T) { |
| 187 | for _, stream := range []string{ |
| 188 | "\x1b[3;4H\x1b7\x1b[1;1H\x1b8", // DECSC and DECRC |
| 189 | "\x1b[3;4H\x1b[s\x1b[1;1H\x1b[u", |
| 190 | "\x1b[3;4H\x1b[?1048h\x1b[1;1H\x1b[?1048l", |
| 191 | } { |
| 192 | if got := feed(10, 5, stream).Cursor(); got != (Cursor{Row: 2, Col: 3}) { |
| 193 | t.Errorf("%q left the cursor at %+v, want row 2 column 3", stream, got) |
| 194 | } |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | func TestIndexAndReverseIndex(t *testing.T) { |
| 199 | // IND and RI move by a line and keep the column; only NEL returns to the |
| 200 | // first column as well. |
| 201 | wantLines(t, feed(4, 3, "a\x1bDb"), "a", " b") // IND, down |
| 202 | wantLines(t, feed(4, 3, "\x1b[2;1Ha\x1bMb"), " b", "a") // RI, up |
| 203 | wantLines(t, feed(4, 3, "ab\x1bEc"), "ab", "c") // NEL, down and home |
| 204 | } |
| 205 | |
| 206 | func TestResetSequence(t *testing.T) { |
| 207 | s := feed(10, 3, "text\x1b[1;31m\x1bc") |
| 208 | |
| 209 | wantLines(t, s, "") |
| 210 | if got := s.Cursor(); got != (Cursor{}) { |
| 211 | t.Errorf("Cursor() = %+v, want the top left", got) |
| 212 | } |
| 213 | if _, _, attrs := s.Style().Decompose(); attrs&tcell.AttrBold != 0 { |
| 214 | t.Error("the style survived the reset") |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | func TestPrivateModes(t *testing.T) { |
| 219 | if feed(10, 3, "\x1b[?25l").CursorVisible() { |
| 220 | t.Error("the cursor is visible after being hidden") |
| 221 | } |
| 222 | if !feed(10, 3, "\x1b[?25l\x1b[?25h").CursorVisible() { |
| 223 | t.Error("the cursor stayed hidden after being shown") |
| 224 | } |
| 225 | if feed(10, 3, "\x1b[?7l").AutoWrap() { |
| 226 | t.Error("wrapping is on after being turned off") |
| 227 | } |
| 228 | if !feed(10, 3, "\x1b[?1049h").Alternate() { |
| 229 | t.Error("the alternate screen did not come up") |
| 230 | } |
| 231 | if feed(10, 3, "\x1b[?1049h\x1b[?1049l").Alternate() { |
| 232 | t.Error("the alternate screen did not go away") |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | func TestTheAlternateScreenKeepsWhatWasThere(t *testing.T) { |
| 237 | s := feed(10, 3, "before\x1b[?1049hduring\x1b[?1049l") |
| 238 | |
| 239 | wantLines(t, s, "before") |
| 240 | } |
| 241 | |
| 242 | func TestSeveralModesInOneSequence(t *testing.T) { |
| 243 | s := feed(10, 3, "\x1b[?25;7l") |
| 244 | |
| 245 | if s.CursorVisible() || s.AutoWrap() { |
| 246 | t.Error("a sequence naming two modes did not turn both off") |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | func TestSGRColours(t *testing.T) { |
| 251 | tests := []struct { |
| 252 | name string |
| 253 | stream string |
| 254 | check func(*testing.T, tcell.Style) |
| 255 | }{ |
| 256 | { |
| 257 | name: "a basic foreground", |
| 258 | stream: "\x1b[31m", |
| 259 | check: func(t *testing.T, style tcell.Style) { |
| 260 | if fg, _, _ := style.Decompose(); fg != tcell.ColorMaroon { |
| 261 | t.Errorf("foreground = %v, want maroon", fg) |
| 262 | } |
| 263 | }, |
| 264 | }, |
| 265 | { |
| 266 | name: "a basic background", |
| 267 | stream: "\x1b[44m", |
| 268 | check: func(t *testing.T, style tcell.Style) { |
| 269 | if _, bg, _ := style.Decompose(); bg != tcell.ColorNavy { |
| 270 | t.Errorf("background = %v, want navy", bg) |
| 271 | } |
| 272 | }, |
| 273 | }, |
| 274 | { |
| 275 | name: "a bright foreground", |
| 276 | stream: "\x1b[92m", |
| 277 | check: func(t *testing.T, style tcell.Style) { |
| 278 | if fg, _, _ := style.Decompose(); fg != tcell.ColorLime { |
| 279 | t.Errorf("foreground = %v, want lime", fg) |
| 280 | } |
| 281 | }, |
| 282 | }, |
| 283 | { |
| 284 | name: "a 256-colour foreground", |
| 285 | stream: "\x1b[38;5;200m", |
| 286 | check: func(t *testing.T, style tcell.Style) { |
| 287 | if fg, _, _ := style.Decompose(); fg != tcell.PaletteColor(200) { |
| 288 | t.Errorf("foreground = %v, want palette colour 200", fg) |
| 289 | } |
| 290 | }, |
| 291 | }, |
| 292 | { |
| 293 | name: "a 24-bit background", |
| 294 | stream: "\x1b[48;2;10;20;30m", |
| 295 | check: func(t *testing.T, style tcell.Style) { |
| 296 | if _, bg, _ := style.Decompose(); bg.Hex() != 0x0a141e { |
| 297 | t.Errorf("background = %#06x, want 0x0a141e", bg.Hex()) |
| 298 | } |
| 299 | }, |
| 300 | }, |
| 301 | { |
| 302 | name: "a colour and an attribute together", |
| 303 | stream: "\x1b[1;31m", |
| 304 | check: func(t *testing.T, style tcell.Style) { |
| 305 | fg, _, attrs := style.Decompose() |
| 306 | if fg != tcell.ColorMaroon || attrs&tcell.AttrBold == 0 { |
| 307 | t.Errorf("style = %v with attributes %v, want bold maroon", fg, attrs) |
| 308 | } |
| 309 | }, |
| 310 | }, |
| 311 | { |
| 312 | name: "default colours", |
| 313 | stream: "\x1b[31;44m\x1b[39;49m", |
| 314 | check: func(t *testing.T, style tcell.Style) { |
| 315 | fg, bg, _ := style.Decompose() |
| 316 | if fg != tcell.ColorDefault || bg != tcell.ColorDefault { |
| 317 | t.Errorf("style = %v on %v, want both back to the default", fg, bg) |
| 318 | } |
| 319 | }, |
| 320 | }, |
| 321 | } |
| 322 | |
| 323 | for _, tc := range tests { |
| 324 | t.Run(tc.name, func(t *testing.T) { |
| 325 | tc.check(t, feed(10, 3, tc.stream).Style()) |
| 326 | }) |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | func TestSGRAttributes(t *testing.T) { |
| 331 | tests := []struct { |
| 332 | name string |
| 333 | stream string |
| 334 | attr tcell.AttrMask |
| 335 | want bool |
| 336 | }{ |
| 337 | {"bold", "\x1b[1m", tcell.AttrBold, true}, |
| 338 | {"bold off", "\x1b[1m\x1b[22m", tcell.AttrBold, false}, |
| 339 | {"dim", "\x1b[2m", tcell.AttrDim, true}, |
| 340 | {"italic", "\x1b[3m", tcell.AttrItalic, true}, |
| 341 | {"italic off", "\x1b[3m\x1b[23m", tcell.AttrItalic, false}, |
| 342 | {"underline", "\x1b[4m", tcell.AttrUnderline, true}, |
| 343 | {"underline off", "\x1b[4m\x1b[24m", tcell.AttrUnderline, false}, |
| 344 | {"blink", "\x1b[5m", tcell.AttrBlink, true}, |
| 345 | {"reverse", "\x1b[7m", tcell.AttrReverse, true}, |
| 346 | {"reverse off", "\x1b[7m\x1b[27m", tcell.AttrReverse, false}, |
| 347 | {"strike through", "\x1b[9m", tcell.AttrStrikeThrough, true}, |
| 348 | {"reset clears everything", "\x1b[1;4;7m\x1b[0m", tcell.AttrBold, false}, |
| 349 | {"a bare m resets", "\x1b[1m\x1b[m", tcell.AttrBold, false}, |
| 350 | } |
| 351 | |
| 352 | for _, tc := range tests { |
| 353 | t.Run(tc.name, func(t *testing.T) { |
| 354 | _, _, attrs := feed(10, 3, tc.stream).Style().Decompose() |
| 355 | if got := attrs&tc.attr != 0; got != tc.want { |
| 356 | t.Errorf("the attribute is %v, want %v", got, tc.want) |
| 357 | } |
| 358 | }) |
| 359 | } |
| 360 | } |
| 361 | |
| 362 | func TestAStyleAppliesToWhatIsWrittenAfterIt(t *testing.T) { |
| 363 | s := feed(10, 2, "a\x1b[31mb") |
| 364 | |
| 365 | if fg, _, _ := s.CellAt(0, 0).Style.Decompose(); fg == tcell.ColorMaroon { |
| 366 | t.Error("the character written before the colour took it") |
| 367 | } |
| 368 | if fg, _, _ := s.CellAt(0, 1).Style.Decompose(); fg != tcell.ColorMaroon { |
| 369 | t.Error("the character written after the colour did not take it") |
| 370 | } |
| 371 | } |
| 372 | |
| 373 | func TestAnIncompleteExtendedColourIsIgnored(t *testing.T) { |
| 374 | // Half a colour is not a colour, and must not be guessed at. |
| 375 | for _, stream := range []string{"\x1b[38m", "\x1b[38;5m", "\x1b[38;2;1;2m"} { |
| 376 | if fg, _, _ := feed(10, 2, stream).Style().Decompose(); fg != tcell.ColorDefault { |
| 377 | t.Errorf("%q set the foreground to %v, want it left alone", stream, fg) |
| 378 | } |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | func TestUTF8SplitAcrossWrites(t *testing.T) { |
| 383 | // A pseudo-terminal hands over whatever has arrived, which can be half a |
| 384 | // character. |
| 385 | screen := NewScreen(10, 2) |
| 386 | p := NewParser(screen) |
| 387 | encoded := []byte("é") |
| 388 | |
| 389 | for _, b := range encoded { |
| 390 | p.Write([]byte{b}) //nolint:errcheck |
| 391 | } |
| 392 | |
| 393 | wantLines(t, screen, "é") |
| 394 | } |
| 395 | |
| 396 | func TestAnEscapeSequenceSplitAcrossWrites(t *testing.T) { |
| 397 | screen := NewScreen(10, 3) |
| 398 | p := NewParser(screen) |
| 399 | |
| 400 | for _, piece := range []string{"\x1b", "[3", ";2", "H", "x"} { |
| 401 | p.Write([]byte(piece)) //nolint:errcheck |
| 402 | } |
| 403 | |
| 404 | wantLines(t, screen, "", "", " x") |
| 405 | } |
| 406 | |
| 407 | func TestInvalidBytesBecomeAReplacementCharacter(t *testing.T) { |
| 408 | s := feed(10, 2, "a\xff\xff\xff\xff\xffb") |
| 409 | |
| 410 | if got := s.LineText(0); !strings.HasPrefix(got, "a") || !strings.HasSuffix(got, "b") { |
| 411 | t.Errorf("row 0 = %q, want the valid characters kept around the invalid ones", got) |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | func TestWindowTitle(t *testing.T) { |
| 416 | tests := []struct { |
| 417 | name string |
| 418 | stream string |
| 419 | want string |
| 420 | }{ |
| 421 | {"set with a bell terminator", "\x1b]0;my title\x07", "my title"}, |
| 422 | {"set with a string terminator", "\x1b]2;other\x1b\\", "other"}, |
| 423 | {"an icon-only command is ignored", "\x1b]1;icon\x07", ""}, |
| 424 | {"never set", "plain text", ""}, |
| 425 | } |
| 426 | |
| 427 | for _, tc := range tests { |
| 428 | t.Run(tc.name, func(t *testing.T) { |
| 429 | p := NewParser(NewScreen(20, 3)) |
| 430 | p.Write([]byte(tc.stream)) //nolint:errcheck |
| 431 | |
| 432 | if got := p.Title(); got != tc.want { |
| 433 | t.Errorf("Title() = %q, want %q", got, tc.want) |
| 434 | } |
| 435 | }) |
| 436 | } |
| 437 | } |
| 438 | |
| 439 | func TestAnOSCDoesNotPrintItsContents(t *testing.T) { |
| 440 | s := feed(20, 2, "a\x1b]0;never printed\x07b") |
| 441 | |
| 442 | wantLines(t, s, "ab") |
| 443 | } |
| 444 | |
| 445 | func TestAnEscapeInsideAnOSCThatIsNotATerminatorEndsIt(t *testing.T) { |
| 446 | s := feed(20, 3, "\x1b]0;half\x1b[2;1Hx") |
| 447 | |
| 448 | wantLines(t, s, "", "x") |
| 449 | } |
| 450 | |
| 451 | func TestAnUnterminatedOSCDoesNotGrowForever(t *testing.T) { |
| 452 | p := NewParser(NewScreen(20, 3)) |
| 453 | |
| 454 | p.Write([]byte("\x1b]0;")) //nolint:errcheck |
| 455 | p.Write([]byte(strings.Repeat("x", maxOSC+100))) //nolint:errcheck |
| 456 | |
| 457 | if len(p.osc) > maxOSC { |
| 458 | t.Errorf("the buffer grew to %d bytes, want it capped at %d", len(p.osc), maxOSC) |
| 459 | } |
| 460 | } |
| 461 | |
| 462 | func TestAFloodOfParametersDoesNotGrowForever(t *testing.T) { |
| 463 | p := NewParser(NewScreen(20, 3)) |
| 464 | |
| 465 | p.Write([]byte("\x1b[" + strings.Repeat("1;", maxParams+100) + "m")) //nolint:errcheck |
| 466 | |
| 467 | if len(p.params) > maxParams { |
| 468 | t.Errorf("the parameters grew to %d, want them capped at %d", len(p.params), maxParams) |
| 469 | } |
| 470 | } |
| 471 | |
| 472 | func TestUnknownSequencesAreSwallowedNotPrinted(t *testing.T) { |
| 473 | // A terminal that prints the sequences it does not implement is worse than |
| 474 | // one that drops them: the display fills with rubbish. |
| 475 | tests := []string{ |
| 476 | "\x1b[6n", // a device status report, which needs a reply we do not send |
| 477 | "\x1b[>c", // secondary device attributes |
| 478 | "\x1b[?2004h", // bracketed paste |
| 479 | "\x1b[?1000h", // mouse reporting |
| 480 | "\x1b(B", // a character-set selector |
| 481 | "\x1b[1 q", // a cursor-style request, with an intermediate byte |
| 482 | "\x1bP1$r\x1b\\", // a device control string |
| 483 | } |
| 484 | |
| 485 | for _, stream := range tests { |
| 486 | t.Run(stream, func(t *testing.T) { |
| 487 | s := feed(20, 3, "a"+stream+"b") |
| 488 | wantLines(t, s, "ab") |
| 489 | }) |
| 490 | } |
| 491 | } |
| 492 | |
| 493 | func TestAControlCharacterInsideASequenceIsActedOn(t *testing.T) { |
| 494 | // A real terminal acts on a carriage return wherever it appears, rather |
| 495 | // than swallowing it into the sequence being collected. |
| 496 | s := feed(10, 3, "abc\x1b[1\r2H") |
| 497 | |
| 498 | if got := s.Cursor().Col; got != 0 { |
| 499 | t.Errorf("Cursor().Col = %d, want the carriage return to have been acted on", got) |
| 500 | } |
| 501 | } |
| 502 | |
| 503 | func TestScreenReturnsTheScreenBeingDrawnOn(t *testing.T) { |
| 504 | screen := NewScreen(10, 2) |
| 505 | p := NewParser(screen) |
| 506 | |
| 507 | if p.Screen() != screen { |
| 508 | t.Error("Screen() returned a different screen") |
| 509 | } |
| 510 | } |
| 511 | |
| 512 | func TestApplicationCursorKeys(t *testing.T) { |
| 513 | // Without this, the arrow keys are encoded the wrong way inside vim and at |
| 514 | // a readline prompt, which is the most visible thing an emulator can get |
| 515 | // wrong. |
| 516 | if feed(10, 3, "").ApplicationCursor() { |
| 517 | t.Error("application cursor keys are on by default") |
| 518 | } |
| 519 | if !feed(10, 3, "\x1b[?1h").ApplicationCursor() { |
| 520 | t.Error("ESC [ ? 1 h did not turn application cursor keys on") |
| 521 | } |
| 522 | if feed(10, 3, "\x1b[?1h\x1b[?1l").ApplicationCursor() { |
| 523 | t.Error("ESC [ ? 1 l did not turn them off again") |
| 524 | } |
| 525 | if feed(10, 3, "\x1b[?1h\x1bc").ApplicationCursor() { |
| 526 | t.Error("a reset left application cursor keys on") |
| 527 | } |
| 528 | } |