Files
woke/ui/config.go
2026-02-02 20:57:41 +02:00

242 lines
5.3 KiB
Go

package ui
import (
"fmt"
"strconv"
"strings"
"woke/db"
tea "github.com/charmbracelet/bubbletea"
)
type cfgField int
const (
cfgSnoozeMinutes cfgField = iota
cfgTimeoutMinutes
cfgDefaultSound
cfgBlinkOnMs
cfgBlinkOffMs
cfgColorClock
cfgColorAlarm
cfgShowSeconds
cfgFieldCount
)
type configModel struct {
fields [cfgFieldCount]string
active cfgField
err string
}
func newConfigModel(cfg db.Settings) *configModel {
c := &configModel{}
c.fields[cfgSnoozeMinutes] = strconv.Itoa(cfg.SnoozeMinutes)
c.fields[cfgTimeoutMinutes] = strconv.Itoa(cfg.TimeoutMinutes)
c.fields[cfgDefaultSound] = cfg.DefaultSound
c.fields[cfgBlinkOnMs] = strconv.Itoa(cfg.BlinkOnMs)
c.fields[cfgBlinkOffMs] = strconv.Itoa(cfg.BlinkOffMs)
c.fields[cfgColorClock] = cfg.ColorClock
c.fields[cfgColorAlarm] = cfg.ColorAlarm
if cfg.ShowSeconds {
c.fields[cfgShowSeconds] = "true"
} else {
c.fields[cfgShowSeconds] = "false"
}
return c
}
func (c *configModel) HandleKey(msg tea.KeyMsg, m *Model) (tea.Model, tea.Cmd) {
key := msg.String()
switch key {
case "esc", "escape":
m.mode = viewMain
m.config = nil
m.statusMsg = ""
return *m, nil
case "tab", "down":
c.active = (c.active + 1) % cfgFieldCount
return *m, nil
case "shift+tab", "up":
c.active = (c.active - 1 + cfgFieldCount) % cfgFieldCount
return *m, nil
case "enter":
return c.save(m)
case "backspace":
field := &c.fields[c.active]
if len(*field) > 0 {
*field = (*field)[:len(*field)-1]
}
c.err = ""
return *m, nil
case "ctrl+u":
c.fields[c.active] = ""
c.err = ""
return *m, nil
case " ":
// Toggle boolean fields
if c.active == cfgShowSeconds {
if c.fields[cfgShowSeconds] == "true" {
c.fields[cfgShowSeconds] = "false"
} else {
c.fields[cfgShowSeconds] = "true"
}
return *m, nil
}
// Space as literal for other fields
c.fields[c.active] += " "
c.err = ""
return *m, nil
default:
if len(key) == 1 {
// Don't allow free typing on boolean fields
if c.active == cfgShowSeconds {
return *m, nil
}
c.fields[c.active] += key
c.err = ""
}
return *m, nil
}
}
func (c *configModel) save(m *Model) (tea.Model, tea.Cmd) {
snooze, err := strconv.Atoi(strings.TrimSpace(c.fields[cfgSnoozeMinutes]))
if err != nil || snooze < 1 || snooze > 60 {
c.err = "Snooze must be 1-60 minutes"
return *m, nil
}
timeout, err := strconv.Atoi(strings.TrimSpace(c.fields[cfgTimeoutMinutes]))
if err != nil || timeout < 1 || timeout > 60 {
c.err = "Timeout must be 1-60 minutes"
return *m, nil
}
blinkOn, err := strconv.Atoi(strings.TrimSpace(c.fields[cfgBlinkOnMs]))
if err != nil || blinkOn < 100 || blinkOn > 5000 {
c.err = "Blink on must be 100-5000 ms"
return *m, nil
}
blinkOff, err := strconv.Atoi(strings.TrimSpace(c.fields[cfgBlinkOffMs]))
if err != nil || blinkOff < 100 || blinkOff > 5000 {
c.err = "Blink off must be 100-5000 ms"
return *m, nil
}
colorClock := strings.TrimSpace(c.fields[cfgColorClock])
if !isValidHexColor(colorClock) {
c.err = "Clock color must be hex like #00FF88"
return *m, nil
}
colorAlarm := strings.TrimSpace(c.fields[cfgColorAlarm])
if !isValidHexColor(colorAlarm) {
c.err = "Alarm color must be hex like #FF4444"
return *m, nil
}
cfg := db.Settings{
SnoozeMinutes: snooze,
TimeoutMinutes: timeout,
DefaultSound: strings.TrimSpace(c.fields[cfgDefaultSound]),
BlinkOnMs: blinkOn,
BlinkOffMs: blinkOff,
ColorClock: colorClock,
ColorAlarm: colorAlarm,
}
showSec := strings.TrimSpace(c.fields[cfgShowSeconds])
cfg.ShowSeconds = showSec == "true"
if cfg.DefaultSound == "" {
cfg.DefaultSound = "default"
}
if err := m.store.SaveSettings(cfg); err != nil {
c.err = fmt.Sprintf("Save failed: %v", err)
return *m, nil
}
m.settings = cfg
m.applySettings()
m.mode = viewMain
m.config = nil
m.statusMsg = "Settings saved"
return *m, nil
}
func (c *configModel) View() string {
labels := [cfgFieldCount]string{
"Snooze (min):",
"Timeout (min):",
"Default sound:",
"Blink on (ms):",
"Blink off (ms):",
"Clock color:",
"Alarm color:",
"Show seconds:",
}
hints := [cfgFieldCount]string{
"1-60",
"1-60, auto-dismiss firing alarm",
"'default' or path to sound file",
"100-5000",
"100-5000",
"hex e.g. #00FF88",
"hex e.g. #FF4444",
"space to toggle",
}
var lines []string
lines = append(lines, TitleStyle.Render("Settings"), "")
for i := cfgField(0); i < cfgFieldCount; i++ {
labelStr := fmt.Sprintf("%15s", labels[i])
value := c.fields[i]
var style = FormLabelStyle
if i == c.active {
style = FormActiveStyle
value += "█"
}
if len(value) > formValueWidth {
value = value[len(value)-formValueWidth:]
}
valueStr := fmt.Sprintf("%-*s", formValueWidth, value)
hintStr := fmt.Sprintf("%-*s", formHintWidth, hints[i])
line := FormLabelStyle.Render(labelStr) + " " + style.Render(valueStr) + " " + HelpStyle.Render(hintStr)
lines = append(lines, line)
}
if c.err != "" {
lines = append(lines, "", FormErrorStyle.Render(" Error: "+c.err))
}
return strings.Join(lines, "\n")
}
func isValidHexColor(s string) bool {
if len(s) != 7 || s[0] != '#' {
return false
}
for _, c := range s[1:] {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
return false
}
}
return true
}