Expensive bee
This commit is contained in:
17
api/api.go
17
api/api.go
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"woke/db"
|
||||
)
|
||||
|
||||
@@ -15,10 +16,11 @@ type Server struct {
|
||||
store *db.Store
|
||||
mux *http.ServeMux
|
||||
notify Notifier
|
||||
password string
|
||||
}
|
||||
|
||||
func New(store *db.Store, notify Notifier) *Server {
|
||||
s := &Server{store: store, notify: notify}
|
||||
func New(store *db.Store, password string, notify Notifier) *Server {
|
||||
s := &Server{store: store, notify: notify, password: password}
|
||||
s.mux = http.NewServeMux()
|
||||
s.mux.HandleFunc("GET /api/alarms", s.listAlarms)
|
||||
s.mux.HandleFunc("GET /api/alarms/{id}", s.getAlarm)
|
||||
@@ -30,7 +32,18 @@ func New(store *db.Store, notify Notifier) *Server {
|
||||
}
|
||||
|
||||
func (s *Server) Handler() http.Handler {
|
||||
if s.password == "" {
|
||||
return s.mux
|
||||
}
|
||||
// Wrap with auth middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(auth, "Bearer ") || auth[7:] != s.password {
|
||||
writeError(w, http.StatusUnauthorized, "invalid or missing authorization")
|
||||
return
|
||||
}
|
||||
s.mux.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
type alarmRequest struct {
|
||||
|
||||
192
api/client.go
Normal file
192
api/client.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
"woke/db"
|
||||
)
|
||||
|
||||
// Client implements db.AlarmStore via HTTP calls to a remote woke server.
|
||||
type Client struct {
|
||||
baseURL string
|
||||
password string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
func NewClient(baseURL, password string) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
password: password,
|
||||
http: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) do(method, path string, body any) (*http.Response, error) {
|
||||
var reqBody *bytes.Buffer
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody = bytes.NewBuffer(data)
|
||||
} else {
|
||||
reqBody = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, c.baseURL+path, reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if c.password != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.password)
|
||||
}
|
||||
return c.http.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) ListAlarms() ([]db.Alarm, error) {
|
||||
resp, err := c.do("GET", "/api/alarms", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("server returned %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var alarms []alarmResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&alarms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]db.Alarm, len(alarms))
|
||||
for i, a := range alarms {
|
||||
result[i] = a.toAlarm()
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetAlarm(id int) (db.Alarm, error) {
|
||||
resp, err := c.do("GET", fmt.Sprintf("/api/alarms/%d", id), nil)
|
||||
if err != nil {
|
||||
return db.Alarm{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return db.Alarm{}, fmt.Errorf("server returned %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var a alarmResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&a); err != nil {
|
||||
return db.Alarm{}, err
|
||||
}
|
||||
return a.toAlarm(), nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateAlarm(a db.Alarm) (int, error) {
|
||||
req := alarmRequest{
|
||||
Name: a.Name,
|
||||
Description: a.Description,
|
||||
Time: a.Time,
|
||||
Trigger: a.Trigger,
|
||||
SoundPath: a.SoundPath,
|
||||
Enabled: &a.Enabled,
|
||||
}
|
||||
resp, err := c.do("POST", "/api/alarms", req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return 0, fmt.Errorf("server returned %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var created alarmResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return created.ID, nil
|
||||
}
|
||||
|
||||
func (c *Client) UpdateAlarm(a db.Alarm) error {
|
||||
req := alarmRequest{
|
||||
Name: a.Name,
|
||||
Description: a.Description,
|
||||
Time: a.Time,
|
||||
Trigger: a.Trigger,
|
||||
SoundPath: a.SoundPath,
|
||||
Enabled: &a.Enabled,
|
||||
}
|
||||
resp, err := c.do("PUT", fmt.Sprintf("/api/alarms/%d", a.ID), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("server returned %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) DeleteAlarm(id int) error {
|
||||
resp, err := c.do("DELETE", fmt.Sprintf("/api/alarms/%d", id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return fmt.Errorf("server returned %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ToggleAlarm(id int) error {
|
||||
resp, err := c.do("PATCH", fmt.Sprintf("/api/alarms/%d/toggle", id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("server returned %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) MarkTriggered(id int) error {
|
||||
// Client mode doesn't mark triggered — server handles this
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a alarmResponse) toAlarm() db.Alarm {
|
||||
alarm := db.Alarm{
|
||||
ID: a.ID,
|
||||
Name: a.Name,
|
||||
Description: a.Description,
|
||||
Time: a.Time,
|
||||
Trigger: a.Trigger,
|
||||
SoundPath: a.SoundPath,
|
||||
Enabled: a.Enabled,
|
||||
SnoozeCount: a.SnoozeCount,
|
||||
}
|
||||
if t, err := time.Parse("2006-01-02T15:04:05Z", a.CreatedAt); err == nil {
|
||||
alarm.CreatedAt = t
|
||||
}
|
||||
if t, err := time.Parse("2006-01-02T15:04:05Z", a.UpdatedAt); err == nil {
|
||||
alarm.UpdatedAt = t
|
||||
}
|
||||
if a.LastTriggered != nil {
|
||||
if t, err := time.Parse("2006-01-02T15:04:05Z", *a.LastTriggered); err == nil {
|
||||
alarm.LastTriggered = &t
|
||||
}
|
||||
}
|
||||
return alarm
|
||||
}
|
||||
18
db/db.go
18
db/db.go
@@ -201,6 +201,12 @@ type Settings struct {
|
||||
ColorClock string
|
||||
ColorAlarm string
|
||||
ShowSeconds bool
|
||||
|
||||
// Client/server mode
|
||||
ServerURL string // If set, run as client connecting to this URL
|
||||
ServerPassword string // Password for authenticating to remote server
|
||||
APIPassword string // Password required for incoming API requests (host mode)
|
||||
PollSeconds int // How often to poll server in client mode
|
||||
}
|
||||
|
||||
func DefaultSettings() Settings {
|
||||
@@ -213,6 +219,10 @@ func DefaultSettings() Settings {
|
||||
ColorClock: "#00FF88",
|
||||
ColorAlarm: "#FF4444",
|
||||
ShowSeconds: true,
|
||||
ServerURL: "",
|
||||
ServerPassword: "",
|
||||
APIPassword: "",
|
||||
PollSeconds: 5,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +236,10 @@ func (s *Store) LoadSettings() Settings {
|
||||
cfg.ColorClock = s.GetSetting("color_clock", cfg.ColorClock)
|
||||
cfg.ColorAlarm = s.GetSetting("color_alarm", cfg.ColorAlarm)
|
||||
cfg.ShowSeconds = s.GetSetting("show_seconds", "true") == "true"
|
||||
cfg.ServerURL = s.GetSetting("server_url", cfg.ServerURL)
|
||||
cfg.ServerPassword = s.GetSetting("server_password", cfg.ServerPassword)
|
||||
cfg.APIPassword = s.GetSetting("api_password", cfg.APIPassword)
|
||||
cfg.PollSeconds = s.getSettingInt("poll_seconds", cfg.PollSeconds)
|
||||
return cfg
|
||||
}
|
||||
|
||||
@@ -239,6 +253,10 @@ func (s *Store) SaveSettings(cfg Settings) error {
|
||||
"color_clock": cfg.ColorClock,
|
||||
"color_alarm": cfg.ColorAlarm,
|
||||
"show_seconds": fmt.Sprintf("%t", cfg.ShowSeconds),
|
||||
"server_url": cfg.ServerURL,
|
||||
"server_password": cfg.ServerPassword,
|
||||
"api_password": cfg.APIPassword,
|
||||
"poll_seconds": fmt.Sprintf("%d", cfg.PollSeconds),
|
||||
}
|
||||
for k, v := range pairs {
|
||||
if err := s.SetSetting(k, v); err != nil {
|
||||
|
||||
13
db/interface.go
Normal file
13
db/interface.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package db
|
||||
|
||||
// AlarmStore defines the interface for alarm CRUD operations.
|
||||
// Implemented by *Store (local SQLite) and api.Client (remote HTTP).
|
||||
type AlarmStore interface {
|
||||
ListAlarms() ([]Alarm, error)
|
||||
GetAlarm(id int) (Alarm, error)
|
||||
CreateAlarm(a Alarm) (int, error)
|
||||
UpdateAlarm(a Alarm) error
|
||||
DeleteAlarm(id int) error
|
||||
ToggleAlarm(id int) error
|
||||
MarkTriggered(id int) error
|
||||
}
|
||||
38
main.go
38
main.go
@@ -16,9 +16,19 @@ import (
|
||||
const helpText = `woke - TUI alarm clock with REST API
|
||||
|
||||
Usage:
|
||||
woke Start the TUI (API server runs on :9119)
|
||||
woke Start the TUI (API server runs on :9119 in host mode)
|
||||
woke --help Show this help
|
||||
|
||||
Modes:
|
||||
Host mode (default): Uses local SQLite, exposes REST API on :9119
|
||||
Client mode: Connects to remote woke server, syncs alarms, fires locally
|
||||
|
||||
Configure mode in Settings (press 'c'):
|
||||
- Server URL: empty = host mode, or http://host:9119 for client mode
|
||||
- Server pass: password to authenticate with remote server
|
||||
- API password: password required for incoming API requests (host mode)
|
||||
- Poll seconds: how often client polls server for alarm updates
|
||||
|
||||
API endpoints (http://localhost:9119):
|
||||
|
||||
List alarms:
|
||||
@@ -48,6 +58,9 @@ API endpoints (http://localhost:9119):
|
||||
Delete alarm:
|
||||
curl -X DELETE http://localhost:9119/api/alarms/1
|
||||
|
||||
With authentication (if API password is set):
|
||||
curl -H 'Authorization: Bearer yourpassword' http://localhost:9119/api/alarms
|
||||
|
||||
TUI keybindings:
|
||||
j/k Navigate alarms
|
||||
a Add alarm
|
||||
@@ -64,6 +77,7 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Always open local store for settings
|
||||
store, err := db.Open()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to open database: %v\n", err)
|
||||
@@ -71,16 +85,29 @@ func main() {
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
settings := store.LoadSettings()
|
||||
clientMode := settings.ServerURL != ""
|
||||
|
||||
// Determine alarm store: local DB or remote API
|
||||
var alarmStore db.AlarmStore
|
||||
if clientMode {
|
||||
alarmStore = api.NewClient(settings.ServerURL, settings.ServerPassword)
|
||||
fmt.Fprintf(os.Stderr, "Client mode: connecting to %s\n", settings.ServerURL)
|
||||
} else {
|
||||
alarmStore = store
|
||||
}
|
||||
|
||||
pl := player.New()
|
||||
sched := scheduler.New(store)
|
||||
sched := scheduler.New(alarmStore, clientMode)
|
||||
sched.Start()
|
||||
defer sched.Stop()
|
||||
|
||||
model := ui.NewModel(store, sched, pl)
|
||||
model := ui.NewModel(store, alarmStore, sched, pl, clientMode)
|
||||
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{}) })
|
||||
// Start HTTP API server only in host mode
|
||||
if !clientMode {
|
||||
srv := api.New(store, settings.APIPassword, func() { p.Send(ui.AlarmsChangedMsg{}) })
|
||||
go func() {
|
||||
addr := ":9119"
|
||||
fmt.Fprintf(os.Stderr, "API server listening on %s\n", addr)
|
||||
@@ -88,6 +115,7 @@ func main() {
|
||||
fmt.Fprintf(os.Stderr, "API server error: %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if _, err := p.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
|
||||
@@ -15,16 +15,18 @@ type AlarmEvent struct {
|
||||
|
||||
// Scheduler checks alarms every second and fires events when they trigger.
|
||||
type Scheduler struct {
|
||||
store *db.Store
|
||||
store db.AlarmStore
|
||||
events chan AlarmEvent
|
||||
stop chan struct{}
|
||||
mu sync.Mutex
|
||||
parser cron.Parser
|
||||
clientMode bool // If true, skip MarkTriggered/ToggleAlarm (server handles it)
|
||||
}
|
||||
|
||||
func New(store *db.Store) *Scheduler {
|
||||
func New(store db.AlarmStore, clientMode bool) *Scheduler {
|
||||
return &Scheduler{
|
||||
store: store,
|
||||
clientMode: clientMode,
|
||||
events: make(chan AlarmEvent, 10),
|
||||
stop: make(chan struct{}),
|
||||
parser: cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow),
|
||||
@@ -90,12 +92,15 @@ func (s *Scheduler) check(now time.Time, firedThisMinute map[int]string) {
|
||||
|
||||
if s.shouldTrigger(a, now) {
|
||||
firedThisMinute[a.ID] = currentMinute
|
||||
_ = s.store.MarkTriggered(a.ID)
|
||||
|
||||
// In host mode, update the DB; in client mode, server handles this
|
||||
if !s.clientMode {
|
||||
_ = s.store.MarkTriggered(a.ID)
|
||||
// Disable one-shot alarms after triggering
|
||||
if a.Trigger == "once" {
|
||||
_ = s.store.ToggleAlarm(a.ID)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case s.events <- AlarmEvent{Alarm: a}:
|
||||
|
||||
27
ui/config.go
27
ui/config.go
@@ -20,6 +20,10 @@ const (
|
||||
cfgColorClock
|
||||
cfgColorAlarm
|
||||
cfgShowSeconds
|
||||
cfgServerURL
|
||||
cfgServerPassword
|
||||
cfgAPIPassword
|
||||
cfgPollSeconds
|
||||
cfgFieldCount
|
||||
)
|
||||
|
||||
@@ -43,6 +47,10 @@ func newConfigModel(cfg db.Settings) *configModel {
|
||||
} else {
|
||||
c.fields[cfgShowSeconds] = "false"
|
||||
}
|
||||
c.fields[cfgServerURL] = cfg.ServerURL
|
||||
c.fields[cfgServerPassword] = cfg.ServerPassword
|
||||
c.fields[cfgAPIPassword] = cfg.APIPassword
|
||||
c.fields[cfgPollSeconds] = strconv.Itoa(cfg.PollSeconds)
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -145,6 +153,12 @@ func (c *configModel) save(m *Model) (tea.Model, tea.Cmd) {
|
||||
return *m, nil
|
||||
}
|
||||
|
||||
pollSeconds, err := strconv.Atoi(strings.TrimSpace(c.fields[cfgPollSeconds]))
|
||||
if err != nil || pollSeconds < 1 || pollSeconds > 60 {
|
||||
c.err = "Poll seconds must be 1-60"
|
||||
return *m, nil
|
||||
}
|
||||
|
||||
cfg := db.Settings{
|
||||
SnoozeMinutes: snooze,
|
||||
TimeoutMinutes: timeout,
|
||||
@@ -158,6 +172,11 @@ func (c *configModel) save(m *Model) (tea.Model, tea.Cmd) {
|
||||
showSec := strings.TrimSpace(c.fields[cfgShowSeconds])
|
||||
cfg.ShowSeconds = showSec == "true"
|
||||
|
||||
cfg.ServerURL = strings.TrimSpace(c.fields[cfgServerURL])
|
||||
cfg.ServerPassword = strings.TrimSpace(c.fields[cfgServerPassword])
|
||||
cfg.APIPassword = strings.TrimSpace(c.fields[cfgAPIPassword])
|
||||
cfg.PollSeconds = pollSeconds
|
||||
|
||||
if cfg.DefaultSound == "" {
|
||||
cfg.DefaultSound = "default"
|
||||
}
|
||||
@@ -185,6 +204,10 @@ func (c *configModel) View() string {
|
||||
"Clock color:",
|
||||
"Alarm color:",
|
||||
"Show seconds:",
|
||||
"Server URL:",
|
||||
"Server pass:",
|
||||
"API password:",
|
||||
"Poll (sec):",
|
||||
}
|
||||
|
||||
hints := [cfgFieldCount]string{
|
||||
@@ -196,6 +219,10 @@ func (c *configModel) View() string {
|
||||
"hex e.g. #00FF88",
|
||||
"hex e.g. #FF4444",
|
||||
"space to toggle",
|
||||
"empty=local, or http://host:9119",
|
||||
"password to auth with server",
|
||||
"password for incoming API requests",
|
||||
"1-60, client poll frequency",
|
||||
}
|
||||
|
||||
var lines []string
|
||||
|
||||
@@ -135,13 +135,13 @@ func (f *formModel) save(m *Model) (tea.Model, tea.Cmd) {
|
||||
if f.editing != nil {
|
||||
alarm.ID = f.editing.ID
|
||||
alarm.Enabled = f.editing.Enabled
|
||||
if err := m.store.UpdateAlarm(alarm); err != nil {
|
||||
if err := m.alarmStore.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 {
|
||||
if _, err := m.alarmStore.CreateAlarm(alarm); err != nil {
|
||||
f.err = fmt.Sprintf("Save failed: %v", err)
|
||||
return *m, nil
|
||||
}
|
||||
|
||||
48
ui/model.go
48
ui/model.go
@@ -29,12 +29,15 @@ type alarmsLoadedMsg []db.Alarm
|
||||
type snoozeFireMsg db.Alarm
|
||||
type autoTimeoutMsg struct{}
|
||||
type AlarmsChangedMsg struct{}
|
||||
type pollTickMsg struct{}
|
||||
|
||||
// Model is the main bubbletea model.
|
||||
type Model struct {
|
||||
store *db.Store
|
||||
store *db.Store // For settings (always local)
|
||||
alarmStore db.AlarmStore // For alarm CRUD (local or remote)
|
||||
scheduler *scheduler.Scheduler
|
||||
player *player.Player
|
||||
clientMode bool
|
||||
|
||||
// State
|
||||
alarms []db.Alarm
|
||||
@@ -63,11 +66,13 @@ type Model struct {
|
||||
statusMsg string
|
||||
}
|
||||
|
||||
func NewModel(store *db.Store, sched *scheduler.Scheduler, pl *player.Player) Model {
|
||||
func NewModel(store *db.Store, alarmStore db.AlarmStore, sched *scheduler.Scheduler, pl *player.Player, clientMode bool) Model {
|
||||
m := Model{
|
||||
store: store,
|
||||
alarmStore: alarmStore,
|
||||
scheduler: sched,
|
||||
player: pl,
|
||||
clientMode: clientMode,
|
||||
now: time.Now(),
|
||||
settings: store.LoadSettings(),
|
||||
}
|
||||
@@ -107,11 +112,15 @@ func (m *Model) applySettings() {
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
cmds := []tea.Cmd{
|
||||
tick(),
|
||||
listenForAlarms(m.scheduler),
|
||||
loadAlarmsCmd(m.store),
|
||||
)
|
||||
loadAlarmsCmd(m.alarmStore),
|
||||
}
|
||||
if m.clientMode {
|
||||
cmds = append(cmds, pollTick(m.settings.PollSeconds))
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func tick() tea.Cmd {
|
||||
@@ -127,7 +136,7 @@ func listenForAlarms(sched *scheduler.Scheduler) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func loadAlarmsCmd(store *db.Store) tea.Cmd {
|
||||
func loadAlarmsCmd(store db.AlarmStore) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
alarms, err := store.ListAlarms()
|
||||
if err != nil {
|
||||
@@ -137,6 +146,12 @@ func loadAlarmsCmd(store *db.Store) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func pollTick(seconds int) tea.Cmd {
|
||||
return tea.Tick(time.Duration(seconds)*time.Second, func(t time.Time) tea.Msg {
|
||||
return pollTickMsg{}
|
||||
})
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
@@ -154,6 +169,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.refreshAlarms()
|
||||
return m, nil
|
||||
|
||||
case pollTickMsg:
|
||||
m.refreshAlarms()
|
||||
return m, pollTick(m.settings.PollSeconds)
|
||||
|
||||
case tickMsg:
|
||||
m.now = time.Time(msg)
|
||||
if m.firingAlarm != nil {
|
||||
@@ -244,7 +263,7 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch key {
|
||||
case "y", "Y":
|
||||
if m.cursor < len(m.alarms) {
|
||||
_ = m.store.DeleteAlarm(m.alarms[m.cursor].ID)
|
||||
_ = m.alarmStore.DeleteAlarm(m.alarms[m.cursor].ID)
|
||||
m.refreshAlarms()
|
||||
if m.cursor >= len(m.alarms) && m.cursor > 0 {
|
||||
m.cursor--
|
||||
@@ -280,7 +299,7 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case " ":
|
||||
// Toggle enabled
|
||||
if m.cursor < len(m.alarms) {
|
||||
_ = m.store.ToggleAlarm(m.alarms[m.cursor].ID)
|
||||
_ = m.alarmStore.ToggleAlarm(m.alarms[m.cursor].ID)
|
||||
m.refreshAlarms()
|
||||
}
|
||||
case "a":
|
||||
@@ -341,7 +360,7 @@ func autoTimeoutCmd(dur time.Duration) tea.Cmd {
|
||||
}
|
||||
|
||||
func (m *Model) refreshAlarms() {
|
||||
alarms, err := m.store.ListAlarms()
|
||||
alarms, err := m.alarmStore.ListAlarms()
|
||||
if err == nil {
|
||||
m.alarms = alarms
|
||||
}
|
||||
@@ -372,6 +391,17 @@ func (m Model) View() string {
|
||||
dateLine = lipgloss.PlaceHorizontal(m.width, lipgloss.Center, dateLine)
|
||||
sections = append(sections, dateLine)
|
||||
|
||||
// Connection status
|
||||
var connStatus string
|
||||
if m.clientMode {
|
||||
connStatus = fmt.Sprintf("[Client: %s]", m.settings.ServerURL)
|
||||
} else {
|
||||
connStatus = "[Local]"
|
||||
}
|
||||
connLine := HelpStyle.Render(connStatus)
|
||||
connLine = lipgloss.PlaceHorizontal(m.width, lipgloss.Center, connLine)
|
||||
sections = append(sections, connLine)
|
||||
|
||||
// Snooze indicator
|
||||
if !m.snoozeUntil.IsZero() && m.now.Before(m.snoozeUntil) {
|
||||
snoozeText := fmt.Sprintf("[Snoozing %s until %s]", m.snoozeName, m.snoozeUntil.Format("15:04:05"))
|
||||
|
||||
Reference in New Issue
Block a user