diff options
| -rw-r--r-- | CHANGELOG.md | 44 | ||||
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | docs/gui-checklist.md | 2 | ||||
| -rw-r--r-- | gui/internal/ui/forms.go | 26 | ||||
| -rw-r--r-- | gui/internal/ui/plan.go | 4 |
5 files changed, 68 insertions, 12 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index a981b39..5e4dd69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,53 @@ ## Unreleased +## 0.0.11 โ 2026-09-17 + +The window, worked over from a fortnight's worth of using it: the plan reads +like a table, a filter and a sort over it, choices that act on everything +checked, the file itself shown beside its explanation, and rules edited as a +tree. The command line is unchanged. + +- The plan is a table with headings: file, size, age, action, where it would + go, rule, and an outcome column once it has been applied. The action is in + capitals and carries the running GTK theme's own colours - accent for a + move, the selection colour for a rename, success, warning and error for the + rest - and stays readable on a selected row. Size, age and rule can each be + turned off. +- A filter over the plan: the letters of a name or a rule, in order, capitals + and accents ignored as `(fold yes)` does. `Select all` then takes what is + shown, and the window says how many checked files the filter is hiding. +- A sort picker: as scanned, by name, size, age, action or rule. +- `With checked` sets every checked file to the Trash, or - after a + confirmation naming the count - to a permanent delete. A row's own menu + still does one file at a time. +- A file krino found another copy of names that copy in full, and offers to + keep this one instead: it takes the other's place and the other goes to the + Trash, which `krino undo` reverses. +- The file behind the selected row is shown: a picture for an image, the + first page for a PDF, the first lines of anything that is text. The divider + above it sets how big it is, and the page is rendered to suit. +- The explanation is laid out rather than printed: the name, the size and + age, the copy it duplicates, then a line per step with its action coloured. +- Rules: the conditions of a rule are a tree - `and`, `or` and `not` holding + the conditions under them - the text has line numbers and colours, a + `Check` button, and `Test rule` lists the files a rule would take. + `Add directory` writes `dirs/NAME.conf` and adds it to the include, as + `krino new` does. +- `Settings`, at the top right: krino's own defaults, written to + `krino.conf` with the same care a rules file is saved with, and how the + window behaves - the layout (the file list beside the explanation or above + it), which columns are shown, the sort, the preview, and whether a scan + starts with every file checked. Every setting says what it does. The window + keeps where each divider was left. +- Nothing on disk changes until Apply, Undo or Save: a review choice says so, + and closing with an unapplied plan asks first. - `FileMatch.DuplicateOf`: the file a `(duplicate)` test matched, absolute, so a front end can say where the other copy is and act on it. The reason text is unchanged. +- Fixed: selecting the exclude row in Forms took the window down with it + (0.0.10). An exclude has conditions but no name, action or `(stop)`, and + the form now opens with only what an exclude has. ## 0.0.10 โ 2026-09-16 @@ -8,7 +8,7 @@ shows the plan before touching anything, lets you approve all of it or file by file, logs every step, and can undo a run. Version 0.0.9 is the engine and this command-line interface: per-directory rule files, placeholders in destinations and new names, a keyword cache that makes a rerun fast, and -builds for Linux, FreeBSD and OpenBSD. Version 0.0.10 adds `krino-gui`, an +builds for Linux, FreeBSD and OpenBSD. Version 0.0.11 has `krino-gui`, an optional GTK 4 window onto the same engine (see below). It is not a watch mode, OCR, EXIF dates, macOS or Windows support, a shell-command action, or a way to look inside archives. @@ -260,7 +260,7 @@ release that changes it. ## Status -**0.0.10, local only.** The module path is `krino`; `go install` only +**0.0.11, local only.** The module path is `krino`; `go install` only works once a remote is chosen and the imports are renamed to match (`docs/design.md`, ยง18). Until then, build from a clone. `krino -n --json`'s output shape is unstable before krino 1.0 โ don't diff --git a/docs/gui-checklist.md b/docs/gui-checklist.md index 3eaeb60..4b646b2 100644 --- a/docs/gui-checklist.md +++ b/docs/gui-checklist.md @@ -146,3 +146,5 @@ A dialog is its own window: take it by its own id, not the main window's. 48. A rule's conditions are a tree: and / or / not hold the conditions indented under them, "+" adds a test or an operator under a line, "-" takes one out, and saving writes the same nesting back. +49. Selecting the exclude row in Forms opens its conditions like a rule's, + with no name, action or `(stop)`, and the window does not fall over. diff --git a/gui/internal/ui/forms.go b/gui/internal/ui/forms.go index 18a8b13..fd97546 100644 --- a/gui/internal/ui/forms.go +++ b/gui/internal/ui/forms.go @@ -386,7 +386,7 @@ func (r *settingRow) set(args string) { // formLabel is one line of the list: what the form is, and what it does. func formLabel(f model.Form) string { - if f.Kind == model.ExcludeForm { + if f.Kind == model.ExcludeForm || f.Rule == nil { return "exclude: " + f.Label } var what []string @@ -677,7 +677,7 @@ func newFormEditor(form model.Form, changed func()) *formEditor { fe.root.SetMarginTop(6) fe.root.SetMarginBottom(6) - if form.Kind == model.RuleForm { + if form.Kind == model.RuleForm && form.Rule != nil { fe.name = gtk.NewEntry() fe.name.SetText(form.Rule.Name) fe.name.SetHExpand(true) @@ -692,18 +692,25 @@ func newFormEditor(form model.Form, changed func()) *formEditor { fe.conditions = append(fe.conditions, &model.Cond{Kind: kind}) }))) - when := form.Rule.When - if form.Kind == model.ExcludeForm { - when = form.Exclude.When - } else if !form.Rule.HasWhen { - when = nil + // An exclude has no rule behind it and a rule may have no (when ...) at + // all, so neither pointer is followed without asking first: reading + // form.Rule for an exclude crashed the window (found by the release + // checklist, 2026-09-17). + var when []*sexp.Node + switch { + case form.Kind == model.ExcludeForm: + if form.Exclude != nil { + when = form.Exclude.When + } + case form.Rule != nil && form.Rule.HasWhen: + when = form.Rule.When } for _, c := range when { fe.conditions = append(fe.conditions, model.ParseCond(c)) } fe.drawConds() - if form.Kind == model.RuleForm { + if form.Kind == model.RuleForm && form.Rule != nil { fe.acts = gtk.NewBox(gtk.OrientationVertical, 4) fe.root.Append(heading("Actions - in order")) fe.root.Append(fe.acts) @@ -756,6 +763,9 @@ func (fe *formEditor) text() (string, error) { } return config.PrintExclude(&config.Exclude{When: when}), nil } + if fe.name == nil || fe.stop == nil { + return "", fmt.Errorf("this form has no rule behind it") + } rule := &config.Rule{ Name: fe.name.Text(), HasWhen: len(when) > 0, diff --git a/gui/internal/ui/plan.go b/gui/internal/ui/plan.go index 0adc6b1..5daba6a 100644 --- a/gui/internal/ui/plan.go +++ b/gui/internal/ui/plan.go @@ -672,7 +672,7 @@ func (p *planView) fillList() { // shown whole rather than cut to an ellipsis. The list scrolls sideways // when the total does not fit (his report, 2026-09-16). func (p *planView) widths() [7]int { - w := [7]int{16, 4, 3, 6, 16, 8, 6} + w := [7]int{16, 5, 4, 6, 16, 8, 6} root := p.dirRoot() now := time.Now() for _, r := range p.tab.Rows { @@ -761,7 +761,7 @@ func (p *planView) showOutcome() bool { // headerFloors are how narrow each heading may become, matching the cells // under it. -var headerFloors = [7]int{12, 4, 3, 6, 16, 10, 8} +var headerFloors = [7]int{12, 5, 4, 6, 16, 10, 8} // columnTitles name the columns of the plan. var columnTitles = [7]string{"file", "size", "age", "action", "where it would go", "rule", "outcome"} |
