// 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 } p := palette{on: colourOn(g, stdout)} for _, r := range runs { fmt.Fprintln(stdout, styleUndone(formatRun(r), p)) } return 0 } // styleUndone styles the "(undone)" formatRun appends, faint (spec §8.2); // a line without it is returned as is. func styleUndone(line string, p palette) string { const mark = " (undone)" if !p.on || !strings.HasSuffix(line, mark) { return line } return strings.TrimSuffix(line, mark) + " " + p.faint("(undone)") } // 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"), display(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, " · ") }