internal refactor of notifications
This commit is contained in:
+13
-17
@@ -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
|
||||||
|
|||||||
+28
-42
@@ -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"
|
|
||||||
}
|
|
||||||
return m.Title, m.Body
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) GetTranscribing() (title, body string) {
|
// Build toml tag → field index map
|
||||||
m := c.Notifications.Messages.Transcribing
|
v := reflect.ValueOf(m).Elem()
|
||||||
if m.Title == "" && m.Body == "" {
|
t := v.Type()
|
||||||
return "Hyprvoice", "Recording Ended... Transcribing"
|
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) {
|
for _, def := range notify.MessageDefs {
|
||||||
m := c.Notifications.Messages.ConfigReloaded
|
msg := notify.Message{
|
||||||
if m.Title == "" && m.Body == "" {
|
Title: def.DefaultTitle,
|
||||||
return "Hyprvoice", "Config Reloaded"
|
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
|
return result
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
func (c *Config) ToRecordingConfig() recording.Config {
|
||||||
|
|||||||
+39
-127
@@ -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 msgs[notify.MsgRecordingStarted].Body != "Recording Started" {
|
||||||
if body != "Recording Started" {
|
t.Errorf("MsgRecordingStarted body = %q, want %q", msgs[notify.MsgRecordingStarted].Body, "Recording Started")
|
||||||
t.Errorf("GetRecordingStarted() body = %q, want %q", 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) {
|
if msgs[notify.MsgRecordingAborted].IsError != true {
|
||||||
title, body := config.GetTranscribing()
|
t.Errorf("MsgRecordingAborted IsError = %v, want true", msgs[notify.MsgRecordingAborted].IsError)
|
||||||
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) {
|
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 msgs[notify.MsgRecordingStarted].Body != "Custom Body" {
|
||||||
if body != "⏳" {
|
t.Errorf("MsgRecordingStarted body = %q, want %q", msgs[notify.MsgRecordingStarted].Body, "Custom Body")
|
||||||
t.Errorf("GetTranscribing() body = %q, want %q", 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
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+55
-32
@@ -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) Error(msg string) {}
|
func (Nop) Send(mt MessageType) {}
|
||||||
func (Nop) Notify(title, message string) {}
|
func (Nop) Error(msg 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{}
|
|
||||||
}
|
|
||||||
|
|||||||
+77
-158
@@ -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)
|
||||||
// Test that the notifier works
|
}
|
||||||
notifier.Notify("Test Title", "Test Message")
|
if def.DefaultBody == "" {
|
||||||
notifier.Error("Test Error")
|
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))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user