prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

commit 944707323a61d63de41a7dc57fb1df09ff30fd8d
parent 93746b2e2a3d6a005aa2f0990833dd2c9360c6d4
Author: Damian Myrda <damian@prime8.dev>
Date:   Thu, 22 Oct 2026 01:42:07 +0000

tui: README images scale with the pane, capped at 96x24 cells

Fit box is the pane width by a third of it in rows (about 3:2 on screen).

Diffstat:
Minternal/tui/markdown_images.go | 25+++++++++++++++++--------
Minternal/tui/markdown_images_test.go | 15+++++++++++++--
2 files changed, 30 insertions(+), 10 deletions(-)

diff --git a/internal/tui/markdown_images.go b/internal/tui/markdown_images.go @@ -19,8 +19,8 @@ import ( const ( imageToken = "PRIME8IMAGE" - imageMaxCols = 60 - imageMaxRows = 14 + imageMaxCols = 96 + imageMaxRows = 24 ) var ( @@ -93,15 +93,24 @@ func extractImages(markdown string, width int) (string, [][]string) { return strings.Join(out, "\n"), blocks } -// renderImageBlock draws one image within imageMaxCols x imageMaxRows (and -// the pane width), or a dim link line when it can't be fetched or decoded. +// imageBox is the cell box an image is fitted into: the pane width and a +// third of it in rows (a cell is about twice as tall as wide, so roughly a +// 3:2 box), capped at imageMaxCols x imageMaxRows. +func imageBox(width int) (cols, rows int) { + cols = max(min(width, imageMaxCols), 8) + rows = max(min(cols/3, imageMaxRows), 4) + return cols, rows +} + +// renderImageBlock draws one image fitted into imageBox(width), keeping its +// aspect, or a dim link line when it can't be fetched or decoded. func renderImageBlock(src string, width int) []string { - cols := max(min(width, imageMaxCols), 8) + cols, rows := imageBox(width) key := fmt.Sprintf("%s|%d", src, cols) if v, ok := imageCache.Load(key); ok { return v.([]string) } - lines := fetchImageLines(src, cols) + lines := fetchImageLines(src, cols, rows) if lines == nil { lines = []string{dimStyle.Render("[image] " + src)} } @@ -109,7 +118,7 @@ func renderImageBlock(src string, width int) []string { return lines } -func fetchImageLines(src string, cols int) []string { +func fetchImageLines(src string, cols, rows int) []string { if !strings.HasPrefix(src, "http://") && !strings.HasPrefix(src, "https://") { return nil } @@ -124,7 +133,7 @@ func fetchImageLines(src string, cols int) []string { return nil } // ansimage sizes are in pixels; a half-block cell is 1 wide, 2 tall. - img, err := ansimage.NewScaledFromReader(res.Body, imageMaxRows*2, cols, + img, err := ansimage.NewScaledFromReader(res.Body, rows*2, cols, color.Black, ansimage.ScaleModeFit, ansimage.NoDithering) if err != nil { return nil diff --git a/internal/tui/markdown_images_test.go b/internal/tui/markdown_images_test.go @@ -31,6 +31,7 @@ func TestReadmeImagesAreCapped(t *testing.T) { })) md := "# demo\n\nsome text\n\n![shot](" + srv.URL + "/a.png)\n\n<p align=\"center\"><img src=\"" + srv.URL + "/b.png\" width=\"900\"></p>\n\nmore text\n" out := renderMarkdown(md, 200) + _, maxRows := imageBox(200) srv.Close() rows := 0 @@ -45,11 +46,21 @@ func TestReadmeImagesAreCapped(t *testing.T) { if rows == 0 { t.Fatalf("%v: no image rendered:\n%s", size, out) } - if rows > 2*imageMaxRows { - t.Errorf("%v: %d image rows for two images, cap is %d each", size, rows, imageMaxRows) + if rows > 2*maxRows { + t.Errorf("%v: %d image rows for two images, cap is %d each", size, rows, maxRows) } if !strings.Contains(ansi.Strip(out), "more text") { t.Errorf("text after images lost") } } } + +func TestImageBoxScalesWithPane(t *testing.T) { + for _, c := range []struct{ width, cols, rows int }{ + {40, 40, 13}, {60, 60, 20}, {90, 90, 24}, {200, 96, 24}, {10, 10, 4}, + } { + if cols, rows := imageBox(c.width); cols != c.cols || rows != c.rows { + t.Errorf("imageBox(%d) = %dx%d, want %dx%d", c.width, cols, rows, c.cols, c.rows) + } + } +}