aboutsummaryrefslogtreecommitdiff
path: root/internal/tui
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/tui.go68
-rw-r--r--internal/tui/tui_test.go35
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) {