summaryrefslogtreecommitdiff
path: root/internal/tui/reader.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 16:39:35 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 16:39:35 +0200
commit102bf0c522c94d36f8363c38711621fe5ad18555 (patch)
treec5be424abf0b2a2316e318502ff698f6029228a5 /internal/tui/reader.go
parent3eb99c1dd63565fbc1f5bf5af7fc8e835aad182e (diff)
downloadlectio-102bf0c522c94d36f8363c38711621fe5ad18555.tar.gz
lectio-102bf0c522c94d36f8363c38711621fe5ad18555.zip
bookmarks: store as universal TSV (header row) with one-time JSON migration; tui note box wraps long notes with hanging indent + hard-break; v0.21.0
Diffstat (limited to 'internal/tui/reader.go')
-rw-r--r--internal/tui/reader.go83
1 files changed, 75 insertions, 8 deletions
diff --git a/internal/tui/reader.go b/internal/tui/reader.go
index 72b7d0a..df92349 100644
--- a/internal/tui/reader.go
+++ b/internal/tui/reader.go
@@ -741,21 +741,88 @@ func (m ReaderModel) View() string {
}
// 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.
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 += "▏"
+ cw := m.width - 12
+ if cw > 56 {
+ cw = 56
+ }
+ if cw < 24 {
+ cw = 24
}
- inner := title + "\n\n" + note + "\n" + tags + "\n\n" + citationStyle.Render(ui.ReaderMarkHelp)
+ title := modalTitleStyle.Render(fmt.Sprintf("★ %s %d:%d", b.Name, m.currentChapter(), m.markVerse))
+ lines := wrapField(ui.ReaderMarkNote, m.markNote, cw, m.markField == 0)
+ lines = append(lines, wrapField(ui.ReaderMarkTags, m.markTags, cw, m.markField == 1)...)
+ inner := title + "\n\n" + strings.Join(lines, "\n") + "\n\n" + citationStyle.Render(ui.ReaderMarkHelp)
return m.modal(inner)
}
+// wrapField renders "label: value" wrapping value to width with a hanging
+// indent under the label; a cursor is appended when the field is active.
+func wrapField(label, value string, width int, active bool) []string {
+ prefix := label + ": "
+ pw := len([]rune(prefix))
+ text := value
+ if active {
+ text += "▏"
+ }
+ tw := width - pw
+ if tw < 8 {
+ tw = 8
+ }
+ wrapped := hardWrap(text, tw)
+ indent := strings.Repeat(" ", pw)
+ out := make([]string, 0, len(wrapped))
+ for i, ln := range wrapped {
+ if i == 0 {
+ out = append(out, prefix+ln)
+ } else {
+ out = append(out, indent+ln)
+ }
+ }
+ return out
+}
+
+// hardWrap wraps on spaces but, unlike render.Wrap, also hard-breaks a single
+// token longer than width (notes can contain arbitrary unbroken text).
+func hardWrap(s string, width int) []string {
+ if width < 1 {
+ width = 1
+ }
+ var lines []string
+ cur := ""
+ for _, word := range strings.Fields(s) {
+ for len([]rune(word)) > width {
+ if cur != "" {
+ lines = append(lines, cur)
+ cur = ""
+ }
+ r := []rune(word)
+ lines = append(lines, string(r[:width]))
+ word = string(r[width:])
+ }
+ switch {
+ case cur == "":
+ cur = word
+ case len([]rune(cur))+1+len([]rune(word)) <= width:
+ cur += " " + word
+ default:
+ lines = append(lines, cur)
+ cur = word
+ }
+ }
+ if cur != "" {
+ lines = append(lines, cur)
+ }
+ if len(lines) == 0 {
+ return []string{""}
+ }
+ return lines
+}
+
// viewBookmarks renders the saved-bookmarks list.
func (m ReaderModel) viewBookmarks() string {
w := m.width