aboutsummaryrefslogtreecommitdiff
path: root/internal/tui/reader.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 22:13:06 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 22:13:06 +0200
commit1b4134d883544beba3fc51704322cef2a875a7d2 (patch)
treec3b74799fcc1f62acd6eedfb734245015ade50cb /internal/tui/reader.go
parent1ab07867b425cfa8da207b904a297df29b1b177a (diff)
downloadlectio-1b4134d883544beba3fc51704322cef2a875a7d2.tar.gz
lectio-1b4134d883544beba3fc51704322cef2a875a7d2.zip
lectio-ui reader: go-to-chapter jump (c)
Inside a book, 'c' opens a centered prompt showing the book's chapter range (e.g. "1-21: ") and accepts a chapter number to jump straight there, instead of scrolling (handy for Psalm 119 etc). Digits only, backspace edits, Enter clamps to the valid range and opens that chapter at its top, Esc cancels. New modeChapterJump + updateChapterJump/jumpToChapter/ viewChapterJump; i18n ReaderJumpChapter/ReaderJumpChapterKeys (en/pl) and 'c' added to the reading keybar. Only offered when the book has >1 chapter.
Diffstat (limited to 'internal/tui/reader.go')
-rw-r--r--internal/tui/reader.go116
1 files changed, 106 insertions, 10 deletions
diff --git a/internal/tui/reader.go b/internal/tui/reader.go
index df92349..7f23f66 100644
--- a/internal/tui/reader.go
+++ b/internal/tui/reader.go
@@ -3,6 +3,7 @@ package tui
import (
"fmt"
"sort"
+ "strconv"
"strings"
tea "github.com/charmbracelet/bubbletea"
@@ -21,11 +22,12 @@ import (
type readerMode int
const (
- modePick readerMode = iota
- modeRead // scrolling chapter view
- modeMarkVerse // picking the verse to bookmark (highlighted cursor)
- modeMark // typing an optional note for the chosen verse
- modeBookmarks // the saved-bookmarks list
+ modePick readerMode = iota
+ modeRead // scrolling chapter view
+ modeMarkVerse // picking the verse to bookmark (highlighted cursor)
+ modeMark // typing an optional note for the chosen verse
+ modeBookmarks // the saved-bookmarks list
+ modeChapterJump // typing a chapter number to jump to
)
// selStyle marks the picker's selected row (reverse video, legible on any theme).
@@ -70,11 +72,12 @@ type ReaderModel struct {
pickTop int // first visible match (list scroll)
// reading
- bookIdx int // index into books
- chapters []int // sorted chapters for current book+version
- chapPos int // index into chapters
- verses []bible.Verse
- scroll int
+ bookIdx int // index into books
+ chapters []int // sorted chapters for current book+version
+ chapPos int // index into chapters
+ verses []bible.Verse
+ scroll int
+ chapJumpBuf string // digits typed in modeChapterJump
// bookmarks
store *bookmarks.Store
@@ -218,6 +221,8 @@ func (m ReaderModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updateMark(msg)
case modeBookmarks:
return m.updateBookmarks(msg)
+ case modeChapterJump:
+ return m.updateChapterJump(msg)
default:
return m.updateRead(msg)
}
@@ -302,6 +307,12 @@ func (m ReaderModel) updateRead(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m.chapterStep(+1), nil
case "p", "[", "left", "h":
return m.chapterStep(-1), nil
+ case "c":
+ if len(m.chapters) > 1 {
+ m.mode = modeChapterJump
+ m.chapJumpBuf = ""
+ }
+ return m, nil
case "j", "down":
m.scroll = m.clampRead(m.scroll + 1)
return m, nil
@@ -457,6 +468,75 @@ func (m ReaderModel) updateBookmarks(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
+// updateChapterJump handles the go-to-chapter prompt: type digits, Enter jumps
+// (clamped to the book's chapter range), Esc cancels. Non-digit keys are
+// ignored so the buffer only ever holds a number.
+func (m ReaderModel) updateChapterJump(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
+ switch msg.Type {
+ case tea.KeyCtrlC:
+ return m, tea.Quit
+ case tea.KeyEsc:
+ m.mode = modeRead
+ m.chapJumpBuf = ""
+ return m, nil
+ case tea.KeyEnter:
+ m = m.jumpToChapter(m.chapJumpBuf)
+ m.chapJumpBuf = ""
+ m.mode = modeRead
+ return m, nil
+ case tea.KeyBackspace:
+ if r := []rune(m.chapJumpBuf); len(r) > 0 {
+ m.chapJumpBuf = string(r[:len(r)-1])
+ }
+ return m, nil
+ case tea.KeyRunes:
+ for _, r := range msg.Runes {
+ if r >= '0' && r <= '9' && len([]rune(m.chapJumpBuf)) < 4 {
+ m.chapJumpBuf += string(r)
+ }
+ }
+ return m, nil
+ }
+ return m, nil
+}
+
+// jumpToChapter parses buf as a chapter number, clamps it to the book's
+// chapter range, and opens that chapter at its top. An empty or non-numeric
+// buf is a no-op (the prompt just closes). Because the corpora number chapters
+// contiguously, the clamped number always names an existing chapter; if a book
+// ever had a gap, the nearest existing chapter at or after it is used.
+func (m ReaderModel) jumpToChapter(buf string) ReaderModel {
+ if len(m.chapters) == 0 {
+ return m
+ }
+ n, err := strconv.Atoi(strings.TrimSpace(buf))
+ if err != nil {
+ return m
+ }
+ lo, hi := m.chapters[0], m.chapters[len(m.chapters)-1]
+ if n < lo {
+ n = lo
+ }
+ if n > hi {
+ n = hi
+ }
+ pos := len(m.chapters) - 1
+ for i, c := range m.chapters {
+ if c >= n {
+ pos = i
+ break
+ }
+ }
+ if pos == m.chapPos {
+ return m
+ }
+ m.chapPos = pos
+ m.scroll = 0
+ m = m.loadVerses()
+ m.savePlace()
+ return m
+}
+
// currentChapter is the chapter number currently shown (0 if none loaded).
func (m ReaderModel) currentChapter() int {
if len(m.chapters) > 0 && m.chapPos < len(m.chapters) {
@@ -735,11 +815,27 @@ func (m ReaderModel) View() string {
return m.viewMark()
case modeBookmarks:
return m.viewBookmarks()
+ case modeChapterJump:
+ return m.viewChapterJump()
default: // modeRead + modeMarkVerse (verse cursor highlighted in the reading)
return m.viewRead()
}
}
+// viewChapterJump renders the go-to-chapter prompt as a prominent centered
+// dialog showing the book's valid chapter range and the digits typed so far.
+func (m ReaderModel) viewChapterJump() string {
+ ui := i18n.Get(m.cfg.UILanguage)
+ lo, hi := 0, 0
+ if len(m.chapters) > 0 {
+ lo, hi = m.chapters[0], m.chapters[len(m.chapters)-1]
+ }
+ title := modalTitleStyle.Render(fmt.Sprintf("%s %s", m.books[m.bookIdx].Name, ui.ReaderJumpChapter))
+ prompt := fmt.Sprintf("%d-%d: %s▏", lo, hi, m.chapJumpBuf)
+ inner := title + "\n\n" + prompt + "\n\n" + citationStyle.Render(ui.ReaderJumpChapterKeys)
+ return m.modal(inner)
+}
+
// viewMark renders the bookmark note/tags box as a prominent centered dialog.
// The note and tags wrap (with a hanging indent) within a bounded width, so a
// long note flows down inside the box instead of overrunning the border.