diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/config/config.go | 2 | ||||
| -rw-r--r-- | internal/i18n/i18n.go | 283 | ||||
| -rw-r--r-- | internal/tui/reader.go | 116 | ||||
| -rw-r--r-- | internal/tui/reader_test.go | 58 |
4 files changed, 310 insertions, 149 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index e2ea0d3..a7365bb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -25,7 +25,7 @@ var seedTOML []byte // Version is lectio's release version, shared by every binary's // -v/--version output (lectio, lectio-ui, lectio-web). -const Version = "0.24.1" +const Version = "0.25.0" // validVersions are the five scripture versions lectio understands. var validVersions = map[string]bool{ diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index 16d489a..36aa288 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/i18n.go @@ -34,6 +34,9 @@ type UI struct { ReaderMarkPrompt, ReaderBookmarksTitle, ReaderBookmarksKeys, ReaderNoBookmarks string ReaderMarkVerseKeys, ReaderConfirmDelete, ReaderConfirmKeys string ReaderMarkNote, ReaderMarkTags, ReaderMarkHelp string + // Reader go-to-chapter prompt: the dialog label (rendered after the book + // name) and its enter/esc help line. + ReaderJumpChapter, ReaderJumpChapterKeys string // CLI banner label words and connective: the banner is // "<word> <BannerConnective> <date>" (e.g. "Gospel for 2026-07-22" / @@ -116,75 +119,77 @@ var enUI = UI{ "aklamacja": "Acclamation", "ewangelia": "Gospel", }, - FooterKeys: "tab/⇧tab version ←/→ day d date j/k scroll space/b page g/G top/bottom r refresh q quit", - Loading: "loading…", - NoReadingsFor: "no readings for ", - ErrorPrefix: "error: ", - ErrorHint: "change date (←/→) or refresh (r)", - JumpPrompt: "go to date (YYYY-MM-DD)", - ReaderTitle: "reader — pick a book", - ReaderPickKeys: "type to filter ↑/↓ move enter open esc quit", - ReaderReadKeys: "n/p chapter tab version j/k scroll m mark b bookmarks esc books q quit", - ReaderNoText: "(no text in this version)", - ReaderNoMatch: "(no matching books)", - ReaderMarkPrompt: "note (enter to save, esc to cancel)", - ReaderBookmarksTitle: "bookmarks", - ReaderBookmarksKeys: "j/k move enter open d delete esc back q quit", - ReaderNoBookmarks: "no bookmarks yet — press m while reading", - ReaderMarkVerseKeys: "↑/↓ pick verse enter add note esc cancel", - ReaderConfirmDelete: "Delete this bookmark?", - ReaderConfirmKeys: "y = delete n = cancel", - ReaderMarkNote: "note", - ReaderMarkTags: "tags", - ReaderMarkHelp: "enter save · tab switch field · esc cancel", - BannerGospel: "Gospel", - BannerReadings: "Readings", - BannerConnective: "for", - Months: [12]string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}, - Lectionary: "lectionary", - OptModern: "modern", - OptTraditional: "traditional", - Parts: "parts", - OptGospel: "Gospel", - OptAll: "all parts", - Layout: "layout", - OptHorizontal: "horizontal", - OptColumns: "columns", - OptInterlinear: "interlinear", - Theme: "theme", - Mono: "mono", - NoInterlinearVerses: "(no verses to align)", - NavReader: "reader", - WebBook: "book", - WebChapter: "chapter", - NavHome: "home", - NavSettings: "settings", - NavBookmarks: "bookmarks", - WebVerseOptional: "verse (optional)", - WebTagsPlaceholder: "tags, comma-separated", - WebAddBookmark: "bookmark", - WebAllTags: "all", - WebNoBookmarks: "No bookmarks yet.", - WebDelete: "delete", - WebSaved: "Saved.", - WebTradLang: "traditional language", - WebUILang: "UI language", - WebSiglaStyle: "sigla style", - WebDefaultVersion: "default version", - WebVersions: "versions", - WebWebVersions: "web versions", - WebOffline: "offline", - WebWidth: "width", - WebPort: "web port", - WebPager: "pager", - WebSave: "Save", - Export: "download", - Calendar: "month calendar", - NoReadingsDay: "no readings for this day", - NoVersion: "(not in %s)", - NoVersionPartial: "(not in %s: %s)", - NoReference: "(no reference)", - NoReferenceErr: "(no reference: %v)", + FooterKeys: "tab/⇧tab version ←/→ day d date j/k scroll space/b page g/G top/bottom r refresh q quit", + Loading: "loading…", + NoReadingsFor: "no readings for ", + ErrorPrefix: "error: ", + ErrorHint: "change date (←/→) or refresh (r)", + JumpPrompt: "go to date (YYYY-MM-DD)", + ReaderTitle: "reader — pick a book", + ReaderPickKeys: "type to filter ↑/↓ move enter open esc quit", + ReaderReadKeys: "n/p chapter c go-to tab version j/k scroll m mark b bookmarks esc books q quit", + ReaderNoText: "(no text in this version)", + ReaderNoMatch: "(no matching books)", + ReaderMarkPrompt: "note (enter to save, esc to cancel)", + ReaderBookmarksTitle: "bookmarks", + ReaderBookmarksKeys: "j/k move enter open d delete esc back q quit", + ReaderNoBookmarks: "no bookmarks yet — press m while reading", + ReaderMarkVerseKeys: "↑/↓ pick verse enter add note esc cancel", + ReaderConfirmDelete: "Delete this bookmark?", + ReaderConfirmKeys: "y = delete n = cancel", + ReaderMarkNote: "note", + ReaderMarkTags: "tags", + ReaderMarkHelp: "enter save · tab switch field · esc cancel", + ReaderJumpChapter: "go to chapter", + ReaderJumpChapterKeys: "enter go · esc cancel", + BannerGospel: "Gospel", + BannerReadings: "Readings", + BannerConnective: "for", + Months: [12]string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}, + Lectionary: "lectionary", + OptModern: "modern", + OptTraditional: "traditional", + Parts: "parts", + OptGospel: "Gospel", + OptAll: "all parts", + Layout: "layout", + OptHorizontal: "horizontal", + OptColumns: "columns", + OptInterlinear: "interlinear", + Theme: "theme", + Mono: "mono", + NoInterlinearVerses: "(no verses to align)", + NavReader: "reader", + WebBook: "book", + WebChapter: "chapter", + NavHome: "home", + NavSettings: "settings", + NavBookmarks: "bookmarks", + WebVerseOptional: "verse (optional)", + WebTagsPlaceholder: "tags, comma-separated", + WebAddBookmark: "bookmark", + WebAllTags: "all", + WebNoBookmarks: "No bookmarks yet.", + WebDelete: "delete", + WebSaved: "Saved.", + WebTradLang: "traditional language", + WebUILang: "UI language", + WebSiglaStyle: "sigla style", + WebDefaultVersion: "default version", + WebVersions: "versions", + WebWebVersions: "web versions", + WebOffline: "offline", + WebWidth: "width", + WebPort: "web port", + WebPager: "pager", + WebSave: "Save", + Export: "download", + Calendar: "month calendar", + NoReadingsDay: "no readings for this day", + NoVersion: "(not in %s)", + NoVersionPartial: "(not in %s: %s)", + NoReference: "(no reference)", + NoReferenceErr: "(no reference: %v)", } var plUI = UI{ @@ -202,73 +207,75 @@ var plUI = UI{ "aklamacja": "Aklamacja", "ewangelia": "Ewangelia", }, - FooterKeys: "tab/⇧tab wersja ←/→ dzień d data j/k przewiń spacja/b strona g/G góra/dół r odśwież q wyjście", - Loading: "ładowanie…", - NoReadingsFor: "brak czytań na ", - ErrorPrefix: "błąd: ", - ErrorHint: "zmień datę (←/→) lub odśwież (r)", - JumpPrompt: "przejdź do daty (RRRR-MM-DD)", - ReaderTitle: "czytnik — wybierz księgę", - ReaderPickKeys: "wpisz, by filtrować ↑/↓ ruch enter otwórz esc wyjście", - ReaderReadKeys: "n/p rozdział tab wersja j/k przewiń m zakładka b zakładki esc księgi q wyjście", - ReaderNoText: "(brak tekstu w tej wersji)", - ReaderNoMatch: "(brak pasujących ksiąg)", - ReaderMarkPrompt: "notatka (enter zapisz, esc anuluj)", - ReaderBookmarksTitle: "zakładki", - ReaderBookmarksKeys: "j/k ruch enter otwórz d usuń esc wróć q wyjście", - ReaderNoBookmarks: "brak zakładek — naciśnij m podczas czytania", - ReaderMarkVerseKeys: "↑/↓ wybierz werset enter notatka esc anuluj", - ReaderConfirmDelete: "Usunąć zakładkę?", - ReaderConfirmKeys: "y = usuń n = anuluj", - ReaderMarkNote: "notatka", - ReaderMarkTags: "tagi", - ReaderMarkHelp: "enter zapisz · tab pole · esc anuluj", - BannerGospel: "Ewangelia", - BannerReadings: "Czytania", - BannerConnective: "na", - Months: [12]string{"Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec", "Sierpień", "Wrzesień", "Październik", "Listopad", "Grudzień"}, - Lectionary: "lekcjonarz", - OptModern: "nowy", - OptTraditional: "tradycyjny", - Parts: "zakres", - OptGospel: "Ewangelia", - OptAll: "wszystkie części", - Layout: "układ", - OptHorizontal: "poziomo", - OptColumns: "kolumny", - OptInterlinear: "interlinearnie", - Theme: "motyw", - Mono: "mono", - NoInterlinearVerses: "(brak wersetów do zestawienia interlinearnego)", - NavReader: "czytnik", - WebBook: "księga", - WebChapter: "rozdział", - NavHome: "strona główna", - NavSettings: "ustawienia", - NavBookmarks: "zakładki", - WebVerseOptional: "werset (opcjonalnie)", - WebTagsPlaceholder: "tagi, oddzielone przecinkami", - WebAddBookmark: "zakładka", - WebAllTags: "wszystkie", - WebNoBookmarks: "Brak zakładek.", - WebDelete: "usuń", - WebSaved: "Zapisano.", - WebTradLang: "język tradycyjny", - WebUILang: "język interfejsu", - WebSiglaStyle: "styl sigli", - WebDefaultVersion: "wersja domyślna", - WebVersions: "wersje", - WebWebVersions: "wersje web", - WebOffline: "offline", - WebWidth: "szerokość", - WebPort: "port web", - WebPager: "pager", - WebSave: "Zapisz", - Export: "pobierz", - Calendar: "kalendarz miesiąca", - NoReadingsDay: "brak czytań na ten dzień", - NoVersion: "(brak w „%s”)", - NoVersionPartial: "(brak w „%s”: %s)", - NoReference: "(brak odwołania)", - NoReferenceErr: "(brak odwołania: %w)", + FooterKeys: "tab/⇧tab wersja ←/→ dzień d data j/k przewiń spacja/b strona g/G góra/dół r odśwież q wyjście", + Loading: "ładowanie…", + NoReadingsFor: "brak czytań na ", + ErrorPrefix: "błąd: ", + ErrorHint: "zmień datę (←/→) lub odśwież (r)", + JumpPrompt: "przejdź do daty (RRRR-MM-DD)", + ReaderTitle: "czytnik — wybierz księgę", + ReaderPickKeys: "wpisz, by filtrować ↑/↓ ruch enter otwórz esc wyjście", + ReaderReadKeys: "n/p rozdział c skok tab wersja j/k przewiń m zakładka b zakładki esc księgi q wyjście", + ReaderNoText: "(brak tekstu w tej wersji)", + ReaderNoMatch: "(brak pasujących ksiąg)", + ReaderMarkPrompt: "notatka (enter zapisz, esc anuluj)", + ReaderBookmarksTitle: "zakładki", + ReaderBookmarksKeys: "j/k ruch enter otwórz d usuń esc wróć q wyjście", + ReaderNoBookmarks: "brak zakładek — naciśnij m podczas czytania", + ReaderMarkVerseKeys: "↑/↓ wybierz werset enter notatka esc anuluj", + ReaderConfirmDelete: "Usunąć zakładkę?", + ReaderConfirmKeys: "y = usuń n = anuluj", + ReaderMarkNote: "notatka", + ReaderMarkTags: "tagi", + ReaderMarkHelp: "enter zapisz · tab pole · esc anuluj", + ReaderJumpChapter: "przejdź do rozdziału", + ReaderJumpChapterKeys: "enter przejdź · esc anuluj", + BannerGospel: "Ewangelia", + BannerReadings: "Czytania", + BannerConnective: "na", + Months: [12]string{"Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec", "Sierpień", "Wrzesień", "Październik", "Listopad", "Grudzień"}, + Lectionary: "lekcjonarz", + OptModern: "nowy", + OptTraditional: "tradycyjny", + Parts: "zakres", + OptGospel: "Ewangelia", + OptAll: "wszystkie części", + Layout: "układ", + OptHorizontal: "poziomo", + OptColumns: "kolumny", + OptInterlinear: "interlinearnie", + Theme: "motyw", + Mono: "mono", + NoInterlinearVerses: "(brak wersetów do zestawienia interlinearnego)", + NavReader: "czytnik", + WebBook: "księga", + WebChapter: "rozdział", + NavHome: "strona główna", + NavSettings: "ustawienia", + NavBookmarks: "zakładki", + WebVerseOptional: "werset (opcjonalnie)", + WebTagsPlaceholder: "tagi, oddzielone przecinkami", + WebAddBookmark: "zakładka", + WebAllTags: "wszystkie", + WebNoBookmarks: "Brak zakładek.", + WebDelete: "usuń", + WebSaved: "Zapisano.", + WebTradLang: "język tradycyjny", + WebUILang: "język interfejsu", + WebSiglaStyle: "styl sigli", + WebDefaultVersion: "wersja domyślna", + WebVersions: "wersje", + WebWebVersions: "wersje web", + WebOffline: "offline", + WebWidth: "szerokość", + WebPort: "port web", + WebPager: "pager", + WebSave: "Zapisz", + Export: "pobierz", + Calendar: "kalendarz miesiąca", + NoReadingsDay: "brak czytań na ten dzień", + NoVersion: "(brak w „%s”)", + NoVersionPartial: "(brak w „%s”: %s)", + NoReference: "(brak odwołania)", + NoReferenceErr: "(brak odwołania: %w)", } 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. diff --git a/internal/tui/reader_test.go b/internal/tui/reader_test.go index 904836e..9d324ba 100644 --- a/internal/tui/reader_test.go +++ b/internal/tui/reader_test.go @@ -165,6 +165,64 @@ func TestReaderBookmarkFlow(t *testing.T) { } } +func TestReaderChapterJump(t *testing.T) { + m := enReader(t) + m = win(m, 80, 24) + m = key(m, runes("jn")) + m = key(m, tea.KeyMsg{Type: tea.KeyEnter}) // open John 1 (21 chapters) + if m.mode != modeRead { + t.Fatal("did not enter reading") + } + // c opens the jump prompt + m = key(m, runes("c")) + if m.mode != modeChapterJump { + t.Fatalf("c did not open the chapter-jump prompt (mode=%v)", m.mode) + } + if hi := m.chapters[len(m.chapters)-1]; hi != 21 { + t.Fatalf("John should have 21 chapters, got %d", hi) + } + if v := m.View(); !strings.Contains(v, "1-21") || !strings.Contains(v, "go to chapter") { + t.Errorf("jump prompt missing range/label:\n%s", v) + } + // non-digit keys are ignored; only digits accumulate + m = key(m, runes("x1")) + m = key(m, runes("4")) + if m.chapJumpBuf != "14" { + t.Fatalf("buffer = %q want 14 (non-digits filtered)", m.chapJumpBuf) + } + m = key(m, tea.KeyMsg{Type: tea.KeyBackspace}) + m = key(m, runes("2")) // -> "12" + m = key(m, tea.KeyMsg{Type: tea.KeyEnter}) + if m.mode != modeRead { + t.Fatalf("enter did not return to reading (mode=%v)", m.mode) + } + if m.currentChapter() != 12 { + t.Errorf("jumped to chapter %d want 12", m.currentChapter()) + } + if m.scroll != 0 { + t.Errorf("jump should reset scroll, got %d", m.scroll) + } + + // out-of-range clamps to the last chapter + m = key(m, runes("c")) + m = key(m, runes("999")) + m = key(m, tea.KeyMsg{Type: tea.KeyEnter}) + if m.currentChapter() != 21 { + t.Errorf("999 should clamp to 21, got %d", m.currentChapter()) + } + + // esc cancels without moving + m = key(m, runes("c")) + m = key(m, runes("3")) + m = key(m, tea.KeyMsg{Type: tea.KeyEsc}) + if m.mode != modeRead { + t.Fatalf("esc did not return to reading (mode=%v)", m.mode) + } + if m.currentChapter() != 21 { + t.Errorf("esc changed chapter to %d want 21", m.currentChapter()) + } +} + func TestReaderRemembersPlace(t *testing.T) { dir := t.TempDir() t.Setenv("XDG_DATA_HOME", dir) |
