aboutsummaryrefslogtreecommitdiff
path: root/gui/internal/model
diff options
context:
space:
mode:
Diffstat (limited to 'gui/internal/model')
-rw-r--r--gui/internal/model/prefs.go13
-rw-r--r--gui/internal/model/preview.go29
-rw-r--r--gui/internal/model/preview_test.go72
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)
+ }
+}