Lucky manta ray
This commit is contained in:
211
ui/config.go
Normal file
211
ui/config.go
Normal file
@@ -0,0 +1,211 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user