package mujica import ( "fmt" "log/slog" "sort" "github.com/egoist/mygo/ui" "mujicaui/internal" "mujicaui/theme" ) // StorageSegment is one category of used space. type StorageSegment struct { Label string Bytes int64 } // StorageUsageOptions configure a StorageUsage. type StorageUsageOptions struct { // Capacity is the total space; required. Capacity int64 // Label names the volume. Label string // Revision changes when segments are edited in place. Revision uint64 } type storageUsageState struct { data *StorageSegment n int revision uint64 capacity, used int64 ends []int64 rows ui.ListState } // StorageUsage displays segmented capacity with keyboard readouts and details. func StorageUsage(c *ui.Context, segments []StorageSegment, opts StorageUsageOptions) *ui.Element { if opts.Capacity <= 0 { panic("mujica: StorageUsage needs a positive Capacity") } k := Tokens(c) u := Density(c).Unit() e := ui.Column(c).Gap(u * 3).Role(ui.RoleGroup).Label(opts.Label) s := ui.Local(e, "storage_usage.data", func() storageUsageState { return storageUsageState{} }) var first *StorageSegment if len(segments) > 0 { first = &segments[0] } if s.data != first || s.n != len(segments) || s.capacity != opts.Capacity || s.revision != opts.Revision { s.used, s.ends = 0, s.ends[:0] for _, seg := range segments { if seg.Bytes < 0 || seg.Bytes > opts.Capacity-s.used { panic("mujica: StorageUsage segment size out of range") } s.used += seg.Bytes s.ends = append(s.ends, s.used) } s.data, s.n, s.capacity, s.revision = first, len(segments), opts.Capacity, opts.Revision } used := s.used pal := flamegraphPalette(k) pct := func(b int64) float64 { return 100 * float64(b) / float64(opts.Capacity) } bytes := func(b int64) string { return processListBytes(uint64(b)) } readout := func(i int) string { if i < 0 || i >= len(segments) { return fmt.Sprintf(msg(c, "storage_usage.free"), bytes(opts.Capacity-used), pct(opts.Capacity-used)) } return fmt.Sprintf("%s: %s (%.1f%%)", segments[i].Label, bytes(segments[i].Bytes), pct(segments[i].Bytes)) } summary := fmt.Sprintf(msg(c, "storage_usage.used"), bytes(used), bytes(opts.Capacity)) focus := ui.Local(e, "storage_usage.focus", func() int { return 0 }) *focus = min(*focus, len(segments)) e.Children(func() { ui.Row(c).Gap(u * 2).Children(func() { if opts.Label != "" { ui.Text(c, opts.Label).Font(theme.SerifFont).FontWeight(600).Grow(1) } ui.Text(c, summary).TextColor(k.TextMuted) }) bar := ui.Box(c).Height(14).Focusable().Role(ui.RoleMeter).Label(summary).Description(readout(*focus)) n := len(segments) + 1 moved := true switch { case bar.Shortcut(0, ui.KeyRight) && !bar.IsDisabled(): *focus = (*focus + 1) % n case bar.Shortcut(0, ui.KeyLeft) && !bar.IsDisabled(): *focus = (*focus + n - 1) % n default: moved = false } if moved { c.Announce(readout(*focus)) slog.Debug("mujica: storage_usage focus", "segment", *focus) } internal.Ring(bar, k.Focus, theme.ControlRadius) focused, at := bar.Focused(), *focus bar.Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(r, k.Border.Mix(k.Surface, 0.4), 3) if len(segments) > int(r.W) { for px := float32(0); px < r.W; px++ { b := int64(float64(px) / float64(r.W) * float64(opts.Capacity)) i := sort.Search(len(s.ends), func(i int) bool { return s.ends[i] > b }) if i == len(segments) { break } p.Fill(ui.Rect{X: internal.Snap(p, r.X+px), Y: r.Y, W: min(1, r.W-px), H: r.H}, pal[i%len(pal)], 0) } return } x := r.X for i, s := range segments { w := internal.Snap(p, r.W*float32(s.Bytes)/float32(opts.Capacity)) if w <= 0 { continue } seg := ui.Rect{X: x, Y: r.Y, W: max(w-2, 1), H: r.H} p.Fill(seg, pal[i%len(pal)], 2) if focused && i == at { p.Stroke(ui.Rect{X: seg.X - 1, Y: seg.Y - 1, W: seg.W + 2, H: seg.H + 2}, k.Text, 3, 1.5) } x += w } }) if focused { ui.Text(c, readout(at)).FontSize(FontSize(c, theme.CaptionSize)).Role(ui.RoleStatus) } ui.List(c, &s.rows, len(segments)+1, func(i int) { if i == len(segments) { storageLegend(c, k.Border.Mix(k.Surface, 0.4), msg(c, "storage_usage.freeLabel"), bytes(opts.Capacity-used), pct(opts.Capacity-used)) return } seg := segments[i] storageLegend(c, pal[i%len(pal)], seg.Label, bytes(seg.Bytes), pct(seg.Bytes)) }).Height(min(float32(len(segments)+1)*Density(c).ControlHeight(), Density(c).ControlHeight()*6)) }) return e } func storageLegend(c *ui.Context, col ui.Color, label, size string, pct float64) { k := Tokens(c) ui.Row(c).MinHeight(Density(c).ControlHeight()).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).Children(func() { ui.Box(c).Size(10, 10).Radius(2).Shrink(0).Background(col).Role(ui.RoleNone) ui.Text(c, label).Grow(1).MinWidth(0).SingleLine() ui.Text(c, size).TextColor(k.TextMuted).Shrink(0) ui.Text(c, fmt.Sprintf("%.1f%%", pct)).TextColor(k.TextMuted).FontFeatures("tnum").Width(52).TextAlign(ui.End).Shrink(0) }) }