Queue UI output and status updates

This commit is contained in:
Brandon McGinty (chatgpt)
2026-08-09 19:37:18 -04:00
committed by Brandon McGinty
parent b10e08477c
commit 12761f29ca
3 changed files with 51 additions and 4 deletions
+13 -4
View File
@@ -63,9 +63,14 @@ func (b *Barnard) GetInputStatus() string {
func (b *Barnard) UpdateInputStatus(status string) { func (b *Barnard) UpdateInputStatus(status string) {
status = truncateInputStatus(status) status = truncateInputStatus(status)
b.UiInputStatus.Text = status if b.Ui == nil {
b.RebuildUserChannelTreePreservingSelection() return
b.Ui.Refresh() }
b.Ui.Post(func() {
b.UiInputStatus.Text = status
b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh()
})
} }
// truncateInputStatus limits terminal cells without splitting UTF-8 runes. // truncateInputStatus limits terminal cells without splitting UTF-8 runes.
@@ -79,7 +84,11 @@ func truncateInputStatus(status string) string {
func (b *Barnard) AddOutputLine(line string) { func (b *Barnard) AddOutputLine(line string) {
now := time.Now() now := time.Now()
b.UiOutput.AddLine(fmt.Sprintf("%s [%02d:%02d:%02d]", line, now.Hour(), now.Minute(), now.Second())) formatted := fmt.Sprintf("%s [%02d:%02d:%02d]", line, now.Hour(), now.Minute(), now.Second())
if b.Ui == nil {
return
}
b.Ui.Post(func() { b.UiOutput.AddLine(formatted) })
} }
func (b *Barnard) AddOutputMessage(sender *gumble.User, message string) { func (b *Barnard) AddOutputMessage(sender *gumble.User, message string) {
+20
View File
@@ -23,6 +23,7 @@ type Ui struct {
close chan struct{} close chan struct{}
closeOnce sync.Once closeOnce sync.Once
events chan func()
manager UiManager manager UiManager
drawCount int32 drawCount int32
@@ -42,6 +43,7 @@ type uiElement struct {
func New(manager UiManager) *Ui { func New(manager UiManager) *Ui {
ui := &Ui{ ui := &Ui{
close: make(chan struct{}), close: make(chan struct{}),
events: make(chan func(), 256),
elements: make(map[string]*uiElement), elements: make(map[string]*uiElement),
manager: manager, manager: manager,
keyListeners: make(map[Key][]KeyListener), keyListeners: make(map[Key][]KeyListener),
@@ -55,6 +57,22 @@ func (ui *Ui) Close() {
ui.closeOnce.Do(func() { close(ui.close) }) ui.closeOnce.Do(func() { close(ui.close) })
} }
// Post schedules UI work on Run's owning goroutine. It is deliberately
// bounded: network callbacks must not block behind slow terminal rendering.
func (ui *Ui) Post(fn func()) bool {
if fn == nil {
return true
}
select {
case <-ui.close:
return false
case ui.events <- fn:
return true
default:
return false
}
}
func (ui *Ui) Refresh() { func (ui *Ui) Refresh() {
if termbox.IsInit { if termbox.IsInit {
ui.beginDraw() ui.beginDraw()
@@ -137,6 +155,8 @@ func (ui *Ui) Run(cmds chan string) error {
select { select {
case <-ui.close: case <-ui.close:
return nil return nil
case fn := <-ui.events:
fn()
case cmd, ok := <-cmds: case cmd, ok := <-cmds:
if !ok { if !ok {
cmds = nil cmds = nil
+18
View File
@@ -4,6 +4,24 @@ import "testing"
// Regression: Close sent to a bounded channel and could block or enqueue // Regression: Close sent to a bounded channel and could block or enqueue
// duplicate shutdowns when called more than once. // duplicate shutdowns when called more than once.
// Regression: network callbacks modified terminal state directly. Post gives
// them a bounded handoff to the UI-owning Run goroutine instead of blocking.
func TestPostIsBoundedAndRejectsClosedUI(t *testing.T) {
ui := New(nil)
for i := 0; i < cap(ui.events); i++ {
if !ui.Post(func() {}) {
t.Fatal("queue filled too early")
}
}
if ui.Post(func() {}) {
t.Fatal("Post accepted work past queue capacity")
}
ui.Close()
if ui.Post(func() {}) {
t.Fatal("Post accepted work after close")
}
}
func TestCloseIsNonblockingAndIdempotent(t *testing.T) { func TestCloseIsNonblockingAndIdempotent(t *testing.T) {
ui := New(nil) ui := New(nil)
ui.Close() ui.Close()