summaryrefslogtreecommitdiff
path: root/gui/internal/model/plan.go
diff options
context:
space:
mode:
Diffstat (limited to 'gui/internal/model/plan.go')
-rw-r--r--gui/internal/model/plan.go65
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.