package tui import ( "fmt" "os" "strings" "testing" "unicode" ) func TestLightRenderer(t *testing.T) { tty_file, _ := os.Open("") renderer, _ := NewLightRenderer( "", tty_file, &ColorTheme{}, true, false, 0, false, true, func(h int) int { return h }) light_renderer := renderer.(*LightRenderer) go func() { for { light_renderer.mutex.Lock() ready := light_renderer.cancel != nil light_renderer.mutex.Unlock() if ready { light_renderer.CancelGetChar() break } } }() event := light_renderer.GetChar(true) if event.Type != Invalid { t.Error("Not cancelled") } assertCharSequence := func(sequence string, name string) { bytes := []byte(sequence) light_renderer.buffer = bytes event := light_renderer.GetChar(true) if event.KeyName() != name { t.Errorf( "sequence: %q | %v | '%s' (%s) != %s", string(bytes), bytes, event.KeyName(), event.Type.String(), name) } } assertEscSequence := func(sequence string, name string) { bytes := []byte(sequence) light_renderer.buffer = bytes sz := 1 event := light_renderer.escSequence(&sz) if fmt.Sprintf("!%s", event.Type.String()) == name { // this is fine } else if event.KeyName() != name { t.Errorf( "sequence: %q | %v | '%s' (%s) != %s", string(bytes), bytes, event.KeyName(), event.Type.String(), name) } } // invalid assertEscSequence("\x1b[<", "!Invalid") assertEscSequence("\x1b[1;1R", "!Invalid") assertEscSequence("\x1b[", "!Invalid") assertEscSequence("\x1b[1", "!Invalid") assertEscSequence("\x1b[3;3~1", "!Invalid") assertEscSequence("\x1b[13", "!Invalid") assertEscSequence("\x1b[1;3", "!Invalid") assertEscSequence("\x1b[1;10", "!Invalid") assertEscSequence("\x1b[220~", "!Invalid") assertEscSequence("\x1b[5;30~", "!Invalid") assertEscSequence("\x1b[6;30~", "!Invalid") // general for r := 'a'; r < 'z'; r++ { lower_r := fmt.Sprintf("%c", r) upper_r := fmt.Sprintf("%c", unicode.ToUpper(r)) assertCharSequence(lower_r, lower_r) assertCharSequence(upper_r, upper_r) } assertCharSequence("\x01", "ctrl-a") assertCharSequence("\x02", "ctrl-b") assertCharSequence("\x03", "ctrl-c") assertCharSequence("\x04", "ctrl-d") assertCharSequence("\x05", "ctrl-e") assertCharSequence("\x06", "ctrl-f") assertCharSequence("\x07", "ctrl-g") // ctrl-h is the same as ctrl-backspace // ctrl-i is the same as tab assertCharSequence("\n", "ctrl-j") assertCharSequence("\x0b", "ctrl-k") assertCharSequence("\x0c", "ctrl-l") assertCharSequence("\r", "enter") // enter assertCharSequence("\x0e", "ctrl-n") assertCharSequence("\x0f", "ctrl-o") assertCharSequence("\x10", "ctrl-p") assertCharSequence("\x11", "ctrl-q") assertCharSequence("\x12", "ctrl-r") assertCharSequence("\x13", "ctrl-s") assertCharSequence("\x14", "ctrl-t") assertCharSequence("\x15", "ctrl-u") assertCharSequence("\x16", "ctrl-v") assertCharSequence("\x17", "ctrl-w") assertCharSequence("\x18", "ctrl-x") assertCharSequence("\x19", "ctrl-y") assertCharSequence("\x1a", "ctrl-z") assertCharSequence("\x00", "ctrl-space") assertCharSequence("\x1c", "ctrl-\\") assertCharSequence("\x1d", "ctrl-]") assertCharSequence("\x1e", "ctrl-^") assertCharSequence("\x1f", "ctrl-/") assertEscSequence("\x1ba", "alt-a") assertEscSequence("\x1bb", "alt-b") assertEscSequence("\x1bc", "alt-c") assertEscSequence("\x1bd", "alt-d") assertEscSequence("\x1be", "alt-e") assertEscSequence("\x1bf", "alt-f") assertEscSequence("\x1bg", "alt-g") assertEscSequence("\x1bh", "alt-h") assertEscSequence("\x1bi", "alt-i") assertEscSequence("\x1bj", "alt-j") assertEscSequence("\x1bk", "alt-k") assertEscSequence("\x1bl", "alt-l") assertEscSequence("\x1bm", "alt-m") assertEscSequence("\x1bn", "alt-n") assertEscSequence("\x1bo", "alt-o") assertEscSequence("\x1bp", "alt-p") assertEscSequence("\x1bq", "alt-q") assertEscSequence("\x1br", "alt-r") assertEscSequence("\x1bs", "alt-s") assertEscSequence("\x1bt", "alt-t") assertEscSequence("\x1bu", "alt-u") assertEscSequence("\x1bv", "alt-v") assertEscSequence("\x1bw", "alt-w") assertEscSequence("\x1bx", "alt-x") assertEscSequence("\x1by", "alt-y") assertEscSequence("\x1bz", "alt-z") assertEscSequence("\x1bOP", "f1") assertEscSequence("\x1bOQ", "f2") assertEscSequence("\x1bOR", "f3") assertEscSequence("\x1bOS", "f4") assertEscSequence("\x1b[15~", "f5") assertEscSequence("\x1b[17~", "f6") assertEscSequence("\x1b[18~", "f7") assertEscSequence("\x1b[19~", "f8") assertEscSequence("\x1b[20~", "f9") assertEscSequence("\x1b[21~", "f10") assertEscSequence("\x1b[23~", "f11") assertEscSequence("\x1b[24~", "f12") assertEscSequence("\x1b", "esc") assertCharSequence("\t", "tab") assertEscSequence("\x1b[Z", "shift-tab") assertCharSequence("\x7f", "backspace") assertEscSequence("\x1b\x7f", "alt-backspace") assertCharSequence("\b", "ctrl-backspace") assertEscSequence("\x1b\b", "ctrl-alt-backspace") assertEscSequence("\x1b[A", "up") assertEscSequence("\x1b[B", "down") assertEscSequence("\x1b[C", "right") assertEscSequence("\x1b[D", "left") assertEscSequence("\x1b[H", "home") assertEscSequence("\x1b[F", "end") assertEscSequence("\x1b[2~", "insert") assertEscSequence("\x1b[3~", "delete") assertEscSequence("\x1b[5~", "page-up") assertEscSequence("\x1b[6~", "page-down") assertEscSequence("\x1b[7~", "home") assertEscSequence("\x1b[8~", "end") assertEscSequence("\x1b[1;2A", "shift-up") assertEscSequence("\x1b[1;2B", "shift-down") assertEscSequence("\x1b[1;2C", "shift-right") assertEscSequence("\x1b[1;2D", "shift-left") assertEscSequence("\x1b[1;2H", "shift-home") assertEscSequence("\x1b[1;2F", "shift-end") assertEscSequence("\x1b[3;2~", "shift-delete") assertEscSequence("\x1b[5;2~", "shift-page-up") assertEscSequence("\x1b[6;2~", "shift-page-down") assertEscSequence("\x1b\x1b", "esc") assertEscSequence("\x1b\x1b[A", "alt-up") assertEscSequence("\x1b\x1b[B", "alt-down") assertEscSequence("\x1b\x1b[C", "alt-right") assertEscSequence("\x1b\x1b[D", "alt-left") assertEscSequence("\x1b[1;3A", "alt-up") assertEscSequence("\x1b[1;3B", "alt-down") assertEscSequence("\x1b[1;3C", "alt-right") assertEscSequence("\x1b[1;3D", "alt-left") assertEscSequence("\x1b[1;3H", "alt-home") assertEscSequence("\x1b[1;3F", "alt-end") assertEscSequence("\x1b[3;3~", "alt-delete") assertEscSequence("\x1b[5;3~", "alt-page-up") assertEscSequence("\x1b[6;3~", "alt-page-down") assertEscSequence("\x1b[1;4A", "alt-shift-up") assertEscSequence("\x1b[1;4B", "alt-shift-down") assertEscSequence("\x1b[1;4C", "alt-shift-right") assertEscSequence("\x1b[1;4D", "alt-shift-left") assertEscSequence("\x1b[1;4H", "alt-shift-home") assertEscSequence("\x1b[1;4F", "alt-shift-end") assertEscSequence("\x1b[3;4~", "alt-shift-delete") assertEscSequence("\x1b[5;4~", "alt-shift-page-up") assertEscSequence("\x1b[6;4~", "alt-shift-page-down") assertEscSequence("\x1b[1;5A", "ctrl-up") assertEscSequence("\x1b[1;5B", "ctrl-down") assertEscSequence("\x1b[1;5C", "ctrl-right") assertEscSequence("\x1b[1;5D", "ctrl-left") assertEscSequence("\x1b[1;5H", "ctrl-home") assertEscSequence("\x1b[1;5F", "ctrl-end") assertEscSequence("\x1b[3;5~", "ctrl-delete") assertEscSequence("\x1b[5;5~", "ctrl-page-up") assertEscSequence("\x1b[6;5~", "ctrl-page-down") assertEscSequence("\x1b[1;7A", "ctrl-alt-up") assertEscSequence("\x1b[1;7B", "ctrl-alt-down") assertEscSequence("\x1b[1;7C", "ctrl-alt-right") assertEscSequence("\x1b[1;7D", "ctrl-alt-left") assertEscSequence("\x1b[1;7H", "ctrl-alt-home") assertEscSequence("\x1b[1;7F", "ctrl-alt-end") assertEscSequence("\x1b[3;7~", "ctrl-alt-delete") assertEscSequence("\x1b[5;7~", "ctrl-alt-page-up") assertEscSequence("\x1b[6;7~", "ctrl-alt-page-down") assertEscSequence("\x1b[1;6A", "ctrl-shift-up") assertEscSequence("\x1b[1;6B", "ctrl-shift-down") assertEscSequence("\x1b[1;6C", "ctrl-shift-right") assertEscSequence("\x1b[1;6D", "ctrl-shift-left") assertEscSequence("\x1b[1;6H", "ctrl-shift-home") assertEscSequence("\x1b[1;6F", "ctrl-shift-end") assertEscSequence("\x1b[3;6~", "ctrl-shift-delete") assertEscSequence("\x1b[5;6~", "ctrl-shift-page-up") assertEscSequence("\x1b[6;6~", "ctrl-shift-page-down") assertEscSequence("\x1b[1;8A", "ctrl-alt-shift-up") assertEscSequence("\x1b[1;8B", "ctrl-alt-shift-down") assertEscSequence("\x1b[1;8C", "ctrl-alt-shift-right") assertEscSequence("\x1b[1;8D", "ctrl-alt-shift-left") assertEscSequence("\x1b[1;8H", "ctrl-alt-shift-home") assertEscSequence("\x1b[1;8F", "ctrl-alt-shift-end") assertEscSequence("\x1b[3;8~", "ctrl-alt-shift-delete") assertEscSequence("\x1b[5;8~", "ctrl-alt-shift-page-up") assertEscSequence("\x1b[6;8~", "ctrl-alt-shift-page-down") // xterm meta & mac assertEscSequence("\x1b[1;9A", "alt-up") assertEscSequence("\x1b[1;9B", "alt-down") assertEscSequence("\x1b[1;9C", "alt-right") assertEscSequence("\x1b[1;9D", "alt-left") assertEscSequence("\x1b[1;9H", "alt-home") assertEscSequence("\x1b[1;9F", "alt-end") assertEscSequence("\x1b[3;9~", "alt-delete") assertEscSequence("\x1b[5;9~", "alt-page-up") assertEscSequence("\x1b[6;9~", "alt-page-down") assertEscSequence("\x1b[1;10A", "alt-shift-up") assertEscSequence("\x1b[1;10B", "alt-shift-down") assertEscSequence("\x1b[1;10C", "alt-shift-right") assertEscSequence("\x1b[1;10D", "alt-shift-left") assertEscSequence("\x1b[1;10H", "alt-shift-home") assertEscSequence("\x1b[1;10F", "alt-shift-end") assertEscSequence("\x1b[3;10~", "alt-shift-delete") assertEscSequence("\x1b[5;10~", "alt-shift-page-up") assertEscSequence("\x1b[6;10~", "alt-shift-page-down") assertEscSequence("\x1b[1;11A", "alt-up") assertEscSequence("\x1b[1;11B", "alt-down") assertEscSequence("\x1b[1;11C", "alt-right") assertEscSequence("\x1b[1;11D", "alt-left") assertEscSequence("\x1b[1;11H", "alt-home") assertEscSequence("\x1b[1;11F", "alt-end") assertEscSequence("\x1b[3;11~", "alt-delete") assertEscSequence("\x1b[5;11~", "alt-page-up") assertEscSequence("\x1b[6;11~", "alt-page-down") assertEscSequence("\x1b[1;12A", "alt-shift-up") assertEscSequence("\x1b[1;12B", "alt-shift-down") assertEscSequence("\x1b[1;12C", "alt-shift-right") assertEscSequence("\x1b[1;12D", "alt-shift-left") assertEscSequence("\x1b[1;12H", "alt-shift-home") assertEscSequence("\x1b[1;12F", "alt-shift-end") assertEscSequence("\x1b[3;12~", "alt-shift-delete") assertEscSequence("\x1b[5;12~", "alt-shift-page-up") assertEscSequence("\x1b[6;12~", "alt-shift-page-down") assertEscSequence("\x1b[1;13A", "ctrl-alt-up") assertEscSequence("\x1b[1;13B", "ctrl-alt-down") assertEscSequence("\x1b[1;13C", "ctrl-alt-right") assertEscSequence("\x1b[1;13D", "ctrl-alt-left") assertEscSequence("\x1b[1;13H", "ctrl-alt-home") assertEscSequence("\x1b[1;13F", "ctrl-alt-end") assertEscSequence("\x1b[3;13~", "ctrl-alt-delete") assertEscSequence("\x1b[5;13~", "ctrl-alt-page-up") assertEscSequence("\x1b[6;13~", "ctrl-alt-page-down") assertEscSequence("\x1b[1;14A", "ctrl-alt-shift-up") assertEscSequence("\x1b[1;14B", "ctrl-alt-shift-down") assertEscSequence("\x1b[1;14C", "ctrl-alt-shift-right") assertEscSequence("\x1b[1;14D", "ctrl-alt-shift-left") assertEscSequence("\x1b[1;14H", "ctrl-alt-shift-home") assertEscSequence("\x1b[1;14F", "ctrl-alt-shift-end") assertEscSequence("\x1b[3;14~", "ctrl-alt-shift-delete") assertEscSequence("\x1b[5;14~", "ctrl-alt-shift-page-up") assertEscSequence("\x1b[6;14~", "ctrl-alt-shift-page-down") assertEscSequence("\x1b[1;15A", "ctrl-alt-up") assertEscSequence("\x1b[1;15B", "ctrl-alt-down") assertEscSequence("\x1b[1;15C", "ctrl-alt-right") assertEscSequence("\x1b[1;15D", "ctrl-alt-left") assertEscSequence("\x1b[1;15H", "ctrl-alt-home") assertEscSequence("\x1b[1;15F", "ctrl-alt-end") assertEscSequence("\x1b[3;15~", "ctrl-alt-delete") assertEscSequence("\x1b[5;15~", "ctrl-alt-page-up") assertEscSequence("\x1b[6;15~", "ctrl-alt-page-down") assertEscSequence("\x1b[1;16A", "ctrl-alt-shift-up") assertEscSequence("\x1b[1;16B", "ctrl-alt-shift-down") assertEscSequence("\x1b[1;16C", "ctrl-alt-shift-right") assertEscSequence("\x1b[1;16D", "ctrl-alt-shift-left") assertEscSequence("\x1b[1;16H", "ctrl-alt-shift-home") assertEscSequence("\x1b[1;16F", "ctrl-alt-shift-end") assertEscSequence("\x1b[3;16~", "ctrl-alt-shift-delete") assertEscSequence("\x1b[5;16~", "ctrl-alt-shift-page-up") assertEscSequence("\x1b[6;16~", "ctrl-alt-shift-page-down") // tmux & emacs assertEscSequence("\x1bOA", "up") assertEscSequence("\x1bOB", "down") assertEscSequence("\x1bOC", "right") assertEscSequence("\x1bOD", "left") assertEscSequence("\x1bOH", "home") assertEscSequence("\x1bOF", "end") // rrvt assertEscSequence("\x1b[1~", "home") assertEscSequence("\x1b[4~", "end") assertEscSequence("\x1b[11~", "f1") assertEscSequence("\x1b[12~", "f2") assertEscSequence("\x1b[13~", "f3") assertEscSequence("\x1b[14~", "f4") } func TestLightRendererScrollWheel(t *testing.T) { tty_file, _ := os.Open("") renderer, _ := NewLightRenderer( "", tty_file, &ColorTheme{}, true, true, 0, false, true, func(h int) int { return h }) light_renderer := renderer.(*LightRenderer) assertScroll := func(sequence string, scroll int, mods string) { bytes := []byte(sequence) light_renderer.buffer = bytes sz := 1 event := light_renderer.escSequence(&sz) me := event.MouseEvent if event.Type != Mouse || me == nil { t.Errorf("sequence: %q | got %s, want a Mouse event", sequence, event.Type.String()) return } got := "" if me.Ctrl { got += "ctrl-" } if me.Alt { got += "alt-" } if me.Shift { got += "shift-" } got = strings.TrimSuffix(got, "-") if me.S != scroll || got != mods || me.Down { t.Errorf( "sequence: %q | scroll=%d mods=%q down=%v != scroll=%d mods=%q down=false", sequence, me.S, got, me.Down, scroll, mods) } } assertScroll("\x1b[<64;1;1M", 1, "") // up assertScroll("\x1b[<65;1;1M", -1, "") // down assertScroll("\x1b[<66;1;1M", 0, "") // left (ignored) assertScroll("\x1b[<67;1;1M", 0, "") // right (ignored) assertScroll("\x1b[<68;1;1M", 1, "shift") // shift + up assertScroll("\x1b[<69;1;1M", -1, "shift") // shift + down assertScroll("\x1b[<70;1;1M", 0, "shift") // shift + left (ignored) assertScroll("\x1b[<71;1;1M", 0, "shift") // shift + right (ignored) assertScroll("\x1b[<72;1;1M", 1, "alt") // alt + up assertScroll("\x1b[<73;1;1M", -1, "alt") // alt + down assertScroll("\x1b[<74;1;1M", 0, "alt") // alt + left (ignored) assertScroll("\x1b[<75;1;1M", 0, "alt") // alt + right (ignored) assertScroll("\x1b[<80;1;1M", 1, "ctrl") // ctrl + up assertScroll("\x1b[<81;1;1M", -1, "ctrl") // ctrl + down assertScroll("\x1b[<82;1;1M", 0, "ctrl") // ctrl + left (ignored) assertScroll("\x1b[<83;1;1M", 0, "ctrl") // ctrl + right (ignored) assertScroll("\x1b[<84;1;1M", 1, "ctrl-shift") // ctrl+shift + up assertScroll("\x1b[<85;1;1M", -1, "ctrl-shift") // ctrl+shift + down assertScroll("\x1b[<86;1;1M", 0, "ctrl-shift") // ctrl+shift + left (ignored) assertScroll("\x1b[<87;1;1M", 0, "ctrl-shift") // ctrl+shift + right (ignored) assertScroll("\x1b[<92;1;1M", 1, "ctrl-alt-shift") // ctrl+alt+shift + up assertScroll("\x1b[<93;1;1M", -1, "ctrl-alt-shift") // ctrl+alt+shift + down assertScroll("\x1b[<94;1;1M", 0, "ctrl-alt-shift") // ctrl+alt+shift + left (ignored) assertScroll("\x1b[<95;1;1M", 0, "ctrl-alt-shift") // ctrl+alt+shift + right (ignored) } // Assert the exact byte stream emitted for a scripted rendering sequence, // locking down the SGR deduplication behavior of the light renderer. func TestLightRendererSGRDeduplication(t *testing.T) { renderer, _ := NewLightRenderer( "", nil, &ColorTheme{}, true, false, 8, false, true, func(h int) int { return h }) r := renderer.(*LightRenderer) r.width = 80 r.height = 24 out, err := os.CreateTemp(t.TempDir(), "fzf-ttyout") if err != nil { t.Fatal(err) } r.ttyout = out w := r.NewWindow(0, 0, 40, 4, WindowList, MakeBorderStyle(BorderNone, true), false).(*LightWindow) w.fg = colDefault w.bg = colDefault r.queued.Reset() red := NewColorPair(196, colDefault, AttrRegular) blue := NewColorPair(21, colDefault, AttrRegular) w.Move(0, 0) w.CPrint(red, "ab") w.CPrint(red, "cd") // same pair; no SGR expected w.CPrint(blue, "ef") w.Print("gh") // default colors; one reset expected w.Print("ij") // still default; no SGR expected w.Move(1, 2) w.CPrint(red, "kl") r.invalidateSGR() w.CPrint(red, "mn") // re-emitted after invalidation r.flush() // adds a trailing reset to leave the default state expected := "\x1b[?2026h\x1b[?7l\x1b[?25l" + "\r" + "\x1b[;38;5;196mab" + "cd" + "\x1b[;38;5;21mef" + "\x1b[0mgh" + "ij" + "\x1b[1B\x1b[3G" + "\x1b[;38;5;196mkl" + "\x1b[;38;5;196mmn" + "\x1b[0m" + "\x1b[?25h\x1b[?7h\x1b[?2026l" bytes, err := os.ReadFile(out.Name()) if err != nil { t.Fatal(err) } if string(bytes) != expected { t.Errorf("expected: %q\n got: %q", expected, string(bytes)) } }