aboutsummaryrefslogtreecommitdiff
path: root/cmd/krino/review.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/krino/review.go')
-rw-r--r--cmd/krino/review.go79
1 files changed, 59 insertions, 20 deletions
diff --git a/cmd/krino/review.go b/cmd/krino/review.go
index 29b3915..36231be 100644
--- a/cmd/krino/review.go
+++ b/cmd/krino/review.go
@@ -49,13 +49,16 @@ func (k keyReader) Read(p []byte) (int, error) {
//
// [y] yes [n] no [a] yes to this and all remaining [t] trash [d] delete permanently [w] write, apply chosen so far [q] quit, apply nothing
//
-// prompt. action is always one of 'a', 'c', 's' or 'q': a [c] session's own
-// [q] ("quit, apply nothing") folds into the same 'q' the caller already
-// handles for the top-level menu, and approved is emptied to match - even a
-// file already marked yes in that session is discarded, per spec §8.3's
-// wording ("apply nothing"), unlike [w] ("apply chosen so far"), which
-// keeps it. replaced holds the files [t] or [d] chose to trash or delete
-// instead of what the rules planned; replaceChains applies it. root is the directory being reviewed - passed only to
+// prompt. action is always one of 'a', 'c', 's', 'w' or 'q': a [c]
+// session's own [q] ("quit, apply nothing") folds into the same 'q' the
+// caller already handles for the top-level menu, and approved is emptied to
+// match - even a file already marked yes in that session is discarded, per
+// spec §8.3's wording ("apply nothing"), unlike [w] ("write, apply chosen so
+// far"), which keeps it and is returned as 'w': the caller applies what was
+// decided and stops. approved holds every file decided, true for yes and
+// false for no; a file [w] left unreviewed is absent. replaced holds the
+// files [t] or [d] chose to trash or delete instead of what the rules
+// planned; replaceChains applies it. Enter is ignored at both prompts. root is the directory being reviewed - passed only to
// reviewPerFile's destination rendering (review finding 1, fix round
// 2026-09-12); nothing here uses it directly.
func reviewChains(in io.Reader, out io.Writer, chains []plan.Chain, root string, p palette) (map[string]bool, map[string]plan.Kind, rune, error) {
@@ -69,6 +72,8 @@ func reviewChains(in io.Reader, out io.Writer, chains []plan.Chain, root string,
return nil, nil, 0, err
}
switch key {
+ case '\r', '\n':
+ continue
case 'a':
return approveAll(chains), nil, 'a', nil
case 's':
@@ -76,12 +81,15 @@ func reviewChains(in io.Reader, out io.Writer, chains []plan.Chain, root string,
case 'q':
return map[string]bool{}, nil, 'q', nil
case 'c':
- approved, replaced, quit, err := reviewPerFile(in, out, chains, root, p)
+ approved, replaced, end, err := reviewPerFile(in, out, chains, root, p)
if err != nil {
return nil, nil, 0, err
}
- if quit {
+ switch end {
+ case 'q':
return map[string]bool{}, nil, 'q', nil
+ case 'w':
+ return approved, replaced, 'w', nil
}
return approved, replaced, 'c', nil
default:
@@ -97,14 +105,14 @@ func reviewChains(in io.Reader, out io.Writer, chains []plan.Chain, root string,
// just that file; [a] approves it and every remaining file without asking
// again; [t] and [d] approve it with its chain replaced by one trash or
// permanent delete step, [d] only after a y to its own confirmation (any
-// other key asks about the same file again); [w] stops asking and applies
-// whatever was already chosen,
-// declining the rest; [q] aborts the review entirely, discarding even files
-// already marked yes - reported back to reviewChains via quit=true. root is
+// other key asks about the same file again); every choice is echoed in red
+// on its own line; [w] stops asking and applies whatever was already chosen,
+// leaving the rest unreviewed - reported back as end 'w'; [q] aborts the
+// review entirely, discarding even files already marked yes - end 'q'. root is
// passed down so a destination inside root renders root-relative and one
// outside it renders ~-abbreviated, exactly as in the plan (review finding
// 1, fix round 2026-09-12).
-func reviewPerFile(in io.Reader, out io.Writer, chains []plan.Chain, root string, p palette) (approved map[string]bool, replaced map[string]plan.Kind, quit bool, err error) {
+func reviewPerFile(in io.Reader, out io.Writer, chains []plan.Chain, root string, p palette) (approved map[string]bool, replaced map[string]plan.Kind, end rune, err error) {
approved = map[string]bool{}
replaced = map[string]plan.Kind{}
yesRest := false
@@ -128,24 +136,31 @@ func reviewPerFile(in io.Reader, out io.Writer, chains []plan.Chain, root string
}
key, kerr := readKey(in)
if kerr != nil {
- return nil, nil, false, kerr
+ return nil, nil, 0, kerr
}
+ var choice string
switch key {
+ case '\r', '\n':
+ continue
case 'y':
approved[c.File.Rel] = true
+ choice = "yes"
case 'n':
- // leave unapproved
+ approved[c.File.Rel] = false
+ choice = "no"
case 'a':
approved[c.File.Rel] = true
yesRest = true
+ choice = "yes, and all remaining"
case 't':
approved[c.File.Rel] = true
replaced[c.File.Rel] = plan.Trash
+ choice = "trash"
case 'd':
fmt.Fprintf(out, " delete %s permanently? [y/N] ", c.File.Rel)
confirm, kerr := readKey(in)
if kerr != nil {
- return nil, nil, false, kerr
+ return nil, nil, 0, kerr
}
fmt.Fprintln(out)
if confirm != 'y' {
@@ -155,18 +170,42 @@ func reviewPerFile(in io.Reader, out io.Writer, chains []plan.Chain, root string
}
approved[c.File.Rel] = true
replaced[c.File.Rel] = plan.DeletePermanent
+ choice = "DELETE permanently"
case 'w':
- return approved, replaced, false, nil
+ return approved, replaced, 'w', nil
case 'q':
- return nil, nil, true, nil
+ return nil, nil, 'q', nil
default:
fmt.Fprintf(out, "%q is not y, n, a, t, d, w or q\n", key)
continue
}
+ fmt.Fprintln(out, " "+p.bad("→ "+choice))
break
}
}
- return approved, replaced, false, nil
+ return approved, replaced, 0, nil
+}
+
+// reviewedChains returns the chains of the files decided holds, yes or no,
+// in order: after [w], only these go to Apply, so a file never reviewed is
+// neither applied nor logged as declined.
+func reviewedChains(chains []plan.Chain, decided map[string]bool) []plan.Chain {
+ var out []plan.Chain
+ for _, c := range chains {
+ if _, ok := decided[c.File.Rel]; ok {
+ out = append(out, c)
+ }
+ }
+ return out
+}
+
+// withNotReviewed appends to an outcome line how many files [w] left
+// unreviewed, when any were.
+func withNotReviewed(line string, n int) string {
+ if n == 0 {
+ return line
+ }
+ return fmt.Sprintf("%s · %d not reviewed", line, n)
}
// perFileKeys is the per-file prompt of spec §8.3, wrapped to the terminal