diff options
Diffstat (limited to 'gui/internal/model')
| -rw-r--r-- | gui/internal/model/prefs.go | 13 | ||||
| -rw-r--r-- | gui/internal/model/preview.go | 29 | ||||
| -rw-r--r-- | gui/internal/model/preview_test.go | 72 |
3 files changed, 98 insertions, 16 deletions
diff --git a/gui/internal/model/prefs.go b/gui/internal/model/prefs.go index ce9ee42..52ed012 100644 --- a/gui/internal/model/prefs.go +++ b/gui/internal/model/prefs.go @@ -22,11 +22,19 @@ type Prefs struct { Preview bool `json:"preview"` // SelectAll starts a plan's rows checked, as the terminal review does. SelectAll bool `json:"select_all"` + // PreviewHeight is how tall the preview under the explanation is, in + // pixels; the divider between them sets it, and so does Settings. + PreviewHeight int `json:"preview_height"` } +// DefaultPreviewHeight is the preview's height until one is chosen, and the +// floor a smaller one is raised to. +const DefaultPreviewHeight = 280 + // DefaultPrefs is what a window does before anything is chosen. func DefaultPrefs() Prefs { - return Prefs{FontSize: 0, Colours: true, Preview: true, SelectAll: true} + return Prefs{FontSize: 0, Colours: true, Preview: true, SelectAll: true, + PreviewHeight: DefaultPreviewHeight} } // PrefsFile is where they are kept. @@ -45,6 +53,9 @@ func LoadPrefs() Prefs { if err := json.Unmarshal(data, &p); err != nil { return DefaultPrefs() } + if p.PreviewHeight < 80 { + p.PreviewHeight = DefaultPreviewHeight + } return p } diff --git a/gui/internal/model/preview.go b/gui/internal/model/preview.go index d2a2830..4ec0569 100644 --- a/gui/internal/model/preview.go +++ b/gui/internal/model/preview.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "unicode/utf8" @@ -47,9 +48,11 @@ const ( ) // MakePreview looks at one file. tmp is a directory the caller owns, where -// a rendered PDF page is written; the caller removes it. Nothing is written -// anywhere else, and the file itself is only read. -func MakePreview(ctx context.Context, path, tmp string) Preview { +// a rendered PDF page is written; the caller removes it. px is how tall the +// preview will be shown, so a page is rendered to suit it rather than to a +// fixed size - 0 takes the default. Nothing is written anywhere else, and +// the file itself is only read. +func MakePreview(ctx context.Context, path, tmp string, px int) Preview { fi, err := os.Stat(path) if err != nil { return Preview{Note: err.Error()} @@ -65,7 +68,7 @@ func MakePreview(ctx context.Context, path, tmp string) Preview { } return Preview{Kind: PreviewImage, Image: path, Note: fmt.Sprintf("image, %s", size(fi.Size()))} case ext == "pdf": - return pdfPreview(ctx, path, tmp, fi.Size()) + return pdfPreview(ctx, path, tmp, fi.Size(), px) } text, ok := textHead(path) if !ok { @@ -82,7 +85,7 @@ func MakePreview(ctx context.Context, path, tmp string) Preview { // called page-1.png and the wrong one could be picked up - which is what // happened: a preview showed the page of a PDF looked at earlier (his // report, 2026-09-17). -func pdfPreview(ctx context.Context, path, tmp string, sz int64) Preview { +func pdfPreview(ctx context.Context, path, tmp string, sz int64, px int) Preview { if _, err := exec.LookPath("pdftoppm"); err == nil { dir, err := os.MkdirTemp(tmp, "page-") if err != nil { @@ -90,7 +93,7 @@ func pdfPreview(ctx context.Context, path, tmp string, sz int64) Preview { } out := filepath.Join(dir, "page") cmd := exec.CommandContext(ctx, "pdftoppm", "-png", "-f", "1", "-l", "1", - "-scale-to", "700", "--", path, out) + "-scale-to", strconv.Itoa(renderPixels(px)), "--", path, out) if err := cmd.Run(); err == nil { if rendered := firstMatch(out + "*.png"); rendered != "" { return Preview{Kind: PreviewImage, Image: rendered, Dir: dir, @@ -111,6 +114,20 @@ func pdfPreview(ctx context.Context, path, tmp string, sz int64) Preview { Note: fmt.Sprintf("text of the first pages, %s", size(sz))} } +// renderPixels is how large a page is rendered for a preview px tall: twice +// the size it is shown at, so it stays sharp when the pane is dragged a +// little wider, within limits that keep the render quick. +func renderPixels(px int) int { + n := px * 2 + switch { + case n < 700: + return 700 + case n > 2400: + return 2400 + } + return n +} + // firstMatch is the first file matching a glob, "" when there is none. func firstMatch(glob string) string { names, err := filepath.Glob(glob) diff --git a/gui/internal/model/preview_test.go b/gui/internal/model/preview_test.go index b620a92..388ff7b 100644 --- a/gui/internal/model/preview_test.go +++ b/gui/internal/model/preview_test.go @@ -27,21 +27,21 @@ func TestPreviewKinds(t *testing.T) { binary := write("a.bin", []byte{0, 1, 2, 3, 0}) tmp := t.TempDir() - if p := MakePreview(context.Background(), text, tmp); p.Kind != PreviewText || + if p := MakePreview(context.Background(), text, tmp, 0); p.Kind != PreviewText || !strings.Contains(p.Text, "second line") { t.Errorf("text preview = %+v", p) } - if p := MakePreview(context.Background(), png, tmp); p.Kind != PreviewImage || p.Image != png { + if p := MakePreview(context.Background(), png, tmp, 0); p.Kind != PreviewImage || p.Image != png { t.Errorf("image preview = %+v", p) } - if p := MakePreview(context.Background(), binary, tmp); p.Kind != PreviewNone || + if p := MakePreview(context.Background(), binary, tmp, 0); p.Kind != PreviewNone || !strings.Contains(p.Note, "no preview") { t.Errorf("binary preview = %+v", p) } - if p := MakePreview(context.Background(), filepath.Join(dir, "gone.txt"), tmp); p.Kind != PreviewNone { + if p := MakePreview(context.Background(), filepath.Join(dir, "gone.txt"), tmp, 0); p.Kind != PreviewNone { t.Errorf("a missing file = %+v", p) } - if p := MakePreview(context.Background(), dir, tmp); p.Kind != PreviewNone { + if p := MakePreview(context.Background(), dir, tmp, 0); p.Kind != PreviewNone { t.Errorf("a directory = %+v", p) } } @@ -59,7 +59,7 @@ func TestPreviewReadsOnly(t *testing.T) { if err != nil { t.Fatal(err) } - MakePreview(context.Background(), p, t.TempDir()) + MakePreview(context.Background(), p, t.TempDir(), 0) after, err := os.Stat(p) if err != nil { t.Fatal(err) @@ -86,7 +86,7 @@ func TestPreviewShowsOnlyTheHead(t *testing.T) { if err := os.WriteFile(p, []byte(b.String()), 0o644); err != nil { t.Fatal(err) } - pv := MakePreview(context.Background(), p, t.TempDir()) + pv := MakePreview(context.Background(), p, t.TempDir(), 0) if pv.Kind != PreviewText { t.Fatalf("preview = %+v", pv) } @@ -129,8 +129,8 @@ func TestPdfPreviewsDoNotMixUp(t *testing.T) { second := pdf("second.pdf", "The second document") tmp := t.TempDir() - a := MakePreview(context.Background(), first, tmp) - b := MakePreview(context.Background(), second, tmp) + a := MakePreview(context.Background(), first, tmp, 0) + b := MakePreview(context.Background(), second, tmp, 0) if a.Kind != PreviewImage || b.Kind != PreviewImage { t.Fatalf("previews = %+v, %+v", a, b) } @@ -152,3 +152,57 @@ func TestPdfPreviewsDoNotMixUp(t *testing.T) { } } } + +// pngSize reads a PNG's dimensions from its header, so a test can say how +// large a page was rendered without decoding it. +func pngSize(t *testing.T, path string) (w, h int) { + t.Helper() + data, err := os.ReadFile(path) + if err != nil || len(data) < 24 || string(data[1:4]) != "PNG" { + t.Fatalf("%s is not a PNG: %v", path, err) + } + be := func(b []byte) int { + return int(b[0])<<24 | int(b[1])<<16 | int(b[2])<<8 | int(b[3]) + } + return be(data[16:20]), be(data[20:24]) +} + +// TestPreviewRendersToTheSizeAsked: a taller preview gets a larger page, so +// dragging the pane open does not just magnify a small render (his request, +// 2026-09-17). +func TestPreviewRendersToTheSizeAsked(t *testing.T) { + if _, err := exec.LookPath("pdftoppm"); err != nil { + t.Skip("pdftoppm is not installed") + } + if _, err := exec.LookPath("groff"); err != nil { + t.Skip("groff is not installed, so there is nothing to make a PDF with") + } + dir := t.TempDir() + path := filepath.Join(dir, "doc.pdf") + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + cmd := exec.Command("groff", "-Tpdf", "-ms") + cmd.Stdin = strings.NewReader(".TL\nA document\n") + cmd.Stdout = f + if err := cmd.Run(); err != nil { + f.Close() + t.Skipf("groff cannot write a PDF here: %v", err) + } + f.Close() + + small := MakePreview(context.Background(), path, t.TempDir(), 200) + large := MakePreview(context.Background(), path, t.TempDir(), 900) + if small.Kind != PreviewImage || large.Kind != PreviewImage { + t.Fatalf("previews = %+v, %+v", small, large) + } + _, sh := pngSize(t, small.Image) + _, lh := pngSize(t, large.Image) + if lh <= sh { + t.Errorf("asking for a taller preview rendered %d px, no more than %d", lh, sh) + } + if sh < 700 { + t.Errorf("the smallest render is %d px, below the floor", sh) + } +} |
