cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / ui / sixel_test.go
sixel_test.go 5.7K
1package ui23import (4 "fmt"5 "strings"6 "testing"7 "time"89 tea "charm.land/bubbletea/v2"10 uv "github.com/charmbracelet/ultraviolet"11 "github.com/charmbracelet/x/ansi/kitty"12)1314// ticked stands for a sixelDrawMsg asked for: tests deliver15// sixelDrawMsg themselves, with the clock where they want it.16type ticked struct{}1718// sixelModel is a wide model with a chart, on a terminal that has said19// what a sixel terminal says: sixel in DA1, 16 color registers and its20// cell size. Its clock stands still unless the test moves it.21func sixelModel(t *testing.T) (*Model, *time.Time) {22 t.Helper()23 m := wideModel()24 m.term.tmux = false // wherever the tests run25 clock := time.Unix(1000, 0)26 m.term.six.now = func() time.Time { return clock }27 m.term.six.tick = func() tea.Cmd { return func() tea.Msg { return ticked{} } }28 chartTable(t, m)29 m.runCommand("insert.chart")30 press(t, m, "<enter>", "<esc>")31 if !strings.Contains(screen(m), "█") {32 t.Fatalf("no bars in the text chart:\n%s", screen(m))33 }34 send(m, uv.UnknownCsiEvent("\x1b[?1;0;16S"))35 if got := sixelOut(m, uv.PrimaryDeviceAttributesEvent{62, 4, 22}); !m.term.sixel || got != "\x1b[16t" {36 t.Fatalf("sixel not detected, or no cell size asked for: %q", got)37 }38 if m.term.registers != 16 {39 t.Errorf("%d registers, want 16", m.term.registers)40 }41 if got := sixelOut(m, uv.CellSizeEvent{Width: 8, Height: 16}); got != "<tick>" {42 t.Fatalf("after the cell size: %q, want images asked for", got)43 }44 return m, &clock45}4647// sixelOut updates m with msg and returns what the commands write to the48// terminal, with "<clear>" for a screen cleared and "<tick>" for images49// asked for.50func sixelOut(m *Model, msg tea.Msg) string {51 _, cmd := m.Update(msg)52 m.View()53 var b strings.Builder54 for _, msg := range flatten(cmd) {55 switch msg := msg.(type) {56 case tea.RawMsg:57 b.WriteString(msg.Msg.(string))58 case ticked:59 b.WriteString("<tick>")60 default:61 if fmt.Sprintf("%T", msg) == "tea.clearScreenMsg" {62 b.WriteString("<clear>")63 }64 }65 }66 return b.String()67}6869func TestSixelImages(t *testing.T) {70 m, clock := sixelModel(t)71 // The text chart stays under the image, for a terminal that says it72 // has sixel but doesn't show it, or a recording of the text alone.73 if len(m.term.six.plan) != 1 || !strings.Contains(screen(m), "█") {74 t.Fatalf("plan %v; no text chart under the image:\n%s", m.term.six.plan, screen(m))75 }76 if !strings.Contains(screen(m), "1,500") {77 t.Error("the axes aren't text around the image")78 }79 // Not drawn until the frame has been still for sixelSettle.80 if got := sixelOut(m, sixelDrawMsg{}); got != "<tick>" {81 t.Errorf("drawn over a frame still changing: %q", got)82 }83 *clock = clock.Add(sixelSettle)84 p := m.term.six.plan[0]85 got := sixelOut(m, sixelDrawMsg{})86 at := fmt.Sprintf("\x1b7\x1b[%d;%dH\x1bP0;1;0q", p.at.Min.Y+1, p.at.Min.X+1)87 if !strings.HasPrefix(got, at) || !strings.HasSuffix(got, "\x1b\\\x1b8") {88 t.Fatalf("image drawn as %.60q, want it at %q with the cursor saved", got, at)89 }90 if n := strings.Count(got, ";2;"); n == 0 || n > 16 {91 t.Errorf("%d colors defined for 16 registers", n)92 }93 // Drawn once: a frame that doesn't change draws nothing.94 if got := sixelOut(m, sixelDrawMsg{}); got != "" {95 t.Errorf("drawn again over the same frame: %.40q", got)96 }97 // A row the image crosses changes: drawn again.98 press(t, m, "<f5>", fmt.Sprintf("A%d", p.at.Min.Y-gridTop+1), "<enter>")99 m.View() // as Bubble Tea does after every update100 *clock = clock.Add(time.Second)101 if got := sixelOut(m, sixelDrawMsg{}); !strings.HasPrefix(got, at) {102 t.Errorf("not drawn again after its rows changed: %.40q", got)103 }104}105106// Scrolling moves the image: the screen is cleared, erasing it, and it is107// drawn where it went. The command palette over it makes it text.108func TestSixelMovesAndHides(t *testing.T) {109 m, clock := sixelModel(t)110 *clock = clock.Add(time.Second)111 sixelOut(m, sixelDrawMsg{})112 was := m.term.six.plan[0].at113 if got := sixelOut(m, tea.MouseWheelMsg{X: 50, Y: gridTop + 2, Button: tea.MouseWheelDown}); !strings.Contains(got, "<clear>") {114 t.Fatalf("scrolled without clearing: %q", got)115 }116 if len(m.term.six.plan) == 1 && m.term.six.plan[0].at == was {117 t.Fatalf("the image didn't move: %v", m.term.six.plan)118 }119 *clock = clock.Add(time.Second)120 if got := sixelOut(m, sixelDrawMsg{}); len(m.term.six.plan) == 1 && !strings.Contains(got, "\x1bP") {121 t.Error("not drawn where it went")122 }123 sixelOut(m, tea.MouseWheelMsg{X: 50, Y: gridTop + 2, Button: tea.MouseWheelUp})124 *clock = clock.Add(time.Second)125 sixelOut(m, sixelDrawMsg{})126 if got := sixelOut(m, tea.KeyPressMsg{Code: 'k', Mod: tea.ModCtrl}); len(m.term.six.plan) != 0 || !strings.Contains(got, "<clear>") {127 t.Errorf("palette open: plan %v, %q", m.term.six.plan, got)128 }129}130131// Sixel is used only when kitty graphics aren't, never inside tmux, and132// not with chart-images off.133func TestSixelDetection(t *testing.T) {134 m := wideModel()135 m.term.tmux = false136 m.Update(uv.PrimaryDeviceAttributesEvent{62, 22})137 if m.term.sixel {138 t.Error("sixel without attribute 4")139 }140 m.Update(uv.KittyGraphicsEvent{Options: kitty.Options{ID: 31}, Payload: []byte("OK")})141 m.Update(uv.PrimaryDeviceAttributesEvent{62, 4})142 if m.term.sixel {143 t.Error("sixel on a terminal with kitty graphics")144 }145 m = wideModel()146 m.term.tmux = true147 m.Update(uv.PrimaryDeviceAttributesEvent{62, 4})148 if m.term.sixel {149 t.Error("sixel inside tmux")150 }151 m, _ = sixelModel(t)152 m.term.noImages = true153 if got := sixelOut(m, nil); m.term.sixelOn() || len(m.term.six.plan) != 0 || got != "" {154 t.Errorf("sixel images with chart-images = false: %q", got)155 }156 for in, want := range map[string]int{"\x1b[?1;0;256S": 256, "\x1b[?1;0;1024S": 256, "\x1b[?1;3;0S": 0, "\x1b[?2;0;4S": 0} {157 if n, _ := parseRegisters(in); n != want {158 t.Errorf("%q: %d registers, want %d", in, n, want)159 }160 }161}