From 01ca0da7ffeb4782d58cf105dd5e5866f1e4a6bf Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 6 Sep 2026 18:24:52 -0400 Subject: [PATCH] Optionally remember transmission state on reconnect. --- README.md | 12 ++++++ barnard.go | 14 +++++++ client.go | 25 +++++++++---- cmd/barnard-ui/main.go | 31 ++++++++++++++++ cmd/barnard-ui/main_test.go | 12 ++++++ config/user_config.go | 74 ++++++++++++++++++++++++++++++------- config/user_config_test.go | 39 +++++++++++++++++++ transmission_state_test.go | 43 +++++++++++++++++++++ 8 files changed, 228 insertions(+), 22 deletions(-) create mode 100644 transmission_state_test.go diff --git a/README.md b/README.md index 0264d0e..f2dc202 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,18 @@ Barnard normalizes the level of your outgoing microphone audio with automatic ga 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 If you pass the --fifo option to Barnard, a FIFO pipe will be created. diff --git a/barnard.go b/barnard.go index 9499b7f..66bdbd7 100644 --- a/barnard.go +++ b/barnard.go @@ -204,6 +204,20 @@ func (b *Barnard) setTransmitting(transmitting bool) { b.stateMutex.Lock() b.Tx = transmitting 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 { diff --git a/client.go b/client.go index 15c6559..b1ad5ec 100644 --- a/client.go +++ b/client.go @@ -81,7 +81,7 @@ func (b *Barnard) connect(reconnect bool) bool { b.toneTestSaverDetach = b.Client.Config.AttachAudio(b.toneTestSaver) b.setConnected(true) - if b.toneTestAutoTransmit() { + if b.shouldStartTransmission() { b.toneTestStop = make(chan struct{}) go StartToneGenerator(b.Client, b.toneTestStop) b.setTransmitting(true) @@ -149,7 +149,7 @@ func (b *Barnard) connect(reconnect bool) bool { b.setConnected(true) // Dial delivers OnConnect before connect creates the OpenAL stream, so // start auto-transmit here as well for initial connections and reconnects. - b.startAutoTransmit() + b.startConfiguredTransmission() return true } @@ -195,11 +195,15 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) { b.AddOutputLine(fmt.Sprintf("Welcome message: %s", wmsg)) } - b.startAutoTransmit() + b.startConfiguredTransmission() } -func (b *Barnard) startAutoTransmit() { - if !b.AutoTransmit || b.isTransmitting() { +func (b *Barnard) shouldStartTransmission() bool { + return b.AutoTransmit || (b.UserConfig.GetRememberTransmissionState() && b.UserConfig.GetTransmissionActive()) +} + +func (b *Barnard) startConfiguredTransmission() { + if !b.shouldStartTransmission() || b.isTransmitting() { return } started := b.withStream(func(stream *gumbleopenal.Stream) { @@ -208,8 +212,13 @@ func (b *Barnard) startAutoTransmit() { return } b.setTransmitting(true) - b.UpdateGeneralStatus(" AutoTx ", true) - b.AddOutputLine("Auto-transmit started") + if b.AutoTransmit { + b.UpdateGeneralStatus(" AutoTx ", true) + b.AddOutputLine("Auto-transmit started") + } else { + b.UpdateGeneralStatus(" Tx ", true) + b.AddOutputLine("Transmission state restored") + } }) if !started { return @@ -249,7 +258,7 @@ func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) { } else { b.AddOutputLine("Disconnected: " + reason) } - b.setTransmitting(false) + b.clearTransmittingForDisconnect() b.setConnected(false) b.postUI(func() { b.UiTree.Rebuild() diff --git a/cmd/barnard-ui/main.go b/cmd/barnard-ui/main.go index 2ee5437..2f4c977 100644 --- a/cmd/barnard-ui/main.go +++ b/cmd/barnard-ui/main.go @@ -14,6 +14,8 @@ import ( "strconv" "strings" "syscall" + + "git.stormux.org/storm/barnard/config" ) const ( @@ -444,6 +446,32 @@ func (app *App) remove_server() error { 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 { file, err := os.Open(path) if err != nil { @@ -600,6 +628,8 @@ func (app *App) run() error { err = app.manage_certificate() case "Logs": err = app.manage_logs() + case "Settings": + err = app.manage_settings() } if err != nil { return err @@ -614,6 +644,7 @@ func main_menu_options() []string { "Remove server", "Manage Certificate", "Logs", + "Settings", "Exit", } } diff --git a/cmd/barnard-ui/main_test.go b/cmd/barnard-ui/main_test.go index c82b351..cfee58d 100644 --- a/cmd/barnard-ui/main_test.go +++ b/cmd/barnard-ui/main_test.go @@ -209,4 +209,16 @@ func TestProgramIdentityAndMainMenuUseBarnardUI(t *testing.T) { 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) + } } diff --git a/config/user_config.go b/config/user_config.go index 546d654..f377685 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -21,20 +21,22 @@ type Config struct { } type exportableConfig struct { - Hotkeys *Hotkeys - AudioDriver *string - MicVolume *float32 - InputDevice *string - OutputDevice *string - Servers []*server - DefaultServer *string - Username *string - NotifyCommand *string - NoiseSuppressionEnabled *bool - AGCEnabled *bool - Certificate *string - RecordingFormat *string - RecordingDirectory *string + Hotkeys *Hotkeys + AudioDriver *string + MicVolume *float32 + InputDevice *string + OutputDevice *string + Servers []*server + DefaultServer *string + Username *string + NotifyCommand *string + NoiseSuppressionEnabled *bool + AGCEnabled *bool + RememberTransmissionState *bool + TransmissionActive *bool + Certificate *string + RecordingFormat *string + RecordingDirectory *string } type server struct { @@ -144,6 +146,14 @@ func (c *Config) LoadConfig() { enabled := true 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 { cert := string("") jc.Certificate = &cert @@ -425,6 +435,42 @@ func (c *Config) SetAGCEnabled(enabled bool) error { 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 { c.mu.Lock() defer c.mu.Unlock() diff --git a/config/user_config_test.go b/config/user_config_test.go index 9e0ed2b..8135403 100644 --- a/config/user_config_test.go +++ b/config/user_config_test.go @@ -3,6 +3,7 @@ package config import ( "os" "path/filepath" + "strings" "testing" "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 // could panic while merely reading a saved user preference. func TestMakeHostPortHandlesIPv6AndMalformedAddress(t *testing.T) { diff --git a/transmission_state_test.go b/transmission_state_test.go new file mode 100644 index 0000000..cca1f96 --- /dev/null +++ b/transmission_state_test.go @@ -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") + } +}