diff options
Diffstat (limited to 'gui/internal/model')
| -rw-r--r-- | gui/internal/model/plan.go | 37 | ||||
| -rw-r--r-- | gui/internal/model/plan_test.go | 47 | ||||
| -rw-r--r-- | gui/internal/model/prefs.go | 6 | ||||
| -rw-r--r-- | gui/internal/model/preview.go | 5 |
4 files changed, 92 insertions, 3 deletions
diff --git a/gui/internal/model/plan.go b/gui/internal/model/plan.go index 6d3e857..51a86d8 100644 --- a/gui/internal/model/plan.go +++ b/gui/internal/model/plan.go @@ -9,16 +9,20 @@ import ( "context" "fmt" "sort" + "time" "krino/internal/engine" "krino/internal/lock" "krino/internal/plan" + "krino/internal/scan" ) // Row is one line of the Plan tab: a file krino would act on, or one it // could not decide about. type Row struct { Rel string // the file, relative to the directory's root + Size int64 + ModTime time.Time Steps []plan.Step Rule string // the rule that matched first, for the Rule column Warnings []string @@ -104,8 +108,10 @@ func (t *PlanTab) fill() { r := t.dp.Result t.Warnings = append([]string(nil), r.Warnings...) warnings := map[string][]string{} + files := map[string]scan.File{} for _, fms := range [][]engine.FileMatch{r.Matched, r.Unmatched} { for _, fm := range fms { + files[fm.File.Rel] = fm.File if len(fm.Warnings) > 0 { warnings[fm.File.Rel] = fm.Warnings } @@ -119,7 +125,8 @@ func (t *PlanTab) fill() { } t.Rows = nil for _, c := range t.dp.Chains { - row := Row{Rel: c.File.Rel, Steps: c.Steps, Warnings: warnings[c.File.Rel]} + row := Row{Rel: c.File.Rel, Size: c.File.Size, ModTime: c.File.ModTime, + Steps: c.Steps, Warnings: warnings[c.File.Rel]} for _, s := range c.Steps { if s.Skip == "" { row.Actable = true @@ -142,7 +149,9 @@ func (t *PlanTab) fill() { } sort.Strings(rest) for _, rel := range rest { - t.Rows = append(t.Rows, Row{Rel: rel, Warnings: warnings[rel]}) + f := files[rel] + t.Rows = append(t.Rows, Row{Rel: rel, Size: f.Size, ModTime: f.ModTime, + Warnings: warnings[rel]}) } t.Counts = Counts{ Scanned: len(r.Matched) + len(r.Unmatched) + len(r.Skipped), @@ -280,3 +289,27 @@ func (t *PlanTab) record(res *engine.ApplyResult) { } } } + +// AgeText is how long ago a file was last written, in the units krino's own +// (age ...) test uses: minutes, hours, days and weeks, and years past that, +// so a plan can be read at a glance (his request, 2026-09-17). +func AgeText(mod time.Time, now time.Time) string { + if mod.IsZero() { + return "" + } + d := now.Sub(mod) + if d < 0 { + return "0m" // a file dated in the future is not aged + } + switch { + case d < time.Hour: + return fmt.Sprintf("%dm", int(d.Minutes())) + case d < 24*time.Hour: + return fmt.Sprintf("%dh", int(d.Hours())) + case d < 7*24*time.Hour: + return fmt.Sprintf("%dd", int(d.Hours()/24)) + case d < 52*7*24*time.Hour: + return fmt.Sprintf("%dw", int(d.Hours()/(24*7))) + } + return fmt.Sprintf("%dy", int(d.Hours()/(24*365))) +} diff --git a/gui/internal/model/plan_test.go b/gui/internal/model/plan_test.go index ecbb81d..a54768c 100644 --- a/gui/internal/model/plan_test.go +++ b/gui/internal/model/plan_test.go @@ -361,3 +361,50 @@ func TestReplaceSelected(t *testing.T) { t.Errorf("the unchecked file did not stay put: %v", err) } } + +// TestRowsCarrySizeAndAge: a plan's rows know how big each file is and when +// it was last written, for the columns that show them. +func TestRowsCarrySizeAndAge(t *testing.T) { + conf := "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n" + e, h := sandboxDir(t, conf, map[string]string{"a.pdf": "three bytes and more"}) + tab := planTab(t, e) + if len(tab.Rows) != 1 { + t.Fatalf("rows = %+v", tab.Rows) + } + fi, err := os.Stat(filepath.Join(h, "dl", "a.pdf")) + if err != nil { + t.Fatal(err) + } + if tab.Rows[0].Size != fi.Size() { + t.Errorf("size = %d, want %d", tab.Rows[0].Size, fi.Size()) + } + if !tab.Rows[0].ModTime.Equal(fi.ModTime()) { + t.Errorf("mtime = %v, want %v", tab.Rows[0].ModTime, fi.ModTime()) + } +} + +// TestAgeText: the units krino's own (age ...) test uses, and nothing for a +// file whose time is unknown. +func TestAgeText(t *testing.T) { + now := time.Date(2026, 9, 17, 12, 0, 0, 0, time.UTC) + for _, c := range []struct { + ago time.Duration + want string + }{ + {30 * time.Minute, "30m"}, + {5 * time.Hour, "5h"}, + {3 * 24 * time.Hour, "3d"}, + {3 * 7 * 24 * time.Hour, "3w"}, + {3 * 365 * 24 * time.Hour, "3y"}, + } { + if got := AgeText(now.Add(-c.ago), now); got != c.want { + t.Errorf("%v ago = %q, want %q", c.ago, got, c.want) + } + } + if got := AgeText(time.Time{}, now); got != "" { + t.Errorf("an unknown time = %q, want nothing", got) + } + if got := AgeText(now.Add(time.Hour), now); got != "0m" { + t.Errorf("a file from the future = %q, want 0m", got) + } +} diff --git a/gui/internal/model/prefs.go b/gui/internal/model/prefs.go index 77aa784..6b3a6cb 100644 --- a/gui/internal/model/prefs.go +++ b/gui/internal/model/prefs.go @@ -31,6 +31,11 @@ type Prefs struct { // moves is kept (his request, 2026-09-17). ListWidth int `json:"list_width"` ListHeight int `json:"list_height"` + // ShowSize, ShowAge and ShowRule are the columns that can be turned off + // when the window is narrow or they are not wanted. + ShowSize bool `json:"show_size"` + ShowAge bool `json:"show_age"` + ShowRule bool `json:"show_rule"` // Layout is how the Plan tab is arranged: "side" puts the file list // beside the explanation, "stacked" puts it above, with the preview // beside the explanation underneath. @@ -55,6 +60,7 @@ const ( // DefaultPrefs is what a window does before anything is chosen. func DefaultPrefs() Prefs { return Prefs{Colours: true, Preview: true, SelectAll: true, + ShowSize: true, ShowAge: true, ShowRule: true, PreviewHeight: DefaultPreviewHeight, PreviewWidth: DefaultPreviewWidth, ListWidth: DefaultListWidth, ListHeight: DefaultListHeight, Layout: LayoutSide} diff --git a/gui/internal/model/preview.go b/gui/internal/model/preview.go index 4ec0569..a180dff 100644 --- a/gui/internal/model/preview.go +++ b/gui/internal/model/preview.go @@ -192,7 +192,10 @@ func isImageExt(ext string) bool { } // size is a file's size in the units krino's own settings use. -func size(n int64) string { +func size(n int64) string { return SizeText(n) } + +// SizeText is a file's size in the units krino's settings are written in. +func SizeText(n int64) string { switch { case n >= 1<<30: return fmt.Sprintf("%.1fG", float64(n)/(1<<30)) |
