Skip to content

Commit 153e1af

Browse files
jspelletierclaude
andauthored
feat: restyle discover output (#37)
Replace plain-text printResult with styled header, KPI block, coverage bar, and FormatTable for command lists. Long Windows paths are truncated via utils.Truncate to prevent column overflow. Add three TestPrintResult* tests using stdout capture. Co-authored-by: Claude Sonnet 4.6 <[email protected]>
1 parent 1c9921b commit 153e1af

File tree

2 files changed

+140
-12
lines changed

2 files changed

+140
-12
lines changed

internal/discover/discover.go

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import (
1111
"time"
1212

1313
"github.com/edouard-claude/snip/internal/config"
14+
"github.com/edouard-claude/snip/internal/display"
1415
"github.com/edouard-claude/snip/internal/filter"
1516
"github.com/edouard-claude/snip/internal/hook"
17+
"github.com/edouard-claude/snip/internal/utils"
1618
)
1719

1820
// sessionLine represents a single JSONL entry from a Claude Code session file.
@@ -357,30 +359,82 @@ func sumMap(m map[string]int) int {
357359

358360
// printResult outputs the discover report to stdout.
359361
func printResult(r Result) {
360-
fmt.Println("snip discover - missed savings analysis")
362+
tty := display.IsTerminal()
363+
361364
fmt.Println()
362-
fmt.Printf("Scanned: %d sessions, %d commands\n", r.SessionsScanned, r.TotalCommands)
365+
if tty {
366+
fmt.Println(display.HeaderStyle.Render(" snip — Discover Report"))
367+
fmt.Println(display.DimStyle.Render(" " + display.FormatSeparator(30)))
368+
} else {
369+
fmt.Println(" snip — Discover Report")
370+
fmt.Println(" " + display.FormatSeparator(30))
371+
}
363372
fmt.Println()
364373

365374
if r.TotalCommands == 0 {
366-
fmt.Println("No Bash commands found in the scanned sessions.")
375+
fmt.Println(" No Bash commands found in the scanned sessions.")
367376
return
368377
}
369378

370379
supportedPct := float64(r.SupportedCount) / float64(r.TotalCommands) * 100
371-
unsupportedPct := float64(r.UnsupportedCount) / float64(r.TotalCommands) * 100
372380

373-
fmt.Printf("Supported (has filter): %d commands (%.0f%%)\n", r.SupportedCount, supportedPct)
374-
for _, s := range r.Supported {
375-
fmt.Printf(" %-22s%d\n", s.Name, s.Count)
381+
printKPI := func(label, value string, styled bool) {
382+
if tty {
383+
styledValue := value
384+
if !styled {
385+
styledValue = display.StatStyle.Render(value)
386+
}
387+
fmt.Printf(" %s %s\n", display.DimStyle.Render(fmt.Sprintf("%-20s", label)), styledValue)
388+
} else {
389+
fmt.Printf(" %-20s %s\n", label, value)
390+
}
376391
}
377-
fmt.Println()
378392

379-
fmt.Printf("Unsupported (no filter): %d commands (%.0f%%)\n", r.UnsupportedCount, unsupportedPct)
380-
for _, s := range r.Unsupported {
381-
fmt.Printf(" %-22s%d\n", s.Name, s.Count)
393+
printKPI("Sessions scanned", fmt.Sprintf("%d", r.SessionsScanned), false)
394+
printKPI("Commands found", fmt.Sprintf("%d", r.TotalCommands), false)
395+
printKPI("Filter coverage", display.ColorSavings(supportedPct), true)
396+
397+
bar := display.ColorBar(r.SupportedCount, r.TotalCommands, 20)
398+
fmt.Println()
399+
if tty {
400+
fmt.Printf(" %s %s\n", bar, display.DimStyle.Render(fmt.Sprintf("%.0f%%", supportedPct)))
401+
} else {
402+
fmt.Printf(" %s %.0f%%\n", bar, supportedPct)
382403
}
383404
fmt.Println()
384405

385-
fmt.Printf("Potential: %.0f%% of your commands already have snip filters.\n", supportedPct)
406+
// Subtract 9 to leave room for the Count column and separator; prevents
407+
// long Windows paths from overflowing into the adjacent column.
408+
maxCmd := display.TerminalWidth() - 9
409+
if maxCmd < 20 {
410+
maxCmd = 20
411+
}
412+
413+
printSection := func(title string, count int, pct float64, stats []CommandStat) {
414+
if tty {
415+
fmt.Println(display.DimStyle.Render(fmt.Sprintf(" %s — %d commands (%.0f%%)", title, count, pct)))
416+
} else {
417+
fmt.Printf(" %s — %d commands (%.0f%%)\n", title, count, pct)
418+
}
419+
fmt.Println()
420+
421+
if len(stats) > 0 {
422+
rows := make([][]string, 0, len(stats))
423+
for _, s := range stats {
424+
rows = append(rows, []string{utils.Truncate(s.Name, maxCmd), fmt.Sprintf("%d", s.Count)})
425+
}
426+
fmt.Print(display.FormatTable([]string{"Command", "Count"}, rows))
427+
fmt.Println()
428+
}
429+
}
430+
431+
printSection("Supported", r.SupportedCount, supportedPct, r.Supported)
432+
printSection("Unsupported", r.UnsupportedCount, 100-supportedPct, r.Unsupported)
433+
434+
if tty {
435+
fmt.Println(display.StatStyle.Render(fmt.Sprintf(" %.0f%% of your commands already have snip filters.", supportedPct)))
436+
} else {
437+
fmt.Printf(" %.0f%% of your commands already have snip filters.\n", supportedPct)
438+
}
439+
fmt.Println()
386440
}

internal/discover/discover_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package discover
22

33
import (
4+
"bytes"
5+
"io"
46
"os"
57
"path/filepath"
8+
"strings"
69
"testing"
710
"time"
811
)
@@ -335,6 +338,77 @@ func TestEmptyScan(t *testing.T) {
335338
}
336339
}
337340

341+
func captureStdout(t *testing.T, fn func()) string {
342+
t.Helper()
343+
old := os.Stdout
344+
r, w, err := os.Pipe()
345+
if err != nil {
346+
t.Fatal(err)
347+
}
348+
os.Stdout = w
349+
fn()
350+
_ = w.Close()
351+
var buf bytes.Buffer
352+
_, _ = io.Copy(&buf, r)
353+
os.Stdout = old
354+
return buf.String()
355+
}
356+
357+
func TestPrintResultEmpty(t *testing.T) {
358+
output := captureStdout(t, func() {
359+
printResult(Result{SessionsScanned: 3, TotalCommands: 0})
360+
})
361+
if !strings.Contains(output, "No Bash commands found") {
362+
t.Errorf("expected 'No Bash commands found' in output, got: %q", output)
363+
}
364+
}
365+
366+
func TestPrintResultWithData(t *testing.T) {
367+
result := Result{
368+
SessionsScanned: 10,
369+
TotalCommands: 100,
370+
SupportedCount: 83,
371+
UnsupportedCount: 17,
372+
Supported: []CommandStat{
373+
{Name: "git", Count: 50},
374+
{Name: "go", Count: 33},
375+
},
376+
Unsupported: []CommandStat{
377+
{Name: "cat", Count: 17},
378+
},
379+
}
380+
output := captureStdout(t, func() {
381+
printResult(result)
382+
})
383+
for _, want := range []string{"Command", "Count", "git", "83%"} {
384+
if !strings.Contains(output, want) {
385+
t.Errorf("expected %q in output, got: %q", want, output)
386+
}
387+
}
388+
}
389+
390+
func TestPrintResultTruncatesLongNames(t *testing.T) {
391+
longName := strings.Repeat("a", 200)
392+
result := Result{
393+
SessionsScanned: 1,
394+
TotalCommands: 1,
395+
SupportedCount: 1,
396+
UnsupportedCount: 0,
397+
Supported: []CommandStat{
398+
{Name: longName, Count: 1},
399+
},
400+
}
401+
output := captureStdout(t, func() {
402+
printResult(result)
403+
})
404+
if strings.Contains(output, longName) {
405+
t.Errorf("expected long name (%d chars) to be truncated in output", len(longName))
406+
}
407+
if !strings.Contains(output, "...") {
408+
t.Errorf("expected truncated form ending in '...' in output")
409+
}
410+
}
411+
338412
func writeLines(t *testing.T, path string, lines []string) {
339413
t.Helper()
340414
data := ""

0 commit comments

Comments
 (0)