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:
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\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)
+ }
+ }
+}