package tui import ( "encoding/base64" "strings" "testing" "github.com/charmbracelet/colorprofile" "github.com/charmbracelet/x/ansi/kitty" ) func TestPlaceholderGridEncodesRowAndColumn(t *testing.T) { const cols, rows = 3, 2 grid := placeholderGrid(cols, rows, baseImageID) if len(grid) != rows { t.Fatalf("got %d rows, want %d", len(grid), rows) } for i, row := range grid { runes := []rune(stripSGR(row)) if len(runes) != cols*4 { t.Fatalf("row %d has %d runes, want %d", i, len(runes), cols*4) } for col := 0; col < cols; col++ { cell := runes[col*4 : col*4+4] if cell[0] != kitty.Placeholder { t.Errorf("row %d col %d: got %#x, want the placeholder rune", i, col, cell[0]) } if cell[1] != kitty.Diacritic(i) { t.Errorf("row %d col %d: row mark = %#x, want %#x", i, col, cell[1], kitty.Diacritic(i)) } if cell[2] != kitty.Diacritic(col) { t.Errorf("row %d col %d: column mark = %#x, want %#x", i, col, cell[2], kitty.Diacritic(col)) } } } } func TestPlaceholderEncodingMatchesKittenIcat(t *testing.T) { got := []rune(stripSGR(placeholderGrid(3, 1, baseImageID)[0])) want := []rune{ 0x10EEEE, 0x0305, 0x0305, 0x0305, 0x10EEEE, 0x0305, 0x030D, 0x0305, 0x10EEEE, 0x0305, 0x030E, 0x0305, } if len(got) != len(want) { t.Fatalf("got %#x, want %#x", got, want) } for i := range want { if got[i] != want[i] { t.Errorf("rune %d = %#x, want %#x", i, got[i], want[i]) } } } func TestTransmissionIsAValidVirtualPlacement(t *testing.T) { p := kittyPlacement{path: "/music/covers/cover.jpg", cols: 30, rows: 15, id: baseImageID, placement: basePlacementID} seq := p.transmit() if !strings.HasPrefix(seq, kittyEscape) { t.Fatalf("must start with the APC introducer, got %q", seq[:min(10, len(seq))]) } if strings.Count(seq, kittyEscape) != 1 { t.Errorf("the introducer must appear exactly once: %q", seq) } if !strings.HasSuffix(seq, st) { t.Error("must be terminated with ST") } for _, want := range []string{ "a=T", "f=100", "t=f", "U=1", "c=30", "r=15", "i=7800", "p=7800", "q=2", } { if !strings.Contains(seq, want) { t.Errorf("transmission is missing %q: %s", want, seq) } } if strings.Contains(seq, "C=1") { t.Errorf("a virtual placement must not set the cursor policy: %s", seq) } payload := seq[strings.Index(seq, ";")+1 : len(seq)-len(st)] decoded, err := base64.StdEncoding.DecodeString(payload) if err != nil { t.Fatalf("path payload is not valid base64: %v", err) } if string(decoded) != p.path { t.Errorf("path = %q, want %q", decoded, p.path) } } func TestClearKeepsTheImageCached(t *testing.T) { p := kittyPlacement{path: "/x.jpg", cols: 10, rows: 5, id: baseImageID, placement: basePlacementID} seq := p.clear() if !strings.Contains(seq, "d=i") { t.Errorf("clear should use the lowercase delete form: %s", seq) } if !strings.Contains(seq, "i=7800") || !strings.Contains(seq, "p=7800") { t.Errorf("clear must target the placement: %s", seq) } } func TestEmptyPlacementProducesNothing(t *testing.T) { for _, p := range []kittyPlacement{ {path: "", cols: 10, rows: 5, id: 1}, {path: "/x.jpg", cols: 1, rows: 5, id: 1}, {path: "/x.jpg", cols: 10, rows: 0, id: 1}, } { if seq := p.transmit(); seq != "" { t.Errorf("expected no sequence for %+v, got %q", p, seq) } } if seq := (kittyPlacement{}).clear(); seq != "" { t.Errorf("clearing an unplaced image should produce nothing, got %q", seq) } } func TestGraphicsPreviewQueuesAPlacementMatchingItsGrid(t *testing.T) { forceChars = false p := &ImagePreviewer{mode: imageKitty, cache: map[string][]string{}, staged: map[string]string{}} dir := t.TempDir() img := dir + "/cover.png" makeImage(t, img, false) grid := p.Render(img, 30, 20) if len(grid) == 0 { t.Fatal("graphics mode should still reserve cells") } place, ok := p.TakePlacement() if !ok { t.Fatal("graphics mode should queue a placement") } if place.path != img { t.Errorf("placement path = %q, want %q", place.path, img) } if n := strings.Count(grid[0], string(kitty.Placeholder)); n != place.cols { t.Errorf("placement is %d columns but the grid has %d cells", place.cols, n) } if len(grid) != place.rows { t.Errorf("placement is %d rows but the grid has %d", place.rows, len(grid)) } if _, ok := p.TakePlacement(); ok { t.Error("taking a placement twice should be empty the second time") } } func TestCharacterModeEmitsNoEscapes(t *testing.T) { forceChars = true defer func() { forceChars = false }() p := NewImagePreviewer() if !p.Available() { t.Skip("chafa is not installed") } dir := t.TempDir() img := dir + "/cover.png" makeImage(t, img, false) lines := p.Render(img, 40, 20) if len(lines) == 0 { t.Fatal("character mode produced no preview") } if strings.Contains(lines[0], kittyEscape) { t.Error("character mode must not emit graphics escapes") } if _, ok := p.TakePlacement(); ok { t.Error("character mode must not queue a placement") } } func TestCellShapeMatchesReference(t *testing.T) { grid := placeholderGrid(2, 1, 8) body := grid[0] body = stripSGR(body) got := []rune(body) want := []rune{ 0x10EEEE, 0x0305, 0x0305, 0x0305, 0x10EEEE, 0x0305, 0x030D, 0x0305, } if len(got) != len(want) { t.Fatalf("cell runes = %#x, want %#x", got, want) } for i := range want { if got[i] != want[i] { t.Errorf("rune %d = %#x, want %#x", i, got[i], want[i]) } } } func TestGridIndexesRowsAndColumns(t *testing.T) { grid := placeholderGrid(3, 2, 8) for row, line := range grid { cells := []rune(stripSGR(line)) for col := 0; col < 3; col++ { cell := cells[col*4 : col*4+4] if cell[1] != kittyDiacritic(row) { t.Errorf("cell (%d,%d) row mark = %#x, want %#x", row, col, cell[1], kittyDiacritic(row)) } if cell[2] != kittyDiacritic(col) { t.Errorf("cell (%d,%d) col mark = %#x, want %#x", row, col, cell[2], kittyDiacritic(col)) } } } } func TestImageIDTravelsInForegroundColour(t *testing.T) { cases := []struct { id int want string }{ {8, "\x1b[38:2:0:0:8m"}, {7800, "\x1b[38:2:0:30:120m"}, {0x010203, "\x1b[38:2:1:2:3m"}, } for _, c := range cases { grid := placeholderGrid(1, 1, c.id) if !strings.HasPrefix(grid[0], c.want) { t.Errorf("id %d: line starts %q, want prefix %q", c.id, grid[0][:min(20, len(grid[0]))], c.want) } } } func TestHighByteGoesInThirdDiacritic(t *testing.T) { grid := placeholderGrid(1, 1, 0xD7000000) if !strings.HasPrefix(grid[0], "\x1b[38:2:0:0:0m") { t.Errorf("expected a black foreground, got %q", grid[0][:min(20, len(grid[0]))]) } cells := []rune(stripSGR(grid[0])) if cells[3] != kittyDiacritic(0xD7) { t.Errorf("msb diacritic = %#x, want %#x", cells[3], kittyDiacritic(0xD7)) } } func TestEachLineResetsColour(t *testing.T) { grid := placeholderGrid(2, 3, 8) for row, line := range grid { if !strings.HasSuffix(line, "\x1b[39m") { t.Errorf("row %d does not reset the foreground colour: %q", row, line) } if !strings.HasPrefix(line, "\x1b[38:2:") { t.Errorf("row %d does not set the id colour: %q", row, line) } } } func kittyDiacritic(i int) rune { return kitty.Diacritic(i) } func stripSGR(s string) string { var b strings.Builder for i := 0; i < len(s); { if s[i] == 0x1b && i+1 < len(s) && s[i+1] == '[' { j := i + 2 for j < len(s) && s[j] != 'm' { j++ } i = j + 1 continue } b.WriteByte(s[i]) i++ } return b.String() } func min(a, b int) int { if a < b { return a } return b } func TestDisplayWidthCountsPlaceholderCellsAsOne(t *testing.T) { const cols = 30 grid := placeholderGrid(cols, 1, baseImageID) if got := displayWidth(grid[0]); got != cols { t.Errorf("placeholder line measures %d cells, want %d", got, cols) } } func TestDisplayWidthIgnoresEscapesAndCombiningMarks(t *testing.T) { cases := []struct { name string in string want int }{ {"escapes are free", "\x1b[31mred\x1b[0m", 3}, {"a truecolor sequence", "\x1b[38:2:0:16:146m", 0}, {"combining mark adds nothing", "a\u0305b", 2}, {"one placeholder cell", "\U0010EEEE\u0305\u0305", 1}, {"two placeholder cells", "\U0010EEEE\u0305\u0305\U0010EEEE\u0305\u030D", 2}, {"empty", "", 0}, } for _, c := range cases { if got := displayWidth(c.in); got != c.want { t.Errorf("%s: displayWidth(%q) = %d, want %d", c.name, c.in, got, c.want) } } } func TestTruncateToLeavesFittingGridAlone(t *testing.T) { grid := placeholderGrid(10, 1, baseImageID) got := truncateTo(grid[0], 10) if got != grid[0] { t.Errorf("a line that fits was rewritten:\n got %q\nwant %q", got, grid[0]) } if !strings.Contains(got, "38:2:") { t.Error("the image id colour was stripped from the grid line") } if n := strings.Count(got, string(placeholderRune)); n != 10 { t.Errorf("kept %d cells, want 10", n) } } func TestKittyImagesRequireATrueColourProfile(t *testing.T) { t.Setenv("TERM", "xterm-kitty") t.Setenv("INTAKE_IMAGES", "kitty") profile, ok := requiredColourProfile() if !ok { t.Fatal("kitty graphics mode must impose a colour profile") } if profile != colorprofile.TrueColor { t.Errorf("profile = %v, want truecolor", profile) } } func TestOtherModesLeaveTheProfileAlone(t *testing.T) { for _, mode := range []string{"chafa", "none"} { t.Setenv("TERM", "xterm-kitty") t.Setenv("INTAKE_IMAGES", mode) if _, ok := requiredColourProfile(); ok { t.Errorf("mode %q should not impose a colour profile", mode) } } }