summaryrefslogtreecommitdiff
path: root/cmd/krino/review.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 15:16:55 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 15:16:55 +0200
commit0b5d0eb92c5be2f0ddb2fa73990f31e5654e57fe (patch)
tree358e331945b8206ed4a72703e3aedfe8ea7cdcdb /cmd/krino/review.go
parent1d3f2d1e4c59867024470d3444e12698b7ebb22e (diff)
downloadkrino-0b5d0eb92c5be2f0ddb2fa73990f31e5654e57fe.tar.gz
krino-0b5d0eb92c5be2f0ddb2fa73990f31e5654e57fe.zip
krino: 0.0.5 — keyword cache, t and d in reviewv0.0.5
Diffstat (limited to 'cmd/krino/review.go')
-rw-r--r--cmd/krino/review.go99
1 files changed, 72 insertions, 27 deletions
diff --git a/cmd/krino/review.go b/cmd/krino/review.go
index 66e17b9..29b3915 100644
--- a/cmd/krino/review.go
+++ b/cmd/krino/review.go
@@ -21,7 +21,7 @@ import (
// directory-level table does (root-relative inside root, ~-abbreviated
// outside it) instead of always falling back to the abbreviated form. p
// styles the prompts and the per-file steps (spec §8.2).
-func reviewDir(out io.Writer, chains []plan.Chain, root string, p palette) (map[string]bool, rune, error) {
+func reviewDir(out io.Writer, chains []plan.Chain, root string, p palette) (map[string]bool, map[string]plan.Kind, rune, error) {
return reviewChains(keyReader{stdin}, out, chains, root, p)
}
@@ -47,17 +47,18 @@ func (k keyReader) Read(p []byte) (int, error) {
//
// menu, and, for [c], the per-file
//
-// [y] yes [n] no [a] yes to this and all remaining [d] done, apply chosen so far [q] quit, apply nothing
+// [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 [d] ("apply chosen so far"), which
-// keeps it. root is the directory being reviewed - passed only to
+// 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
// 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, rune, error) {
+func reviewChains(in io.Reader, out io.Writer, chains []plan.Chain, root string, p palette) (map[string]bool, map[string]plan.Kind, rune, error) {
fmt.Fprintln(out)
for _, l := range wrapped("", "[a] apply all [c] choose per file [s] skip this directory [q] quit", 0, widthPolicy(out), p.keys) {
fmt.Fprintln(out, l)
@@ -65,24 +66,24 @@ func reviewChains(in io.Reader, out io.Writer, chains []plan.Chain, root string,
for {
key, err := readKey(in)
if err != nil {
- return nil, 0, err
+ return nil, nil, 0, err
}
switch key {
case 'a':
- return approveAll(chains), 'a', nil
+ return approveAll(chains), nil, 'a', nil
case 's':
- return map[string]bool{}, 's', nil
+ return map[string]bool{}, nil, 's', nil
case 'q':
- return map[string]bool{}, 'q', nil
+ return map[string]bool{}, nil, 'q', nil
case 'c':
- approved, quit, err := reviewPerFile(in, out, chains, root, p)
+ approved, replaced, quit, err := reviewPerFile(in, out, chains, root, p)
if err != nil {
- return nil, 0, err
+ return nil, nil, 0, err
}
if quit {
- return map[string]bool{}, 'q', nil
+ return map[string]bool{}, nil, 'q', nil
}
- return approved, 'c', nil
+ return approved, replaced, 'c', nil
default:
fmt.Fprintf(out, "%q is not a, c, s or q\n", key)
}
@@ -94,14 +95,18 @@ func reviewChains(in io.Reader, out io.Writer, chains []plan.Chain, root string,
// Each file shows the same block body the plan shows (stepLines): every
// step, its rule and its reason, wrapped to the terminal. [y]/[n] decide
// just that file; [a] approves it and every remaining file without asking
-// again; [d] stops asking and applies whatever was already chosen,
+// 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
// 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, 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, quit bool, err error) {
approved = map[string]bool{}
+ replaced = map[string]plan.Kind{}
yesRest := false
for i, c := range chains {
if yesRest {
@@ -113,14 +118,17 @@ func reviewPerFile(in io.Reader, out io.Writer, chains []plan.Chain, root string
for _, l := range stepLines(c, 7, root, p, widthPolicy(out)) {
fmt.Fprintln(out, l)
}
- for _, l := range wrapped(" ", perFileKeys, 2, widthPolicy(out), p.keys) {
- fmt.Fprintln(out, l)
- }
-
+ showKeys := true
for {
+ if showKeys {
+ for _, l := range wrapped(" ", perFileKeys, 2, widthPolicy(out), p.keys) {
+ fmt.Fprintln(out, l)
+ }
+ showKeys = false
+ }
key, kerr := readKey(in)
if kerr != nil {
- return nil, false, kerr
+ return nil, nil, false, kerr
}
switch key {
case 'y':
@@ -130,23 +138,60 @@ func reviewPerFile(in io.Reader, out io.Writer, chains []plan.Chain, root string
case 'a':
approved[c.File.Rel] = true
yesRest = true
+ case 't':
+ approved[c.File.Rel] = true
+ replaced[c.File.Rel] = plan.Trash
case 'd':
- return approved, false, nil
+ fmt.Fprintf(out, " delete %s permanently? [y/N] ", c.File.Rel)
+ confirm, kerr := readKey(in)
+ if kerr != nil {
+ return nil, nil, false, kerr
+ }
+ fmt.Fprintln(out)
+ if confirm != 'y' {
+ fmt.Fprintln(out, " not deleted")
+ showKeys = true
+ continue
+ }
+ approved[c.File.Rel] = true
+ replaced[c.File.Rel] = plan.DeletePermanent
+ case 'w':
+ return approved, replaced, false, nil
case 'q':
- return nil, true, nil
+ return nil, nil, true, nil
default:
- fmt.Fprintf(out, "%q is not y, n, a, d or q\n", key)
+ fmt.Fprintf(out, "%q is not y, n, a, t, d, w or q\n", key)
continue
}
break
}
}
- return approved, false, nil
+ return approved, replaced, false, nil
}
-// perFileKeys is the per-file prompt of spec §8.3, shared by review and
-// undo, and wrapped to the terminal like every other long line.
-const perFileKeys = "[y] yes [n] no [a] yes to this and all remaining [d] done, apply chosen so far [q] quit, apply nothing"
+// perFileKeys is the per-file prompt of spec §8.3, wrapped to the terminal
+// like every other long line. Undo's has no [t] or [d] (undoPerFileKeys).
+const perFileKeys = "[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"
+
+// reviewRule is the rule name a step chosen in review is planned and logged
+// under, in parentheses so no configured rule can be mistaken for it.
+const reviewRule = "(review)"
+
+// replaceChains returns chains with the chain of every file in replaced
+// swapped for one step of the chosen kind on the file itself, under
+// reviewRule: [t] and [d] set aside what the rules planned. chains itself
+// is not modified.
+func replaceChains(chains []plan.Chain, replaced map[string]plan.Kind) []plan.Chain {
+ out := make([]plan.Chain, len(chains))
+ for i, c := range chains {
+ if k, ok := replaced[c.File.Rel]; ok {
+ c.Steps = []plan.Step{{Kind: k, Rule: reviewRule, Src: c.File.Path, Reason: "chosen in review"}}
+ c.Warnings = nil
+ }
+ out[i] = c
+ }
+ return out
+}
// readKey reads the single byte reviewChains treats as one keypress. Over
// the real terminal that byte already came from tui.ReadKey (via