First draft
This commit is contained in:
161
ui/clock.go
Normal file
161
ui/clock.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Each digit is 8 chars wide x 7 lines tall, using block characters.
|
||||
var bigDigits = [10][]string{
|
||||
// 0
|
||||
{
|
||||
" ██████ ",
|
||||
"██ ██",
|
||||
"██ ████",
|
||||
"██ ██ ██",
|
||||
"████ ██",
|
||||
"██ ██",
|
||||
" ██████ ",
|
||||
},
|
||||
// 1
|
||||
{
|
||||
" ██ ",
|
||||
" ████ ",
|
||||
" ██ ",
|
||||
" ██ ",
|
||||
" ██ ",
|
||||
" ██ ",
|
||||
" ██████",
|
||||
},
|
||||
// 2
|
||||
{
|
||||
" ██████ ",
|
||||
"██ ██",
|
||||
" ██",
|
||||
" █████ ",
|
||||
"██ ",
|
||||
"██ ██",
|
||||
"████████",
|
||||
},
|
||||
// 3
|
||||
{
|
||||
" ██████ ",
|
||||
"██ ██",
|
||||
" ██",
|
||||
" █████ ",
|
||||
" ██",
|
||||
"██ ██",
|
||||
" ██████ ",
|
||||
},
|
||||
// 4
|
||||
{
|
||||
"██ ██",
|
||||
"██ ██",
|
||||
"██ ██",
|
||||
"████████",
|
||||
" ██",
|
||||
" ██",
|
||||
" ██",
|
||||
},
|
||||
// 5
|
||||
{
|
||||
"████████",
|
||||
"██ ",
|
||||
"██ ",
|
||||
"███████ ",
|
||||
" ██",
|
||||
"██ ██",
|
||||
" ██████ ",
|
||||
},
|
||||
// 6
|
||||
{
|
||||
" ██████ ",
|
||||
"██ ",
|
||||
"██ ",
|
||||
"███████ ",
|
||||
"██ ██",
|
||||
"██ ██",
|
||||
" ██████ ",
|
||||
},
|
||||
// 7
|
||||
{
|
||||
"████████",
|
||||
"██ ██",
|
||||
" ██ ",
|
||||
" ██ ",
|
||||
" ██ ",
|
||||
" ██ ",
|
||||
" ██ ",
|
||||
},
|
||||
// 8
|
||||
{
|
||||
" ██████ ",
|
||||
"██ ██",
|
||||
"██ ██",
|
||||
" ██████ ",
|
||||
"██ ██",
|
||||
"██ ██",
|
||||
" ██████ ",
|
||||
},
|
||||
// 9
|
||||
{
|
||||
" ██████ ",
|
||||
"██ ██",
|
||||
"██ ██",
|
||||
" ███████",
|
||||
" ██",
|
||||
" ██",
|
||||
" ██████ ",
|
||||
},
|
||||
}
|
||||
|
||||
var bigColon = []string{
|
||||
" ",
|
||||
" ██ ",
|
||||
" ██ ",
|
||||
" ",
|
||||
" ██ ",
|
||||
" ██ ",
|
||||
" ",
|
||||
}
|
||||
|
||||
var bigColonBlink = []string{
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
}
|
||||
|
||||
// RenderBigClock renders the current time as massive ASCII block digits.
|
||||
// Format: HH:MM:SS in 24h. The colon blinks every second.
|
||||
func RenderBigClock(t time.Time) string {
|
||||
h := t.Hour()
|
||||
m := t.Minute()
|
||||
s := t.Second()
|
||||
|
||||
timeStr := fmt.Sprintf("%02d:%02d:%02d", h, m, s)
|
||||
|
||||
var lines [7]string
|
||||
for i := range lines {
|
||||
var parts []string
|
||||
for _, ch := range timeStr {
|
||||
switch {
|
||||
case ch >= '0' && ch <= '9':
|
||||
parts = append(parts, bigDigits[ch-'0'][i])
|
||||
case ch == ':':
|
||||
if s%2 == 0 {
|
||||
parts = append(parts, bigColon[i])
|
||||
} else {
|
||||
parts = append(parts, bigColonBlink[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
lines[i] = strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
return strings.Join(lines[:], "\n")
|
||||
}
|
||||
215
ui/form.go
Normal file
215
ui/form.go
Normal 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'
|
||||
}
|
||||
347
ui/model.go
Normal file
347
ui/model.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"woke/db"
|
||||
"woke/player"
|
||||
"woke/scheduler"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// View modes
|
||||
type viewMode int
|
||||
|
||||
const (
|
||||
viewMain viewMode = iota
|
||||
viewForm
|
||||
viewConfirmDelete
|
||||
)
|
||||
|
||||
// Messages
|
||||
type tickMsg time.Time
|
||||
type alarmFiredMsg scheduler.AlarmEvent
|
||||
|
||||
// Model is the main bubbletea model.
|
||||
type Model struct {
|
||||
store *db.Store
|
||||
scheduler *scheduler.Scheduler
|
||||
player *player.Player
|
||||
|
||||
// State
|
||||
alarms []db.Alarm
|
||||
cursor int
|
||||
mode viewMode
|
||||
width int
|
||||
height int
|
||||
now time.Time
|
||||
|
||||
// Alarm firing state
|
||||
firingAlarm *db.Alarm
|
||||
firingBlink bool
|
||||
|
||||
// Form state
|
||||
form *formModel
|
||||
|
||||
// Error/status
|
||||
statusMsg string
|
||||
}
|
||||
|
||||
func NewModel(store *db.Store, sched *scheduler.Scheduler, pl *player.Player) Model {
|
||||
return Model{
|
||||
store: store,
|
||||
scheduler: sched,
|
||||
player: pl,
|
||||
now: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
tickEverySecond(),
|
||||
listenForAlarms(m.scheduler),
|
||||
m.loadAlarms,
|
||||
)
|
||||
}
|
||||
|
||||
func tickEverySecond() tea.Cmd {
|
||||
return tea.Every(time.Second, func(t time.Time) tea.Msg {
|
||||
return tickMsg(t)
|
||||
})
|
||||
}
|
||||
|
||||
func listenForAlarms(sched *scheduler.Scheduler) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
event := <-sched.Events()
|
||||
return alarmFiredMsg(event)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) loadAlarms() tea.Msg {
|
||||
alarms, err := m.store.ListAlarms()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
m.alarms = alarms
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
return m, nil
|
||||
|
||||
case tickMsg:
|
||||
m.now = time.Time(msg)
|
||||
if m.firingAlarm != nil {
|
||||
m.firingBlink = !m.firingBlink
|
||||
}
|
||||
return m, tickEverySecond()
|
||||
|
||||
case alarmFiredMsg:
|
||||
alarm := msg.Alarm
|
||||
m.firingAlarm = &alarm
|
||||
m.firingBlink = true
|
||||
m.player.Play(alarm.SoundPath)
|
||||
m.refreshAlarms()
|
||||
return m, listenForAlarms(m.scheduler)
|
||||
|
||||
case tea.KeyMsg:
|
||||
return m.handleKey(msg)
|
||||
}
|
||||
|
||||
// Forward to form if active
|
||||
if m.mode == viewForm && m.form != nil {
|
||||
return m.form.Update(msg, &m)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
key := msg.String()
|
||||
|
||||
// Global: quit
|
||||
if key == "ctrl+c" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
// If alarm is firing, handle dismiss/snooze
|
||||
if m.firingAlarm != nil {
|
||||
switch key {
|
||||
case "enter", " ", "d":
|
||||
m.player.Stop()
|
||||
m.firingAlarm = nil
|
||||
m.statusMsg = "Alarm dismissed"
|
||||
case "s":
|
||||
m.player.Stop()
|
||||
m.firingAlarm = nil
|
||||
m.statusMsg = "Snoozed for 5 minutes (not yet implemented)"
|
||||
// TODO: implement snooze by creating a one-shot alarm 5min from now
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Form mode
|
||||
if m.mode == viewForm && m.form != nil {
|
||||
return m.form.HandleKey(msg, &m)
|
||||
}
|
||||
|
||||
// Confirm delete mode
|
||||
if m.mode == viewConfirmDelete {
|
||||
switch key {
|
||||
case "y", "Y":
|
||||
if m.cursor < len(m.alarms) {
|
||||
_ = m.store.DeleteAlarm(m.alarms[m.cursor].ID)
|
||||
m.refreshAlarms()
|
||||
if m.cursor >= len(m.alarms) && m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
m.statusMsg = "Alarm deleted"
|
||||
}
|
||||
m.mode = viewMain
|
||||
case "n", "N", "escape", "esc":
|
||||
m.mode = viewMain
|
||||
m.statusMsg = ""
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Main view keys
|
||||
switch key {
|
||||
case "q":
|
||||
return m, tea.Quit
|
||||
case "j", "down":
|
||||
if m.cursor < len(m.alarms)-1 {
|
||||
m.cursor++
|
||||
}
|
||||
case "k", "up":
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
case "g":
|
||||
m.cursor = 0
|
||||
case "G":
|
||||
if len(m.alarms) > 0 {
|
||||
m.cursor = len(m.alarms) - 1
|
||||
}
|
||||
case " ":
|
||||
// Toggle enabled
|
||||
if m.cursor < len(m.alarms) {
|
||||
_ = m.store.ToggleAlarm(m.alarms[m.cursor].ID)
|
||||
m.refreshAlarms()
|
||||
}
|
||||
case "a":
|
||||
m.form = newFormModel(nil)
|
||||
m.mode = viewForm
|
||||
m.statusMsg = ""
|
||||
case "e":
|
||||
if m.cursor < len(m.alarms) {
|
||||
alarm := m.alarms[m.cursor]
|
||||
m.form = newFormModel(&alarm)
|
||||
m.mode = viewForm
|
||||
m.statusMsg = ""
|
||||
}
|
||||
case "d":
|
||||
if m.cursor < len(m.alarms) {
|
||||
m.mode = viewConfirmDelete
|
||||
m.statusMsg = fmt.Sprintf("Delete '%s'? (y/n)", m.alarms[m.cursor].Name)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) refreshAlarms() {
|
||||
alarms, err := m.store.ListAlarms()
|
||||
if err == nil {
|
||||
m.alarms = alarms
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
if m.width == 0 {
|
||||
return "Loading..."
|
||||
}
|
||||
|
||||
var sections []string
|
||||
|
||||
// Big clock
|
||||
clockStr := RenderBigClock(m.now)
|
||||
if m.firingAlarm != nil && m.firingBlink {
|
||||
clockStr = ClockAlarmStyle.Render(clockStr)
|
||||
} else {
|
||||
clockStr = ClockStyle.Render(clockStr)
|
||||
}
|
||||
clockStr = lipgloss.PlaceHorizontal(m.width, lipgloss.Center, clockStr)
|
||||
sections = append(sections, clockStr)
|
||||
|
||||
// Date line
|
||||
dateLine := StatusStyle.Render(m.now.Format("Monday, 02 January 2006"))
|
||||
dateLine = lipgloss.PlaceHorizontal(m.width, lipgloss.Center, dateLine)
|
||||
sections = append(sections, dateLine)
|
||||
|
||||
// Firing alarm overlay
|
||||
if m.firingAlarm != nil {
|
||||
firingText := fmt.Sprintf("🔔 ALARM: %s 🔔", m.firingAlarm.Name)
|
||||
if m.firingAlarm.Description != "" {
|
||||
firingText += "\n" + m.firingAlarm.Description
|
||||
}
|
||||
firingText += "\n\n[Enter/Space/d] Dismiss [s] Snooze"
|
||||
styled := AlarmFiringStyle.Render(firingText)
|
||||
styled = lipgloss.PlaceHorizontal(m.width, lipgloss.Center, styled)
|
||||
sections = append(sections, "", styled)
|
||||
}
|
||||
|
||||
// Divider
|
||||
divider := DividerStyle.Render(strings.Repeat("─", min(m.width, 80)))
|
||||
divider = lipgloss.PlaceHorizontal(m.width, lipgloss.Center, divider)
|
||||
sections = append(sections, "", divider, "")
|
||||
|
||||
// Form view
|
||||
if m.mode == viewForm && m.form != nil {
|
||||
formView := m.form.View()
|
||||
formView = lipgloss.PlaceHorizontal(m.width, lipgloss.Center, formView)
|
||||
sections = append(sections, formView)
|
||||
} else {
|
||||
// Alarm list
|
||||
alarmList := m.renderAlarmList()
|
||||
alarmList = lipgloss.PlaceHorizontal(m.width, lipgloss.Center, alarmList)
|
||||
sections = append(sections, alarmList)
|
||||
}
|
||||
|
||||
// Status message
|
||||
if m.statusMsg != "" {
|
||||
status := StatusStyle.Render(m.statusMsg)
|
||||
sections = append(sections, "", lipgloss.PlaceHorizontal(m.width, lipgloss.Center, status))
|
||||
}
|
||||
|
||||
// Help bar at the bottom
|
||||
help := m.renderHelp()
|
||||
sections = append(sections, "", lipgloss.PlaceHorizontal(m.width, lipgloss.Center, help))
|
||||
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Top,
|
||||
strings.Join(sections, "\n"),
|
||||
)
|
||||
}
|
||||
|
||||
func (m Model) renderAlarmList() string {
|
||||
if len(m.alarms) == 0 {
|
||||
return StatusStyle.Render("No alarms. Press 'a' to add one.")
|
||||
}
|
||||
|
||||
title := TitleStyle.Render("Alarms")
|
||||
var lines []string
|
||||
lines = append(lines, title, "")
|
||||
|
||||
for i, a := range m.alarms {
|
||||
cursor := " "
|
||||
if i == m.cursor {
|
||||
cursor = "▸ "
|
||||
}
|
||||
|
||||
status := "●"
|
||||
var style lipgloss.Style
|
||||
if a.Enabled {
|
||||
style = AlarmEnabledStyle
|
||||
} else {
|
||||
style = AlarmDisabledStyle
|
||||
status = "○"
|
||||
}
|
||||
|
||||
if i == m.cursor {
|
||||
style = AlarmSelectedStyle
|
||||
}
|
||||
|
||||
trigger := a.Time
|
||||
if a.Trigger != "once" {
|
||||
trigger = a.Trigger
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("%s%s %s %-20s %s", cursor, status, trigger, a.Name, a.Description)
|
||||
lines = append(lines, style.Render(line))
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (m Model) renderHelp() string {
|
||||
if m.mode == viewForm {
|
||||
return HelpStyle.Render("Tab: next field Shift+Tab: prev field Enter: save Esc: cancel")
|
||||
}
|
||||
if m.mode == viewConfirmDelete {
|
||||
return HelpStyle.Render("y: confirm delete n: cancel")
|
||||
}
|
||||
return HelpStyle.Render("j/k: navigate a: add e: edit d: delete space: toggle q: quit")
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
57
ui/styles.go
Normal file
57
ui/styles.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package ui
|
||||
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
var (
|
||||
// Clock colors
|
||||
ClockStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00FF88")).
|
||||
Bold(true)
|
||||
|
||||
ClockAlarmStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF4444")).
|
||||
Bold(true)
|
||||
|
||||
// Alarm list
|
||||
AlarmEnabledStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00FF88"))
|
||||
|
||||
AlarmDisabledStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#666666"))
|
||||
|
||||
AlarmSelectedStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFF00")).
|
||||
Bold(true)
|
||||
|
||||
// Status bar
|
||||
StatusStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
HelpStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#555555"))
|
||||
|
||||
// Alarm firing overlay
|
||||
AlarmFiringStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000")).
|
||||
Bold(true).
|
||||
Blink(true)
|
||||
|
||||
// Form
|
||||
FormLabelStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#AAAAAA"))
|
||||
|
||||
FormActiveStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00FF88")).
|
||||
Bold(true)
|
||||
|
||||
FormErrorStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF4444"))
|
||||
|
||||
TitleStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00FF88")).
|
||||
Bold(true).
|
||||
Underline(true)
|
||||
|
||||
DividerStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#333333"))
|
||||
)
|
||||
Reference in New Issue
Block a user