Add callbacks, crescendo sound and better configs

This commit is contained in:
2026-02-04 20:55:03 +02:00
parent cdcdf2c644
commit 621815ed0f
5 changed files with 235 additions and 33 deletions

View File

@@ -24,6 +24,11 @@ const (
cfgServerPassword
cfgAPIPassword
cfgPollSeconds
cfgCrescendoEnabled
cfgCrescendoStartPct
cfgCrescendoEndPct
cfgCrescendoDurationS
cfgCallbackScript
cfgFieldCount
)
@@ -51,6 +56,15 @@ func newConfigModel(cfg db.Settings) *configModel {
c.fields[cfgServerPassword] = cfg.ServerPassword
c.fields[cfgAPIPassword] = cfg.APIPassword
c.fields[cfgPollSeconds] = strconv.Itoa(cfg.PollSeconds)
if cfg.CrescendoEnabled {
c.fields[cfgCrescendoEnabled] = "true"
} else {
c.fields[cfgCrescendoEnabled] = "false"
}
c.fields[cfgCrescendoStartPct] = strconv.Itoa(cfg.CrescendoStartPct)
c.fields[cfgCrescendoEndPct] = strconv.Itoa(cfg.CrescendoEndPct)
c.fields[cfgCrescendoDurationS] = strconv.Itoa(cfg.CrescendoDurationS)
c.fields[cfgCallbackScript] = cfg.CallbackScript
return c
}
@@ -90,11 +104,12 @@ func (c *configModel) HandleKey(msg tea.KeyMsg, m *Model) (tea.Model, tea.Cmd) {
case " ":
// Toggle boolean fields
if c.active == cfgShowSeconds {
if c.fields[cfgShowSeconds] == "true" {
c.fields[cfgShowSeconds] = "false"
if c.active == cfgShowSeconds || c.active == cfgCrescendoEnabled {
field := &c.fields[c.active]
if *field == "true" {
*field = "false"
} else {
c.fields[cfgShowSeconds] = "true"
*field = "true"
}
return *m, nil
}
@@ -106,7 +121,7 @@ func (c *configModel) HandleKey(msg tea.KeyMsg, m *Model) (tea.Model, tea.Cmd) {
default:
if len(key) == 1 {
// Don't allow free typing on boolean fields
if c.active == cfgShowSeconds {
if c.active == cfgShowSeconds || c.active == cfgCrescendoEnabled {
return *m, nil
}
c.fields[c.active] += key
@@ -159,6 +174,24 @@ func (c *configModel) save(m *Model) (tea.Model, tea.Cmd) {
return *m, nil
}
crescStartPct, err := strconv.Atoi(strings.TrimSpace(c.fields[cfgCrescendoStartPct]))
if err != nil || crescStartPct < 0 || crescStartPct > 100 {
c.err = "Crescendo start must be 0-100%"
return *m, nil
}
crescEndPct, err := strconv.Atoi(strings.TrimSpace(c.fields[cfgCrescendoEndPct]))
if err != nil || crescEndPct < 0 || crescEndPct > 150 {
c.err = "Crescendo end must be 0-150%"
return *m, nil
}
crescDuration, err := strconv.Atoi(strings.TrimSpace(c.fields[cfgCrescendoDurationS]))
if err != nil || crescDuration < 1 || crescDuration > 300 {
c.err = "Crescendo duration must be 1-300 seconds"
return *m, nil
}
cfg := db.Settings{
SnoozeMinutes: snooze,
TimeoutMinutes: timeout,
@@ -177,6 +210,12 @@ func (c *configModel) save(m *Model) (tea.Model, tea.Cmd) {
cfg.APIPassword = strings.TrimSpace(c.fields[cfgAPIPassword])
cfg.PollSeconds = pollSeconds
cfg.CrescendoEnabled = strings.TrimSpace(c.fields[cfgCrescendoEnabled]) == "true"
cfg.CrescendoStartPct = crescStartPct
cfg.CrescendoEndPct = crescEndPct
cfg.CrescendoDurationS = crescDuration
cfg.CallbackScript = strings.TrimSpace(c.fields[cfgCallbackScript])
if cfg.DefaultSound == "" {
cfg.DefaultSound = "default"
}
@@ -208,6 +247,11 @@ func (c *configModel) View() string {
"Server pass:",
"API password:",
"Poll (sec):",
"Enabled:",
"Start %:",
"End %:",
"Duration (s):",
"Script path:",
}
hints := [cfgFieldCount]string{
@@ -223,12 +267,31 @@ func (c *configModel) View() string {
"password to auth with server",
"password for incoming API requests",
"1-60, client poll frequency",
"space to toggle",
"0-100, starting volume",
"0-150, ending volume (>100 = overdrive)",
"1-300, ramp duration in seconds",
"called with: start|dismiss|snooze|timeout <name>",
}
// Section headers: field index -> section name
sections := map[cfgField]string{
cfgSnoozeMinutes: "─── Alarm ───",
cfgBlinkOnMs: "─── Display ───",
cfgServerURL: "─── Network ───",
cfgCrescendoEnabled: "─── Crescendo (pactl) ───",
cfgCallbackScript: "─── Hooks ───",
}
var lines []string
lines = append(lines, TitleStyle.Render("Settings"), "")
for i := cfgField(0); i < cfgFieldCount; i++ {
// Insert section header if this field starts a new section
if section, ok := sections[i]; ok {
lines = append(lines, "", DividerStyle.Render(section))
}
labelStr := fmt.Sprintf("%15s", labels[i])
value := c.fields[i]

View File

@@ -2,6 +2,7 @@ package ui
import (
"fmt"
"os/exec"
"strings"
"time"
"woke/db"
@@ -200,6 +201,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case autoTimeoutMsg:
if m.firingAlarm != nil {
m.player.Stop()
m.runCallback("timeout", m.firingAlarm.Name)
m.statusMsg = fmt.Sprintf("Alarm auto-dismissed after %d minutes", m.settings.TimeoutMinutes)
m.firingAlarm = nil
m.snoozeCount = 0
@@ -232,12 +234,14 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case "enter", " ", "d":
m.player.Stop()
alarm := m.firingAlarm
m.runCallback("dismiss", alarm.Name)
m.firingAlarm = nil
m.snoozeCount = 0
m.statusMsg = fmt.Sprintf("Alarm '%s' dismissed", alarm.Name)
case "s":
m.player.Stop()
alarm := *m.firingAlarm
m.runCallback("snooze", alarm.Name)
dur := m.snoozeDuration()
m.firingAlarm = nil
m.snoozeUntil = time.Now().Add(dur)
@@ -342,7 +346,30 @@ func (m *Model) startFiring(alarm *db.Alarm) {
if sound == "default" || sound == "" {
sound = m.settings.DefaultSound
}
m.player.PlayLoop(sound)
// Use crescendo if enabled
cresc := player.CrescendoConfig{
Enabled: m.settings.CrescendoEnabled,
StartPct: m.settings.CrescendoStartPct,
EndPct: m.settings.CrescendoEndPct,
DurationS: m.settings.CrescendoDurationS,
}
m.player.PlayLoopWithCrescendo(sound, cresc)
// Fire callback
m.runCallback("start", alarm.Name)
}
// runCallback executes the configured callback script with the event type and alarm name.
func (m *Model) runCallback(event, alarmName string) {
script := m.settings.CallbackScript
if script == "" {
return
}
// Run in background, don't block
go func() {
_ = exec.Command(script, event, alarmName).Run()
}()
}
func snoozeCmd(alarm db.Alarm, after time.Duration) tea.Cmd {