internal refactor of notifications

This commit is contained in:
leonardotrapani
2026-01-02 23:29:33 +01:00
parent 3a8695edb4
commit 6a3781567b
7 changed files with 261 additions and 393 deletions
+13 -17
View File
@@ -12,6 +12,7 @@ import (
"github.com/leonardotrapani/hyprvoice/internal/bus" "github.com/leonardotrapani/hyprvoice/internal/bus"
"github.com/leonardotrapani/hyprvoice/internal/config" "github.com/leonardotrapani/hyprvoice/internal/config"
"github.com/leonardotrapani/hyprvoice/internal/daemon" "github.com/leonardotrapani/hyprvoice/internal/daemon"
"github.com/leonardotrapani/hyprvoice/internal/notify"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -361,23 +362,18 @@ func runInteractiveConfig() error {
input := strings.TrimSpace(strings.ToLower(scanner.Text())) input := strings.TrimSpace(strings.ToLower(scanner.Text()))
if input == "y" || input == "yes" { if input == "y" || input == "yes" {
fmt.Println() fmt.Println()
// Get current/default values for display // Get resolved values (user config merged with defaults)
recTitle, recBody := cfg.GetRecordingStarted() msgs := cfg.Notifications.Messages.Resolve()
transTitle, transBody := cfg.GetTranscribing()
reloadTitle, reloadBody := cfg.GetConfigReloaded()
cancelTitle, cancelBody := cfg.GetOperationCancelled()
abortRecBody := cfg.GetRecordingAborted()
abortInjBody := cfg.GetInjectionAborted()
// Recording Started // Recording Started
fmt.Println(" Recording Started notification:") fmt.Println(" Recording Started notification:")
fmt.Printf(" Title (current: %s): ", recTitle) fmt.Printf(" Title (current: %s): ", msgs[notify.MsgRecordingStarted].Title)
if scanner.Scan() { if scanner.Scan() {
if t := strings.TrimSpace(scanner.Text()); t != "" { if t := strings.TrimSpace(scanner.Text()); t != "" {
cfg.Notifications.Messages.RecordingStarted.Title = 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 scanner.Scan() {
if b := strings.TrimSpace(scanner.Text()); b != "" { if b := strings.TrimSpace(scanner.Text()); b != "" {
cfg.Notifications.Messages.RecordingStarted.Body = b cfg.Notifications.Messages.RecordingStarted.Body = b
@@ -387,13 +383,13 @@ func runInteractiveConfig() error {
// Transcribing // Transcribing
fmt.Println(" Transcribing notification:") fmt.Println(" Transcribing notification:")
fmt.Printf(" Title (current: %s): ", transTitle) fmt.Printf(" Title (current: %s): ", msgs[notify.MsgTranscribing].Title)
if scanner.Scan() { if scanner.Scan() {
if t := strings.TrimSpace(scanner.Text()); t != "" { if t := strings.TrimSpace(scanner.Text()); t != "" {
cfg.Notifications.Messages.Transcribing.Title = 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 scanner.Scan() {
if b := strings.TrimSpace(scanner.Text()); b != "" { if b := strings.TrimSpace(scanner.Text()); b != "" {
cfg.Notifications.Messages.Transcribing.Body = b cfg.Notifications.Messages.Transcribing.Body = b
@@ -403,13 +399,13 @@ func runInteractiveConfig() error {
// Config Reloaded // Config Reloaded
fmt.Println(" Config Reloaded notification:") fmt.Println(" Config Reloaded notification:")
fmt.Printf(" Title (current: %s): ", reloadTitle) fmt.Printf(" Title (current: %s): ", msgs[notify.MsgConfigReloaded].Title)
if scanner.Scan() { if scanner.Scan() {
if t := strings.TrimSpace(scanner.Text()); t != "" { if t := strings.TrimSpace(scanner.Text()); t != "" {
cfg.Notifications.Messages.ConfigReloaded.Title = 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 scanner.Scan() {
if b := strings.TrimSpace(scanner.Text()); b != "" { if b := strings.TrimSpace(scanner.Text()); b != "" {
cfg.Notifications.Messages.ConfigReloaded.Body = b cfg.Notifications.Messages.ConfigReloaded.Body = b
@@ -419,13 +415,13 @@ func runInteractiveConfig() error {
// Operation Cancelled // Operation Cancelled
fmt.Println(" Operation Cancelled notification:") fmt.Println(" Operation Cancelled notification:")
fmt.Printf(" Title (current: %s): ", cancelTitle) fmt.Printf(" Title (current: %s): ", msgs[notify.MsgOperationCancelled].Title)
if scanner.Scan() { if scanner.Scan() {
if t := strings.TrimSpace(scanner.Text()); t != "" { if t := strings.TrimSpace(scanner.Text()); t != "" {
cfg.Notifications.Messages.OperationCancelled.Title = 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 scanner.Scan() {
if b := strings.TrimSpace(scanner.Text()); b != "" { if b := strings.TrimSpace(scanner.Text()); b != "" {
cfg.Notifications.Messages.OperationCancelled.Body = b cfg.Notifications.Messages.OperationCancelled.Body = b
@@ -435,7 +431,7 @@ func runInteractiveConfig() error {
// Recording Aborted (body only) // Recording Aborted (body only)
fmt.Println(" Recording Aborted notification:") fmt.Println(" Recording Aborted notification:")
fmt.Printf(" Body (current: %s): ", abortRecBody) fmt.Printf(" Body (current: %s): ", msgs[notify.MsgRecordingAborted].Body)
if scanner.Scan() { if scanner.Scan() {
if b := strings.TrimSpace(scanner.Text()); b != "" { if b := strings.TrimSpace(scanner.Text()); b != "" {
cfg.Notifications.Messages.RecordingAborted.Body = b cfg.Notifications.Messages.RecordingAborted.Body = b
@@ -445,7 +441,7 @@ func runInteractiveConfig() error {
// Injection Aborted (body only) // Injection Aborted (body only)
fmt.Println(" Injection Aborted notification:") fmt.Println(" Injection Aborted notification:")
fmt.Printf(" Body (current: %s): ", abortInjBody) fmt.Printf(" Body (current: %s): ", msgs[notify.MsgInjectionAborted].Body)
if scanner.Scan() { if scanner.Scan() {
if b := strings.TrimSpace(scanner.Text()); b != "" { if b := strings.TrimSpace(scanner.Text()); b != "" {
cfg.Notifications.Messages.InjectionAborted.Body = b cfg.Notifications.Messages.InjectionAborted.Body = b
+25 -39
View File
@@ -5,10 +5,12 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"time" "time"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/leonardotrapani/hyprvoice/internal/injection" "github.com/leonardotrapani/hyprvoice/internal/injection"
"github.com/leonardotrapani/hyprvoice/internal/notify"
"github.com/leonardotrapani/hyprvoice/internal/recording" "github.com/leonardotrapani/hyprvoice/internal/recording"
"github.com/leonardotrapani/hyprvoice/internal/transcriber" "github.com/leonardotrapani/hyprvoice/internal/transcriber"
) )
@@ -64,52 +66,36 @@ type MessagesConfig struct {
InjectionAborted MessageConfig `toml:"injection_aborted"` InjectionAborted MessageConfig `toml:"injection_aborted"`
} }
func (c *Config) GetRecordingStarted() (title, body string) { // Resolve merges user config with defaults from MessageDefs
m := c.Notifications.Messages.RecordingStarted func (m *MessagesConfig) Resolve() map[notify.MessageType]notify.Message {
if m.Title == "" && m.Body == "" { result := make(map[notify.MessageType]notify.Message)
return "Hyprvoice", "Recording Started"
} // Build toml tag → field index map
return m.Title, m.Body 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
} }
func (c *Config) GetTranscribing() (title, body string) { for _, def := range notify.MessageDefs {
m := c.Notifications.Messages.Transcribing msg := notify.Message{
if m.Title == "" && m.Body == "" { Title: def.DefaultTitle,
return "Hyprvoice", "Recording Ended... Transcribing" Body: def.DefaultBody,
IsError: def.IsError,
} }
return m.Title, m.Body if idx, ok := tagToField[def.ConfigKey]; ok {
userMsg := v.Field(idx).Interface().(MessageConfig)
if userMsg.Title != "" {
msg.Title = userMsg.Title
} }
if userMsg.Body != "" {
func (c *Config) GetConfigReloaded() (title, body string) { msg.Body = userMsg.Body
m := c.Notifications.Messages.ConfigReloaded
if m.Title == "" && m.Body == "" {
return "Hyprvoice", "Config Reloaded"
} }
return m.Title, m.Body
} }
result[def.Type] = msg
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 return result
}
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 { func (c *Config) ToRecordingConfig() recording.Config {
+32 -120
View File
@@ -5,6 +5,8 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
"github.com/leonardotrapani/hyprvoice/internal/notify"
) )
// createTestConfig returns a valid configuration for testing // 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) { func TestMessagesConfig_Resolve_Defaults(t *testing.T) {
config := createTestConfig() cfg := createTestConfig()
msgs := cfg.Notifications.Messages.Resolve()
t.Run("GetRecordingStarted returns defaults", func(t *testing.T) { // Check defaults are applied
title, body := config.GetRecordingStarted() if msgs[notify.MsgRecordingStarted].Title != "Hyprvoice" {
if title != "Hyprvoice" { t.Errorf("MsgRecordingStarted title = %q, want %q", msgs[notify.MsgRecordingStarted].Title, "Hyprvoice")
t.Errorf("GetRecordingStarted() title = %q, want %q", title, "Hyprvoice")
} }
if body != "Recording Started" { if msgs[notify.MsgRecordingStarted].Body != "Recording Started" {
t.Errorf("GetRecordingStarted() body = %q, want %q", 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")
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" { if msgs[notify.MsgRecordingAborted].IsError != true {
t.Errorf("GetTranscribing() body = %q, want %q", body, "Recording Ended... Transcribing") t.Errorf("MsgRecordingAborted IsError = %v, want true", msgs[notify.MsgRecordingAborted].IsError)
} }
})
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) { func TestMessagesConfig_Resolve_CustomOverrides(t *testing.T) {
config := createTestConfig() cfg := createTestConfig()
config.Notifications.Messages = MessagesConfig{ cfg.Notifications.Messages = MessagesConfig{
RecordingStarted: MessageConfig{ RecordingStarted: MessageConfig{
Title: "", Title: "Custom Title",
Body: "🎤", Body: "Custom Body",
},
Transcribing: MessageConfig{
Title: "",
Body: "⏳",
},
ConfigReloaded: MessageConfig{
Title: "",
Body: "🔧",
},
OperationCancelled: MessageConfig{
Title: "Custom",
Body: "Cancelled!",
}, },
RecordingAborted: MessageConfig{ RecordingAborted: MessageConfig{
Body: "Recording stopped", Body: "Custom Abort",
},
InjectionAborted: MessageConfig{
Body: "Inject failed",
}, },
} }
t.Run("GetRecordingStarted returns custom emoji", func(t *testing.T) { msgs := cfg.Notifications.Messages.Resolve()
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) { // Custom values should override defaults
title, body := config.GetTranscribing() if msgs[notify.MsgRecordingStarted].Title != "Custom Title" {
if title != "" { t.Errorf("MsgRecordingStarted title = %q, want %q", msgs[notify.MsgRecordingStarted].Title, "Custom Title")
t.Errorf("GetTranscribing() title = %q, want %q", title, "")
} }
if body != "" { if msgs[notify.MsgRecordingStarted].Body != "Custom Body" {
t.Errorf("GetTranscribing() body = %q, want %q", 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) { // Non-customized messages should still have defaults
title, body := config.GetConfigReloaded() if msgs[notify.MsgTranscribing].Title != "Hyprvoice" {
if title != "" { t.Errorf("MsgTranscribing title = %q, want %q", msgs[notify.MsgTranscribing].Title, "Hyprvoice")
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")
}
})
} }
+10 -17
View File
@@ -32,19 +32,15 @@ type Daemon struct {
func New() (*Daemon, error) { func New() (*Daemon, error) {
configMgr, err := config.NewManager() configMgr, err := config.NewManager()
conf := configMgr.GetConfig()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create config manager: %w", err) return nil, fmt.Errorf("failed to create config manager: %w", err)
} }
conf := configMgr.GetConfig()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
n := notify.GetNotifierBasedOnConfig(conf)
d := &Daemon{ d := &Daemon{
notifier: n, notifier: notify.NewNotifier(conf.Notifications.Type, conf.Notifications.Messages.Resolve()),
configMgr: configMgr, configMgr: configMgr,
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
@@ -58,12 +54,12 @@ func (d *Daemon) onConfigReload() {
d.stopPipeline() d.stopPipeline()
conf := d.configMgr.GetConfig() conf := d.configMgr.GetConfig()
title, body := conf.GetConfigReloaded()
d.notifier.Notify(title, body)
d.mu.Lock() d.mu.Lock()
d.notifier = notify.GetNotifierBasedOnConfig(conf) d.notifier = notify.NewNotifier(conf.Notifications.Type, conf.Notifications.Messages.Resolve())
d.mu.Unlock() d.mu.Unlock()
d.notifier.Send(notify.MsgConfigReloaded)
} }
func (d *Daemon) status() pipeline.Status { func (d *Daemon) status() pipeline.Status {
@@ -192,13 +188,12 @@ func (d *Daemon) toggle() {
d.pipeline = p d.pipeline = p
d.mu.Unlock() d.mu.Unlock()
title, body := conf.GetRecordingStarted() go d.notifier.Send(notify.MsgRecordingStarted)
go d.notifier.Notify(title, body)
go d.monitorPipelineErrors(p) go d.monitorPipelineErrors(p)
case pipeline.Recording: case pipeline.Recording:
d.stopPipeline() d.stopPipeline()
go d.notifier.Error(conf.GetRecordingAborted()) go d.notifier.Send(notify.MsgRecordingAborted)
case pipeline.Transcribing: case pipeline.Transcribing:
d.mu.RLock() d.mu.RLock()
@@ -210,12 +205,11 @@ func (d *Daemon) toggle() {
} else { } else {
d.mu.RUnlock() d.mu.RUnlock()
} }
title, body := conf.GetTranscribing() go d.notifier.Send(notify.MsgTranscribing)
go d.notifier.Notify(title, body)
case pipeline.Injecting: case pipeline.Injecting:
d.stopPipeline() 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") log.Printf("Daemon: Cancel requested but pipeline is idle, ignoring")
default: default:
d.stopPipeline() d.stopPipeline()
title, body := d.configMgr.GetConfig().GetOperationCancelled() go d.notifier.Send(notify.MsgOperationCancelled)
go d.notifier.Notify(title, body)
} }
} }
+39
View File
@@ -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
}
+54 -31
View File
@@ -1,63 +1,86 @@
package notify package notify
import ( import (
"github.com/leonardotrapani/hyprvoice/internal/config"
"log" "log"
"os/exec" "os/exec"
) )
type Notifier interface { type Notifier interface {
Error(msg string) Send(mt MessageType)
Notify(title, message string) Error(msg string) // for dynamic errors (e.g., pipeline errors)
} }
type Desktop struct{} // NewNotifier creates a notifier based on type with resolved messages
func NewNotifier(notifType string, messages map[MessageType]Message) Notifier {
func (d Desktop) RecordingStarted() { switch notifType {
d.Notify("Hyprvoice", "Recording Started") case "desktop":
return NewDesktop(messages)
case "log":
return NewLog(messages)
default:
return &Nop{}
}
} }
func (d Desktop) Transcribing() { type Desktop struct {
d.Notify("Hyprvoice", "Transcribing...") 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) cmd := exec.Command("notify-send", "-a", "Hyprvoice", "-u", "critical", "Hyprvoice Error", msg)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
log.Printf("Failed to send error notification: %v", err) log.Printf("Failed to send error notification: %v", err)
} }
} }
func (Desktop) Notify(title, message string) { func (d *Desktop) notify(title, body string) {
cmd := exec.Command("notify-send", "-a", "Hyprvoice", title, message) cmd := exec.Command("notify-send", "-a", "Hyprvoice", title, body)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
log.Printf("Failed to send notification: %v", err) log.Printf("Failed to send notification: %v", err)
} }
} }
type Log struct{} type Log struct {
messages map[MessageType]Message
func (l Log) Error(msg string) {
l.Notify("Hyprvoice Error", msg)
} }
func (Log) Notify(title, message string) { func NewLog(messages map[MessageType]Message) *Log {
log.Printf("%s: %s", title, message) 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{} type Nop struct{}
func (Nop) Send(mt MessageType) {}
func (Nop) Error(msg string) {} 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{}
}
+76 -157
View File
@@ -2,202 +2,121 @@ package notify
import ( import (
"testing" "testing"
"github.com/leonardotrapani/hyprvoice/internal/config"
) )
func TestDesktop_Notify(t *testing.T) { func testMessages() map[MessageType]Message {
desktop := Desktop{} return map[MessageType]Message{
MsgRecordingStarted: {Title: "Hyprvoice", Body: "Recording Started", IsError: false},
// Test normal notification MsgTranscribing: {Title: "Hyprvoice", Body: "Transcribing", IsError: false},
desktop.Notify("Test Title", "Test Message") MsgConfigReloaded: {Title: "Hyprvoice", Body: "Config Reloaded", IsError: false},
MsgOperationCancelled: {Title: "Hyprvoice", Body: "Operation Cancelled", IsError: false},
// Test error notification MsgRecordingAborted: {Title: "", Body: "Recording Aborted", IsError: true},
desktop.Error("Test Error Message") MsgInjectionAborted: {Title: "", Body: "Injection Aborted", IsError: true},
}
// Test specific methods
desktop.RecordingStarted()
desktop.Transcribing()
} }
func TestLog_Notify(t *testing.T) { func TestDesktop_Send(t *testing.T) {
logNotifier := Log{} desktop := NewDesktop(testMessages())
// Test normal notification // Test Send for different message types (won't actually send, just verify no panic)
logNotifier.Notify("Test Title", "Test Message") 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") logNotifier.Error("Test Error Message")
} }
func TestNop_Notify(t *testing.T) { func TestNop_Send(t *testing.T) {
nop := Nop{} nop := Nop{}
nop.Send(MsgRecordingStarted)
nop.Send(MsgRecordingAborted)
}
// Test that these methods don't panic func TestNop_Error(t *testing.T) {
nop.Notify("Test Title", "Test Message") nop := Nop{}
nop.Error("Test Error Message") nop.Error("Test Error Message")
} }
func TestGetNotifierBasedOnConfig(t *testing.T) { func TestNewNotifier(t *testing.T) {
msgs := testMessages()
tests := []struct { tests := []struct {
name string name string
config *config.Config notifType string
expected string expectType string
}{ }{
{ {"desktop", "desktop", "*notify.Desktop"},
name: "desktop notification type", {"log", "log", "*notify.Log"},
config: &config.Config{ {"none", "none", "*notify.Nop"},
Notifications: config.NotificationsConfig{ {"unknown", "unknown", "*notify.Nop"},
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",
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
notifier := GetNotifierBasedOnConfig(tt.config) notifier := NewNotifier(tt.notifType, msgs)
// Test the notifier by calling its methods // Test the notifier works
notifier.Notify("Test", "Message") notifier.Send(MsgRecordingStarted)
notifier.Error("Error") 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) { func TestNotifierInterface(t *testing.T) {
msgs := testMessages()
// Test that all notifiers implement the Notifier interface // Test that all notifiers implement the Notifier interface
var notifier Notifier var notifier Notifier
// Test Desktop notifier = NewDesktop(msgs)
notifier = Desktop{} notifier.Send(MsgRecordingStarted)
notifier.Notify("Test", "Message")
notifier.Error("Error") notifier.Error("Error")
// Test Log notifier = NewLog(msgs)
notifier = Log{} notifier.Send(MsgRecordingStarted)
notifier.Notify("Test", "Message")
notifier.Error("Error") notifier.Error("Error")
// Test Nop notifier = &Nop{}
notifier = Nop{} notifier.Send(MsgRecordingStarted)
notifier.Notify("Test", "Message")
notifier.Error("Error") notifier.Error("Error")
} }
func TestNotificationTypes(t *testing.T) { func TestMessageDefs(t *testing.T) {
// Test different notification configurations // Verify MessageDefs contains expected entries
configs := []*config.Config{ if len(MessageDefs) != 6 {
{ t.Errorf("Expected 6 MessageDefs, got %d", len(MessageDefs))
Notifications: config.NotificationsConfig{
Type: "desktop",
},
},
{
Notifications: config.NotificationsConfig{
Type: "log",
},
},
{
Notifications: config.NotificationsConfig{
Type: "none",
},
},
} }
for _, cfg := range configs { // Verify each has required fields
t.Run("type_"+cfg.Notifications.Type, func(t *testing.T) { for _, def := range MessageDefs {
notifier := GetNotifierBasedOnConfig(cfg) 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)
}
}
}
// Test that the notifier works func TestSend_UnknownMessageType(t *testing.T) {
notifier.Notify("Test Title", "Test Message") msgs := testMessages()
notifier.Error("Test Error") desktop := NewDesktop(msgs)
})
} // Should not panic with unknown message type
desktop.Send(MessageType(999))
} }