aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md44
-rw-r--r--README.md4
-rw-r--r--docs/gui-checklist.md2
-rw-r--r--gui/internal/ui/forms.go26
-rw-r--r--gui/internal/ui/plan.go4
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
diff --git a/README.md b/README.md
index 3d7f368..ef7eca4 100644
--- a/README.md
+++ b/README.md
@@ -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"}