1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
|
# lectio — design spec
Date: 2026-07-23
Status: approved (brainstorm), pre-plan
## Goal
Reimplement the Python `daily-reading` tool (`ewangelia.py`) as a self-contained
Go project with two binaries:
- **`lectio`** — terminal CLI (subcommand-based).
- **`lectio-ui`** — TUI reader (Bubble Tea), modelled on the user's `bread-calc`.
It fetches the daily Catholic liturgy readings from niezbednik.niedziela.pl and
shows them in Polish plus four other versions (Wujek Polish, Vulgate Latin,
Greek, Douay-Rheims English), with correct psalm versification across them.
This is a **fork**: the Python `daily-reading` project is left untouched and
keeps working. `lectio` is a fresh project.
## Non-goals
- Offline lectionary computation (computing the day's citations without the
site) remains the separate `offline_readings` effort. `lectio` fetches the
site like `ewangelia.py`; when an offline engine exists it slots in behind
`internal/liturgy` without changing consumers.
- No new translations beyond the five already supported.
## Key decisions
| Decision | Choice |
|---|---|
| Language / stack | Go; Bubble Tea + Lipgloss (TUI), go-toml/v2 (config). Match bread-calc. |
| Binaries | `lectio` (CLI), `lectio-ui` (TUI). |
| Self-contained | Embed all four Bible corpora via `go:embed`; verse lookup in Go. No external `vul`/`grb`/`wuj`/`drb` tools. |
| Daily data | Still fetched from niedziela.pl (source of citations + Polish text); cached on disk. |
| CLI shape | Subcommands (hand-rolled dispatch, no cobra). |
| TUI shape | Reader + version-switch: full day in one pane, `tab` cycles versions, arrows change date. |
| Config | `~/.config/lectio/config.toml`, auto-seeded, flags override. |
| Versification | Port `psalm_versify.py` behaviour exactly. |
## Project layout
Module: `github.com/lukaszkasprzak/lectio`. Location: `~/git/projects/lectio`.
```
cmd/lectio/main.go -> cli.Run(args, stdin, stdout, stderr) int
cmd/lectio-ui/main.go -> load config+data, tui.New(...), tea.NewProgram(...).Run()
internal/liturgy/ fetch + parse niedziela.pl into []Section
internal/bible/ embedded corpora + reference lookup + book aliases
internal/psalter/ psalm versification (port of psalm_versify.py)
internal/render/ text rendering: compare columns, section text (CLI)
internal/config/ TOML config load + auto-seed
internal/cli/ subcommand dispatch
internal/tui/ Bubble Tea reader
Makefile, README.md, LICENSE, go.mod
```
Each `internal/*` package has one clear purpose, a small exported surface, and
is unit-testable in isolation. CLI and TUI are thin front-ends over the same
core (liturgy + bible + psalter + render).
## Data architecture
### Embedded corpora
`internal/bible` embeds four TSV files with `go:embed`:
- `wuj.tsv` — Biblia Wujka (Polish), from `offline_readings/corpus/wuj.tsv`.
- `drb.tsv` — Douay-Rheims (English), from `offline_readings/drb/drb.tsv`.
- `vul.tsv` — Vulgate (Latin), extracted from the installed `vul` tool
(`sed '1,/^#EOF$/d' $(command -v vul) | tar xzf - -O vul.tsv`).
- `grb.tsv` — Greek, extracted from the installed `grb` tool the same way.
All four share the 6-column format `Book | Abbrev | BookNum | Chapter | Verse | Text`
(the kjv-family format). Total ~15 MB, embedded into the binary.
### Reference lookup (replaces the awk engine + shell-out)
`internal/bible` reimplements the reference grammar in Go. It must support the
subset the tool actually feeds it:
- Book match: canonical English name (`John`), English prefix (`Joh`), and the
Polish/English alias table (`J`, `Łk`, `1 Kor`, `Jana`, `Mdr`, ...) resolved
to a canonical book. Longest alias wins; exact before prefix (fixing the old
`J`→Joshua bug).
- Chapter+verse forms: `Book C:V`, `Book C:V-V` (range), `Book C:V,V,...`
(list). Mixed lists like `John 20:1,11-18` are split into single-group
queries and merged (port of `split_ref`).
- Returns `[]Verse{Chapter, Verse, Text}` in reference order; reports the groups
a corpus lacked (for the "brak w …" note).
### Daily readings
`internal/liturgy`:
- `Fetch(date, refresh) (html, error)` — GET
`https://niezbednik.niedziela.pl/liturgia/{date}/Ewangelia` with a browser
User-Agent; cache to `~/.cache/lectio/{date}.html` (honor `XDG_CACHE_HOME`).
Cache only fully-published pages.
- `Parse(html) ([]Section, error)` — pick the **new** lectionary tab
(`tabnowy0all`), fall back to `tabstary0all` with a warning; extract each
section's heading, subtitle, citation, and paragraphs. Fail loudly if the tab
is present but empty or absent (layout change).
- `Section{Heading, Subtitle, Citation, Paragraphs}`.
## Versions and psalm systems
Ported from `ewangelia.py` constants:
```
versions: pl, wuj, vul, grb, drb (canonical order; = config `versions` default)
labels: pl="Polski (niedziela.pl)" wuj="Wujek (pol.)" vul="Wulgata (lac.)"
grb="Grecki" drb="Douay-Rheims (ang.)"
bible tools: wuj, vul, grb, drb (pl comes from the fetched paragraphs)
psalm system: vul/grb/wuj -> "vulgate" drb -> "drb"
```
`lectio show VERSION` accepts any of the five (pl renders the fetched paragraphs;
the others render looked-up verses).
`pl` renders the fetched Polish paragraphs (dropping the "Słowa Ewangelii"
incipit, and de-duplicating a repeated responsorial refrain). The four bible
versions render verses looked up from the embedded corpora, with the citation
converted per that version's psalm system.
## Reference conversion & psalm versification
`internal/bible` (conversion) + `internal/psalter` (versification) port
`to_english_ref` / `_psalm_ref` / `psalm_versify.py`:
- Strip a leading `por.` (compare marker) and a trailing `(R.: ...)`
responsorial refrain from the citation.
- Convert the Polish citation to English style and normalise verse groups
(`Mt 7, 1-5` -> `Mat 7:1-5`; disjoint groups preserved as a comma list).
- Psalms: the lectionary cites `Ps H (V)`. The Vulgate versions use chapter `V`,
verses unchanged. `drb` uses chapter `H` and shifts each verse by the
title-fold count `k` for that psalm: `drb_verse = lectionary_verse - k`
(clamped ≥ 1). `k` comes from the `DRB_TITLE_FOLD` table (1 or 2 title lines;
0 for untitled), derived by aligning the Wujek/DRB corpora. ~140 psalms exact,
~10 approximate (documented in the package).
## CLI surface (`internal/cli`)
Hand-rolled subcommand dispatch. `lectio` with no args = `lectio today`.
```
lectio today [--all] [--raw] [--width N] [--refresh]
lectio date D [--all] [--raw] [--width N] [--refresh] # D = YYYY-MM-DD
lectio compare LIST [--date D] [--all] [--width N] [--refresh]
LIST = comma versions (default: config `versions`)
lectio show VERSION [--date D] [--all] [--refresh] # one version's text
lectio --version | lectio -v
lectio help | lectio -h | lectio <cmd> -h
```
- `--all` shows every reading (1st/2nd, psalm, acclamation, gospel); default is
the gospel only, unless config `all = true`.
- `--raw` drops banner/headings for piping.
- `--refresh` bypasses the cache.
- Flags override config. Exit codes: 0 ok, 1 runtime error (fetch/parse), 2
usage error.
## TUI (`internal/tui`)
Bubble Tea Elm architecture, per the approved mockup (reader + version-switch):
- One scrolling pane showing the whole day's readings for the **active version**.
- Header: date + active version label. Footer: keybar.
- Keys: `tab`/`shift+tab` cycle versions (order = config `versions`, starting at
`default_version`); `←/→` change date (async re-fetch with a loading state);
`j/k` and `space`/`b` scroll; `g/G` top/bottom; `r` refresh; `q`/`ctrl+c` quit.
- Async fetch via `tea.Cmd` returning a `readingsMsg` or `errMsg`; a spinner or
"ładowanie…" line while in flight.
- Lipgloss styling, theme-neutral (works on light/dark terminals); width-aware
wrapping from `tea.WindowSizeMsg`.
- `Model{ config, cache, date, version, sections, scroll, width, loading, err }`.
The TUI shows one version at a time (not columns); the CLI `compare` is where
side-by-side lives.
### Colour scheme
Colours distinguish the parts of a reading. Each role is a named Lipgloss style
in one place (`internal/tui`), using `lipgloss.AdaptiveColor` so it reads on both
light and dark terminals; a `NO_COLOR` env / non-TTY output degrades to plain.
| Role | Style |
|---|---|
| Section heading (`1. czytanie`, `Ewangelia`) | bold, accent colour |
| Citation (`Pnp 8, 6-7`) | dim / muted, next to the heading |
| Verse number (`20:1`) | distinct muted colour, separated from text |
| Verse text | default foreground |
| Responsorial refrain (psalm) | italic / secondary colour |
| Header (date + active version) | accent background or bold |
| Footer keybar | dim |
Exact hues are finalised in planning, but the role → style mapping above is
fixed. The CLI stays plain text (colour is a TUI concern).
## Config (`internal/config`)
`Config` struct loaded via go-toml. Resolution: `LECTIO_CONFIG` env →
`~/.config/lectio/config.toml` (auto-seeded from an embedded default on first
run, honoring `XDG_CONFIG_HOME`) → built-in defaults. Flags override.
```toml
schema_version = 1
versions = ["pl", "wuj", "vul", "grb", "drb"] # compare set + TUI cycle order
default_version = "pl" # TUI start / `lectio show` default
width = 0 # CLI wrap width; 0 = detect terminal
all = false # default to all readings (true) or just the gospel (false)
```
Unknown versions in config are rejected with a clear error. Invalid TOML falls
back to defaults with a stderr warning.
## Porting map (Python → Go)
| Python (`ewangelia.py` / `psalm_versify.py`) | Go |
|---|---|
| `fetch`, `parse_sections`, `html_to_lines`, `extract_reference` | `internal/liturgy` |
| awk engine, `run_bible_tool`, `split_ref` | `internal/bible` (lookup) |
| `POLISH_TO_EN`, `to_english_ref`, `_psalm_ref` | `internal/bible` (aliases + conversion) |
| `psalm_versify.py` (`DRB_TITLE_FOLD`, `drb_verse`, chapter map) | `internal/psalter` |
| `gather_version`, `run_bible_section`, `render_compare`, `render_section`, refrain dedup | `internal/render` |
| `VERSION_LABELS`, `PSALM_SYSTEM`, `COMPARE_CODES`, `BIBLE_TOOLS` | `internal/bible` / `internal/render` constants |
| `argparse` main | `internal/cli` |
## Error handling
- Network failure / unpublished date: clear stderr message, exit 1 (CLI) or an
error line in the TUI (stay usable, let the user change date).
- Layout change (tab missing/empty): explicit "site layout may have changed"
error, exit 1.
- A corpus lacking a passage (e.g. deuterocanonical book absent from a version):
per-section note, not a crash; other versions still render.
- Unknown version code: usage error, exit 2.
## Testing
- **bible**: table tests for book matching (incl. `J`→John, `Łk`→Luke, `1 Kor`),
reference grammar (ranges, lists, split groups), and known verses across all
four corpora (Gen 1:1, John 20:1, a deuterocanonical, a psalm).
- **psalter**: unit tests for `drb_verse` on titled/untitled/2-line-title psalms.
- **liturgy.Parse**: fixture tests against a few cached HTML pages (a normal day,
a split-reading feast, an unpublished date), asserting sections/citations.
- **render**: golden-text tests for a compare block and a section, including the
refrain dedup and psalm-number divergence.
- **config**: load/seed/override tests with a temp `XDG_CONFIG_HOME`.
- `go vet ./...` clean; `gofmt`-formatted.
## Build & install
Makefile cloned from bread-calc:
```
make build -> ./lectio and ./lectio-ui
make install -> $(PREFIX)/bin (default ~/.local/bin)
make cross -> dist/ for linux/darwin/windows amd64+arm64
make test | vet | fmt | clean
```
`.gitignore`: `/lectio`, `/lectio-ui`, `dist/`, `*.test`, coverage, cache.
## Open items (decide during planning)
- Exact Lipgloss hues for the colour roles (mapping is fixed in the TUI section;
only the specific colours are open).
- Whether `lectio show` and the TUI share a single "gather one version" function
(they should).
- Whether to vendor the extracted `vul.tsv`/`grb.tsv` into the repo or fetch them
at build time (lean: vendor them under `internal/bible/`, like wuj/drb).
|