Add interactive snapshot comparison, CLI tests and treegen
--compare opens the TUI with growth/shrink colouring unless a report is requested. treegen builds deterministic synthetic trees (Unicode, hostile names, sparse files). MAPSIZE_DEBUG_PANIC verifies terminal restoration on UI and scanner panics.
This commit is contained in:
@@ -212,7 +212,7 @@ func (v *dupView) paint(m *Model, cv *Canvas, r treemap.Rect) {
|
||||
if d.err != nil {
|
||||
title += " — stopped: " + textutil.Sanitize(d.err.Error())
|
||||
}
|
||||
v.tb.paint(m, cv, r, []column{{"WASTED", 10, true}, {"COPIES", 16, false}, {"", 0, false}}, tr, title)
|
||||
v.tb.paint(m, cv, r, []column{{"WASTED", 10, true, false}, {"COPIES", 16, false, false}, {"", 0, false, false}}, tr, title)
|
||||
}
|
||||
|
||||
func (v *dupView) key(m *Model, k string) (bool, tea.Cmd) {
|
||||
|
||||
@@ -131,14 +131,14 @@ func (v *infoView) paint(m *Model, cv *Canvas, r treemap.Rect) {
|
||||
if e.Name != "" {
|
||||
p += "/" + e.Name
|
||||
}
|
||||
rows[i] = tableRow{cells: []string{e.Kind.String(), textutil.Sanitize(e.Msg), textutil.Sanitize(p)},
|
||||
styles: []*Style{{FG: t.Err, BG: t.Bg}, {FG: t.Muted, BG: t.Bg}, nil}, bar: -1}
|
||||
rows[i] = tableRow{cells: []string{e.Kind.String(), textutil.Sanitize(p), textutil.Sanitize(e.Msg)},
|
||||
styles: []*Style{{FG: t.Err, BG: t.Bg}, nil, {FG: t.Muted, BG: t.Bg}}, bar: -1}
|
||||
}
|
||||
title := fmt.Sprintf("ERROR LIST (%s)", textutil.Count(st.TotalErrors()))
|
||||
if int64(len(st.Errors)) < st.TotalErrors() {
|
||||
title += fmt.Sprintf(" — first %s shown", textutil.Count(int64(len(st.Errors))))
|
||||
}
|
||||
v.tb.paint(m, cv, er, []column{{"KIND", 20, false}, {"DETAIL", 24, false}, {"PATH", 0, false}}, rows, title)
|
||||
v.tb.paint(m, cv, er, []column{{"KIND", 18, false, false}, {"PATH", 0, false, true}, {"DETAIL", 22, false, false}}, rows, title)
|
||||
}
|
||||
|
||||
func (v *infoView) key(m *Model, k string) (bool, tea.Cmd) {
|
||||
|
||||
@@ -150,6 +150,11 @@ func newModel(opts Options, s *scan.Scanner, t *inventory.Tree, cancel context.C
|
||||
|
||||
const noSel int64 = 1 << 62
|
||||
|
||||
// debugPanic lets developers verify terminal restoration on crashes:
|
||||
// MAPSIZE_DEBUG_PANIC=ui panics on the first key press; =scanner panics in
|
||||
// a background goroutine shortly after start.
|
||||
var debugPanic = os.Getenv("MAPSIZE_DEBUG_PANIC")
|
||||
|
||||
// Init starts ticking and waits for scan completion.
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
cmds := []tea.Cmd{tea.RequestWindowSize}
|
||||
@@ -271,6 +276,9 @@ func (m *Model) update(msg tea.Msg) tea.Cmd {
|
||||
m.fatal = &msg
|
||||
m.quit = true
|
||||
case tea.KeyPressMsg:
|
||||
if debugPanic == "ui" {
|
||||
panic("MAPSIZE_DEBUG_PANIC=ui: deliberate panic to test terminal restoration")
|
||||
}
|
||||
m.dirty = true
|
||||
prev := m.sel
|
||||
m.zoomed = false
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/charmbracelet/colorprofile"
|
||||
@@ -47,6 +49,17 @@ func Run(opts Options) error {
|
||||
select {} // park the broken goroutine; the program is quitting
|
||||
}
|
||||
defer func() { scan.OnPanic = prev }()
|
||||
if debugPanic == "scanner" {
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
scan.OnPanic(v, debug.Stack())
|
||||
}
|
||||
}()
|
||||
panic("MAPSIZE_DEBUG_PANIC=scanner: deliberate panic to test terminal restoration")
|
||||
}()
|
||||
}
|
||||
|
||||
_, err = p.Run()
|
||||
cancel()
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
|
||||
// column describes one table column.
|
||||
type column struct {
|
||||
title string
|
||||
width int // 0 = flexible (takes the remaining space)
|
||||
right bool
|
||||
title string
|
||||
width int // 0 = flexible (takes the remaining space)
|
||||
right bool
|
||||
clipLeft bool // truncate from the left (paths: the tail matters)
|
||||
}
|
||||
|
||||
// tableRow is one rendered row: cell strings plus optional per-cell styles.
|
||||
@@ -129,6 +130,8 @@ func (tb *table) paint(m *Model, cv *Canvas, r treemap.Rect, cols []column, rows
|
||||
m.paintBar(cv, x, y, w, row.bar, row.barCol, sel)
|
||||
} else if c.right {
|
||||
cv.Text(x, y, textutil.PadLeft(s, w), w, st)
|
||||
} else if c.clipLeft {
|
||||
cv.Text(x, y, textutil.TruncateLeft(s, w), w, st)
|
||||
} else {
|
||||
cv.Text(x, y, textutil.Truncate(s, w), w, st)
|
||||
}
|
||||
|
||||
@@ -75,12 +75,12 @@ func (v *listView) paint(m *Model, cv *Canvas, r treemap.Rect) {
|
||||
rows[i].cells[4] = textutil.SignedSize(m.opts.Diff.Delta(id, m.sizeMode))
|
||||
}
|
||||
}
|
||||
cols := []column{{"SIZE", 10, true}, {"%", 5, true}, {"", 12, false}, {"FILES", 10, true}, {"MODIFIED", 10, false}, {"NAME", 0, false}}
|
||||
cols := []column{{"SIZE", 10, true, false}, {"%", 5, true, false}, {"", 12, false, false}, {"FILES", 10, true, false}, {"MODIFIED", 10, false, false}, {"NAME", 0, false, false}}
|
||||
if m.opts.Diff != nil {
|
||||
cols[4] = column{"CHANGE", 12, true}
|
||||
cols[4] = column{"CHANGE", 12, true, false}
|
||||
}
|
||||
if r.W < 70 {
|
||||
cols = []column{{"SIZE", 10, true}, {"%", 5, true}, {"NAME", 0, false}}
|
||||
cols = []column{{"SIZE", 10, true, false}, {"%", 5, true, false}, {"NAME", 0, false, false}}
|
||||
for i := range rows {
|
||||
rows[i].cells = []string{rows[i].cells[0], rows[i].cells[1], rows[i].cells[5]}
|
||||
rows[i].styles = []*Style{nil, rows[i].styles[1], rows[i].styles[5]}
|
||||
@@ -160,7 +160,7 @@ func (v *extView) paint(m *Model, cv *Canvas, r treemap.Rect) {
|
||||
bar: frac, barCol: col,
|
||||
}
|
||||
}
|
||||
cols := []column{{"EXTENSION", 14, false}, {"CATEGORY", 12, false}, {"FILES", 11, true}, {"SIZE", 10, true}, {"%", 5, true}, {"", 0, false}}
|
||||
cols := []column{{"EXTENSION", 14, false, false}, {"CATEGORY", 12, false, false}, {"FILES", 11, true, false}, {"SIZE", 10, true, false}, {"%", 5, true, false}, {"", 0, false, false}}
|
||||
v.tb.paint(m, cv, r, cols, rows, fmt.Sprintf("File types under %s — Enter filters the map by extension", textutil.Sanitize(m.tree.Node(m.zoom).Name)))
|
||||
}
|
||||
|
||||
@@ -342,7 +342,7 @@ func (v *topView) paint(m *Model, cv *Canvas, r treemap.Rect) {
|
||||
case topSparse:
|
||||
valTitle = "SAVED"
|
||||
}
|
||||
cols := []column{{valTitle, 10, true}, {"MODIFIED", 10, false}, {"", 10, false}, {"PATH", 0, false}}
|
||||
cols := []column{{valTitle, 10, true, false}, {"MODIFIED", 10, false, false}, {"", 10, false, false}, {"PATH", 0, false, true}}
|
||||
v.tb.paint(m, cv, r, cols, rows, title)
|
||||
}
|
||||
|
||||
@@ -413,7 +413,8 @@ func (v *changesView) paint(m *Model, cv *Canvas, r treemap.Rect) {
|
||||
bar: float64(abs64(c.Delta)) / float64(maxAbs), barCol: col,
|
||||
}
|
||||
}
|
||||
cols := []column{{"CHANGE", 13, true}, {"STATUS", 9, false}, {"OLD", 10, true}, {"NEW", 10, true}, {"", 10, false}, {"PATH", 0, false}}
|
||||
cols := []column{{"CHANGE", 13, true, false}, {"STATUS", 9, false, false}, {"OLD", 10, true, false}, {"NEW", 10, true, false},
|
||||
{"", 10, false, false}, {"PATH", 0, false, true}}
|
||||
ro, rn := d.Old.Node(0).Total(m.sizeMode), d.New.Node(0).Total(m.sizeMode)
|
||||
title := fmt.Sprintf("Changes %s → %s (%s)", textutil.Size(ro), textutil.Size(rn), textutil.SignedSize(rn-ro))
|
||||
v.tb.paint(m, cv, r, cols, rows, title)
|
||||
|
||||
Reference in New Issue
Block a user