Merge pull request #5 from ghillb/feat/configurable-notifications
feat: add configurable notification messages
This commit is contained in:
@@ -424,6 +424,30 @@ type = "desktop" # "desktop", "log", or "none"
|
|||||||
|
|
||||||
Always keep `type = "desktop"` unless debugging.
|
Always keep `type = "desktop"` unless debugging.
|
||||||
|
|
||||||
|
##### Custom Notification Messages
|
||||||
|
|
||||||
|
You can customize notification text via the `[notifications.messages]` section.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[notifications.messages]
|
||||||
|
[notifications.messages.recording_started]
|
||||||
|
title = "Hyprvoice"
|
||||||
|
body = "Recording Started"
|
||||||
|
[notifications.messages.transcribing]
|
||||||
|
title = "Hyprvoice"
|
||||||
|
body = "Recording Ended... Transcribing"
|
||||||
|
[notifications.messages.config_reloaded]
|
||||||
|
title = "Hyprvoice"
|
||||||
|
body = "Config Reloaded"
|
||||||
|
[notifications.messages.operation_cancelled]
|
||||||
|
title = "Hyprvoice"
|
||||||
|
body = "Operation Cancelled"
|
||||||
|
[notifications.messages.recording_aborted]
|
||||||
|
body = "Recording Aborted"
|
||||||
|
[notifications.messages.injection_aborted]
|
||||||
|
body = "Injection Aborted"
|
||||||
|
```
|
||||||
|
|
||||||
### Configuration Hot-Reloading
|
### Configuration Hot-Reloading
|
||||||
|
|
||||||
The daemon automatically watches the config file for changes and applies them immediately:
|
The daemon automatically watches the config file for changes and applies them immediately:
|
||||||
|
|||||||
@@ -45,8 +45,71 @@ type InjectionConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NotificationsConfig struct {
|
type NotificationsConfig struct {
|
||||||
Enabled bool `toml:"enabled"`
|
Enabled bool `toml:"enabled"`
|
||||||
Type string `toml:"type"` // "desktop", "log", "none"
|
Type string `toml:"type"` // "desktop", "log", "none"
|
||||||
|
Messages MessagesConfig `toml:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageConfig struct {
|
||||||
|
Title string `toml:"title"`
|
||||||
|
Body string `toml:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessagesConfig struct {
|
||||||
|
RecordingStarted MessageConfig `toml:"recording_started"`
|
||||||
|
Transcribing MessageConfig `toml:"transcribing"`
|
||||||
|
ConfigReloaded MessageConfig `toml:"config_reloaded"`
|
||||||
|
OperationCancelled MessageConfig `toml:"operation_cancelled"`
|
||||||
|
RecordingAborted MessageConfig `toml:"recording_aborted"`
|
||||||
|
InjectionAborted MessageConfig `toml:"injection_aborted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetRecordingStarted() (title, body string) {
|
||||||
|
m := c.Notifications.Messages.RecordingStarted
|
||||||
|
if m.Title == "" && m.Body == "" {
|
||||||
|
return "Hyprvoice", "Recording Started"
|
||||||
|
}
|
||||||
|
return m.Title, m.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetTranscribing() (title, body string) {
|
||||||
|
m := c.Notifications.Messages.Transcribing
|
||||||
|
if m.Title == "" && m.Body == "" {
|
||||||
|
return "Hyprvoice", "Recording Ended... Transcribing"
|
||||||
|
}
|
||||||
|
return m.Title, m.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetConfigReloaded() (title, body string) {
|
||||||
|
m := c.Notifications.Messages.ConfigReloaded
|
||||||
|
if m.Title == "" && m.Body == "" {
|
||||||
|
return "Hyprvoice", "Config Reloaded"
|
||||||
|
}
|
||||||
|
return m.Title, m.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetOperationCancelled() (title, body string) {
|
||||||
|
m := c.Notifications.Messages.OperationCancelled
|
||||||
|
if m.Title == "" && m.Body == "" {
|
||||||
|
return "Hyprvoice", "Operation Cancelled"
|
||||||
|
}
|
||||||
|
return m.Title, m.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetRecordingAborted() string {
|
||||||
|
m := c.Notifications.Messages.RecordingAborted
|
||||||
|
if m.Body == "" {
|
||||||
|
return "Recording Aborted"
|
||||||
|
}
|
||||||
|
return m.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetInjectionAborted() string {
|
||||||
|
m := c.Notifications.Messages.InjectionAborted
|
||||||
|
if m.Body == "" {
|
||||||
|
return "Injection Aborted"
|
||||||
|
}
|
||||||
|
return m.Body
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) ToRecordingConfig() recording.Config {
|
func (c *Config) ToRecordingConfig() recording.Config {
|
||||||
@@ -358,6 +421,37 @@ func SaveDefaultConfig() error {
|
|||||||
enabled = true # Enable desktop notifications
|
enabled = true # Enable desktop notifications
|
||||||
type = "desktop" # Notification type ("desktop", "log", "none")
|
type = "desktop" # Notification type ("desktop", "log", "none")
|
||||||
|
|
||||||
|
# Custom notification messages (optional - defaults shown below)
|
||||||
|
# Uncomment and modify to customize notification text
|
||||||
|
# [notifications.messages]
|
||||||
|
# [notifications.messages.recording_started]
|
||||||
|
# title = "Hyprvoice"
|
||||||
|
# body = "Recording Started"
|
||||||
|
# [notifications.messages.transcribing]
|
||||||
|
# title = "Hyprvoice"
|
||||||
|
# body = "Recording Ended... Transcribing"
|
||||||
|
# [notifications.messages.config_reloaded]
|
||||||
|
# title = "Hyprvoice"
|
||||||
|
# body = "Config Reloaded"
|
||||||
|
# [notifications.messages.operation_cancelled]
|
||||||
|
# title = "Hyprvoice"
|
||||||
|
# body = "Operation Cancelled"
|
||||||
|
# [notifications.messages.recording_aborted]
|
||||||
|
# body = "Recording Aborted"
|
||||||
|
# [notifications.messages.injection_aborted]
|
||||||
|
# body = "Injection Aborted"
|
||||||
|
#
|
||||||
|
# Emoji-only example (for minimal pill-style notifications):
|
||||||
|
# [notifications.messages.recording_started]
|
||||||
|
# title = ""
|
||||||
|
# body = "🎤"
|
||||||
|
# [notifications.messages.transcribing]
|
||||||
|
# title = ""
|
||||||
|
# body = "⏳"
|
||||||
|
# [notifications.messages.config_reloaded]
|
||||||
|
# title = ""
|
||||||
|
# body = "🔧"
|
||||||
|
|
||||||
# Backend explanations:
|
# Backend explanations:
|
||||||
# - "ydotool": Uses ydotool (requires ydotoold daemon running). Most compatible with Chromium/Electron apps.
|
# - "ydotool": Uses ydotool (requires ydotoold daemon running). Most compatible with Chromium/Electron apps.
|
||||||
# - "wtype": Uses wtype for Wayland. May have issues with some Chromium-based apps.
|
# - "wtype": Uses wtype for Wayland. May have issues with some Chromium-based apps.
|
||||||
|
|||||||
@@ -1228,3 +1228,143 @@ func TestConfig_Validate_GroqTranslation_RejectsTurbo(t *testing.T) {
|
|||||||
t.Errorf("Unexpected error message: %v", err)
|
t.Errorf("Unexpected error message: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfig_MessageGetters_Defaults(t *testing.T) {
|
||||||
|
config := createTestConfig()
|
||||||
|
|
||||||
|
t.Run("GetRecordingStarted returns defaults", func(t *testing.T) {
|
||||||
|
title, body := config.GetRecordingStarted()
|
||||||
|
if title != "Hyprvoice" {
|
||||||
|
t.Errorf("GetRecordingStarted() title = %q, want %q", title, "Hyprvoice")
|
||||||
|
}
|
||||||
|
if body != "Recording Started" {
|
||||||
|
t.Errorf("GetRecordingStarted() body = %q, want %q", body, "Recording Started")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTranscribing returns defaults", func(t *testing.T) {
|
||||||
|
title, body := config.GetTranscribing()
|
||||||
|
if title != "Hyprvoice" {
|
||||||
|
t.Errorf("GetTranscribing() title = %q, want %q", title, "Hyprvoice")
|
||||||
|
}
|
||||||
|
if body != "Recording Ended... Transcribing" {
|
||||||
|
t.Errorf("GetTranscribing() body = %q, want %q", body, "Recording Ended... Transcribing")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetConfigReloaded returns defaults", func(t *testing.T) {
|
||||||
|
title, body := config.GetConfigReloaded()
|
||||||
|
if title != "Hyprvoice" {
|
||||||
|
t.Errorf("GetConfigReloaded() title = %q, want %q", title, "Hyprvoice")
|
||||||
|
}
|
||||||
|
if body != "Config Reloaded" {
|
||||||
|
t.Errorf("GetConfigReloaded() body = %q, want %q", body, "Config Reloaded")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetOperationCancelled returns defaults", func(t *testing.T) {
|
||||||
|
title, body := config.GetOperationCancelled()
|
||||||
|
if title != "Hyprvoice" {
|
||||||
|
t.Errorf("GetOperationCancelled() title = %q, want %q", title, "Hyprvoice")
|
||||||
|
}
|
||||||
|
if body != "Operation Cancelled" {
|
||||||
|
t.Errorf("GetOperationCancelled() body = %q, want %q", body, "Operation Cancelled")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetRecordingAborted returns default", func(t *testing.T) {
|
||||||
|
body := config.GetRecordingAborted()
|
||||||
|
if body != "Recording Aborted" {
|
||||||
|
t.Errorf("GetRecordingAborted() = %q, want %q", body, "Recording Aborted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetInjectionAborted returns default", func(t *testing.T) {
|
||||||
|
body := config.GetInjectionAborted()
|
||||||
|
if body != "Injection Aborted" {
|
||||||
|
t.Errorf("GetInjectionAborted() = %q, want %q", body, "Injection Aborted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_MessageGetters_Custom(t *testing.T) {
|
||||||
|
config := createTestConfig()
|
||||||
|
config.Notifications.Messages = MessagesConfig{
|
||||||
|
RecordingStarted: MessageConfig{
|
||||||
|
Title: "",
|
||||||
|
Body: "🎤",
|
||||||
|
},
|
||||||
|
Transcribing: MessageConfig{
|
||||||
|
Title: "",
|
||||||
|
Body: "⏳",
|
||||||
|
},
|
||||||
|
ConfigReloaded: MessageConfig{
|
||||||
|
Title: "",
|
||||||
|
Body: "🔧",
|
||||||
|
},
|
||||||
|
OperationCancelled: MessageConfig{
|
||||||
|
Title: "Custom",
|
||||||
|
Body: "Cancelled!",
|
||||||
|
},
|
||||||
|
RecordingAborted: MessageConfig{
|
||||||
|
Body: "Recording stopped",
|
||||||
|
},
|
||||||
|
InjectionAborted: MessageConfig{
|
||||||
|
Body: "Inject failed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("GetRecordingStarted returns custom emoji", func(t *testing.T) {
|
||||||
|
title, body := config.GetRecordingStarted()
|
||||||
|
if title != "" {
|
||||||
|
t.Errorf("GetRecordingStarted() title = %q, want %q", title, "")
|
||||||
|
}
|
||||||
|
if body != "🎤" {
|
||||||
|
t.Errorf("GetRecordingStarted() body = %q, want %q", body, "🎤")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTranscribing returns custom emoji", func(t *testing.T) {
|
||||||
|
title, body := config.GetTranscribing()
|
||||||
|
if title != "" {
|
||||||
|
t.Errorf("GetTranscribing() title = %q, want %q", title, "")
|
||||||
|
}
|
||||||
|
if body != "⏳" {
|
||||||
|
t.Errorf("GetTranscribing() body = %q, want %q", body, "⏳")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetConfigReloaded returns custom emoji", func(t *testing.T) {
|
||||||
|
title, body := config.GetConfigReloaded()
|
||||||
|
if title != "" {
|
||||||
|
t.Errorf("GetConfigReloaded() title = %q, want %q", title, "")
|
||||||
|
}
|
||||||
|
if body != "🔧" {
|
||||||
|
t.Errorf("GetConfigReloaded() body = %q, want %q", body, "🔧")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetOperationCancelled returns custom values", func(t *testing.T) {
|
||||||
|
title, body := config.GetOperationCancelled()
|
||||||
|
if title != "Custom" {
|
||||||
|
t.Errorf("GetOperationCancelled() title = %q, want %q", title, "Custom")
|
||||||
|
}
|
||||||
|
if body != "Cancelled!" {
|
||||||
|
t.Errorf("GetOperationCancelled() body = %q, want %q", body, "Cancelled!")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetRecordingAborted returns custom value", func(t *testing.T) {
|
||||||
|
body := config.GetRecordingAborted()
|
||||||
|
if body != "Recording stopped" {
|
||||||
|
t.Errorf("GetRecordingAborted() = %q, want %q", body, "Recording stopped")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetInjectionAborted returns custom value", func(t *testing.T) {
|
||||||
|
body := config.GetInjectionAborted()
|
||||||
|
if body != "Inject failed" {
|
||||||
|
t.Errorf("GetInjectionAborted() = %q, want %q", body, "Inject failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,10 +57,12 @@ func (d *Daemon) onConfigReload() {
|
|||||||
log.Printf("Config reloaded, restarting pipeline")
|
log.Printf("Config reloaded, restarting pipeline")
|
||||||
d.stopPipeline()
|
d.stopPipeline()
|
||||||
|
|
||||||
d.notifier.Notify("Hyprvoice", "Config Reloaded")
|
conf := d.configMgr.GetConfig()
|
||||||
|
title, body := conf.GetConfigReloaded()
|
||||||
|
d.notifier.Notify(title, body)
|
||||||
|
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
d.notifier = notify.GetNotifierBasedOnConfig(d.configMgr.GetConfig())
|
d.notifier = notify.GetNotifierBasedOnConfig(conf)
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,22 +182,23 @@ func (d *Daemon) handle(c net.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) toggle() {
|
func (d *Daemon) toggle() {
|
||||||
|
conf := d.configMgr.GetConfig()
|
||||||
switch d.status() {
|
switch d.status() {
|
||||||
case pipeline.Idle:
|
case pipeline.Idle:
|
||||||
config := d.configMgr.GetConfig()
|
p := pipeline.New(conf)
|
||||||
p := pipeline.New(config)
|
|
||||||
p.Run(d.ctx)
|
p.Run(d.ctx)
|
||||||
|
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
d.pipeline = p
|
d.pipeline = p
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
go d.notifier.Notify("Hyprvoice", "Recording Started")
|
title, body := conf.GetRecordingStarted()
|
||||||
|
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("Recording Aborted")
|
go d.notifier.Error(conf.GetRecordingAborted())
|
||||||
|
|
||||||
case pipeline.Transcribing:
|
case pipeline.Transcribing:
|
||||||
d.mu.RLock()
|
d.mu.RLock()
|
||||||
@@ -207,11 +210,12 @@ func (d *Daemon) toggle() {
|
|||||||
} else {
|
} else {
|
||||||
d.mu.RUnlock()
|
d.mu.RUnlock()
|
||||||
}
|
}
|
||||||
go d.notifier.Notify("Hyprvoice", "Recording Ended... Transcribing")
|
title, body := conf.GetTranscribing()
|
||||||
|
go d.notifier.Notify(title, body)
|
||||||
|
|
||||||
case pipeline.Injecting:
|
case pipeline.Injecting:
|
||||||
d.stopPipeline()
|
d.stopPipeline()
|
||||||
go d.notifier.Error("Injection Aborted")
|
go d.notifier.Error(conf.GetInjectionAborted())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +225,8 @@ func (d *Daemon) cancelPipeline() {
|
|||||||
log.Printf("Daemon: Cancel requested but pipeline is idle, ignoring")
|
log.Printf("Daemon: Cancel requested but pipeline is idle, ignoring")
|
||||||
default:
|
default:
|
||||||
d.stopPipeline()
|
d.stopPipeline()
|
||||||
go d.notifier.Notify("Hyprvoice", "Operation Cancelled")
|
title, body := d.configMgr.GetConfig().GetOperationCancelled()
|
||||||
|
go d.notifier.Notify(title, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user