diff options
Diffstat (limited to 'internal/tui')
| -rw-r--r-- | internal/tui/reader.go | 97 | ||||
| -rw-r--r-- | internal/tui/reader_test.go | 11 |
2 files changed, 88 insertions, 20 deletions
diff --git a/internal/tui/reader.go b/internal/tui/reader.go index 09d3210..72b7d0a 100644 --- a/internal/tui/reader.go +++ b/internal/tui/reader.go @@ -31,6 +31,25 @@ const ( // selStyle marks the picker's selected row (reverse video, legible on any theme). var selStyle = lipgloss.NewStyle().Reverse(true) +// modalStyle / modalTitleStyle render the bookmark note box and the delete +// confirmation as a prominent centered, bordered dialog (not a footer line). +var ( + modalStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(1, 3) + modalTitleStyle = lipgloss.NewStyle().Bold(true) +) + +// modal centres inner in a bordered box over the whole screen. +func (m ReaderModel) modal(inner string) string { + w, h := m.width, m.height + if w <= 0 { + w = 80 + } + if h <= 0 { + h = 24 + } + return lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, modalStyle.Render(inner)) +} + // ReaderModel is lectio-ui's --reader mode: a fuzzy book picker over the sigla // dialect's book names, then a scrolling view of a chapter from the embedded // corpora with chapter navigation and version cycling. It implements tea.Model @@ -62,6 +81,8 @@ type ReaderModel struct { markVerseIdx int // verse cursor in modeMarkVerse (index into verses) markVerse int // the chosen verse number carried into modeMark markNote string // note being typed in modeMark + markTags string // tags being typed in modeMark + markField int // 0 = note field, 1 = tags field marks []bookmarks.Bookmark // loaded list for modeBookmarks markSel int // selection in the bookmarks list markTop int // list scroll offset @@ -328,6 +349,8 @@ func (m ReaderModel) updateMarkVerse(msg tea.KeyMsg) (tea.Model, tea.Cmd) { if m.markVerseIdx >= 0 && m.markVerseIdx < len(m.verses) { m.markVerse = m.verses[m.markVerseIdx].Verse m.markNote = "" + m.markTags = "" + m.markField = 0 m.mode = modeMark } return m, nil @@ -335,31 +358,39 @@ func (m ReaderModel) updateMarkVerse(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } -// updateMark handles the note box for the chosen verse: type an optional note, -// Enter saves (an empty note is fine -- "skip"), Esc cancels. +// updateMark handles the note/tags box for the chosen verse: type into the +// active field, Tab switches note<->tags, Enter saves (both optional), Esc +// cancels. func (m ReaderModel) updateMark(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + field := &m.markNote + if m.markField == 1 { + field = &m.markTags + } switch msg.Type { case tea.KeyCtrlC: return m, tea.Quit case tea.KeyEsc: m.mode = modeRead - m.markNote = "" + m.markNote, m.markTags = "", "" + return m, nil + case tea.KeyTab, tea.KeyShiftTab, tea.KeyDown, tea.KeyUp: + m.markField = 1 - m.markField return m, nil case tea.KeyEnter: - m = m.saveBookmark(m.markVerse, m.markNote) - m.markNote = "" + m = m.saveBookmark(m.markVerse, m.markNote, m.markTags) + m.markNote, m.markTags = "", "" m.mode = modeRead return m, nil case tea.KeyBackspace: - if r := []rune(m.markNote); len(r) > 0 { - m.markNote = string(r[:len(r)-1]) + if r := []rune(*field); len(r) > 0 { + *field = string(r[:len(r)-1]) } return m, nil case tea.KeySpace: - m.markNote += " " + *field += " " return m, nil case tea.KeyRunes: - m.markNote += string(msg.Runes) + *field += string(msg.Runes) return m, nil } return m, nil @@ -524,7 +555,7 @@ 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. -func (m ReaderModel) saveBookmark(verse int, note string) ReaderModel { +func (m ReaderModel) saveBookmark(verse int, note, tags string) ReaderModel { if m.store == nil || len(m.verses) == 0 { return m } @@ -538,6 +569,7 @@ func (m ReaderModel) saveBookmark(verse int, note string) ReaderModel { Chapter: chap, Verse: verse, Note: strings.TrimSpace(note), + Tags: bookmarks.ParseTags(tags), }) m.flash = fmt.Sprintf("bookmarked %s %d:%d", book.Name, chap, verse) return m @@ -699,13 +731,31 @@ func (m ReaderModel) View() string { switch m.mode { case modePick: return m.viewPick() + case modeMark: + return m.viewMark() case modeBookmarks: return m.viewBookmarks() - default: // modeRead + modeMark (the mark prompt overlays the reading footer) + default: // modeRead + modeMarkVerse (verse cursor highlighted in the reading) return m.viewRead() } } +// viewMark renders the bookmark note/tags box as a prominent centered dialog. +func (m ReaderModel) viewMark() string { + ui := i18n.Get(m.cfg.UILanguage) + b := m.books[m.bookIdx] + title := modalTitleStyle.Render(fmt.Sprintf("★ %s %d:%d", b.Name, m.currentChapter(), m.markVerse)) + note := " " + ui.ReaderMarkNote + ": " + m.markNote + tags := " " + ui.ReaderMarkTags + ": " + m.markTags + if m.markField == 0 { + note += "▏" + } else { + tags += "▏" + } + inner := title + "\n\n" + note + "\n" + tags + "\n\n" + citationStyle.Render(ui.ReaderMarkHelp) + return m.modal(inner) +} + // viewBookmarks renders the saved-bookmarks list. func (m ReaderModel) viewBookmarks() string { w := m.width @@ -713,12 +763,27 @@ func (m ReaderModel) viewBookmarks() string { w = 80 } ui := i18n.Get(m.cfg.UILanguage) - header := headerStyle.Width(w).Render(ui.ReaderBookmarksTitle) - footerKeys := ui.ReaderBookmarksKeys - if m.confirmDelete { - footerKeys = ui.ReaderConfirmDelete + + // Deleting -> a prominent centered confirmation dialog naming the bookmark. + if m.confirmDelete && m.markSel >= 0 && m.markSel < len(m.marks) { + bm := m.marks[m.markSel] + name := bm.Book + if idx := m.bookIndex(bm.Book); idx >= 0 { + name = m.books[idx].Name + } + ref := fmt.Sprintf("%s %d", name, bm.Chapter) + if bm.Verse > 0 { + ref += fmt.Sprintf(":%d", bm.Verse) + } + if bm.Note != "" { + ref += " — " + bm.Note + } + inner := modalTitleStyle.Render(ui.ReaderConfirmDelete) + "\n\n " + ref + "\n\n" + citationStyle.Render(ui.ReaderConfirmKeys) + return m.modal(inner) } - footer := footerStyle.Width(w).Render(footerKeys) + + header := headerStyle.Width(w).Render(ui.ReaderBookmarksTitle) + footer := footerStyle.Width(w).Render(ui.ReaderBookmarksKeys) if len(m.marks) == 0 { return header + "\n" + citationStyle.Render(ui.ReaderNoBookmarks) + "\n" + footer } diff --git a/internal/tui/reader_test.go b/internal/tui/reader_test.go index f3e43d5..904836e 100644 --- a/internal/tui/reader_test.go +++ b/internal/tui/reader_test.go @@ -124,8 +124,10 @@ func TestReaderBookmarkFlow(t *testing.T) { if m.mode != modeMark { t.Fatal("enter did not open the note box") } - m = key(m, runes("hi")) - m = key(m, tea.KeyMsg{Type: tea.KeyEnter}) + m = key(m, runes("hi")) // note field + m = key(m, tea.KeyMsg{Type: tea.KeyTab}) // switch to tags field + m = key(m, runes("grace")) // tags field + m = key(m, tea.KeyMsg{Type: tea.KeyEnter}) // save if m.mode != modeRead { t.Fatal("enter did not save + return to reading") } @@ -134,8 +136,9 @@ func TestReaderBookmarkFlow(t *testing.T) { if m.mode != modeBookmarks || len(m.marks) != 1 { t.Fatalf("bookmarks mode=%v n=%d", m.mode, len(m.marks)) } - if m.marks[0].Book != "John" || m.marks[0].Verse != 2 || m.marks[0].Note != "hi" { - t.Errorf("mark = %+v (want John 1:2 note hi)", m.marks[0]) + if m.marks[0].Book != "John" || m.marks[0].Verse != 2 || m.marks[0].Note != "hi" || + len(m.marks[0].Tags) != 1 || m.marks[0].Tags[0] != "grace" { + t.Errorf("mark = %+v (want John 1:2 note hi tag grace)", m.marks[0]) } if !strings.Contains(m.View(), "John") { t.Errorf("bookmarks view missing John:\n%s", m.View()) |
