From 8929edfe59c81b6c20c4c59ce2a013584d2efc27 Mon Sep 17 00:00:00 2001 From: Gero Hillebrandt Date: Tue, 23 Dec 2025 19:33:25 +0100 Subject: [PATCH] feat: add configurable notification messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow users to customize notification messages via config.toml. Messages fall back to defaults when not configured, preserving existing behavior. Supports custom text or emoji-only notifications. Configuration example: [notifications.messages.recording_started] title = "" body = "🎤" --- internal/config/config.go | 98 ++++++++++++++++++++++- internal/config/config_test.go | 140 +++++++++++++++++++++++++++++++++ internal/daemon/daemon.go | 23 +++--- 3 files changed, 250 insertions(+), 11 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index b65b36b..4aabd5d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -45,8 +45,71 @@ type InjectionConfig struct { } type NotificationsConfig struct { - Enabled bool `toml:"enabled"` - Type string `toml:"type"` // "desktop", "log", "none" + Enabled bool `toml:"enabled"` + Type string `toml:"type"` // "desktop", "log", "none" + Messages MessagesConfig `toml:"messages"` +} + +type MessageConfig struct { + Title string `toml:"title"` + Body string `toml:"body"` +} + +type MessagesConfig struct { + RecordingStarted MessageConfig `toml:"recording_started"` + Transcribing MessageConfig `toml:"transcribing"` + ConfigReloaded MessageConfig `toml:"config_reloaded"` + OperationCancelled MessageConfig `toml:"operation_cancelled"` + RecordingAborted MessageConfig `toml:"recording_aborted"` + InjectionAborted MessageConfig `toml:"injection_aborted"` +} + +func (c *Config) GetRecordingStarted() (title, body string) { + m := c.Notifications.Messages.RecordingStarted + if m.Title == "" && m.Body == "" { + return "Hyprvoice", "Recording Started" + } + return m.Title, m.Body +} + +func (c *Config) GetTranscribing() (title, body string) { + m := c.Notifications.Messages.Transcribing + if m.Title == "" && m.Body == "" { + return "Hyprvoice", "Recording Ended... Transcribing" + } + return m.Title, m.Body +} + +func (c *Config) GetConfigReloaded() (title, body string) { + m := c.Notifications.Messages.ConfigReloaded + if m.Title == "" && m.Body == "" { + return "Hyprvoice", "Config Reloaded" + } + return m.Title, m.Body +} + +func (c *Config) GetOperationCancelled() (title, body string) { + m := c.Notifications.Messages.OperationCancelled + if m.Title == "" && m.Body == "" { + return "Hyprvoice", "Operation Cancelled" + } + return m.Title, m.Body +} + +func (c *Config) GetRecordingAborted() string { + m := c.Notifications.Messages.RecordingAborted + if m.Body == "" { + return "Recording Aborted" + } + return m.Body +} + +func (c *Config) GetInjectionAborted() string { + m := c.Notifications.Messages.InjectionAborted + if m.Body == "" { + return "Injection Aborted" + } + return m.Body } func (c *Config) ToRecordingConfig() recording.Config { @@ -358,6 +421,37 @@ func SaveDefaultConfig() error { enabled = true # Enable desktop notifications type = "desktop" # Notification type ("desktop", "log", "none") + # Custom notification messages (optional - defaults shown below) + # Uncomment and modify to customize notification text + # [notifications.messages] + # [notifications.messages.recording_started] + # title = "Hyprvoice" + # body = "Recording Started" + # [notifications.messages.transcribing] + # title = "Hyprvoice" + # body = "Recording Ended... Transcribing" + # [notifications.messages.config_reloaded] + # title = "Hyprvoice" + # body = "Config Reloaded" + # [notifications.messages.operation_cancelled] + # title = "Hyprvoice" + # body = "Operation Cancelled" + # [notifications.messages.recording_aborted] + # body = "Recording Aborted" + # [notifications.messages.injection_aborted] + # body = "Injection Aborted" + # + # Emoji-only example (for minimal pill-style notifications): + # [notifications.messages.recording_started] + # title = "" + # body = "🎤" + # [notifications.messages.transcribing] + # title = "" + # body = "⏳" + # [notifications.messages.config_reloaded] + # title = "" + # body = "🔧" + # Backend explanations: # - "ydotool": Uses ydotool (requires ydotoold daemon running). Most compatible with Chromium/Electron apps. # - "wtype": Uses wtype for Wayland. May have issues with some Chromium-based apps. diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 3a0fa78..bf0cffa 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1228,3 +1228,143 @@ func TestConfig_Validate_GroqTranslation_RejectsTurbo(t *testing.T) { t.Errorf("Unexpected error message: %v", err) } } + +func TestConfig_MessageGetters_Defaults(t *testing.T) { + config := createTestConfig() + + t.Run("GetRecordingStarted returns defaults", func(t *testing.T) { + title, body := config.GetRecordingStarted() + if title != "Hyprvoice" { + t.Errorf("GetRecordingStarted() title = %q, want %q", title, "Hyprvoice") + } + if body != "Recording Started" { + t.Errorf("GetRecordingStarted() body = %q, want %q", body, "Recording Started") + } + }) + + t.Run("GetTranscribing returns defaults", func(t *testing.T) { + title, body := config.GetTranscribing() + if title != "Hyprvoice" { + t.Errorf("GetTranscribing() title = %q, want %q", title, "Hyprvoice") + } + if body != "Recording Ended... Transcribing" { + t.Errorf("GetTranscribing() body = %q, want %q", body, "Recording Ended... Transcribing") + } + }) + + t.Run("GetConfigReloaded returns defaults", func(t *testing.T) { + title, body := config.GetConfigReloaded() + if title != "Hyprvoice" { + t.Errorf("GetConfigReloaded() title = %q, want %q", title, "Hyprvoice") + } + if body != "Config Reloaded" { + t.Errorf("GetConfigReloaded() body = %q, want %q", body, "Config Reloaded") + } + }) + + t.Run("GetOperationCancelled returns defaults", func(t *testing.T) { + title, body := config.GetOperationCancelled() + if title != "Hyprvoice" { + t.Errorf("GetOperationCancelled() title = %q, want %q", title, "Hyprvoice") + } + if body != "Operation Cancelled" { + t.Errorf("GetOperationCancelled() body = %q, want %q", body, "Operation Cancelled") + } + }) + + t.Run("GetRecordingAborted returns default", func(t *testing.T) { + body := config.GetRecordingAborted() + if body != "Recording Aborted" { + t.Errorf("GetRecordingAborted() = %q, want %q", body, "Recording Aborted") + } + }) + + t.Run("GetInjectionAborted returns default", func(t *testing.T) { + body := config.GetInjectionAborted() + if body != "Injection Aborted" { + t.Errorf("GetInjectionAborted() = %q, want %q", body, "Injection Aborted") + } + }) +} + +func TestConfig_MessageGetters_Custom(t *testing.T) { + config := createTestConfig() + config.Notifications.Messages = MessagesConfig{ + RecordingStarted: MessageConfig{ + Title: "", + Body: "🎤", + }, + Transcribing: MessageConfig{ + Title: "", + Body: "⏳", + }, + ConfigReloaded: MessageConfig{ + Title: "", + Body: "🔧", + }, + OperationCancelled: MessageConfig{ + Title: "Custom", + Body: "Cancelled!", + }, + RecordingAborted: MessageConfig{ + Body: "Recording stopped", + }, + InjectionAborted: MessageConfig{ + Body: "Inject failed", + }, + } + + t.Run("GetRecordingStarted returns custom emoji", func(t *testing.T) { + title, body := config.GetRecordingStarted() + if title != "" { + t.Errorf("GetRecordingStarted() title = %q, want %q", title, "") + } + if body != "🎤" { + t.Errorf("GetRecordingStarted() body = %q, want %q", body, "🎤") + } + }) + + t.Run("GetTranscribing returns custom emoji", func(t *testing.T) { + title, body := config.GetTranscribing() + if title != "" { + t.Errorf("GetTranscribing() title = %q, want %q", title, "") + } + if body != "⏳" { + t.Errorf("GetTranscribing() body = %q, want %q", body, "⏳") + } + }) + + t.Run("GetConfigReloaded returns custom emoji", func(t *testing.T) { + title, body := config.GetConfigReloaded() + if title != "" { + t.Errorf("GetConfigReloaded() title = %q, want %q", title, "") + } + if body != "🔧" { + t.Errorf("GetConfigReloaded() body = %q, want %q", body, "🔧") + } + }) + + t.Run("GetOperationCancelled returns custom values", func(t *testing.T) { + title, body := config.GetOperationCancelled() + if title != "Custom" { + t.Errorf("GetOperationCancelled() title = %q, want %q", title, "Custom") + } + if body != "Cancelled!" { + t.Errorf("GetOperationCancelled() body = %q, want %q", body, "Cancelled!") + } + }) + + t.Run("GetRecordingAborted returns custom value", func(t *testing.T) { + body := config.GetRecordingAborted() + if body != "Recording stopped" { + t.Errorf("GetRecordingAborted() = %q, want %q", body, "Recording stopped") + } + }) + + t.Run("GetInjectionAborted returns custom value", func(t *testing.T) { + body := config.GetInjectionAborted() + if body != "Inject failed" { + t.Errorf("GetInjectionAborted() = %q, want %q", body, "Inject failed") + } + }) +} diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 4de2036..516d15a 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -57,10 +57,12 @@ func (d *Daemon) onConfigReload() { log.Printf("Config reloaded, restarting pipeline") d.stopPipeline() - d.notifier.Notify("Hyprvoice", "Config Reloaded") + conf := d.configMgr.GetConfig() + title, body := conf.GetConfigReloaded() + d.notifier.Notify(title, body) d.mu.Lock() - d.notifier = notify.GetNotifierBasedOnConfig(d.configMgr.GetConfig()) + d.notifier = notify.GetNotifierBasedOnConfig(conf) d.mu.Unlock() } @@ -180,22 +182,23 @@ func (d *Daemon) handle(c net.Conn) { } func (d *Daemon) toggle() { + conf := d.configMgr.GetConfig() switch d.status() { case pipeline.Idle: - config := d.configMgr.GetConfig() - p := pipeline.New(config) + p := pipeline.New(conf) p.Run(d.ctx) d.mu.Lock() d.pipeline = p d.mu.Unlock() - go d.notifier.Notify("Hyprvoice", "Recording Started") + title, body := conf.GetRecordingStarted() + go d.notifier.Notify(title, body) go d.monitorPipelineErrors(p) case pipeline.Recording: d.stopPipeline() - go d.notifier.Error("Recording Aborted") + go d.notifier.Error(conf.GetRecordingAborted()) case pipeline.Transcribing: d.mu.RLock() @@ -207,11 +210,12 @@ func (d *Daemon) toggle() { } else { d.mu.RUnlock() } - go d.notifier.Notify("Hyprvoice", "Recording Ended... Transcribing") + title, body := conf.GetTranscribing() + go d.notifier.Notify(title, body) case pipeline.Injecting: d.stopPipeline() - go d.notifier.Error("Injection Aborted") + go d.notifier.Error(conf.GetInjectionAborted()) } } @@ -221,7 +225,8 @@ func (d *Daemon) cancelPipeline() { log.Printf("Daemon: Cancel requested but pipeline is idle, ignoring") default: d.stopPipeline() - go d.notifier.Notify("Hyprvoice", "Operation Cancelled") + title, body := d.configMgr.GetConfig().GetOperationCancelled() + go d.notifier.Notify(title, body) } }