Files
MujicaUI/code/process_list.go
T
ZacharyZhang-NYandClaude Opus 5.5 bcce34cda1 Upgrade MyGo to v0.5.5
MyGo v0.5.5 returns ui.Element as a checked value handle instead of a
pointer. Migrated with `mygo migrate-ui`, then fixed what the codemod
leaves to review:

- Element nil checks become .Valid(); absent elements are ui.Element{}.
- Input handlers and paint callbacks run outside the build pass, where
  elements expire and Context methods are inert: read IsDisabled into a
  local during the build, keep a ui.Handle for Focus, use c.Services()
  for clipboard access, create ui.Local state during the build.
- Add core.AutoFocus: v0.5.5 records an overlay's opener after its
  content builds, so same-frame autofocus lost focus restore on close.
  It focuses one frame later; cascader rows defer their focus likewise.
- core.OverlayLayer identifies build passes by the root element, not
  c.Now(), which repeats across passes and under the Tester clock.
- Context menus survive the opening press counted as PressedOutside;
  split button tooltip no longer reorders its open menu; disabled
  menubar rows pass presses to the panel; Form defers blur-started
  async checks so the growing message no longer moves Submit away
  from the release; Combobox drops its disabled copy now that MyGo
  blocks typing under a disabled wrapper (canary flipped to a guard).
- The Tester clock is virtual: tests use Advance/Now instead of
  time.Sleep/time.Now and read element state inside the view. The web
  mirror advances the clock to wall time before each frame and input.
- AGENTS.md and docs describe the value Element API and the new pin.

Snapshot goldens are darwin-only and were not re-verified here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 01:37:54 -04:00

189 lines
5.4 KiB
Go

package code
import (
"cmp"
"fmt"
"log/slog"
"slices"
"strings"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/data"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/icons"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/overlay"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
)
// Process is one row of a ProcessList.
type Process struct {
PID int
Name string
// CPU is percent of one core; Memory is resident bytes.
CPU float64
Memory uint64
}
// ProcessListState is a ProcessList's sort, selection and pending end.
type ProcessListState struct {
// Sort is by name, pid, cpu or memory.
Sort ui.SortOrder
rows ui.ListState
selected int
order []int
sorted ui.SortOrder
data *Process
n int
confirm bool
ending Process
started bool
}
// ProcessListOptions configure a ProcessList.
type ProcessListOptions struct {
Disabled bool
}
// ProcessListResult carries a confirmed end request.
type ProcessListResult struct {
Element ui.Element
// End is the confirmed process to end, or nil.
End *Process
}
// ProcessList is a sortable process table with confirmed ending.
func ProcessList(c *ui.Context, procs []Process, s *ProcessListState, opts ProcessListOptions) ProcessListResult {
if s == nil {
panic("mujica: ProcessList needs a state")
}
if !s.started {
s.started, s.selected, s.Sort = true, -1, ui.SortOrder{Column: "cpu", Descending: true}
}
processListSort(procs, s)
k := core.Tokens(c)
var res ProcessListResult
res.Element = CodePanel(c).Disabled(opts.Disabled)
disabled := res.Element.IsDisabled()
if disabled {
s.confirm = false
}
ask := func(p Process) {
if !disabled {
s.ending, s.confirm = p, true
}
}
res.Element.Children(func() {
if len(procs) == 0 {
CodeCaption(c, core.Msg(c, "process_list.empty")).Padding(core.Density(c).Unit() * 4)
return
}
cols := []ui.TableColumn{
{Title: core.Msg(c, "process_list.name"), ID: "name", Sortable: true, MinWidth: 120},
{Title: core.Msg(c, "process_list.pid"), ID: "pid", Width: 80, Align: ui.End, Sortable: true},
{Title: core.Msg(c, "process_list.cpu"), ID: "cpu", Width: 90, Align: ui.End, Sortable: true},
{Title: core.Msg(c, "process_list.memory"), ID: "memory", Width: 100, Align: ui.End, Sortable: true},
{Title: core.Msg(c, "process_list.actions"), ID: "end", Width: 56, Fixed: true, Align: ui.Center},
}
s.rows.Sort = &s.Sort
s.rows.Selected = &s.selected
s.rows.Key = func(i int) any { return procs[s.order[i]].PID }
s.rows.Label = func(i int) string { return procs[s.order[i]].Name }
restore := data.DataSelectTheme(c, 0)
table := ui.Table(c, &s.rows, cols, len(procs), func(row, col int) {
p := procs[s.order[row]]
switch col {
case 0:
ui.Row(c).Gap(core.Density(c).Unit() * 2).AlignItems(ui.Center).Children(func() {
CodeSelectMark(c, s.selected == row)
ui.Text(c, p.Name).SingleLine()
})
case 1:
CodeMono(c, fmt.Sprint(p.PID))
case 2:
CodeMono(c, fmt.Sprintf("%.1f%%", p.CPU))
case 3:
CodeMono(c, ProcessListBytes(p.Memory))
case 4:
if core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "process_list.end"), p.Name)).Clicked() {
ask(p)
}
}
}).Grow(1).MinHeight(core.Density(c).ControlHeight() * 4)
restore()
internal.Ring(table, k.Focus, theme.ControlRadius)
if table.Changed() {
processListSort(procs, s)
}
if table.Shortcut(0, ui.KeyDelete) && s.selected >= 0 && s.selected < len(procs) {
ask(procs[s.order[s.selected]])
}
})
if s.confirm {
r := overlay.AlertDialog(c, &s.confirm, overlay.AlertDialogOptions{
Title: fmt.Sprintf(core.Msg(c, "process_list.confirm"), s.ending.Name, s.ending.PID),
Description: core.Msg(c, "process_list.warning"),
ConfirmLabel: core.Msg(c, "process_list.endButton"),
})
if r.Confirmed && !disabled {
p := s.ending
res.End = &p
slog.Debug("mujica: process_list end", "pid", p.PID)
}
}
return res
}
// processListSort reorders when data or sort change.
func processListSort(procs []Process, s *ProcessListState) {
var first *Process
if len(procs) > 0 {
first = &procs[0]
}
if s.data == first && s.n == len(procs) && s.sorted == s.Sort && len(s.order) == len(procs) {
return
}
s.data, s.n, s.sorted = first, len(procs), s.Sort
s.order = s.order[:0]
for i := range procs {
s.order = append(s.order, i)
}
by := func(a, b int) int {
pa, pb := procs[a], procs[b]
switch s.Sort.Column {
case "name":
return strings.Compare(strings.ToLower(pa.Name), strings.ToLower(pb.Name))
case "pid":
return cmp.Compare(pa.PID, pb.PID)
case "cpu":
return cmp.Compare(pa.CPU, pb.CPU)
case "memory":
return cmp.Compare(pa.Memory, pb.Memory)
case "":
return 0
}
panic("mujica: ProcessList sorts by name, pid, cpu or memory, not " + s.Sort.Column)
}
slices.SortStableFunc(s.order, func(a, b int) int {
if s.Sort.Descending {
return by(b, a)
}
return by(a, b)
})
slog.Debug("mujica: process_list sort", "by", s.Sort.Column, "descending", s.Sort.Descending)
}
func ProcessListBytes(b uint64) string {
const unit = 1024
switch {
case b >= unit*unit*unit:
return fmt.Sprintf("%.1f GB", float64(b)/(unit*unit*unit))
case b >= unit*unit:
return fmt.Sprintf("%.1f MB", float64(b)/(unit*unit))
case b >= unit:
return fmt.Sprintf("%.0f KB", float64(b)/unit)
}
return fmt.Sprintf("%d B", b)
}