diff --git a/cmd/hyprvoice/main.go b/cmd/hyprvoice/main.go index bc09e1a..15b1c75 100644 --- a/cmd/hyprvoice/main.go +++ b/cmd/hyprvoice/main.go @@ -12,6 +12,7 @@ import ( "github.com/leonardotrapani/hyprvoice/internal/bus" "github.com/leonardotrapani/hyprvoice/internal/config" "github.com/leonardotrapani/hyprvoice/internal/daemon" + "github.com/leonardotrapani/hyprvoice/internal/notify" "github.com/spf13/cobra" ) @@ -361,23 +362,18 @@ func runInteractiveConfig() error { input := strings.TrimSpace(strings.ToLower(scanner.Text())) if input == "y" || input == "yes" { fmt.Println() - // Get current/default values for display - recTitle, recBody := cfg.GetRecordingStarted() - transTitle, transBody := cfg.GetTranscribing() - reloadTitle, reloadBody := cfg.GetConfigReloaded() - cancelTitle, cancelBody := cfg.GetOperationCancelled() - abortRecBody := cfg.GetRecordingAborted() - abortInjBody := cfg.GetInjectionAborted() + // Get resolved values (user config merged with defaults) + msgs := cfg.Notifications.Messages.Resolve() // Recording Started fmt.Println(" Recording Started notification:") - fmt.Printf(" Title (current: %s): ", recTitle) + fmt.Printf(" Title (current: %s): ", msgs[notify.MsgRecordingStarted].Title) if scanner.Scan() { if t := strings.TrimSpace(scanner.Text()); t != "" { cfg.Notifications.Messages.RecordingStarted.Title = t } } - fmt.Printf(" Body (current: %s): ", recBody) + fmt.Printf(" Body (current: %s): ", msgs[notify.MsgRecordingStarted].Body) if scanner.Scan() { if b := strings.TrimSpace(scanner.Text()); b != "" { cfg.Notifications.Messages.RecordingStarted.Body = b @@ -387,13 +383,13 @@ func runInteractiveConfig() error { // Transcribing fmt.Println(" Transcribing notification:") - fmt.Printf(" Title (current: %s): ", transTitle) + fmt.Printf(" Title (current: %s): ", msgs[notify.MsgTranscribing].Title) if scanner.Scan() { if t := strings.TrimSpace(scanner.Text()); t != "" { cfg.Notifications.Messages.Transcribing.Title = t } } - fmt.Printf(" Body (current: %s): ", transBody) + fmt.Printf(" Body (current: %s): ", msgs[notify.MsgTranscribing].Body) if scanner.Scan() { if b := strings.TrimSpace(scanner.Text()); b != "" { cfg.Notifications.Messages.Transcribing.Body = b @@ -403,13 +399,13 @@ func runInteractiveConfig() error { // Config Reloaded fmt.Println(" Config Reloaded notification:") - fmt.Printf(" Title (current: %s): ", reloadTitle) + fmt.Printf(" Title (current: %s): ", msgs[notify.MsgConfigReloaded].Title) if scanner.Scan() { if t := strings.TrimSpace(scanner.Text()); t != "" { cfg.Notifications.Messages.ConfigReloaded.Title = t } } - fmt.Printf(" Body (current: %s): ", reloadBody) + fmt.Printf(" Body (current: %s): ", msgs[notify.MsgConfigReloaded].Body) if scanner.Scan() { if b := strings.TrimSpace(scanner.Text()); b != "" { cfg.Notifications.Messages.ConfigReloaded.Body = b @@ -419,13 +415,13 @@ func runInteractiveConfig() error { // Operation Cancelled fmt.Println(" Operation Cancelled notification:") - fmt.Printf(" Title (current: %s): ", cancelTitle) + fmt.Printf(" Title (current: %s): ", msgs[notify.MsgOperationCancelled].Title) if scanner.Scan() { if t := strings.TrimSpace(scanner.Text()); t != "" { cfg.Notifications.Messages.OperationCancelled.Title = t } } - fmt.Printf(" Body (current: %s): ", cancelBody) + fmt.Printf(" Body (current: %s): ", msgs[notify.MsgOperationCancelled].Body) if scanner.Scan() { if b := strings.TrimSpace(scanner.Text()); b != "" { cfg.Notifications.Messages.OperationCancelled.Body = b @@ -435,7 +431,7 @@ func runInteractiveConfig() error { // Recording Aborted (body only) fmt.Println(" Recording Aborted notification:") - fmt.Printf(" Body (current: %s): ", abortRecBody) + fmt.Printf(" Body (current: %s): ", msgs[notify.MsgRecordingAborted].Body) if scanner.Scan() { if b := strings.TrimSpace(scanner.Text()); b != "" { cfg.Notifications.Messages.RecordingAborted.Body = b @@ -445,7 +441,7 @@ func runInteractiveConfig() error { // Injection Aborted (body only) fmt.Println(" Injection Aborted notification:") - fmt.Printf(" Body (current: %s): ", abortInjBody) + fmt.Printf(" Body (current: %s): ", msgs[notify.MsgInjectionAborted].Body) if scanner.Scan() { if b := strings.TrimSpace(scanner.Text()); b != "" { cfg.Notifications.Messages.InjectionAborted.Body = b diff --git a/internal/config/config.go b/internal/config/config.go index 4aabd5d..44775e8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,10 +5,12 @@ import ( "log" "os" "path/filepath" + "reflect" "time" "github.com/BurntSushi/toml" "github.com/leonardotrapani/hyprvoice/internal/injection" + "github.com/leonardotrapani/hyprvoice/internal/notify" "github.com/leonardotrapani/hyprvoice/internal/recording" "github.com/leonardotrapani/hyprvoice/internal/transcriber" ) @@ -64,52 +66,36 @@ type MessagesConfig struct { 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 -} +// Resolve merges user config with defaults from MessageDefs +func (m *MessagesConfig) Resolve() map[notify.MessageType]notify.Message { + result := make(map[notify.MessageType]notify.Message) -func (c *Config) GetTranscribing() (title, body string) { - m := c.Notifications.Messages.Transcribing - if m.Title == "" && m.Body == "" { - return "Hyprvoice", "Recording Ended... Transcribing" + // Build toml tag → field index map + v := reflect.ValueOf(m).Elem() + t := v.Type() + tagToField := make(map[string]int) + for i := 0; i < t.NumField(); i++ { + tagToField[t.Field(i).Tag.Get("toml")] = i } - 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" + for _, def := range notify.MessageDefs { + msg := notify.Message{ + Title: def.DefaultTitle, + Body: def.DefaultBody, + IsError: def.IsError, + } + if idx, ok := tagToField[def.ConfigKey]; ok { + userMsg := v.Field(idx).Interface().(MessageConfig) + if userMsg.Title != "" { + msg.Title = userMsg.Title + } + if userMsg.Body != "" { + msg.Body = userMsg.Body + } + } + result[def.Type] = msg } - 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 + return result } func (c *Config) ToRecordingConfig() recording.Config { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index bf0cffa..97a2edb 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -5,6 +5,8 @@ import ( "path/filepath" "testing" "time" + + "github.com/leonardotrapani/hyprvoice/internal/notify" ) // createTestConfig returns a valid configuration for testing @@ -1229,142 +1231,52 @@ func TestConfig_Validate_GroqTranslation_RejectsTurbo(t *testing.T) { } } -func TestConfig_MessageGetters_Defaults(t *testing.T) { - config := createTestConfig() +func TestMessagesConfig_Resolve_Defaults(t *testing.T) { + cfg := createTestConfig() + msgs := cfg.Notifications.Messages.Resolve() - 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") - } - }) + // Check defaults are applied + if msgs[notify.MsgRecordingStarted].Title != "Hyprvoice" { + t.Errorf("MsgRecordingStarted title = %q, want %q", msgs[notify.MsgRecordingStarted].Title, "Hyprvoice") + } + if msgs[notify.MsgRecordingStarted].Body != "Recording Started" { + t.Errorf("MsgRecordingStarted body = %q, want %q", msgs[notify.MsgRecordingStarted].Body, "Recording Started") + } + if msgs[notify.MsgTranscribing].Body != "Recording Ended... Transcribing" { + t.Errorf("MsgTranscribing body = %q, want %q", msgs[notify.MsgTranscribing].Body, "Recording Ended... Transcribing") + } + if msgs[notify.MsgRecordingAborted].IsError != true { + t.Errorf("MsgRecordingAborted IsError = %v, want true", msgs[notify.MsgRecordingAborted].IsError) + } } -func TestConfig_MessageGetters_Custom(t *testing.T) { - config := createTestConfig() - config.Notifications.Messages = MessagesConfig{ +func TestMessagesConfig_Resolve_CustomOverrides(t *testing.T) { + cfg := createTestConfig() + cfg.Notifications.Messages = MessagesConfig{ RecordingStarted: MessageConfig{ - Title: "", - Body: "🎤", - }, - Transcribing: MessageConfig{ - Title: "", - Body: "⏳", - }, - ConfigReloaded: MessageConfig{ - Title: "", - Body: "🔧", - }, - OperationCancelled: MessageConfig{ - Title: "Custom", - Body: "Cancelled!", + Title: "Custom Title", + Body: "Custom Body", }, RecordingAborted: MessageConfig{ - Body: "Recording stopped", - }, - InjectionAborted: MessageConfig{ - Body: "Inject failed", + Body: "Custom Abort", }, } - 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, "🎤") - } - }) + msgs := cfg.Notifications.Messages.Resolve() - 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, "⏳") - } - }) + // Custom values should override defaults + if msgs[notify.MsgRecordingStarted].Title != "Custom Title" { + t.Errorf("MsgRecordingStarted title = %q, want %q", msgs[notify.MsgRecordingStarted].Title, "Custom Title") + } + if msgs[notify.MsgRecordingStarted].Body != "Custom Body" { + t.Errorf("MsgRecordingStarted body = %q, want %q", msgs[notify.MsgRecordingStarted].Body, "Custom Body") + } + if msgs[notify.MsgRecordingAborted].Body != "Custom Abort" { + t.Errorf("MsgRecordingAborted body = %q, want %q", msgs[notify.MsgRecordingAborted].Body, "Custom Abort") + } - 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") - } - }) + // Non-customized messages should still have defaults + if msgs[notify.MsgTranscribing].Title != "Hyprvoice" { + t.Errorf("MsgTranscribing title = %q, want %q", msgs[notify.MsgTranscribing].Title, "Hyprvoice") + } } diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 516d15a..37ddf35 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -32,19 +32,15 @@ type Daemon struct { func New() (*Daemon, error) { configMgr, err := config.NewManager() - - conf := configMgr.GetConfig() - if err != nil { return nil, fmt.Errorf("failed to create config manager: %w", err) } + conf := configMgr.GetConfig() ctx, cancel := context.WithCancel(context.Background()) - n := notify.GetNotifierBasedOnConfig(conf) - d := &Daemon{ - notifier: n, + notifier: notify.NewNotifier(conf.Notifications.Type, conf.Notifications.Messages.Resolve()), configMgr: configMgr, ctx: ctx, cancel: cancel, @@ -58,12 +54,12 @@ func (d *Daemon) onConfigReload() { d.stopPipeline() conf := d.configMgr.GetConfig() - title, body := conf.GetConfigReloaded() - d.notifier.Notify(title, body) d.mu.Lock() - d.notifier = notify.GetNotifierBasedOnConfig(conf) + d.notifier = notify.NewNotifier(conf.Notifications.Type, conf.Notifications.Messages.Resolve()) d.mu.Unlock() + + d.notifier.Send(notify.MsgConfigReloaded) } func (d *Daemon) status() pipeline.Status { @@ -192,13 +188,12 @@ func (d *Daemon) toggle() { d.pipeline = p d.mu.Unlock() - title, body := conf.GetRecordingStarted() - go d.notifier.Notify(title, body) + go d.notifier.Send(notify.MsgRecordingStarted) go d.monitorPipelineErrors(p) case pipeline.Recording: d.stopPipeline() - go d.notifier.Error(conf.GetRecordingAborted()) + go d.notifier.Send(notify.MsgRecordingAborted) case pipeline.Transcribing: d.mu.RLock() @@ -210,12 +205,11 @@ func (d *Daemon) toggle() { } else { d.mu.RUnlock() } - title, body := conf.GetTranscribing() - go d.notifier.Notify(title, body) + go d.notifier.Send(notify.MsgTranscribing) case pipeline.Injecting: d.stopPipeline() - go d.notifier.Error(conf.GetInjectionAborted()) + go d.notifier.Send(notify.MsgInjectionAborted) } } @@ -225,8 +219,7 @@ func (d *Daemon) cancelPipeline() { log.Printf("Daemon: Cancel requested but pipeline is idle, ignoring") default: d.stopPipeline() - title, body := d.configMgr.GetConfig().GetOperationCancelled() - go d.notifier.Notify(title, body) + go d.notifier.Send(notify.MsgOperationCancelled) } } diff --git a/internal/notify/message.go b/internal/notify/message.go new file mode 100644 index 0000000..a46caf9 --- /dev/null +++ b/internal/notify/message.go @@ -0,0 +1,39 @@ +package notify + +// MessageType identifies a notification event +type MessageType int + +const ( + MsgRecordingStarted MessageType = iota + MsgTranscribing + MsgConfigReloaded + MsgOperationCancelled + MsgRecordingAborted + MsgInjectionAborted +) + +// MessageDef defines a message type with its config key and defaults +type MessageDef struct { + Type MessageType + ConfigKey string // TOML key under [notifications.messages] + DefaultTitle string + DefaultBody string + IsError bool // error notifications use critical urgency, no custom title +} + +// MessageDefs is the single source of truth for all notification messages +var MessageDefs = []MessageDef{ + {MsgRecordingStarted, "recording_started", "Hyprvoice", "Recording Started", false}, + {MsgTranscribing, "transcribing", "Hyprvoice", "Recording Ended... Transcribing", false}, + {MsgConfigReloaded, "config_reloaded", "Hyprvoice", "Config Reloaded", false}, + {MsgOperationCancelled, "operation_cancelled", "Hyprvoice", "Operation Cancelled", false}, + {MsgRecordingAborted, "recording_aborted", "", "Recording Aborted", true}, + {MsgInjectionAborted, "injection_aborted", "", "Injection Aborted", true}, +} + +// Message is a resolved message ready for display +type Message struct { + Title string + Body string + IsError bool +} diff --git a/internal/notify/notify.go b/internal/notify/notify.go index 4b1a3c5..617d3b0 100644 --- a/internal/notify/notify.go +++ b/internal/notify/notify.go @@ -1,63 +1,86 @@ package notify import ( - "github.com/leonardotrapani/hyprvoice/internal/config" "log" "os/exec" ) type Notifier interface { - Error(msg string) - Notify(title, message string) + Send(mt MessageType) + Error(msg string) // for dynamic errors (e.g., pipeline errors) } -type Desktop struct{} - -func (d Desktop) RecordingStarted() { - d.Notify("Hyprvoice", "Recording Started") +// NewNotifier creates a notifier based on type with resolved messages +func NewNotifier(notifType string, messages map[MessageType]Message) Notifier { + switch notifType { + case "desktop": + return NewDesktop(messages) + case "log": + return NewLog(messages) + default: + return &Nop{} + } } -func (d Desktop) Transcribing() { - d.Notify("Hyprvoice", "Transcribing...") +type Desktop struct { + messages map[MessageType]Message } -func (Desktop) Error(msg string) { +func NewDesktop(messages map[MessageType]Message) *Desktop { + return &Desktop{messages: messages} +} + +func (d *Desktop) Send(mt MessageType) { + msg, ok := d.messages[mt] + if !ok { + return + } + if msg.IsError { + d.Error(msg.Body) + return + } + d.notify(msg.Title, msg.Body) +} + +func (d *Desktop) Error(msg string) { cmd := exec.Command("notify-send", "-a", "Hyprvoice", "-u", "critical", "Hyprvoice Error", msg) if err := cmd.Run(); err != nil { log.Printf("Failed to send error notification: %v", err) } } -func (Desktop) Notify(title, message string) { - cmd := exec.Command("notify-send", "-a", "Hyprvoice", title, message) +func (d *Desktop) notify(title, body string) { + cmd := exec.Command("notify-send", "-a", "Hyprvoice", title, body) if err := cmd.Run(); err != nil { log.Printf("Failed to send notification: %v", err) } } -type Log struct{} - -func (l Log) Error(msg string) { - l.Notify("Hyprvoice Error", msg) +type Log struct { + messages map[MessageType]Message } -func (Log) Notify(title, message string) { - log.Printf("%s: %s", title, message) +func NewLog(messages map[MessageType]Message) *Log { + return &Log{messages: messages} +} + +func (l *Log) Send(mt MessageType) { + msg, ok := l.messages[mt] + if !ok { + return + } + if msg.IsError { + l.Error(msg.Body) + return + } + log.Printf("%s: %s", msg.Title, msg.Body) +} + +func (l *Log) Error(msg string) { + log.Printf("Hyprvoice Error: %s", msg) } type Nop struct{} -func (Nop) Error(msg string) {} -func (Nop) Notify(title, message string) {} - -func GetNotifierBasedOnConfig(c *config.Config) Notifier { - switch c.Notifications.Type { - case "desktop": - return Desktop{} - case "log": - return Log{} - case "none": - return Nop{} - } - return Nop{} -} +func (Nop) Send(mt MessageType) {} +func (Nop) Error(msg string) {} diff --git a/internal/notify/notify_test.go b/internal/notify/notify_test.go index 47ada05..ac3f5ff 100644 --- a/internal/notify/notify_test.go +++ b/internal/notify/notify_test.go @@ -2,202 +2,121 @@ package notify import ( "testing" - - "github.com/leonardotrapani/hyprvoice/internal/config" ) -func TestDesktop_Notify(t *testing.T) { - desktop := Desktop{} - - // Test normal notification - desktop.Notify("Test Title", "Test Message") - - // Test error notification - desktop.Error("Test Error Message") - - // Test specific methods - desktop.RecordingStarted() - desktop.Transcribing() +func testMessages() map[MessageType]Message { + return map[MessageType]Message{ + MsgRecordingStarted: {Title: "Hyprvoice", Body: "Recording Started", IsError: false}, + MsgTranscribing: {Title: "Hyprvoice", Body: "Transcribing", IsError: false}, + MsgConfigReloaded: {Title: "Hyprvoice", Body: "Config Reloaded", IsError: false}, + MsgOperationCancelled: {Title: "Hyprvoice", Body: "Operation Cancelled", IsError: false}, + MsgRecordingAborted: {Title: "", Body: "Recording Aborted", IsError: true}, + MsgInjectionAborted: {Title: "", Body: "Injection Aborted", IsError: true}, + } } -func TestLog_Notify(t *testing.T) { - logNotifier := Log{} +func TestDesktop_Send(t *testing.T) { + desktop := NewDesktop(testMessages()) - // Test normal notification - logNotifier.Notify("Test Title", "Test Message") + // Test Send for different message types (won't actually send, just verify no panic) + desktop.Send(MsgRecordingStarted) + desktop.Send(MsgTranscribing) + desktop.Send(MsgRecordingAborted) // error type +} - // Test error notification +func TestDesktop_Error(t *testing.T) { + desktop := NewDesktop(testMessages()) + desktop.Error("Test Error Message") +} + +func TestLog_Send(t *testing.T) { + logNotifier := NewLog(testMessages()) + + logNotifier.Send(MsgRecordingStarted) + logNotifier.Send(MsgRecordingAborted) // error type +} + +func TestLog_Error(t *testing.T) { + logNotifier := NewLog(testMessages()) logNotifier.Error("Test Error Message") } -func TestNop_Notify(t *testing.T) { +func TestNop_Send(t *testing.T) { nop := Nop{} + nop.Send(MsgRecordingStarted) + nop.Send(MsgRecordingAborted) +} - // Test that these methods don't panic - nop.Notify("Test Title", "Test Message") +func TestNop_Error(t *testing.T) { + nop := Nop{} nop.Error("Test Error Message") } -func TestGetNotifierBasedOnConfig(t *testing.T) { +func TestNewNotifier(t *testing.T) { + msgs := testMessages() + tests := []struct { - name string - config *config.Config - expected string + name string + notifType string + expectType string }{ - { - name: "desktop notification type", - config: &config.Config{ - Notifications: config.NotificationsConfig{ - Type: "desktop", - }, - }, - expected: "desktop", - }, - { - name: "log notification type", - config: &config.Config{ - Notifications: config.NotificationsConfig{ - Type: "log", - }, - }, - expected: "log", - }, - { - name: "none notification type", - config: &config.Config{ - Notifications: config.NotificationsConfig{ - Type: "none", - }, - }, - expected: "nop", - }, - { - name: "unknown notification type", - config: &config.Config{ - Notifications: config.NotificationsConfig{ - Type: "unknown", - }, - }, - expected: "nop", - }, + {"desktop", "desktop", "*notify.Desktop"}, + {"log", "log", "*notify.Log"}, + {"none", "none", "*notify.Nop"}, + {"unknown", "unknown", "*notify.Nop"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - notifier := GetNotifierBasedOnConfig(tt.config) + notifier := NewNotifier(tt.notifType, msgs) - // Test the notifier by calling its methods - notifier.Notify("Test", "Message") + // Test the notifier works + notifier.Send(MsgRecordingStarted) notifier.Error("Error") - - // Check the type by testing behavior - switch tt.expected { - case "desktop": - // Desktop notifier should not panic - if desktop, ok := notifier.(Desktop); ok { - desktop.RecordingStarted() - desktop.Transcribing() - } - case "log": - // Log notifier should not panic - if logNotifier, ok := notifier.(Log); ok { - logNotifier.Notify("Test", "Message") - logNotifier.Error("Error") - } - case "nop": - // Nop notifier should not panic - if nop, ok := notifier.(Nop); ok { - nop.Notify("Test", "Message") - nop.Error("Error") - } - } }) } } -func TestDesktop_Methods(t *testing.T) { - desktop := Desktop{} - - // Test RecordingStarted method - desktop.RecordingStarted() - - // Test Transcribing method - desktop.Transcribing() - - // Test Error method - desktop.Error("Test error") - - // Test Notify method - desktop.Notify("Test Title", "Test Message") -} - -func TestLog_Methods(t *testing.T) { - logNotifier := Log{} - - // Test Error method - logNotifier.Error("Test error") - - // Test Notify method - logNotifier.Notify("Test Title", "Test Message") -} - -func TestNop_Methods(t *testing.T) { - nop := Nop{} - - // Test Error method - nop.Error("Test error") - - // Test Notify method - nop.Notify("Test Title", "Test Message") -} - func TestNotifierInterface(t *testing.T) { + msgs := testMessages() + // Test that all notifiers implement the Notifier interface var notifier Notifier - // Test Desktop - notifier = Desktop{} - notifier.Notify("Test", "Message") + notifier = NewDesktop(msgs) + notifier.Send(MsgRecordingStarted) notifier.Error("Error") - // Test Log - notifier = Log{} - notifier.Notify("Test", "Message") + notifier = NewLog(msgs) + notifier.Send(MsgRecordingStarted) notifier.Error("Error") - // Test Nop - notifier = Nop{} - notifier.Notify("Test", "Message") + notifier = &Nop{} + notifier.Send(MsgRecordingStarted) notifier.Error("Error") } -func TestNotificationTypes(t *testing.T) { - // Test different notification configurations - configs := []*config.Config{ - { - Notifications: config.NotificationsConfig{ - Type: "desktop", - }, - }, - { - Notifications: config.NotificationsConfig{ - Type: "log", - }, - }, - { - Notifications: config.NotificationsConfig{ - Type: "none", - }, - }, +func TestMessageDefs(t *testing.T) { + // Verify MessageDefs contains expected entries + if len(MessageDefs) != 6 { + t.Errorf("Expected 6 MessageDefs, got %d", len(MessageDefs)) } - for _, cfg := range configs { - t.Run("type_"+cfg.Notifications.Type, func(t *testing.T) { - notifier := GetNotifierBasedOnConfig(cfg) - - // Test that the notifier works - notifier.Notify("Test Title", "Test Message") - notifier.Error("Test Error") - }) + // Verify each has required fields + for _, def := range MessageDefs { + if def.ConfigKey == "" { + t.Errorf("MessageDef type %d has empty ConfigKey", def.Type) + } + if def.DefaultBody == "" { + t.Errorf("MessageDef type %d has empty DefaultBody", def.Type) + } } } + +func TestSend_UnknownMessageType(t *testing.T) { + msgs := testMessages() + desktop := NewDesktop(msgs) + + // Should not panic with unknown message type + desktop.Send(MessageType(999)) +}