aboutsummaryrefslogtreecommitdiff
path: root/cmd/krino/log.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 20:14:47 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 20:14:47 +0200
commit3f8679be9373ee7508d512dfdfc1dda0839c7f90 (patch)
treeec02eb075f6c4e90f21baa2fe674e86a2f7f6a62 /cmd/krino/log.go
parent24a84671ace373ae331fa83a1ff484990f4dff0e (diff)
downloadkrino-3f8679be9373ee7508d512dfdfc1dda0839c7f90.tar.gz
krino-3f8679be9373ee7508d512dfdfc1dda0839c7f90.zip
krino: acting — trash, journal, apply, lock, review, undo
Diffstat (limited to 'cmd/krino/log.go')
-rw-r--r--cmd/krino/log.go122
1 files changed, 122 insertions, 0 deletions
diff --git a/cmd/krino/log.go b/cmd/krino/log.go
new file mode 100644
index 0000000..6eb15d6
--- /dev/null
+++ b/cmd/krino/log.go
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package main
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "krino/internal/engine"
+ "krino/internal/journal"
+)
+
+func init() { commands["log"] = cmdLog }
+
+// pastTense renders one of the log's own action words (spec §9's wire
+// format - journal.Run.Counts is keyed by these exact strings) as the past
+// tense krino log displays. Those strings are never renamed to suit the
+// display: krino undo parses them back out of the log, so this map is a
+// display-only translation, not a second source of truth. Two families of
+// action are deliberately absent, and so never appear in a run's counts
+// line at all: run-start/run-end (bookkeeping, not something done to a
+// file) and mkdir/undo-mkdir (a side effect of another step, not an
+// outcome the user asked for).
+var pastTense = map[string]string{
+ "copy": "copied",
+ "move": "moved",
+ "rename": "renamed",
+ "trash": "trashed",
+ "delete": "deleted",
+ "displace": "displaced",
+ "undo-copy": "undo-copied",
+ "undo-move": "undo-moved",
+ "undo-rename": "undo-renamed",
+ "undo-trash": "undo-trashed",
+ "undo-displace": "undo-displaced",
+}
+
+// countOrder is the fixed order krino log renders a run's counts in.
+// journal.Run.Counts is a map, whose iteration order is random; a listing
+// that reshuffled its own columns between two invocations of the same
+// command would be unreadable.
+var countOrder = []string{
+ "copy", "move", "rename", "trash", "delete", "displace",
+ "undo-copy", "undo-move", "undo-rename", "undo-trash", "undo-displace",
+}
+
+// cmdLog lists recent runs, newest first (spec §11: "krino log [-n N]").
+func cmdLog(g *globals, args []string, stdout, stderr io.Writer) int {
+ fs := flagSet("log", g)
+ n := fs.Int("n", 10, "")
+ if code, ok := parse(fs, args, stdout, stderr); !ok {
+ return code
+ }
+ if fs.NArg() > 0 {
+ return usageError(stderr, "usage: krino log [-n N]")
+ }
+
+ e, errs := engine.Load(mainFile(g))
+ if len(errs) > 0 {
+ printDiags(stderr, errs)
+ return 2
+ }
+
+ runs, err := e.Runs(*n)
+ if err != nil {
+ // journal.Runs (via Engine.Runs) fails closed on a genuine read
+ // error - permissions, a corrupt file - and that must stay
+ // distinguishable from the ordinary "no log yet" case below rather
+ // than collapsing into the same message (dispatch notes).
+ if errors.Is(err, os.ErrNotExist) {
+ fmt.Fprintln(stdout, "nothing logged yet")
+ return 0
+ }
+ fmt.Fprintf(stderr, "krino: %v\n", err)
+ return 1
+ }
+ // A log file can exist and still hold no runs (a real run with nothing
+ // actionable still opens the journal - dispatch notes). That is just as
+ // ordinary as no log file at all: same message, same exit 0, and no
+ // table header is printed over zero rows.
+ if len(runs) == 0 {
+ fmt.Fprintln(stdout, "nothing logged yet")
+ return 0
+ }
+
+ for _, r := range runs {
+ fmt.Fprintln(stdout, formatRun(r))
+ }
+ return 0
+}
+
+// formatRun renders one journal.Run as krino log lists it: id, start time,
+// the directories it touched, and its counts (see countsText), with
+// "(undone)" appended when a later run has reversed it.
+func formatRun(r journal.Run) string {
+ line := fmt.Sprintf("%s %s %s %s", r.ID, r.Start.Format("2006-01-02 15:04"), strings.Join(r.Dirs, ", "), countsText(r.Counts))
+ if r.Undone {
+ line += " (undone)"
+ }
+ return line
+}
+
+// countsText renders a run's counts in countOrder, past tense, skipping any
+// action with no "ok" entries. "nothing applied" covers a run every one of
+// whose files was declined or failed before anything ran - Counts only
+// tallies "ok" entries, so such a run's map holds nothing this function
+// would otherwise print.
+func countsText(counts map[string]int) string {
+ var parts []string
+ for _, action := range countOrder {
+ if n := counts[action]; n > 0 {
+ parts = append(parts, fmt.Sprintf("%d %s", n, pastTense[action]))
+ }
+ }
+ if len(parts) == 0 {
+ return "nothing applied"
+ }
+ return strings.Join(parts, " · ")
+}