From d50c6726ad635d22c6dab2c532b3a861da78f18e Mon Sep 17 00:00:00 2001 From: "Brandon McGinty (chatgpt)" Date: Sun, 9 Aug 2026 19:47:14 -0400 Subject: [PATCH] Route network UI callbacks through UI queue --- client.go | 42 +++++++++++++++++++++--------------------- fix.txt | 2 +- ui.go | 8 ++++++++ 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/client.go b/client.go index dddc5dd..b7691fb 100644 --- a/client.go +++ b/client.go @@ -120,9 +120,11 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) { b.recordingStarting = false b.RecordingMutex.Unlock() - b.Ui.SetActive(uiViewInput) - b.UiTree.Rebuild() - b.Ui.Refresh() + b.postUI(func() { + b.Ui.SetActive(uiViewInput) + b.UiTree.Rebuild() + b.Ui.Refresh() + }) var users []*gumble.User b.Client.Do(func() { @@ -146,7 +148,6 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) { if wmsg != "" { b.AddOutputLine(fmt.Sprintf("Welcome message: %s", wmsg)) } - b.Ui.Refresh() // Auto-start transmission if requested if b.AutoTransmit && b.Stream != nil && !b.Tx { @@ -197,8 +198,10 @@ func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) { } b.Tx = false b.Connected = false - b.UiTree.Rebuild() - b.Ui.Refresh() + b.postUI(func() { + b.UiTree.Rebuild() + b.Ui.Refresh() + }) go b.reconnectGoroutine() } @@ -285,8 +288,10 @@ func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) { b.AddOutputLine(formatUserStats(e.User)) } b.updateUserChannel(e) - b.RebuildUserChannelTreePreservingSelection() - b.Ui.Refresh() + b.postUI(func() { + b.RebuildUserChannelTreePreservingSelection() + b.Ui.Refresh() + }) } type userChangeNotification struct { @@ -386,8 +391,10 @@ func (b *Barnard) OnChannelChange(e *gumble.ChannelChangeEvent) { b.AddOutputLine(fmt.Sprintf("Channel permissions for %s: %s", e.Channel.Name, permissionList(*permission))) } } - b.RebuildUserChannelTreePreservingSelection() - b.Ui.Refresh() + b.postUI(func() { + b.RebuildUserChannelTreePreservingSelection() + b.Ui.Refresh() + }) } func formatUserStats(user *gumble.User) string { @@ -465,34 +472,27 @@ func (b *Barnard) OnPermissionDenied(e *gumble.PermissionDeniedEvent) { } func (b *Barnard) OnUserList(e *gumble.UserListEvent) { - b.adminUserList = e.UserList b.AddOutputLine(fmt.Sprintf("Admin: received %d registered users", len(e.UserList))) - b.UiAdmin.Rebuild() - b.Ui.Refresh() + b.postUI(func() { b.adminUserList = e.UserList; b.UiAdmin.Rebuild(); b.Ui.Refresh() }) } func (b *Barnard) OnACL(e *gumble.ACLEvent) { - b.adminACL = e.ACL if e.ACL != nil && e.ACL.Channel != nil { b.AddOutputLine(fmt.Sprintf("Admin: received ACLs for %s", e.ACL.Channel.Name)) } - b.UiAdmin.Rebuild() - b.Ui.Refresh() + b.postUI(func() { b.adminACL = e.ACL; b.UiAdmin.Rebuild(); b.Ui.Refresh() }) } func (b *Barnard) OnBanList(e *gumble.BanListEvent) { - b.adminBanList = e.BanList b.AddOutputLine(fmt.Sprintf("Admin: received %d bans", len(e.BanList))) - b.UiAdmin.Rebuild() - b.Ui.Refresh() + b.postUI(func() { b.adminBanList = e.BanList; b.UiAdmin.Rebuild(); b.Ui.Refresh() }) } func (b *Barnard) OnContextActionChange(e *gumble.ContextActionChangeEvent) { if e.ContextAction != nil { b.AddOutputLine(fmt.Sprintf("Admin: context action updated: %s", e.ContextAction.Name)) } - b.UiAdmin.Rebuild() - b.Ui.Refresh() + b.postUI(func() { b.UiAdmin.Rebuild(); b.Ui.Refresh() }) } func (b *Barnard) OnServerConfig(e *gumble.ServerConfigEvent) { diff --git a/fix.txt b/fix.txt index 6eecf3b..79bd3d3 100644 --- a/fix.txt +++ b/fix.txt @@ -51,7 +51,7 @@ Priority 0: security and crashers Add a binding test that creates and deletes a single buffer and checks openal.Err(). -6. UI writes are concurrent and termbox is not protected +[x] 6. UI writes are concurrent and termbox is not protected Files: ui.go, client.go, gumble/gumbleopenal/stream.go, uiterm/*.go Network callbacks, audio capture error callbacks, and reconnect goroutines directly update Ui, Textview, Tree, Label, and termbox while Ui.Run updates diff --git a/ui.go b/ui.go index 702321f..9c9d3cc 100644 --- a/ui.go +++ b/ui.go @@ -37,6 +37,14 @@ func esc(str string) string { return sanitize.HTML(clean) } +// postUI is the only path network and audio callbacks use to touch terminal +// widgets. Work is dropped during shutdown or queue overload. +func (b *Barnard) postUI(fn func()) { + if b.Ui != nil { + b.Ui.Post(fn) + } +} + func (b *Barnard) Notify(event string, who string, what string) { // Notifications are best-effort: a slow external command must not block a // UI or network callback. New events are dropped once the bounded queue is full.