Files
mapsize/internal/tui/table.go
T
Andy Dixon 71556b76de Speed up snapshot loading; polish help and table bars
Decompression and checksumming overlap with parsing via a pipe, and a
concrete-typed varint reader removes per-byte interface calls
(0.39s -> 0.29s for 458K nodes).
2026-10-02 08:35:47 +01:00

211 lines
4.7 KiB
Go

package tui
import (
tea "charm.land/bubbletea/v2"
"github.com/andydixon/mapsize/internal/textutil"
"github.com/andydixon/mapsize/internal/treemap"
)
// column describes one table column.
type column struct {
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.
type tableRow struct {
cells []string
styles []*Style // nil = default
bar float64 // 0..1 for an optional bar column (-1 = none)
barCol Color
}
// table is a scrollable list with a cursor, shared by the list-style views.
type table struct {
cursor, offset int
height int // rows visible in the last paint
}
func (tb *table) move(d, n int) {
if n == 0 {
tb.cursor = 0
return
}
tb.cursor = min(max(tb.cursor+d, 0), n-1)
}
// key handles navigation keys common to all tables.
func (tb *table) key(k string, n int) bool {
page := max(tb.height-1, 1)
switch k {
case "up", "k":
tb.move(-1, n)
case "down", "j":
tb.move(1, n)
case "pgup", "ctrl+b":
tb.move(-page, n)
case "pgdown", "ctrl+f":
tb.move(page, n)
case "end", "G":
tb.move(n, n)
case "g":
tb.cursor = 0
default:
return false
}
return true
}
func (tb *table) paint(m *Model, cv *Canvas, r treemap.Rect, cols []column, rows []tableRow, title string) {
t := m.theme
cv.Fill(r, " ", t.base())
if r.H < 3 {
return
}
y := r.Y
if title != "" {
cv.Text(r.X+1, y, title, r.W-2, Style{FG: t.Accent, BG: t.Bg, Attr: Bold})
y++
}
// Resolve flexible width.
fixed := 0
for _, c := range cols {
fixed += c.width + 1
}
flex := max(r.W-2-fixed, 8)
x := r.X + 1
hdr := Style{FG: t.Muted, BG: t.Bg, Attr: Bold}
for _, c := range cols {
w := c.width
if w == 0 {
w = flex
}
s := c.title
if c.right {
s = textutil.PadLeft(s, w)
}
cv.Text(x, y, s, w, hdr)
x += w + 1
}
y++
cv.HLine(r.X+1, y, r.W-2, "─", Style{FG: t.Faint, BG: t.Bg})
y++
tb.height = r.Y + r.H - y
if tb.cursor >= len(rows) {
tb.cursor = max(len(rows)-1, 0)
}
if tb.cursor < tb.offset {
tb.offset = tb.cursor
}
if tb.cursor >= tb.offset+tb.height {
tb.offset = tb.cursor - tb.height + 1
}
tb.offset = max(0, min(tb.offset, len(rows)-tb.height))
for i := tb.offset; i < len(rows) && y < r.Y+r.H; i++ {
row := rows[i]
sel := i == tb.cursor
base := t.base()
if sel {
base = Style{FG: t.textOn(t.Sel), BG: t.Sel, Attr: Bold | m.monoRev()}
cv.Fill(treemap.Rect{X: r.X, Y: y, W: r.W, H: 1}, " ", base)
}
x := r.X + 1
for ci, c := range cols {
w := c.width
if w == 0 {
w = flex
}
s := ""
if ci < len(row.cells) {
s = row.cells[ci]
}
st := base
if !sel && ci < len(row.styles) && row.styles[ci] != nil {
st = *row.styles[ci]
}
if c.title == "" && row.bar >= 0 {
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)
}
x += w + 1
}
idx := i
m.hits = append(m.hits, hit{treemap.Rect{X: r.X, Y: y, W: r.W, H: 1}, func(m *Model, double bool) tea.Cmd {
tb.cursor = idx
if double {
return m.handleKeyName("enter")
}
return nil
}})
y++
}
if len(rows) == 0 {
cv.TextCentered(r.X, r.Y+r.H/2, r.W, "Nothing to show", t.muted())
}
if len(rows) > tb.height && tb.height > 0 {
pos := textutil.Count(int64(tb.cursor+1)) + "/" + textutil.Count(int64(len(rows)))
cv.TextRight(r.X, r.Y, r.W-1, pos, t.muted())
}
}
// paintBar draws a proportional bar with eighth-block precision.
func (m *Model) paintBar(cv *Canvas, x, y, w int, frac float64, col Color, sel bool) {
t := m.theme
if col == 0 {
col = t.Accent
}
bg := t.Bg.Mix(t.Fg, 0.08)
if sel {
bg = t.Sel.Mix(RGB(0, 0, 0), 0.25)
}
eighths := int(frac*float64(w*8) + 0.5)
parts := []string{"", "▏", "▎", "▍", "▌", "▋", "▊", "▉"}
for i := 0; i < w; i++ {
ch := " "
switch {
case eighths >= 8:
ch = "█"
case eighths > 0:
ch = parts[eighths]
}
if t.ASCII {
ch = map[bool]string{true: "#", false: " "}[eighths >= 4]
}
eighths -= 8
st := Style{FG: col, BG: bg}
if t.Mono {
st = Style{}
}
cv.Text(x+i, y, ch, 1, st)
}
}
// handleKeyName lets mouse actions reuse key handling (lock already held).
func (m *Model) handleKeyName(k string) tea.Cmd {
if handled, cmd := m.views[m.view].key(m, k); handled {
return cmd
}
switch k {
case "enter":
return m.inspectSelection()
case "space":
return m.zoomIntoSelection()
}
return nil
}
func plural(n int, s string) string {
if n == 1 {
return "1 " + s
}
return textutil.Count(int64(n)) + " " + s + "s"
}