Add mouse tests; tidy TUI idioms

This commit is contained in:
Andy Dixon
2026-10-02 08:36:23 +01:00
parent 71556b76de
commit 8559616b65
7 changed files with 85 additions and 36 deletions
+1 -1
View File
@@ -245,6 +245,6 @@ func (v *dupView) key(m *Model, k string) (bool, tea.Cmd) {
}
func (v *dupView) wheel(m *Model, up bool) tea.Cmd {
v.tb.move(map[bool]int{true: -3, false: 3}[up], len(v.rows))
v.tb.scroll(up, len(v.rows))
return nil
}
+6 -2
View File
@@ -112,7 +112,11 @@ func (v *infoView) paint(m *Model, cv *Canvas, r treemap.Rect) {
line("One filesystem", fmt.Sprint(st.OneFileSystem), value)
line("Follow symlinks", st.Follow, value)
line("Size mode", m.sizeMode.String(), value)
line("Units", map[bool]string{true: "SI (1000)", false: "IEC (1024)"}[textutil.SI], value)
units := "IEC (1024)"
if textutil.SI {
units = "SI (1000)"
}
line("Units", units, value)
if root.Flags&inventory.FlagAllocUnknown != 0 {
line("Note", "allocated size unavailable on this platform", Style{FG: t.Warn, BG: t.Bg})
}
@@ -154,6 +158,6 @@ func (v *infoView) key(m *Model, k string) (bool, tea.Cmd) {
}
func (v *infoView) wheel(m *Model, up bool) tea.Cmd {
v.tb.move(map[bool]int{true: -3, false: 3}[up], len(m.tree.Stats.Errors))
v.tb.scroll(up, len(m.tree.Stats.Errors))
return nil
}
+6 -2
View File
@@ -313,7 +313,7 @@ func (m *Model) describeSelection() string {
parts = append(parts, fmt.Sprintf("%d errors", n.Errors))
}
if m.opts.Diff != nil {
parts = append(parts, textutil.SignedSize(m.opts.Diff.Delta(id, m.sizeMode))+" since "+"snapshot")
parts = append(parts, textutil.SignedSize(m.opts.Diff.Delta(id, m.sizeMode))+" vs old snapshot")
}
return strings.Join(parts, " · ")
}
@@ -403,6 +403,10 @@ func (m *Model) paintStatus(cv *Canvas, r treemap.Rect, class sizeClass) {
if x+textutil.Width(s) > r.X+avail {
break
}
x += cv.Text(x, r.Y, s, avail-x, Style{FG: p.fg, BG: t.StatusBg, Attr: map[bool]Attr{true: Bold}[i == 0]})
st := Style{FG: p.fg, BG: t.StatusBg}
if i == 0 {
st.Attr = Bold
}
x += cv.Text(x, r.Y, s, avail-x, st)
}
}
+1 -8
View File
@@ -241,17 +241,10 @@ func (m *Model) paintSearch(cv *Canvas, r treemap.Rect) {
if s.ed.cur < len(text) {
under = textutil.Sanitize(string(text[s.ed.cur]))
}
cv.Text(cx, r.Y, under, 2, Style{FG: t.PanelBg, BG: t.Fg, Attr: Reverse * Attr(boolInt(t.Mono))})
cv.Text(cx, r.Y, under, 2, Style{FG: t.PanelBg, BG: t.Fg, Attr: m.monoRev()})
cv.TextRight(r.X, r.Y, r.W-1, textutil.Truncate(status, sw), statusSt)
}
func boolInt(b bool) int {
if b {
return 1
}
return 0
}
func (m *Model) paintFilterSummary(cv *Canvas, r treemap.Rect) {
t := m.theme
s := &m.search
+13 -1
View File
@@ -29,6 +29,15 @@ type table struct {
height int // rows visible in the last paint
}
// scroll moves the cursor three rows for a mouse wheel step.
func (tb *table) scroll(up bool, n int) {
if up {
tb.move(-3, n)
} else {
tb.move(3, n)
}
}
func (tb *table) move(d, n int) {
if n == 0 {
tb.cursor = 0
@@ -177,7 +186,10 @@ func (m *Model) paintBar(cv *Canvas, x, y, w int, frac float64, col Color, sel b
ch = parts[eighths]
}
if t.ASCII {
ch = map[bool]string{true: "#", false: " "}[eighths >= 4]
ch = " "
if eighths >= 4 {
ch = "#"
}
}
eighths -= 8
st := Style{FG: col, BG: bg}
+36
View File
@@ -418,3 +418,39 @@ func BenchmarkResizeRelayout(b *testing.B) {
i++
}
}
func TestMouse(t *testing.T) {
m := newTestModel(t)
send(m, size(120, 40))
m.View()
// Click the centre of a block other than the selected one.
var target treemap.Block
for _, b := range m.tm.blocks {
if b.ID != m.sel && b.ID >= 0 && m.tree.Node(inventory.NodeID(b.ID)).IsDir() && b.W > 4 {
target = b
break
}
}
click := tea.MouseClickMsg{X: target.X + target.W/2, Y: target.Y + target.H/2, Button: tea.MouseLeft}
send(m, click)
if m.sel != target.ID {
t.Fatalf("click selected %d, want %d", m.sel, target.ID)
}
m.View()
send(m, click) // second click within the double-click window
if m.zoom != inventory.NodeID(target.ID) {
t.Fatalf("double click should zoom into %d (zoom=%d)", target.ID, m.zoom)
}
m.View()
send(m, tea.MouseWheelMsg{X: 50, Y: 20, Button: tea.MouseWheelDown})
if m.zoom != 0 {
t.Fatal("wheel down should zoom out")
}
// A modal swallows clicks; clicking outside closes it.
send(m, key("?"))
m.View()
send(m, tea.MouseClickMsg{X: 0, Y: 39, Button: tea.MouseLeft})
if len(m.modals) != 0 {
t.Fatal("click outside modal should close it")
}
}
+22 -22
View File
@@ -98,7 +98,7 @@ func (v *listView) key(m *Model, k string) (bool, tea.Cmd) {
func (v *listView) wheel(m *Model, up bool) tea.Cmd {
v.sync(m)
v.tb.move(map[bool]int{true: -3, false: 3}[up], len(m.kidsOf(m.zoom).ids))
v.tb.scroll(up, len(m.kidsOf(m.zoom).ids))
v.sync(m)
return nil
}
@@ -106,16 +106,16 @@ func (v *listView) wheel(m *Model, up bool) tea.Cmd {
// ---- Extension statistics ------------------------------------------------
type extView struct {
tb table
cache []inventory.ExtStat
key_ [2]int
tb table
cache []inventory.ExtStat
cacheKey [2]int
}
func (*extView) name() string { return "Types" }
func (v *extView) stats(m *Model) []inventory.ExtStat {
k := [2]int{int(m.zoom), m.dataVer*2 + int(m.sizeMode)}
if v.cache == nil || v.key_ != k {
if v.cache == nil || v.cacheKey != k {
v.cache = m.tree.ExtStats(m.zoom)
sort.Slice(v.cache, func(i, j int) bool {
a, b := v.cache[i], v.cache[j]
@@ -128,7 +128,7 @@ func (v *extView) stats(m *Model) []inventory.ExtStat {
}
return a.Ext < b.Ext
})
v.key_ = k
v.cacheKey = k
}
return v.cache
}
@@ -189,7 +189,7 @@ func (v *extView) key(m *Model, k string) (bool, tea.Cmd) {
}
func (v *extView) wheel(m *Model, up bool) tea.Cmd {
v.tb.move(map[bool]int{true: -3, false: 3}[up], len(v.stats(m)))
v.tb.scroll(up, len(v.stats(m)))
return nil
}
@@ -212,12 +212,12 @@ var topNames = [...]string{"Largest files", "Largest directories (own files)", "
"Newest large files", "Most files", "Sparse files", "Hard-linked files"}
type topView struct {
tb table
mode topMode
ids []inventory.NodeID
vals []int64
key_ [3]int
limit int
tb table
mode topMode
ids []inventory.NodeID
vals []int64
cacheKey [3]int
limit int
}
func (*topView) name() string { return "Top" }
@@ -226,10 +226,10 @@ const topLimit = 500
func (v *topView) compute(m *Model) {
k := [3]int{int(m.zoom), m.dataVer*2 + int(m.sizeMode), int(v.mode)}
if v.ids != nil && v.key_ == k {
if v.ids != nil && v.cacheKey == k {
return
}
v.key_ = k
v.cacheKey = k
tr := m.tree
isFile := func(n *inventory.Node) bool {
return n.Kind == inventory.KindFile && n.Flags&(inventory.FlagHardlinkDup|inventory.FlagDeleted) == 0
@@ -378,16 +378,16 @@ func (v *topView) key(m *Model, k string) (bool, tea.Cmd) {
}
func (v *topView) wheel(m *Model, up bool) tea.Cmd {
v.tb.move(map[bool]int{true: -3, false: 3}[up], len(v.ids))
v.tb.scroll(up, len(v.ids))
return nil
}
// ---- Snapshot comparison -----------------------------------------------------
type changesView struct {
tb table
rows []snapshot.Change
key_ int
tb table
rows []snapshot.Change
cacheKey int
}
func (*changesView) name() string { return "Changes" }
@@ -395,9 +395,9 @@ func (*changesView) name() string { return "Changes" }
func (v *changesView) paint(m *Model, cv *Canvas, r treemap.Rect) {
t := m.theme
d := m.opts.Diff
if v.rows == nil || v.key_ != int(m.sizeMode) {
if v.rows == nil || v.cacheKey != int(m.sizeMode) {
v.rows = d.Changes(1000, m.sizeMode)
v.key_ = int(m.sizeMode)
v.cacheKey = int(m.sizeMode)
}
var maxAbs int64 = 1
for _, c := range v.rows {
@@ -452,6 +452,6 @@ func (v *changesView) key(m *Model, k string) (bool, tea.Cmd) {
}
func (v *changesView) wheel(m *Model, up bool) tea.Cmd {
v.tb.move(map[bool]int{true: -3, false: 3}[up], len(v.rows))
v.tb.scroll(up, len(v.rows))
return nil
}