First draft

This commit is contained in:
2026-02-01 21:15:17 +02:00
parent 191ec01e73
commit 06d5a6a779
11 changed files with 1352 additions and 0 deletions

215
ui/form.go Normal file
View File

@@ -0,0 +1,215 @@
package ui
import (
"fmt"
"strings"
"woke/db"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/robfig/cron/v3"
)
type formField int
const (
fieldName formField = iota
fieldDescription
fieldTime
fieldTrigger
fieldSound
fieldCount // sentinel
)
type formModel struct {
editing *db.Alarm // nil = creating new
fields [fieldCount]string
active formField
err string
}
func newFormModel(alarm *db.Alarm) *formModel {
f := &formModel{}
if alarm != nil {
f.editing = alarm
f.fields[fieldName] = alarm.Name
f.fields[fieldDescription] = alarm.Description
f.fields[fieldTime] = alarm.Time
f.fields[fieldTrigger] = alarm.Trigger
f.fields[fieldSound] = alarm.SoundPath
} else {
f.fields[fieldTrigger] = "once"
f.fields[fieldSound] = "default"
}
return f
}
func (f *formModel) Update(msg tea.Msg, m *Model) (tea.Model, tea.Cmd) {
return *m, nil
}
func (f *formModel) HandleKey(msg tea.KeyMsg, m *Model) (tea.Model, tea.Cmd) {
key := msg.String()
switch key {
case "esc", "escape":
m.mode = viewMain
m.form = nil
m.statusMsg = ""
return *m, nil
case "tab", "down":
f.active = (f.active + 1) % fieldCount
return *m, nil
case "shift+tab", "up":
f.active = (f.active - 1 + fieldCount) % fieldCount
return *m, nil
case "enter":
return f.save(m)
case "backspace":
field := &f.fields[f.active]
if len(*field) > 0 {
*field = (*field)[:len(*field)-1]
}
f.err = ""
return *m, nil
case "ctrl+u":
f.fields[f.active] = ""
f.err = ""
return *m, nil
default:
// Only accept printable characters
if len(key) == 1 {
f.fields[f.active] += key
f.err = ""
}
return *m, nil
}
}
func (f *formModel) save(m *Model) (tea.Model, tea.Cmd) {
name := strings.TrimSpace(f.fields[fieldName])
if name == "" {
f.err = "Name is required"
return *m, nil
}
timeStr := strings.TrimSpace(f.fields[fieldTime])
trigger := strings.TrimSpace(f.fields[fieldTrigger])
if trigger == "once" || trigger == "" {
trigger = "once"
// Validate HH:MM format
if !isValidTime(timeStr) {
f.err = "Time must be in HH:MM format (00:00-23:59)"
return *m, nil
}
} else {
// Validate cron expression
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
if _, err := parser.Parse(trigger); err != nil {
f.err = fmt.Sprintf("Invalid cron expression: %v", err)
return *m, nil
}
}
soundPath := strings.TrimSpace(f.fields[fieldSound])
if soundPath == "" {
soundPath = "default"
}
alarm := db.Alarm{
Name: name,
Description: strings.TrimSpace(f.fields[fieldDescription]),
Time: timeStr,
Trigger: trigger,
SoundPath: soundPath,
Enabled: true,
}
if f.editing != nil {
alarm.ID = f.editing.ID
alarm.Enabled = f.editing.Enabled
if err := m.store.UpdateAlarm(alarm); err != nil {
f.err = fmt.Sprintf("Save failed: %v", err)
return *m, nil
}
m.statusMsg = "Alarm updated"
} else {
if _, err := m.store.CreateAlarm(alarm); err != nil {
f.err = fmt.Sprintf("Save failed: %v", err)
return *m, nil
}
m.statusMsg = "Alarm created"
}
m.refreshAlarms()
m.mode = viewMain
m.form = nil
return *m, nil
}
func (f *formModel) View() string {
title := "Add Alarm"
if f.editing != nil {
title = "Edit Alarm"
}
labels := [fieldCount]string{
"Name: ",
"Description: ",
"Time (HH:MM):",
"Trigger: ",
"Sound: ",
}
hints := [fieldCount]string{
"",
"(optional)",
"e.g. 07:30",
"'once' or cron: 30 7 * * 1-5",
"'default' or path to sound file",
}
var lines []string
lines = append(lines, TitleStyle.Render(title), "")
for i := formField(0); i < fieldCount; i++ {
label := FormLabelStyle.Render(labels[i])
value := f.fields[i]
hint := HelpStyle.Render(hints[i])
var style lipgloss.Style
if i == f.active {
style = FormActiveStyle
value += "█" // cursor
} else {
style = FormLabelStyle
}
line := fmt.Sprintf(" %s %s %s", label, style.Render(value), hint)
lines = append(lines, line)
}
if f.err != "" {
lines = append(lines, "", FormErrorStyle.Render(" Error: "+f.err))
}
return strings.Join(lines, "\n")
}
func isValidTime(s string) bool {
if len(s) != 5 || s[2] != ':' {
return false
}
h := (s[0]-'0')*10 + (s[1] - '0')
m := (s[3]-'0')*10 + (s[4] - '0')
return h <= 23 && m <= 59 && s[0] >= '0' && s[0] <= '9' &&
s[1] >= '0' && s[1] <= '9' && s[3] >= '0' && s[3] <= '9' &&
s[4] >= '0' && s[4] <= '9'
}