all repos — bibel @ 0be549fc2abddcf0f0e574436c2607ed7f844ab0

Unnamed repository; edit this file 'description' to name the repository.

internal/tui/model.go (view raw)

  1package tui
  2
  3import (
  4	"strings"
  5
  6	tea "github.com/charmbracelet/bubbletea"
  7	"github.com/charmbracelet/lipgloss"
  8	bible "maxwelljensen/bibel/internal"
  9)
 10
 11// Model represents the TUI application state
 12type Model struct {
 13	bibleData *bible.Bible
 14	bookmark  *bible.Bookmark
 15	width     int
 16	height    int
 17	quitting  bool
 18	plainMode bool
 19	styles    *Styles
 20	formatter *bible.Formatter
 21}
 22
 23// Styles contains the lipgloss styles for the TUI
 24type Styles struct {
 25	box         lipgloss.Style
 26	header      lipgloss.Style
 27	content     lipgloss.Style
 28	quitMessage lipgloss.Style
 29}
 30
 31// NewModel creates a new TUI model with the given Bible data and bookmark
 32func NewModel(bibleData *bible.Bible, bookmark *bible.Bookmark, plainMode bool) Model {
 33	m := Model{
 34		bibleData: bibleData,
 35		bookmark:  bookmark,
 36		plainMode: plainMode,
 37		styles:    createStyles(),
 38		formatter: &bible.Formatter{},
 39	}
 40	return m
 41}
 42
 43// createStyles creates the lipgloss styles, adapting to terminal background color
 44func createStyles() *Styles {
 45	// Detect if terminal has dark background
 46	hasDarkBG := lipgloss.HasDarkBackground()
 47
 48	// Helper function for light/dark colors
 49	lightDark := func(light, dark lipgloss.TerminalColor) lipgloss.TerminalColor {
 50		if hasDarkBG {
 51			return dark
 52		}
 53		return light
 54	}
 55
 56	// Colors that match the existing formatter colors (green/yellow)
 57	// but adapted for light/dark backgrounds
 58	green := lightDark(lipgloss.Color("#00AA00"), lipgloss.Color("#00FF00"))
 59	borderColor := lightDark(lipgloss.Color("#666666"), lipgloss.Color("#999999"))
 60	textColor := lightDark(lipgloss.Color("#000000"), lipgloss.Color("#FFFFFF"))
 61	quitColor := lightDark(lipgloss.Color("#555555"), lipgloss.Color("#AAAAAA"))
 62
 63	return &Styles{
 64		box: lipgloss.NewStyle().
 65			Border(lipgloss.RoundedBorder()).
 66			BorderForeground(borderColor).
 67			Padding(1, 2).
 68			Margin(1, 0),
 69
 70		header: lipgloss.NewStyle().
 71			Foreground(green).
 72			Bold(true).
 73			MarginBottom(1),
 74
 75		content: lipgloss.NewStyle().
 76			Foreground(textColor),
 77
 78		quitMessage: lipgloss.NewStyle().
 79			Foreground(quitColor).
 80			Italic(true).
 81			MarginTop(1),
 82	}
 83}
 84
 85// Init initializes the model (required by bubbletea)
 86func (m Model) Init() tea.Cmd {
 87	// No initial command needed
 88	return nil
 89}
 90
 91// Update handles messages and updates the model
 92func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 93	switch msg := msg.(type) {
 94	case tea.KeyMsg:
 95		switch msg.String() {
 96		case "q", "Q", "ctrl+c", "esc":
 97			m.quitting = true
 98			return m, tea.Quit
 99		}
100	case tea.WindowSizeMsg:
101		// Update window dimensions
102		m.width = msg.Width
103		m.height = msg.Height
104	}
105
106	return m, nil
107}
108
109// View renders the UI
110func (m Model) View() string {
111	if m.quitting {
112		return ""
113	}
114
115	// Get formatted content using the formatter
116	header := m.formatter.FormatHeader(m.bookmark)
117	content := m.formatter.ExtractAndFormat(m.bibleData, m.bookmark)
118
119	// In plain mode, just return the original formatted text (with ANSI colors)
120	if m.plainMode {
121		var output strings.Builder
122		output.WriteString(header)
123		output.WriteString("\n")
124		output.WriteString(content)
125		return output.String()
126	}
127
128	// For TUI mode, we need to parse the header to extract the book/chapter and verse range
129	// The header from formatter has ANSI codes, but we want to style with lipgloss
130	// Let's extract just the text without ANSI codes
131	headerText := stripANSI(header)
132
133	// Build the box content
134	var boxContent strings.Builder
135
136	// Add header styled with lipgloss
137	boxContent.WriteString(m.styles.header.Render(headerText))
138	boxContent.WriteString("\n\n")
139
140	// Add verse content with wrapping based on available width
141	// Calculate available width for content
142	// Box has: Padding(1, 2) = 2 left + 2 right = 4
143	// Border = 1 left + 1 right = 2
144	// Total horizontal frame = 6
145	// Use window width with some margin
146	availableWidth := m.width - 10 // Leave some terminal margin
147	if availableWidth < 30 {
148		availableWidth = 30 // Minimum reasonable width
149	}
150	contentWidth := availableWidth - 6 // Account for box frame
151
152	// Apply width constraint for text wrapping
153	wrappedContent := m.styles.content.Width(contentWidth).Render(content)
154	boxContent.WriteString(wrappedContent)
155
156	// Create the box with max width constraint
157	box := m.styles.box.MaxWidth(availableWidth).Render(boxContent.String())
158
159	// Add quit message below the box
160	quitMsg := m.styles.quitMessage.Render("Press q to quit...")
161
162	// Combine box and quit message
163	return lipgloss.JoinVertical(lipgloss.Center, box, quitMsg)
164}
165
166// CreateTUIProgram creates and returns a bubbletea program for the Bible verse
167func CreateTUIProgram(bible *bible.Bible, bookmark *bible.Bookmark, plainMode bool) *tea.Program {
168	model := NewModel(bible, bookmark, plainMode)
169	return tea.NewProgram(model)
170}
171
172// stripANSI removes ANSI escape codes from a string
173func stripANSI(str string) string {
174	var result strings.Builder
175	inEscape := false
176
177	for i := 0; i < len(str); i++ {
178		if str[i] == '\033' && i+1 < len(str) && str[i+1] == '[' {
179			inEscape = true
180			i++ // Skip the '['
181			continue
182		}
183
184		if inEscape {
185			// Look for the end of the escape sequence (letter m)
186			if str[i] >= 'A' && str[i] <= 'Z' || str[i] >= 'a' && str[i] <= 'z' {
187				inEscape = false
188			}
189			continue
190		}
191
192		result.WriteByte(str[i])
193	}
194
195	return result.String()
196}
197