diff options
Diffstat (limited to 'internal/tui/reader.go')
| -rw-r--r-- | internal/tui/reader.go | 116 |
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. |
