Compare commits
2 Commits
d1307a75b9
...
d3f29e3927
| Author | SHA1 | Date | |
|---|---|---|---|
| d3f29e3927 | |||
| b309ccd9cd |
249
api/api.go
Normal file
249
api/api.go
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"woke/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Notifier is called after mutating operations to notify the TUI.
|
||||||
|
type Notifier func()
|
||||||
|
|
||||||
|
// Server provides a REST API for alarm CRUD.
|
||||||
|
type Server struct {
|
||||||
|
store *db.Store
|
||||||
|
mux *http.ServeMux
|
||||||
|
notify Notifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(store *db.Store, notify Notifier) *Server {
|
||||||
|
s := &Server{store: store, notify: notify}
|
||||||
|
s.mux = http.NewServeMux()
|
||||||
|
s.mux.HandleFunc("GET /api/alarms", s.listAlarms)
|
||||||
|
s.mux.HandleFunc("GET /api/alarms/{id}", s.getAlarm)
|
||||||
|
s.mux.HandleFunc("POST /api/alarms", s.createAlarm)
|
||||||
|
s.mux.HandleFunc("PUT /api/alarms/{id}", s.updateAlarm)
|
||||||
|
s.mux.HandleFunc("DELETE /api/alarms/{id}", s.deleteAlarm)
|
||||||
|
s.mux.HandleFunc("PATCH /api/alarms/{id}/toggle", s.toggleAlarm)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Handler() http.Handler {
|
||||||
|
return s.mux
|
||||||
|
}
|
||||||
|
|
||||||
|
type alarmRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
Trigger string `json:"trigger"`
|
||||||
|
SoundPath string `json:"sound_path"`
|
||||||
|
Enabled *bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type alarmResponse struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
Trigger string `json:"trigger"`
|
||||||
|
SoundPath string `json:"sound_path"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
LastTriggered *string `json:"last_triggered"`
|
||||||
|
SnoozeCount int `json:"snooze_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toResponse(a db.Alarm) alarmResponse {
|
||||||
|
r := alarmResponse{
|
||||||
|
ID: a.ID,
|
||||||
|
Name: a.Name,
|
||||||
|
Description: a.Description,
|
||||||
|
Time: a.Time,
|
||||||
|
Trigger: a.Trigger,
|
||||||
|
SoundPath: a.SoundPath,
|
||||||
|
Enabled: a.Enabled,
|
||||||
|
CreatedAt: a.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
UpdatedAt: a.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
SnoozeCount: a.SnoozeCount,
|
||||||
|
}
|
||||||
|
if a.LastTriggered != nil {
|
||||||
|
t := a.LastTriggered.Format("2006-01-02T15:04:05Z")
|
||||||
|
r.LastTriggered = &t
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeError(w http.ResponseWriter, status int, msg string) {
|
||||||
|
writeJSON(w, status, map[string]string{"error": msg})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) listAlarms(w http.ResponseWriter, r *http.Request) {
|
||||||
|
alarms, err := s.store.ListAlarms()
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "failed to list alarms")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp := make([]alarmResponse, len(alarms))
|
||||||
|
for i, a := range alarms {
|
||||||
|
resp[i] = toResponse(a)
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getAlarm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.Atoi(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
alarm, err := s.store.GetAlarm(id)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusNotFound, "alarm not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, toResponse(alarm))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) createAlarm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req alarmRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid JSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Name == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Trigger == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "trigger is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
alarm := db.Alarm{
|
||||||
|
Name: req.Name,
|
||||||
|
Description: req.Description,
|
||||||
|
Time: req.Time,
|
||||||
|
Trigger: req.Trigger,
|
||||||
|
SoundPath: req.SoundPath,
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
if alarm.SoundPath == "" {
|
||||||
|
alarm.SoundPath = "default"
|
||||||
|
}
|
||||||
|
if req.Enabled != nil {
|
||||||
|
alarm.Enabled = *req.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := s.store.CreateAlarm(alarm)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "failed to create alarm")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
created, err := s.store.GetAlarm(id)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "failed to read created alarm")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.notify()
|
||||||
|
writeJSON(w, http.StatusCreated, toResponse(created))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) updateAlarm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.Atoi(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, err := s.store.GetAlarm(id)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusNotFound, "alarm not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req alarmRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid JSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != "" {
|
||||||
|
existing.Name = req.Name
|
||||||
|
}
|
||||||
|
if req.Description != "" {
|
||||||
|
existing.Description = req.Description
|
||||||
|
}
|
||||||
|
if req.Time != "" {
|
||||||
|
existing.Time = req.Time
|
||||||
|
}
|
||||||
|
if req.Trigger != "" {
|
||||||
|
existing.Trigger = req.Trigger
|
||||||
|
}
|
||||||
|
if req.SoundPath != "" {
|
||||||
|
existing.SoundPath = req.SoundPath
|
||||||
|
}
|
||||||
|
if req.Enabled != nil {
|
||||||
|
existing.Enabled = *req.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.store.UpdateAlarm(existing); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "failed to update alarm")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, _ := s.store.GetAlarm(id)
|
||||||
|
s.notify()
|
||||||
|
writeJSON(w, http.StatusOK, toResponse(updated))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) deleteAlarm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.Atoi(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.store.GetAlarm(id); err != nil {
|
||||||
|
writeError(w, http.StatusNotFound, "alarm not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.store.DeleteAlarm(id); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "failed to delete alarm")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.notify()
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) toggleAlarm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.Atoi(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.store.GetAlarm(id); err != nil {
|
||||||
|
writeError(w, http.StatusNotFound, "alarm not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.store.ToggleAlarm(id); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "failed to toggle alarm")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toggled, _ := s.store.GetAlarm(id)
|
||||||
|
s.notify()
|
||||||
|
writeJSON(w, http.StatusOK, toResponse(toggled))
|
||||||
|
}
|
||||||
4
db/db.go
4
db/db.go
@@ -200,6 +200,7 @@ type Settings struct {
|
|||||||
BlinkOffMs int
|
BlinkOffMs int
|
||||||
ColorClock string
|
ColorClock string
|
||||||
ColorAlarm string
|
ColorAlarm string
|
||||||
|
ShowSeconds bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultSettings() Settings {
|
func DefaultSettings() Settings {
|
||||||
@@ -211,6 +212,7 @@ func DefaultSettings() Settings {
|
|||||||
BlinkOffMs: 500,
|
BlinkOffMs: 500,
|
||||||
ColorClock: "#00FF88",
|
ColorClock: "#00FF88",
|
||||||
ColorAlarm: "#FF4444",
|
ColorAlarm: "#FF4444",
|
||||||
|
ShowSeconds: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +225,7 @@ func (s *Store) LoadSettings() Settings {
|
|||||||
cfg.BlinkOffMs = s.getSettingInt("blink_off_ms", cfg.BlinkOffMs)
|
cfg.BlinkOffMs = s.getSettingInt("blink_off_ms", cfg.BlinkOffMs)
|
||||||
cfg.ColorClock = s.GetSetting("color_clock", cfg.ColorClock)
|
cfg.ColorClock = s.GetSetting("color_clock", cfg.ColorClock)
|
||||||
cfg.ColorAlarm = s.GetSetting("color_alarm", cfg.ColorAlarm)
|
cfg.ColorAlarm = s.GetSetting("color_alarm", cfg.ColorAlarm)
|
||||||
|
cfg.ShowSeconds = s.GetSetting("show_seconds", "true") == "true"
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +238,7 @@ func (s *Store) SaveSettings(cfg Settings) error {
|
|||||||
"blink_off_ms": fmt.Sprintf("%d", cfg.BlinkOffMs),
|
"blink_off_ms": fmt.Sprintf("%d", cfg.BlinkOffMs),
|
||||||
"color_clock": cfg.ColorClock,
|
"color_clock": cfg.ColorClock,
|
||||||
"color_alarm": cfg.ColorAlarm,
|
"color_alarm": cfg.ColorAlarm,
|
||||||
|
"show_seconds": fmt.Sprintf("%t", cfg.ShowSeconds),
|
||||||
}
|
}
|
||||||
for k, v := range pairs {
|
for k, v := range pairs {
|
||||||
if err := s.SetSetting(k, v); err != nil {
|
if err := s.SetSetting(k, v); err != nil {
|
||||||
|
|||||||
62
main.go
62
main.go
@@ -2,7 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"woke/api"
|
||||||
"woke/db"
|
"woke/db"
|
||||||
"woke/player"
|
"woke/player"
|
||||||
"woke/scheduler"
|
"woke/scheduler"
|
||||||
@@ -11,7 +13,57 @@ import (
|
|||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const helpText = `woke - TUI alarm clock with REST API
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
woke Start the TUI (API server runs on :9119)
|
||||||
|
woke --help Show this help
|
||||||
|
|
||||||
|
API endpoints (http://localhost:9119):
|
||||||
|
|
||||||
|
List alarms:
|
||||||
|
curl http://localhost:9119/api/alarms
|
||||||
|
|
||||||
|
Get alarm:
|
||||||
|
curl http://localhost:9119/api/alarms/1
|
||||||
|
|
||||||
|
Create alarm (one-shot):
|
||||||
|
curl -X POST http://localhost:9119/api/alarms \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"name": "Standup", "time": "09:00", "trigger": "once"}'
|
||||||
|
|
||||||
|
Create alarm (cron, weekdays at 7:30):
|
||||||
|
curl -X POST http://localhost:9119/api/alarms \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"name": "Morning", "time": "07:30", "trigger": "30 7 * * 1-5"}'
|
||||||
|
|
||||||
|
Update alarm:
|
||||||
|
curl -X PUT http://localhost:9119/api/alarms/1 \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"name": "New name", "time": "08:00"}'
|
||||||
|
|
||||||
|
Toggle alarm on/off:
|
||||||
|
curl -X PATCH http://localhost:9119/api/alarms/1/toggle
|
||||||
|
|
||||||
|
Delete alarm:
|
||||||
|
curl -X DELETE http://localhost:9119/api/alarms/1
|
||||||
|
|
||||||
|
TUI keybindings:
|
||||||
|
j/k Navigate alarms
|
||||||
|
a Add alarm
|
||||||
|
e Edit alarm
|
||||||
|
d Delete alarm
|
||||||
|
space Toggle enabled
|
||||||
|
c Settings
|
||||||
|
q Quit
|
||||||
|
`
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if len(os.Args) > 1 && (os.Args[1] == "--help" || os.Args[1] == "-h") {
|
||||||
|
fmt.Print(helpText)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
store, err := db.Open()
|
store, err := db.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to open database: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to open database: %v\n", err)
|
||||||
@@ -27,6 +79,16 @@ func main() {
|
|||||||
model := ui.NewModel(store, sched, pl)
|
model := ui.NewModel(store, sched, pl)
|
||||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||||
|
|
||||||
|
// Start HTTP API server — notify TUI on mutations via p.Send
|
||||||
|
srv := api.New(store, func() { p.Send(ui.AlarmsChangedMsg{}) })
|
||||||
|
go func() {
|
||||||
|
addr := ":9119"
|
||||||
|
fmt.Fprintf(os.Stderr, "API server listening on %s\n", addr)
|
||||||
|
if err := http.ListenAndServe(addr, srv.Handler()); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "API server error: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if _, err := p.Run(); err != nil {
|
if _, err := p.Run(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -132,12 +132,17 @@ var bigColonBlink = []string{
|
|||||||
|
|
||||||
// RenderBigClock renders the current time as massive ASCII block digits.
|
// RenderBigClock renders the current time as massive ASCII block digits.
|
||||||
// Format: HH:MM:SS in 24h. blinkOnMs/blinkOffMs control the colon blink cycle.
|
// Format: HH:MM:SS in 24h. blinkOnMs/blinkOffMs control the colon blink cycle.
|
||||||
func RenderBigClock(t time.Time, blinkOnMs, blinkOffMs int) string {
|
func RenderBigClock(t time.Time, blinkOnMs, blinkOffMs int, showSeconds bool) string {
|
||||||
h := t.Hour()
|
h := t.Hour()
|
||||||
m := t.Minute()
|
m := t.Minute()
|
||||||
s := t.Second()
|
s := t.Second()
|
||||||
|
|
||||||
timeStr := fmt.Sprintf("%02d:%02d:%02d", h, m, s)
|
var timeStr string
|
||||||
|
if showSeconds {
|
||||||
|
timeStr = fmt.Sprintf("%02d:%02d:%02d", h, m, s)
|
||||||
|
} else {
|
||||||
|
timeStr = fmt.Sprintf("%02d:%02d", h, m)
|
||||||
|
}
|
||||||
|
|
||||||
cycle := int64(blinkOnMs + blinkOffMs)
|
cycle := int64(blinkOnMs + blinkOffMs)
|
||||||
colonVisible := t.UnixMilli()%cycle < int64(blinkOnMs)
|
colonVisible := t.UnixMilli()%cycle < int64(blinkOnMs)
|
||||||
|
|||||||
30
ui/config.go
30
ui/config.go
@@ -19,6 +19,7 @@ const (
|
|||||||
cfgBlinkOffMs
|
cfgBlinkOffMs
|
||||||
cfgColorClock
|
cfgColorClock
|
||||||
cfgColorAlarm
|
cfgColorAlarm
|
||||||
|
cfgShowSeconds
|
||||||
cfgFieldCount
|
cfgFieldCount
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,6 +38,11 @@ func newConfigModel(cfg db.Settings) *configModel {
|
|||||||
c.fields[cfgBlinkOffMs] = strconv.Itoa(cfg.BlinkOffMs)
|
c.fields[cfgBlinkOffMs] = strconv.Itoa(cfg.BlinkOffMs)
|
||||||
c.fields[cfgColorClock] = cfg.ColorClock
|
c.fields[cfgColorClock] = cfg.ColorClock
|
||||||
c.fields[cfgColorAlarm] = cfg.ColorAlarm
|
c.fields[cfgColorAlarm] = cfg.ColorAlarm
|
||||||
|
if cfg.ShowSeconds {
|
||||||
|
c.fields[cfgShowSeconds] = "true"
|
||||||
|
} else {
|
||||||
|
c.fields[cfgShowSeconds] = "false"
|
||||||
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +80,27 @@ func (c *configModel) HandleKey(msg tea.KeyMsg, m *Model) (tea.Model, tea.Cmd) {
|
|||||||
c.err = ""
|
c.err = ""
|
||||||
return *m, nil
|
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:
|
default:
|
||||||
if len(key) == 1 {
|
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.fields[c.active] += key
|
||||||
c.err = ""
|
c.err = ""
|
||||||
}
|
}
|
||||||
@@ -130,6 +155,9 @@ func (c *configModel) save(m *Model) (tea.Model, tea.Cmd) {
|
|||||||
ColorAlarm: colorAlarm,
|
ColorAlarm: colorAlarm,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showSec := strings.TrimSpace(c.fields[cfgShowSeconds])
|
||||||
|
cfg.ShowSeconds = showSec == "true"
|
||||||
|
|
||||||
if cfg.DefaultSound == "" {
|
if cfg.DefaultSound == "" {
|
||||||
cfg.DefaultSound = "default"
|
cfg.DefaultSound = "default"
|
||||||
}
|
}
|
||||||
@@ -156,6 +184,7 @@ func (c *configModel) View() string {
|
|||||||
"Blink off (ms):",
|
"Blink off (ms):",
|
||||||
"Clock color:",
|
"Clock color:",
|
||||||
"Alarm color:",
|
"Alarm color:",
|
||||||
|
"Show seconds:",
|
||||||
}
|
}
|
||||||
|
|
||||||
hints := [cfgFieldCount]string{
|
hints := [cfgFieldCount]string{
|
||||||
@@ -166,6 +195,7 @@ func (c *configModel) View() string {
|
|||||||
"100-5000",
|
"100-5000",
|
||||||
"hex e.g. #00FF88",
|
"hex e.g. #00FF88",
|
||||||
"hex e.g. #FF4444",
|
"hex e.g. #FF4444",
|
||||||
|
"space to toggle",
|
||||||
}
|
}
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type alarmFiredMsg scheduler.AlarmEvent
|
|||||||
type alarmsLoadedMsg []db.Alarm
|
type alarmsLoadedMsg []db.Alarm
|
||||||
type snoozeFireMsg db.Alarm
|
type snoozeFireMsg db.Alarm
|
||||||
type autoTimeoutMsg struct{}
|
type autoTimeoutMsg struct{}
|
||||||
|
type AlarmsChangedMsg struct{}
|
||||||
|
|
||||||
// Model is the main bubbletea model.
|
// Model is the main bubbletea model.
|
||||||
type Model struct {
|
type Model struct {
|
||||||
@@ -149,6 +150,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
case AlarmsChangedMsg:
|
||||||
|
m.refreshAlarms()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case tickMsg:
|
case tickMsg:
|
||||||
m.now = time.Time(msg)
|
m.now = time.Time(msg)
|
||||||
if m.firingAlarm != nil {
|
if m.firingAlarm != nil {
|
||||||
@@ -350,7 +355,7 @@ func (m Model) View() string {
|
|||||||
var sections []string
|
var sections []string
|
||||||
|
|
||||||
// Big clock
|
// Big clock
|
||||||
clockStr := RenderBigClock(m.now, m.settings.BlinkOnMs, m.settings.BlinkOffMs)
|
clockStr := RenderBigClock(m.now, m.settings.BlinkOnMs, m.settings.BlinkOffMs, m.settings.ShowSeconds)
|
||||||
if m.firingAlarm != nil && m.firingBlink {
|
if m.firingAlarm != nil && m.firingBlink {
|
||||||
clockStr = ClockAlarmStyle.Render(clockStr)
|
clockStr = ClockAlarmStyle.Render(clockStr)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user