diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-12 20:14:47 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-12 20:14:47 +0200 |
| commit | 3f8679be9373ee7508d512dfdfc1dda0839c7f90 (patch) | |
| tree | ec02eb075f6c4e90f21baa2fe674e86a2f7f6a62 /cmd/krino/log.go | |
| parent | 24a84671ace373ae331fa83a1ff484990f4dff0e (diff) | |
| download | krino-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.go | 122 |
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, " · ") +} |
