Route network UI callbacks through UI queue

This commit is contained in:
Brandon McGinty (chatgpt)
2026-08-09 19:47:14 -04:00
committed by Brandon McGinty
parent 12761f29ca
commit d50c6726ad
3 changed files with 30 additions and 22 deletions
+21 -21
View File
@@ -120,9 +120,11 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) {
b.recordingStarting = false b.recordingStarting = false
b.RecordingMutex.Unlock() b.RecordingMutex.Unlock()
b.Ui.SetActive(uiViewInput) b.postUI(func() {
b.UiTree.Rebuild() b.Ui.SetActive(uiViewInput)
b.Ui.Refresh() b.UiTree.Rebuild()
b.Ui.Refresh()
})
var users []*gumble.User var users []*gumble.User
b.Client.Do(func() { b.Client.Do(func() {
@@ -146,7 +148,6 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) {
if wmsg != "" { if wmsg != "" {
b.AddOutputLine(fmt.Sprintf("Welcome message: %s", wmsg)) b.AddOutputLine(fmt.Sprintf("Welcome message: %s", wmsg))
} }
b.Ui.Refresh()
// Auto-start transmission if requested // Auto-start transmission if requested
if b.AutoTransmit && b.Stream != nil && !b.Tx { if b.AutoTransmit && b.Stream != nil && !b.Tx {
@@ -197,8 +198,10 @@ func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) {
} }
b.Tx = false b.Tx = false
b.Connected = false b.Connected = false
b.UiTree.Rebuild() b.postUI(func() {
b.Ui.Refresh() b.UiTree.Rebuild()
b.Ui.Refresh()
})
go b.reconnectGoroutine() go b.reconnectGoroutine()
} }
@@ -285,8 +288,10 @@ func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
b.AddOutputLine(formatUserStats(e.User)) b.AddOutputLine(formatUserStats(e.User))
} }
b.updateUserChannel(e) b.updateUserChannel(e)
b.RebuildUserChannelTreePreservingSelection() b.postUI(func() {
b.Ui.Refresh() b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh()
})
} }
type userChangeNotification struct { 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.AddOutputLine(fmt.Sprintf("Channel permissions for %s: %s", e.Channel.Name, permissionList(*permission)))
} }
} }
b.RebuildUserChannelTreePreservingSelection() b.postUI(func() {
b.Ui.Refresh() b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh()
})
} }
func formatUserStats(user *gumble.User) string { func formatUserStats(user *gumble.User) string {
@@ -465,34 +472,27 @@ func (b *Barnard) OnPermissionDenied(e *gumble.PermissionDeniedEvent) {
} }
func (b *Barnard) OnUserList(e *gumble.UserListEvent) { func (b *Barnard) OnUserList(e *gumble.UserListEvent) {
b.adminUserList = e.UserList
b.AddOutputLine(fmt.Sprintf("Admin: received %d registered users", len(e.UserList))) b.AddOutputLine(fmt.Sprintf("Admin: received %d registered users", len(e.UserList)))
b.UiAdmin.Rebuild() b.postUI(func() { b.adminUserList = e.UserList; b.UiAdmin.Rebuild(); b.Ui.Refresh() })
b.Ui.Refresh()
} }
func (b *Barnard) OnACL(e *gumble.ACLEvent) { func (b *Barnard) OnACL(e *gumble.ACLEvent) {
b.adminACL = e.ACL
if e.ACL != nil && e.ACL.Channel != nil { if e.ACL != nil && e.ACL.Channel != nil {
b.AddOutputLine(fmt.Sprintf("Admin: received ACLs for %s", e.ACL.Channel.Name)) b.AddOutputLine(fmt.Sprintf("Admin: received ACLs for %s", e.ACL.Channel.Name))
} }
b.UiAdmin.Rebuild() b.postUI(func() { b.adminACL = e.ACL; b.UiAdmin.Rebuild(); b.Ui.Refresh() })
b.Ui.Refresh()
} }
func (b *Barnard) OnBanList(e *gumble.BanListEvent) { func (b *Barnard) OnBanList(e *gumble.BanListEvent) {
b.adminBanList = e.BanList
b.AddOutputLine(fmt.Sprintf("Admin: received %d bans", len(e.BanList))) b.AddOutputLine(fmt.Sprintf("Admin: received %d bans", len(e.BanList)))
b.UiAdmin.Rebuild() b.postUI(func() { b.adminBanList = e.BanList; b.UiAdmin.Rebuild(); b.Ui.Refresh() })
b.Ui.Refresh()
} }
func (b *Barnard) OnContextActionChange(e *gumble.ContextActionChangeEvent) { func (b *Barnard) OnContextActionChange(e *gumble.ContextActionChangeEvent) {
if e.ContextAction != nil { if e.ContextAction != nil {
b.AddOutputLine(fmt.Sprintf("Admin: context action updated: %s", e.ContextAction.Name)) b.AddOutputLine(fmt.Sprintf("Admin: context action updated: %s", e.ContextAction.Name))
} }
b.UiAdmin.Rebuild() b.postUI(func() { b.UiAdmin.Rebuild(); b.Ui.Refresh() })
b.Ui.Refresh()
} }
func (b *Barnard) OnServerConfig(e *gumble.ServerConfigEvent) { func (b *Barnard) OnServerConfig(e *gumble.ServerConfigEvent) {
+1 -1
View File
@@ -51,7 +51,7 @@ Priority 0: security and crashers
Add a binding test that creates and deletes a single buffer and checks Add a binding test that creates and deletes a single buffer and checks
openal.Err(). 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 Files: ui.go, client.go, gumble/gumbleopenal/stream.go, uiterm/*.go
Network callbacks, audio capture error callbacks, and reconnect goroutines Network callbacks, audio capture error callbacks, and reconnect goroutines
directly update Ui, Textview, Tree, Label, and termbox while Ui.Run updates directly update Ui, Textview, Tree, Label, and termbox while Ui.Run updates
+8
View File
@@ -37,6 +37,14 @@ func esc(str string) string {
return sanitize.HTML(clean) 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) { func (b *Barnard) Notify(event string, who string, what string) {
// Notifications are best-effort: a slow external command must not block a // 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. // UI or network callback. New events are dropped once the bounded queue is full.