Agreeable goat
This commit is contained in:
@@ -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])
|
||||||
|
|||||||
24
ui/form.go
24
ui/form.go
@@ -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 {
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
82
ui/model.go
82
ui/model.go
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user