From 2586386cc3942f7bbee7e4bcda24936696b0ce50 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Thu, 20 Nov 2025 15:59:14 +0100 Subject: [PATCH] Fix up the summary to log as a table --- filesystem.go | 81 ++++++++++++++++++++++++++++++++++++++++++--------- main.go | 71 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 136 insertions(+), 16 deletions(-) diff --git a/filesystem.go b/filesystem.go index c3c8aad..19b918b 100644 --- a/filesystem.go +++ b/filesystem.go @@ -17,6 +17,7 @@ type FileSystem interface { Link(source, target string) error RecordLinkAttempt(kind string, source, target string, err error, dryRun bool) SummaryLines() []string + SummaryRecords() []operationRecord IsDryRun() bool } @@ -37,26 +38,72 @@ type operationRecord struct { } func (r operationRecord) summaryLine() (string, bool) { - if r.kind != opSymlink && r.kind != opHardlink { - return "", false + detail := r.detail() + if detail != "" { + return fmt.Sprintf("%s %s → %s (%s) %s", + r.kindLabel(), + r.formattedSource(), + r.formattedTarget(), + r.resultLabel(), + detail), true } - kindLabel := "Symlink" - if r.kind == opHardlink { - kindLabel = "Hardlink" - } + return fmt.Sprintf("%s %s → %s (%s)", + r.kindLabel(), + r.formattedSource(), + r.formattedTarget(), + r.resultLabel()), true +} - status := "OK" +func (r operationRecord) kindLabel() string { + switch r.kind { + case opSymlink: + return "Symlink" + case opHardlink: + return "Hardlink" + case opRemove: + return "Remove" + case opRemoveAll: + return "RemoveAll" + case opMkdirAll: + return "MkdirAll" + default: + return r.kind + } +} + +func (r operationRecord) resultLabel() string { if r.err != nil { - status = fmt.Sprintf("%sFAIL%s: %v", BRed, Reset, r.err) - } else if r.dryRun { - status = "DRY-RUN" + return fmt.Sprintf("%sFAIL%s", BRed, Reset) } + if r.dryRun { + return fmt.Sprintf("%sDRY-RUN%s", BCyan, Reset) + } + return fmt.Sprintf("%sOK%s", BGreen, Reset) +} - source := FormatSourcePath(r.source) - target := FormatTargetPath(r.target) +func (r operationRecord) formattedSource() string { + if r.source == "" { + return "-" + } + return FormatSourcePath(r.source) +} - return fmt.Sprintf("%s %s → %s (%s)", kindLabel, source, target, status), true +func (r operationRecord) formattedTarget() string { + if r.target == "" { + return "-" + } + return FormatTargetPath(r.target) +} + +func (r operationRecord) detail() string { + if r.err != nil { + return r.err.Error() + } + if r.dryRun { + return "dry-run" + } + return "" } type baseFileSystem struct { @@ -141,6 +188,10 @@ func (fs *realFileSystem) SummaryLines() []string { return summarizeOperations(fs.snapshot()) } +func (fs *realFileSystem) SummaryRecords() []operationRecord { + return fs.snapshot() +} + func (fs *realFileSystem) IsDryRun() bool { return false } @@ -187,6 +238,10 @@ func (fs *dryRunFileSystem) SummaryLines() []string { return summarizeOperations(fs.snapshot()) } +func (fs *dryRunFileSystem) SummaryRecords() []operationRecord { + return fs.snapshot() +} + func (fs *dryRunFileSystem) IsDryRun() bool { return true } diff --git a/main.go b/main.go index f80c688..9c83e80 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,9 @@ import ( "log" "os" "path/filepath" + "regexp" "runtime/debug" + "strings" "sync/atomic" utils "git.site.quack-lab.dev/dave/cyutils" @@ -185,14 +187,77 @@ func processInstructions(instructions chan *LinkInstruction) int32 { } func printSummary(fs FileSystem) { - lines := fs.SummaryLines() - if len(lines) == 0 { + records := fs.SummaryRecords() + if len(records) == 0 { return } + + header := []string{"RESULT", "OPERATION", "SOURCE", "TARGET", "DETAIL"} + widths := make([]int, len(header)) + for i, h := range header { + widths[i] = len(h) + } + + type row struct { + values []string + err error + } + rows := make([]row, len(records)) + + for i, record := range records { + values := []string{ + record.resultLabel(), + record.kindLabel(), + FormatSourcePath(record.source), + FormatTargetPath(record.target), + record.detail(), + } + rows[i] = row{values: values, err: record.err} + + for j, val := range values { + if l := visibleLength(val); l > widths[j] { + widths[j] = l + } + } + } + LogInfo("Summary:") - for _, line := range lines { + var lineBuilder strings.Builder + + writeSummaryRow(&lineBuilder, header, widths) + for _, line := range strings.Split(strings.TrimRight(lineBuilder.String(), "\n"), "\n") { LogInfo("%s", line) } + + for _, r := range rows { + lineBuilder.Reset() + writeSummaryRow(&lineBuilder, r.values, widths) + line := strings.TrimRight(lineBuilder.String(), "\n") + LogInfo("%s", line) + } +} + +func writeSummaryRow(b *strings.Builder, cols []string, widths []int) { + for i, val := range cols { + if val == "" { + val = "-" + } + b.WriteString(val) + pad := widths[i] - visibleLength(val) + if pad < 0 { + pad = 0 + } + if i < len(cols)-1 { + b.WriteString(strings.Repeat(" ", pad+2)) + } + } + b.WriteByte('\n') +} + +var ansiRegexp = regexp.MustCompile(`\x1b\[[0-9;]*m`) + +func visibleLength(s string) int { + return len(ansiRegexp.ReplaceAllString(s, "")) } func IsPipeInput() bool {