diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-17 10:15:45 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-17 10:15:45 +0200 |
| commit | a2cb20851499e9c00bd3bf642680a9ce3148cae8 (patch) | |
| tree | 09ef80b9662308b758a88ed2f43078c92e525f29 /gui/internal/model/plan.go | |
| parent | 47b6776c2cb9b017ad95acb5aae8e34b8776fc7c (diff) | |
| download | krino-a2cb20851499e9c00bd3bf642680a9ce3148cae8.tar.gz krino-a2cb20851499e9c00bd3bf642680a9ce3148cae8.zip | |
gui: name the other copy of a duplicate, and offer to keep this one instead
Diffstat (limited to 'gui/internal/model/plan.go')
| -rw-r--r-- | gui/internal/model/plan.go | 65 |
1 files changed, 56 insertions, 9 deletions
diff --git a/gui/internal/model/plan.go b/gui/internal/model/plan.go index 51a86d8..7c24991 100644 --- a/gui/internal/model/plan.go +++ b/gui/internal/model/plan.go @@ -20,12 +20,16 @@ import ( // 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 + Rel string // the file, relative to the directory's root + Path string // absolute, for acting on the file itself + Size int64 + ModTime time.Time + // DuplicateOf is the copy a (duplicate) test matched this file + // against, absolute; "" when none did. + DuplicateOf string + Steps []plan.Step + Rule string // the rule that matched first, for the Rule column + Warnings []string // Selected is the checkbox. A row that cannot be applied - every step // skipped, or nothing but warnings - is never selected and has no box. Selected bool @@ -109,9 +113,13 @@ func (t *PlanTab) fill() { t.Warnings = append([]string(nil), r.Warnings...) warnings := map[string][]string{} files := map[string]scan.File{} + dupes := map[string]string{} for _, fms := range [][]engine.FileMatch{r.Matched, r.Unmatched} { for _, fm := range fms { files[fm.File.Rel] = fm.File + if fm.DuplicateOf != "" { + dupes[fm.File.Rel] = fm.DuplicateOf + } if len(fm.Warnings) > 0 { warnings[fm.File.Rel] = fm.Warnings } @@ -125,7 +133,8 @@ func (t *PlanTab) fill() { } t.Rows = nil for _, c := range t.dp.Chains { - row := Row{Rel: c.File.Rel, Size: c.File.Size, ModTime: c.File.ModTime, + row := Row{Rel: c.File.Rel, Path: c.File.Path, Size: c.File.Size, + ModTime: c.File.ModTime, DuplicateOf: dupes[c.File.Rel], Steps: c.Steps, Warnings: warnings[c.File.Rel]} for _, s := range c.Steps { if s.Skip == "" { @@ -150,8 +159,8 @@ func (t *PlanTab) fill() { sort.Strings(rest) for _, rel := range rest { f := files[rel] - t.Rows = append(t.Rows, Row{Rel: rel, Size: f.Size, ModTime: f.ModTime, - Warnings: warnings[rel]}) + t.Rows = append(t.Rows, Row{Rel: rel, Path: f.Path, Size: f.Size, + ModTime: f.ModTime, DuplicateOf: dupes[rel], Warnings: warnings[rel]}) } t.Counts = Counts{ Scanned: len(r.Matched) + len(r.Unmatched) + len(r.Skipped), @@ -247,6 +256,44 @@ func (t *PlanTab) ReplaceSelected(kind plan.Kind) (int, error) { return n, nil } +// KeepThisCopy is the answer to "I want this one, not the one already +// filed": the file takes the other copy's place, and the other copy goes to +// the Trash, where krino undo can still reach it. It is a review decision, +// like trashing a file by hand, so the rule that a duplicate is never +// deleted - which binds rules, not the person reading the plan - does not +// stand in its way (his request, 2026-09-17). +func (t *PlanTab) KeepThisCopy(i int) error { + if i < 0 || i >= len(t.Rows) { + return fmt.Errorf("model: no row %d", i) + } + row := t.Rows[i] + if row.DuplicateOf == "" { + return fmt.Errorf("model: %s is not a duplicate of anything krino looked at", row.Rel) + } + if row.Path == "" { + return fmt.Errorf("model: %s has no path", row.Rel) + } + for j, c := range t.dp.Chains { + if c.File.Rel != row.Rel { + continue + } + t.dp.Chains[j].Steps = []plan.Step{{ + Kind: plan.Move, + Rule: "(review)", + Src: row.Path, + Dst: row.DuplicateOf, + Displaces: row.DuplicateOf, + Reason: "chosen in review: this copy replaces the one already there", + }} + t.Rows[i].Steps = t.dp.Chains[j].Steps + t.Rows[i].Rule = "(review)" + t.Rows[i].Actable = true + t.Rows[i].Selected = true + return nil + } + return fmt.Errorf("model: %s is not in this plan", row.Rel) +} + // Apply acts on the selected files and logs the rest as declined, exactly // as choosing per file in the terminal does. Each row then carries its // outcome. |
