diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-29 13:55:27 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-29 13:55:27 +0200 |
| commit | 18578434f41d4fe34438c2f171388109a288c18c (patch) | |
| tree | ffeb1108c564840be530f4a6006aa9acb222a412 /internal | |
| parent | 6964ee56de01f9769a39ba480dc787339b642f7a (diff) | |
| download | lectio-18578434f41d4fe34438c2f171388109a288c18c.tar.gz lectio-18578434f41d4fe34438c2f171388109a288c18c.zip | |
tui(reader): flag bookmarked verses with a red * in the reading pane
A verse that has a bookmark now shows a red "*" at the end of its last line, so
saved verses are visible while reading. The set of bookmarked verses for the
current book+chapter is refreshed on chapter load, on save, and on returning
from the bookmarks list (one store read each). Test covers the flag + render.
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/tui/reader.go | 48 | ||||
| -rw-r--r-- | internal/tui/reader_test.go | 30 |
2 files changed, 71 insertions, 7 deletions
diff --git a/internal/tui/reader.go b/internal/tui/reader.go index de89a40..973161d 100644 --- a/internal/tui/reader.go +++ b/internal/tui/reader.go @@ -33,6 +33,9 @@ const ( // selStyle marks the picker's selected row (reverse video, legible on any theme). var selStyle = lipgloss.NewStyle().Reverse(true) +// markStyle renders the red "*" that flags a verse carrying a bookmark. +var markStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true) + // modalStyle / modalTitleStyle render the bookmark note box and the delete // confirmation as a prominent centered, bordered dialog (not a footer line). var ( @@ -92,6 +95,7 @@ type ReaderModel struct { markFilter string // active tag filter (substring, case-insensitive) markFiltering bool // typing into the tag filter confirmDelete bool // bookmarks list is awaiting delete confirmation + markedVerses map[int]bool // verse numbers in the current book+chapter that carry a bookmark flash string // transient status line (e.g. "bookmarked ...") width, height int @@ -488,7 +492,7 @@ func (m ReaderModel) updateBookmarks(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } m.mode = modeRead - return m, nil + return m.withMarks(), nil case "/": m.markFiltering = true return m, nil @@ -689,6 +693,30 @@ func (m ReaderModel) scrollToVerse(verse int) int { // saveBookmark stores a bookmark of the current book+chapter at the chosen // verse with an optional note, and sets a flash message. +// withMarks recomputes the set of verse numbers in the current book+chapter that +// carry a bookmark, so the reader can flag them with a red "*". One store read; +// refreshed on chapter load, on save, and on returning from the bookmarks list. +func (m ReaderModel) withMarks() ReaderModel { + m.markedVerses = nil + if m.store == nil || m.bookIdx < 0 || m.bookIdx >= len(m.books) { + return m + } + all, err := m.store.List("") + if err != nil { + return m + } + canon := m.books[m.bookIdx].Canonical + chap := m.currentChapter() + set := map[int]bool{} + for _, bm := range all { + if bm.Book == canon && bm.Chapter == chap { + set[bm.Verse] = true + } + } + m.markedVerses = set + return m +} + func (m ReaderModel) saveBookmark(verse int, note, tags string) ReaderModel { if m.store == nil || len(m.verses) == 0 { return m @@ -706,7 +734,7 @@ func (m ReaderModel) saveBookmark(verse int, note, tags string) ReaderModel { Tags: bookmarks.ParseTags(tags), }) m.flash = fmt.Sprintf("bookmarked %s %d:%d", book.Name, chap, verse) - return m + return m.withMarks() } // savePlace persists the current reading position (book+chapter+top verse) so @@ -752,7 +780,7 @@ func (m ReaderModel) openBook() ReaderModel { func (m ReaderModel) loadVerses() ReaderModel { if len(m.chapters) == 0 { m.verses = nil - return m + return m.withMarks() } if m.chapPos < 0 { m.chapPos = 0 @@ -761,7 +789,7 @@ func (m ReaderModel) loadVerses() ReaderModel { m.chapPos = len(m.chapters) - 1 } m.verses = bible.Verses(m.version(), m.books[m.bookIdx].Canonical, m.chapters[m.chapPos]) - return m + return m.withMarks() } func (m ReaderModel) chapterStep(d int) ReaderModel { @@ -850,13 +878,19 @@ func (m ReaderModel) readBodyHL(w, hl int) []string { numW := maxNumWidth(blocks) var lines []string for i, b := range blocks { + var vlines []string if i == hl { for _, ln := range strings.Split(render.Wrap(b, w), "\n") { - lines = append(lines, selStyle.Render(ln)) + vlines = append(vlines, selStyle.Render(ln)) } - continue + } else { + vlines = styleBlock(b, false, w, numW) + } + // Flag a bookmarked verse with a red "*" at the end of its last line. + if m.markedVerses[m.verses[i].Verse] && len(vlines) > 0 { + vlines[len(vlines)-1] += markStyle.Render("*") } - lines = append(lines, styleBlock(b, false, w, numW)...) + lines = append(lines, vlines...) } return lines } diff --git a/internal/tui/reader_test.go b/internal/tui/reader_test.go index da8d4d4..b741732 100644 --- a/internal/tui/reader_test.go +++ b/internal/tui/reader_test.go @@ -98,6 +98,36 @@ func TestReaderBookmarkTagFilter(t *testing.T) { } } +func TestReaderBookmarkMarker(t *testing.T) { + m := win(enReader(t), 80, 30) + // vul is always embedded, so verses load without -tags fullbible. + for i, v := range m.versions { + if v == "vul" { + m.verIdx = i + } + } + if _, err := m.store.Add(bookmarks.Bookmark{Book: "John", Chapter: 3, Verse: 16, Note: "x"}); err != nil { + t.Fatal(err) + } + m.bookIdx = m.bookIndex("John") + if m.bookIdx < 0 { + t.Fatal("book John not found") + } + m = m.openAt(3, 1) // loads John 3 and refreshes the marked-verse set + + if !m.markedVerses[16] { + t.Fatalf("verse 16 not flagged; markedVerses=%v", m.markedVerses) + } + if m.markedVerses[15] { + t.Fatal("verse 15 should not be flagged") + } + // the rendered body carries the "*" marker (styles are stripped in tests, so + // the star is plain, but present). + if !strings.Contains(strings.Join(m.readBodyHL(m.innerW(), -1), "\n"), "*") { + t.Fatal("rendered body has no bookmark marker") + } +} + func TestReaderInit(t *testing.T) { m := enReader(t) if m.mode != modePick { |
