Make terminal UI shutdown idempotent

This commit is contained in:
Brandon McGinty (chatgpt)
2026-08-09 14:30:55 -04:00
committed by Brandon McGinty
parent 41759b95f6
commit 5bf3ed8b84
3 changed files with 30 additions and 9 deletions
+1 -1
View File
@@ -231,7 +231,7 @@ Priority 3: configuration, UI, and binding hardening
never closes the FIFO descriptor. Exit the reader on terminal errors,
close the descriptor, and make shutdown cancellable.
30. Empty UI tree can panic; UI startup errors are swallowed
[x] 30. Empty UI tree can panic; UI startup errors are swallowed
Files: uiterm/tree.go, uiterm/ui.go
Tree.uiKeyEvent indexes lines[activeLine] for a non-arrow key even when
no lines exist. Guard empty trees. Ui.Run returns nil when termbox.Init
+13 -8
View File
@@ -3,6 +3,7 @@ package uiterm
import (
"errors"
"strings"
"sync"
"sync/atomic"
"github.com/nsf/termbox-go"
@@ -20,8 +21,9 @@ type UiManager interface {
type Ui struct {
Fg, Bg Attribute
close chan bool
manager UiManager
close chan struct{}
closeOnce sync.Once
manager UiManager
drawCount int32
elements map[string]*uiElement
@@ -39,7 +41,7 @@ type uiElement struct {
func New(manager UiManager) *Ui {
ui := &Ui{
close: make(chan bool, 10),
close: make(chan struct{}),
elements: make(map[string]*uiElement),
manager: manager,
keyListeners: make(map[Key][]KeyListener),
@@ -48,10 +50,9 @@ func New(manager UiManager) *Ui {
return ui
}
// Close is safe to call repeatedly and never blocks a caller.
func (ui *Ui) Close() {
if termbox.IsInit {
ui.close <- true
}
ui.closeOnce.Do(func() { close(ui.close) })
}
func (ui *Ui) Refresh() {
@@ -97,7 +98,7 @@ func (ui *Ui) Run(cmds chan string) error {
return nil
}
if err := termbox.Init(); err != nil {
return nil
return err
}
defer termbox.Close()
termbox.SetInputMode(termbox.InputAlt)
@@ -119,7 +120,11 @@ func (ui *Ui) Run(cmds chan string) error {
select {
case <-ui.close:
return nil
case cmd := <-cmds:
case cmd, ok := <-cmds:
if !ok {
cmds = nil
continue
}
ui.onCommandEvent(cmd)
case event := <-events:
switch event.Type {
+16
View File
@@ -0,0 +1,16 @@
package uiterm
import "testing"
// Regression: Close sent to a bounded channel and could block or enqueue
// duplicate shutdowns when called more than once.
func TestCloseIsNonblockingAndIdempotent(t *testing.T) {
ui := New(nil)
ui.Close()
ui.Close()
select {
case <-ui.close:
default:
t.Fatal("Close did not signal shutdown")
}
}