Optionally remember transmission state on reconnect.

This commit is contained in:
Storm Dragon
2026-09-06 18:24:52 -04:00
parent 6817ecd90b
commit 01ca0da7ff
8 changed files with 228 additions and 22 deletions
+12
View File
@@ -54,6 +54,18 @@ Barnard normalizes the level of your outgoing microphone audio with automatic ga
agcenabled = true agcenabled = true
``` ```
To restore the last transmit state after reconnecting or restarting Barnard,
enable the option in `barnard-ui` under **Settings**, or set it directly in
`~/.barnard.toml`:
```toml
remembertransmissionstate = true
```
This option defaults to `false`, preserving the normal behavior of starting
with transmission off after a disconnect. When enabled, Barnard updates the
internal `transmissionactive` value as transmission is started or stopped.
## FIFO Control ## FIFO Control
If you pass the --fifo option to Barnard, a FIFO pipe will be created. If you pass the --fifo option to Barnard, a FIFO pipe will be created.
+14
View File
@@ -204,6 +204,20 @@ func (b *Barnard) setTransmitting(transmitting bool) {
b.stateMutex.Lock() b.stateMutex.Lock()
b.Tx = transmitting b.Tx = transmitting
b.stateMutex.Unlock() b.stateMutex.Unlock()
if b.UserConfig != nil && b.UserConfig.GetRememberTransmissionState() {
if err := b.UserConfig.SetTransmissionActive(transmitting); err != nil && b.Ui != nil {
b.AddOutputLine("Could not save transmission state: " + err.Error())
}
}
}
// clearTransmittingForDisconnect changes only the live state. When transmission
// remembering is enabled, an interrupted connection must retain the last state
// so the next successful connection can restore it.
func (b *Barnard) clearTransmittingForDisconnect() {
b.stateMutex.Lock()
b.Tx = false
b.stateMutex.Unlock()
} }
func (b *Barnard) isConnected() bool { func (b *Barnard) isConnected() bool {
+17 -8
View File
@@ -81,7 +81,7 @@ func (b *Barnard) connect(reconnect bool) bool {
b.toneTestSaverDetach = b.Client.Config.AttachAudio(b.toneTestSaver) b.toneTestSaverDetach = b.Client.Config.AttachAudio(b.toneTestSaver)
b.setConnected(true) b.setConnected(true)
if b.toneTestAutoTransmit() { if b.shouldStartTransmission() {
b.toneTestStop = make(chan struct{}) b.toneTestStop = make(chan struct{})
go StartToneGenerator(b.Client, b.toneTestStop) go StartToneGenerator(b.Client, b.toneTestStop)
b.setTransmitting(true) b.setTransmitting(true)
@@ -149,7 +149,7 @@ func (b *Barnard) connect(reconnect bool) bool {
b.setConnected(true) b.setConnected(true)
// Dial delivers OnConnect before connect creates the OpenAL stream, so // Dial delivers OnConnect before connect creates the OpenAL stream, so
// start auto-transmit here as well for initial connections and reconnects. // start auto-transmit here as well for initial connections and reconnects.
b.startAutoTransmit() b.startConfiguredTransmission()
return true return true
} }
@@ -195,11 +195,15 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) {
b.AddOutputLine(fmt.Sprintf("Welcome message: %s", wmsg)) b.AddOutputLine(fmt.Sprintf("Welcome message: %s", wmsg))
} }
b.startAutoTransmit() b.startConfiguredTransmission()
} }
func (b *Barnard) startAutoTransmit() { func (b *Barnard) shouldStartTransmission() bool {
if !b.AutoTransmit || b.isTransmitting() { return b.AutoTransmit || (b.UserConfig.GetRememberTransmissionState() && b.UserConfig.GetTransmissionActive())
}
func (b *Barnard) startConfiguredTransmission() {
if !b.shouldStartTransmission() || b.isTransmitting() {
return return
} }
started := b.withStream(func(stream *gumbleopenal.Stream) { started := b.withStream(func(stream *gumbleopenal.Stream) {
@@ -208,8 +212,13 @@ func (b *Barnard) startAutoTransmit() {
return return
} }
b.setTransmitting(true) b.setTransmitting(true)
b.UpdateGeneralStatus(" AutoTx ", true) if b.AutoTransmit {
b.AddOutputLine("Auto-transmit started") b.UpdateGeneralStatus(" AutoTx ", true)
b.AddOutputLine("Auto-transmit started")
} else {
b.UpdateGeneralStatus(" Tx ", true)
b.AddOutputLine("Transmission state restored")
}
}) })
if !started { if !started {
return return
@@ -249,7 +258,7 @@ func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) {
} else { } else {
b.AddOutputLine("Disconnected: " + reason) b.AddOutputLine("Disconnected: " + reason)
} }
b.setTransmitting(false) b.clearTransmittingForDisconnect()
b.setConnected(false) b.setConnected(false)
b.postUI(func() { b.postUI(func() {
b.UiTree.Rebuild() b.UiTree.Rebuild()
+31
View File
@@ -14,6 +14,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
"git.stormux.org/storm/barnard/config"
) )
const ( const (
@@ -444,6 +446,32 @@ func (app *App) remove_server() error {
return app.ui.message("Removed server " + name) return app.ui.message("Removed server " + name)
} }
func settings_menu_options(rememberTransmissionState bool) []string {
state := "Off"
if rememberTransmissionState {
state = "On"
}
return []string{
"Remember transmission state: " + state,
"Go Back",
}
}
func (app *App) manage_settings() error {
cfg := config.NewConfig(&app.paths.BarnardTOML)
for {
options := settings_menu_options(cfg.GetRememberTransmissionState())
selection, cancelled, err := app.ui.menu(options)
if err != nil || cancelled || selection == len(options)-1 {
return err
}
enabled := !cfg.GetRememberTransmissionState()
if err := cfg.SetRememberTransmissionState(enabled); err != nil {
return app.ui.message("Could not save settings: " + err.Error())
}
}
}
func config_has_value(path, wantedKey string) bool { func config_has_value(path, wantedKey string) bool {
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
@@ -600,6 +628,8 @@ func (app *App) run() error {
err = app.manage_certificate() err = app.manage_certificate()
case "Logs": case "Logs":
err = app.manage_logs() err = app.manage_logs()
case "Settings":
err = app.manage_settings()
} }
if err != nil { if err != nil {
return err return err
@@ -614,6 +644,7 @@ func main_menu_options() []string {
"Remove server", "Remove server",
"Manage Certificate", "Manage Certificate",
"Logs", "Logs",
"Settings",
"Exit", "Exit",
} }
} }
+12
View File
@@ -209,4 +209,16 @@ func TestProgramIdentityAndMainMenuUseBarnardUI(t *testing.T) {
t.Fatalf("legacy go-ui name remains in main menu: %q", option) t.Fatalf("legacy go-ui name remains in main menu: %q", option)
} }
} }
if !slices.Contains(options, "Settings") {
t.Fatalf("main menu does not contain Settings: %v", options)
}
}
func TestSettingsMenuReportsRememberTransmissionState(t *testing.T) {
if got := settings_menu_options(false)[0]; got != "Remember transmission state: Off" {
t.Fatalf("disabled setting label = %q", got)
}
if got := settings_menu_options(true)[0]; got != "Remember transmission state: On" {
t.Fatalf("enabled setting label = %q", got)
}
} }
+60 -14
View File
@@ -21,20 +21,22 @@ type Config struct {
} }
type exportableConfig struct { type exportableConfig struct {
Hotkeys *Hotkeys Hotkeys *Hotkeys
AudioDriver *string AudioDriver *string
MicVolume *float32 MicVolume *float32
InputDevice *string InputDevice *string
OutputDevice *string OutputDevice *string
Servers []*server Servers []*server
DefaultServer *string DefaultServer *string
Username *string Username *string
NotifyCommand *string NotifyCommand *string
NoiseSuppressionEnabled *bool NoiseSuppressionEnabled *bool
AGCEnabled *bool AGCEnabled *bool
Certificate *string RememberTransmissionState *bool
RecordingFormat *string TransmissionActive *bool
RecordingDirectory *string Certificate *string
RecordingFormat *string
RecordingDirectory *string
} }
type server struct { type server struct {
@@ -144,6 +146,14 @@ func (c *Config) LoadConfig() {
enabled := true enabled := true
jc.AGCEnabled = &enabled jc.AGCEnabled = &enabled
} }
if c.config.RememberTransmissionState == nil {
enabled := false
jc.RememberTransmissionState = &enabled
}
if c.config.TransmissionActive == nil {
active := false
jc.TransmissionActive = &active
}
if c.config.Certificate == nil { if c.config.Certificate == nil {
cert := string("") cert := string("")
jc.Certificate = &cert jc.Certificate = &cert
@@ -425,6 +435,42 @@ func (c *Config) SetAGCEnabled(enabled bool) error {
return c.saveConfigLocked() return c.saveConfigLocked()
} }
func (c *Config) GetRememberTransmissionState() bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.config.RememberTransmissionState == nil {
return false
}
return *c.config.RememberTransmissionState
}
func (c *Config) SetRememberTransmissionState(enabled bool) error {
c.mu.Lock()
defer c.mu.Unlock()
c.config.RememberTransmissionState = &enabled
if !enabled {
active := false
c.config.TransmissionActive = &active
}
return c.saveConfigLocked()
}
func (c *Config) GetTransmissionActive() bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.config.TransmissionActive == nil {
return false
}
return *c.config.TransmissionActive
}
func (c *Config) SetTransmissionActive(active bool) error {
c.mu.Lock()
defer c.mu.Unlock()
c.config.TransmissionActive = &active
return c.saveConfigLocked()
}
func (c *Config) GetRecordingFormat() string { func (c *Config) GetRecordingFormat() string {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
+39
View File
@@ -3,6 +3,7 @@ package config
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"git.stormux.org/storm/barnard/gumble/gumble" "git.stormux.org/storm/barnard/gumble/gumble"
@@ -179,6 +180,44 @@ func TestAGCDefaultsOnAndPersists(t *testing.T) {
} }
} }
func TestRememberTransmissionStateDefaultsOffAndPersists(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "barnard.toml")
cfg := NewConfig(&configPath)
if cfg.GetRememberTransmissionState() {
t.Fatal("expected transmission-state remembering to default off")
}
if cfg.GetTransmissionActive() {
t.Fatal("expected remembered transmission state to default off")
}
if err := cfg.SetRememberTransmissionState(true); err != nil {
t.Fatal(err)
}
if err := cfg.SetTransmissionActive(true); err != nil {
t.Fatal(err)
}
saved, err := os.ReadFile(configPath)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(saved), "RememberTransmissionState = true") ||
!strings.Contains(string(saved), "TransmissionActive = true") {
t.Fatalf("saved TOML does not contain the transmission settings:\n%s", saved)
}
reloaded := NewConfig(&configPath)
if !reloaded.GetRememberTransmissionState() || !reloaded.GetTransmissionActive() {
t.Fatal("expected enabled transmission-state remembering and active state to persist")
}
if err := reloaded.SetRememberTransmissionState(false); err != nil {
t.Fatal(err)
}
reloaded = NewConfig(&configPath)
if reloaded.GetRememberTransmissionState() || reloaded.GetTransmissionActive() {
t.Fatal("expected disabling transmission-state remembering to clear the saved state")
}
}
// Regression: malformed and IPv6 addresses were split at every colon and // Regression: malformed and IPv6 addresses were split at every colon and
// could panic while merely reading a saved user preference. // could panic while merely reading a saved user preference.
func TestMakeHostPortHandlesIPv6AndMalformedAddress(t *testing.T) { func TestMakeHostPortHandlesIPv6AndMalformedAddress(t *testing.T) {
+43
View File
@@ -0,0 +1,43 @@
package main
import (
"path/filepath"
"testing"
"git.stormux.org/storm/barnard/config"
)
func TestDisconnectClearPreservesRememberedTransmissionState(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "barnard.toml")
userConfig := config.NewConfig(&configPath)
if err := userConfig.SetRememberTransmissionState(true); err != nil {
t.Fatal(err)
}
b := &Barnard{UserConfig: userConfig}
b.setTransmitting(true)
b.clearTransmittingForDisconnect()
if b.isTransmitting() {
t.Fatal("disconnect did not clear live transmission state")
}
if !userConfig.GetTransmissionActive() {
t.Fatal("disconnect cleared the remembered transmission state")
}
if !b.shouldStartTransmission() {
t.Fatal("remembered active transmission was not selected for restoration")
}
}
func TestRememberedTransmissionStateIsIgnoredWhenDisabled(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "barnard.toml")
userConfig := config.NewConfig(&configPath)
if err := userConfig.SetTransmissionActive(true); err != nil {
t.Fatal(err)
}
b := &Barnard{UserConfig: userConfig}
if b.shouldStartTransmission() {
t.Fatal("disabled transmission-state remembering selected transmission for restoration")
}
}