aboutsummaryrefslogtreecommitdiff
path: root/internal/tui/reader.go
diff options
context:
space:
mode:
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.