Fix force-quit-gui losing row selection on every refresh

refresh() rebuilt the GtkListStore from scratch every 2s via
store.Clear(), which invalidates GtkTreeSelection's TreeIter -- so the
highlighted row silently dropped on every tick even when the process
list hadn't meaningfully changed. Now the selected PID is captured
before the rebuild and the matching row is reselected after, mirroring
the PID-based (rather than row-reference-based) selection tracking the
nucular prototype already used.

Verified live: selected PID 19777 stayed highlighted across a refresh
cycle where its RSS/Swap/Total values visibly changed, confirming the
reselect logic runs on real data, not a no-op refresh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Philip Peterson 2026-10-06 07:08:59 -08:00
parent 141d4f7690
commit 889a2c4d50

View file

@ -129,8 +129,38 @@ func main() {
killButton, _ := gtk.ButtonNewWithLabel("Force Quit Selected")
box.PackStart(killButton, false, false, 0)
// selectedPID reads the PID column of the current selection, if any.
// Used both to act on the selected row and, in refresh, to find the
// matching row again after the store is rebuilt — GtkTreeSelection
// tracks a TreeIter, which store.Clear() invalidates, so without this
// every refresh would silently drop the highlight even though nothing
// the user cares about changed.
selectedPID := func() (int, bool) {
sel, err := treeView.GetSelection()
if err != nil {
return 0, false
}
model, iter, ok := sel.GetSelected()
if !ok {
return 0, false
}
val, err := model.(*gtk.TreeModel).GetValue(iter, 0)
if err != nil {
return 0, false
}
pidVal, err := val.GoValue()
if err != nil {
return 0, false
}
pid, ok := pidVal.(int)
return pid, ok
}
refresh := func() {
prevPID, hadSelection := selectedPID()
store.Clear()
var reselect *gtk.TreeIter
for _, p := range readProcesses() {
iter := store.Append()
store.Set(iter,
@ -143,6 +173,14 @@ func main() {
fmt.Sprintf("%.1f", float64(p.totalKB)/1024),
},
)
if hadSelection && p.pid == prevPID {
reselect = iter
}
}
if reselect != nil {
if sel, err := treeView.GetSelection(); err == nil {
sel.SelectIter(reselect)
}
}
}
refresh()
@ -153,27 +191,11 @@ func main() {
})
killButton.Connect("clicked", func() {
sel, err := treeView.GetSelection()
if err != nil {
return
}
model, iter, ok := sel.GetSelected()
pid, ok := selectedPID()
if !ok {
statusLabel.SetText("No process selected.")
return
}
val, err := model.(*gtk.TreeModel).GetValue(iter, 0)
if err != nil {
return
}
pidVal, err := val.GoValue()
if err != nil {
return
}
pid, ok := pidVal.(int)
if !ok {
return
}
if err := syscall.Kill(pid, syscall.SIGKILL); err != nil {
statusLabel.SetText(fmt.Sprintf("Failed to kill PID %d: %v", pid, err))
} else {