diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-24 10:32:28 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-24 10:32:28 +0200 |
| commit | c9f3bf46f0de09edb796e35f3dcff349febf75c9 (patch) | |
| tree | c1a0b98b484e4da557c1ab205dfff0d92ce8ac36 /internal/tui | |
| parent | a865374755480a4aef2a6156afccdf08a91422ea (diff) | |
| download | lectio-c9f3bf46f0de09edb796e35f3dcff349febf75c9.tar.gz lectio-c9f3bf46f0de09edb796e35f3dcff349febf75c9.zip | |
dayinfo: show feast/day name + colour (modern niedziela + traditional missalemeum) in cli/tui/web; v0.5.0
Diffstat (limited to 'internal/tui')
| -rw-r--r-- | internal/tui/tui.go | 68 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 35 |
2 files changed, 87 insertions, 16 deletions
diff --git a/internal/tui/tui.go b/internal/tui/tui.go index db0e8da..c58db6d 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -26,6 +26,7 @@ type Model struct { verIdx int date string sections []liturgy.Section + dayInfo liturgy.DayInfo scroll int width int height int @@ -33,9 +34,11 @@ type Model struct { err error } -// readingsMsg carries a successful fetch's sections back to Update. +// readingsMsg carries a successful fetch's sections and DayInfo back to +// Update. type readingsMsg struct { sections []liturgy.Section + dayInfo liturgy.DayInfo } // errMsg carries a failed fetch's error back to Update. @@ -136,7 +139,7 @@ func (m Model) fetchCmd(refresh bool) tea.Cmd { cfg := m.cfg date := m.date return func() tea.Msg { - secs, err := readings.Load(cfg, readings.Options{ + secs, info, err := readings.Load(cfg, readings.Options{ Date: date, Refresh: refresh, Offline: cfg.Offline, @@ -145,7 +148,7 @@ func (m Model) fetchCmd(refresh bool) tea.Cmd { if err != nil { return errMsg{err} } - return readingsMsg{secs} + return readingsMsg{secs, info} } } @@ -166,6 +169,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.loading = false m.err = nil m.sections = msg.sections + m.dayInfo = msg.dayInfo m.scroll = 0 return m, nil @@ -203,10 +207,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.scroll = m.scrollTo(m.scroll - 1) return m, nil case " ": - m.scroll = m.scrollTo(m.scroll + pageSize(m.height)) + m.scroll = m.scrollTo(m.scroll + m.pageSize()) return m, nil case "b": - m.scroll = m.scrollTo(m.scroll - pageSize(m.height)) + m.scroll = m.scrollTo(m.scroll - m.pageSize()) return m, nil case "g": m.scroll = 0 @@ -219,15 +223,26 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -// pageSize is how many reading lines fit between the header and footer bars -// for a given terminal height; it falls back to a sane default before the -// first tea.WindowSizeMsg arrives (height == 0). -func pageSize(height int) int { - const chrome = 4 // header + blank + footer + margin - if height <= chrome { +// headerLines is how many lines the top header block renders as: 1 (just +// the "lectio DATE [version]" bar) or 2 when a day-info line (the +// celebration name, optionally with its temporal Season) is shown beneath +// it -- see dayInfoLine. +func (m Model) headerLines() int { + if m.dayInfo.Name == "" { + return 1 + } + return 2 +} + +// pageSize is how many reading lines fit between the header block and +// footer bar for the model's current terminal height; it falls back to a +// sane default before the first tea.WindowSizeMsg arrives (height == 0). +func (m Model) pageSize() int { + chrome := m.headerLines() + 3 // header block + blank + footer + margin + if m.height <= chrome { return 10 } - return height - chrome + return m.height - chrome } // clampScroll keeps scroll within [0, total-visible] (never negative). @@ -263,11 +278,12 @@ func (m Model) innerWidth() int { // view can't scroll past the end -- keeping m.scroll bounded in Update, not // merely clamped for display in View. func (m Model) scrollTo(s int) int { - return clampScroll(s, len(m.bodyLines(m.innerWidth())), pageSize(m.height)) + return clampScroll(s, len(m.bodyLines(m.innerWidth())), m.pageSize()) } -// View renders the header (date + active version label), the scrolling -// reading, and the footer keybar. +// View renders the header (date + active version label, plus a day-info +// line when the source carries one), the scrolling reading, and the +// footer keybar. func (m Model) View() string { w := m.width if w <= 0 { @@ -276,11 +292,14 @@ func (m Model) View() string { innerW := m.innerWidth() header := headerStyle.Width(w).Render(m.headerText()) + if line := m.dayInfoLine(); line != "" { + header += "\n" + line + } footer := footerStyle.Width(w).Render(i18n.Get(m.cfg.UILanguage).FooterKeys) bodyLines := m.bodyLines(innerW) - visible := pageSize(m.height) + visible := m.pageSize() scroll := clampScroll(m.scroll, len(bodyLines), visible) end := scroll + visible if end > len(bodyLines) { @@ -303,6 +322,23 @@ func (m Model) headerText() string { return fmt.Sprintf("lectio %s [%s]", m.date, label) } +// dayInfoLine renders the day's celebration name (heading/accent style, +// source-language, never translated -- like the readings/citations +// themselves; see liturgy.DayInfo) with its temporal Season, if any, +// appended in the dim citation style, as the header block's second line. +// Empty when the active source yielded no DayInfo (m.dayInfo.Name == ""), +// which is never an error -- the header is simply omitted. +func (m Model) dayInfoLine() string { + if m.dayInfo.Name == "" { + return "" + } + line := headingStyle.Render(m.dayInfo.Name) + if m.dayInfo.Season != "" { + line += " " + citationStyle.Render(m.dayInfo.Season) + } + return line +} + // bodyLines returns the styled, wrapped lines the reading pane scrolls // through: a loading/error/empty notice, or each section's heading + // render.GatherVersion blocks for the active version. diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index 85a8175..330259d 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -113,6 +113,41 @@ func TestBodyLinesLocalisesHeading(t *testing.T) { } } +// TestDayInfoLineShown checks the day-info line (celebration Name, plus +// its temporal Season if any) is rendered under the top header bar when +// the loaded source carries one, and that pageSize grows its chrome +// allowance to account for the extra line. +func TestDayInfoLineShown(t *testing.T) { + m := Model{ + cfg: config.Config{UILanguage: "en"}, + date: "2026-07-22", + dayInfo: liturgy.DayInfo{Name: "St. Mary Magdalene", Season: "Feria IV after VIII Sunday after Pentecost"}, + } + out := m.View() + if !strings.Contains(out, "St. Mary Magdalene") { + t.Errorf("View() missing day-info name: %q", out) + } + if !strings.Contains(out, "Feria IV after VIII Sunday after Pentecost") { + t.Errorf("View() missing day-info season: %q", out) + } + if got := m.headerLines(); got != 2 { + t.Errorf("headerLines() = %d, want 2 when DayInfo.Name is set", got) + } +} + +// TestDayInfoLineOmittedWhenEmpty checks the header block stays a single +// line (headerLines() == 1) when the source yielded no DayInfo -- e.g. a +// harvested-sigla offline load, which carries citations only. +func TestDayInfoLineOmittedWhenEmpty(t *testing.T) { + m := Model{cfg: config.Config{UILanguage: "en"}, date: "2026-07-22"} + if line := m.dayInfoLine(); line != "" { + t.Errorf("dayInfoLine() = %q, want empty when DayInfo is zero", line) + } + if got := m.headerLines(); got != 1 { + t.Errorf("headerLines() = %d, want 1 when DayInfo is zero", got) + } +} + // TestFooterKeysLocalised checks the footer keybar text follows // cfg.UILanguage. func TestFooterKeysLocalised(t *testing.T) { |
