From 102bf0c522c94d36f8363c38711621fe5ad18555 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 24 Jul 2026 16:39:35 +0200 Subject: 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 --- internal/tui/reader.go | 83 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 8 deletions(-) (limited to 'internal/tui') 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 -- cgit v1.3