212 lines
4.6 KiB
Go
212 lines
4.6 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
|
|
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
|
|
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
|
|
|
|
default:
|
|
if len(key) == 1 {
|
|
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,
|
|
}
|
|
|
|
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:",
|
|
}
|
|
|
|
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",
|
|
}
|
|
|
|
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
|
|
}
|