Agreeable goat

This commit is contained in:
2026-02-01 21:27:52 +02:00
parent 06d5a6a779
commit f283c5e99e
4 changed files with 91 additions and 33 deletions

View File

@@ -131,7 +131,8 @@ 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. The colon blinks every second. // Format: HH:MM:SS in 24h. The colon blinks on a 500ms cycle (offset from
// the second boundary so it doesn't flip in sync with the digits).
func RenderBigClock(t time.Time) string { func RenderBigClock(t time.Time) string {
h := t.Hour() h := t.Hour()
m := t.Minute() m := t.Minute()
@@ -139,6 +140,9 @@ func RenderBigClock(t time.Time) string {
timeStr := fmt.Sprintf("%02d:%02d:%02d", h, m, s) timeStr := fmt.Sprintf("%02d:%02d:%02d", h, m, s)
// Colon visible for first 500ms of each second, hidden for last 500ms.
colonVisible := t.UnixMilli()%1000 < 500
var lines [7]string var lines [7]string
for i := range lines { for i := range lines {
var parts []string var parts []string
@@ -147,7 +151,7 @@ func RenderBigClock(t time.Time) string {
case ch >= '0' && ch <= '9': case ch >= '0' && ch <= '9':
parts = append(parts, bigDigits[ch-'0'][i]) parts = append(parts, bigDigits[ch-'0'][i])
case ch == ':': case ch == ':':
if s%2 == 0 { if colonVisible {
parts = append(parts, bigColon[i]) parts = append(parts, bigColon[i])
} else { } else {
parts = append(parts, bigColonBlink[i]) parts = append(parts, bigColonBlink[i])

View File

@@ -154,6 +154,12 @@ func (f *formModel) save(m *Model) (tea.Model, tea.Cmd) {
return *m, nil return *m, nil
} }
const (
formLabelWidth = 14 // longest label is "Time (HH:MM):" = 13 + 1 padding
formValueWidth = 30
formHintWidth = 32
)
func (f *formModel) View() string { func (f *formModel) View() string {
title := "Add Alarm" title := "Add Alarm"
if f.editing != nil { if f.editing != nil {
@@ -161,11 +167,11 @@ func (f *formModel) View() string {
} }
labels := [fieldCount]string{ labels := [fieldCount]string{
"Name: ", "Name:",
"Description: ", "Description:",
"Time (HH:MM):", "Time (HH:MM):",
"Trigger: ", "Trigger:",
"Sound: ", "Sound:",
} }
hints := [fieldCount]string{ hints := [fieldCount]string{
@@ -180,19 +186,27 @@ func (f *formModel) View() string {
lines = append(lines, TitleStyle.Render(title), "") lines = append(lines, TitleStyle.Render(title), "")
for i := formField(0); i < fieldCount; i++ { for i := formField(0); i < fieldCount; i++ {
label := FormLabelStyle.Render(labels[i]) // Right-align labels in a fixed-width column
value := f.fields[i] labelStr := fmt.Sprintf("%*s", formLabelWidth, labels[i])
hint := HelpStyle.Render(hints[i])
value := f.fields[i]
var style lipgloss.Style var style lipgloss.Style
if i == f.active { if i == f.active {
style = FormActiveStyle style = FormActiveStyle
value += "█" // cursor value += "█"
} else { } else {
style = FormLabelStyle style = FormLabelStyle
} }
line := fmt.Sprintf(" %s %s %s", label, style.Render(value), hint) // Truncate from the left if too long, then pad to fixed width
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) lines = append(lines, line)
} }

View File

@@ -10,6 +10,7 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/mattn/go-runewidth"
) )
// View modes // View modes
@@ -24,6 +25,7 @@ const (
// Messages // Messages
type tickMsg time.Time type tickMsg time.Time
type alarmFiredMsg scheduler.AlarmEvent type alarmFiredMsg scheduler.AlarmEvent
type alarmsLoadedMsg []db.Alarm
// Model is the main bubbletea model. // Model is the main bubbletea model.
type Model struct { type Model struct {
@@ -61,14 +63,14 @@ func NewModel(store *db.Store, sched *scheduler.Scheduler, pl *player.Player) Mo
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
return tea.Batch( return tea.Batch(
tickEverySecond(), tickEveryHalfSecond(),
listenForAlarms(m.scheduler), listenForAlarms(m.scheduler),
m.loadAlarms, loadAlarmsCmd(m.store),
) )
} }
func tickEverySecond() tea.Cmd { func tickEveryHalfSecond() tea.Cmd {
return tea.Every(time.Second, func(t time.Time) tea.Msg { return tea.Every(500*time.Millisecond, func(t time.Time) tea.Msg {
return tickMsg(t) return tickMsg(t)
}) })
} }
@@ -80,13 +82,14 @@ func listenForAlarms(sched *scheduler.Scheduler) tea.Cmd {
} }
} }
func (m *Model) loadAlarms() tea.Msg { func loadAlarmsCmd(store *db.Store) tea.Cmd {
alarms, err := m.store.ListAlarms() return func() tea.Msg {
alarms, err := store.ListAlarms()
if err != nil { if err != nil {
return nil return alarmsLoadedMsg(nil)
}
return alarmsLoadedMsg(alarms)
} }
m.alarms = alarms
return nil
} }
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -96,12 +99,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height m.height = msg.Height
return m, nil return m, nil
case alarmsLoadedMsg:
if msg != nil {
m.alarms = []db.Alarm(msg)
}
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 {
m.firingBlink = !m.firingBlink m.firingBlink = !m.firingBlink
} }
return m, tickEverySecond() return m, tickEveryHalfSecond()
case alarmFiredMsg: case alarmFiredMsg:
alarm := msg.Alarm alarm := msg.Alarm
@@ -284,37 +293,64 @@ func (m Model) View() string {
help := m.renderHelp() help := m.renderHelp()
sections = append(sections, "", lipgloss.PlaceHorizontal(m.width, lipgloss.Center, help)) sections = append(sections, "", lipgloss.PlaceHorizontal(m.width, lipgloss.Center, help))
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Top, return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center,
strings.Join(sections, "\n"), strings.Join(sections, "\n"),
) )
} }
const (
alarmColPrefix = 6 // "▸ ● " = 4 display cols + 2 padding
alarmColTrigger = 18 // enough for cron like "30 7 * * 1-5"
alarmColName = 22
)
// padRight pads s to exactly width display columns using runewidth.
func padRight(s string, width int) string {
w := runewidth.StringWidth(s)
if w >= width {
return runewidth.Truncate(s, width, "…")
}
return s + strings.Repeat(" ", width-w)
}
func (m Model) renderAlarmList() string { func (m Model) renderAlarmList() string {
if len(m.alarms) == 0 { if len(m.alarms) == 0 {
return StatusStyle.Render("No alarms. Press 'a' to add one.") return StatusStyle.Render("No alarms. Press 'a' to add one.")
} }
title := TitleStyle.Render("Alarms") title := TitleStyle.Render("Alarms")
// Header — prefix padded to same display width as row prefixes
header := padRight("", alarmColPrefix) +
padRight("Time/Trigger", alarmColTrigger) +
padRight("Name", alarmColName) +
"Description"
var lines []string var lines []string
lines = append(lines, title, "") lines = append(lines, title, "")
lines = append(lines, DividerStyle.Render(header))
for i, a := range m.alarms { for i, a := range m.alarms {
cursor := " " var prefix string
if i == m.cursor { if i == m.cursor {
cursor = "▸ " prefix = "▸ "
} else {
prefix = " "
} }
status := "●"
var style lipgloss.Style
if a.Enabled { if a.Enabled {
prefix += "● "
} else {
prefix += "○ "
}
var style lipgloss.Style
if i == m.cursor {
style = AlarmSelectedStyle
} else if a.Enabled {
style = AlarmEnabledStyle style = AlarmEnabledStyle
} else { } else {
style = AlarmDisabledStyle style = AlarmDisabledStyle
status = "○"
}
if i == m.cursor {
style = AlarmSelectedStyle
} }
trigger := a.Time trigger := a.Time
@@ -322,7 +358,11 @@ func (m Model) renderAlarmList() string {
trigger = a.Trigger trigger = a.Trigger
} }
line := fmt.Sprintf("%s%s %s %-20s %s", cursor, status, trigger, a.Name, a.Description) line := padRight(prefix, alarmColPrefix) +
padRight(trigger, alarmColTrigger) +
padRight(a.Name, alarmColName) +
a.Description
lines = append(lines, style.Render(line)) lines = append(lines, style.Render(line))
} }

BIN
woke

Binary file not shown.