all repos — bibel @ 63474ea3c260b3efb9d8af56effc2a24b2a3568f

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

feat: add numbered verse output and paragraph break handling

- Add `-n/--numbered` flag to print each verse on a numbered line with
  verse number
- Add `-g/--paragraphs` flag to render pilcrows (¶) as blank lines
  (paragraph breaks)
- Integrate both options into TUI with darker shade styling for verse
  numbers
- Add `numbered` and `paragraphs` fields to `[formatter]` section in
  TOML config
- BREAKING: Remove `-g` short flag from `--generate-config` (now
  `--paragraphs` uses `-g`)
- Update formatter with `NewFormatterWithFullConfig()` and
  `FormatSnippetWithOptions()` for option propagation
- Add `FormatSnippetForTUI()` method for TUI‑specific layout with
  numbered/paragraph modes
- Update `cmd/bibel.go`, `internal/config.go`, `internal/formatter.go`,
  `internal/tui/model.go`
- Update `README.md` with usage examples and configuration documentation
- Update `CHANGELOG.md` to version 0.8.0
- Update `configs/config.example.toml` with new formatter fields
Maxwell Jensen 85795372+maxwelljens@users.noreply.github.com
Fri, 17 Apr 2026 20:20:01 +0200
commit

63474ea3c260b3efb9d8af56effc2a24b2a3568f

parent

24fb7ab0d94eef6e9a9dab5d2553b861096d5889

M CHANGELOG.mdCHANGELOG.md

@@ -8,7 +8,31 @@ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased] -## [1.5.0] - 2026-04-17 +## [0.8.0] - 2026-04-17 + +### Added + +- **Verse numbering option**: New `-n/--numbered` flag prints each verse on a +numbered line with verse number +- **Paragraph handling option**: New `-g/--paragraphs` flag renders pilcrows +(¶) as blank lines instead of ignoring them +- **TUI numbered styling**: Verse numbers use darker shade styling via +`lipgloss` for better readability +- **Configuration integration**: Both options configurable via TOML file in +`[formatter]` section (`numbered` and `paragraphs` fields) + +### Changed + +- **Formatter system enhanced**: New `FormatSnippetWithOptions()` and +`FormatSnippetForTUI()` methods for advanced formatting +- **TUI width handling**: Improved width calculations for numbered verses to +prevent text clipping +- **Paragraph logic refined**: Fixed newline handling to only insert blank +lines for pilcrows, not between all verses +- **Phased out short flag**: `--generate-config` no longer has the `-g` short +flag. This is now under `--paragraphs`. + +## [0.7.0] - 2026-04-17 ### Added

@@ -38,7 +62,7 @@ for selected reading mode

- **Date progression robustness**: Added check for zero total verses to prevent divide-by-zero panics -## [1.4.0] - 2026-04-17 +## [0.6.0] - 2026-04-17 ### Added

@@ -76,7 +100,7 @@ - **Configuration validation**: Allow empty reading mode during initial load

- **Build errors**: Fixed Go compilation issues in configuration unmarshalling - **Backward compatibility**: Maintains existing behavior for default mode -## [1.3.0] - 2026-04-17 +## [0.5.0] - 2026-04-17 ### Added

@@ -118,7 +142,7 @@ command-line arguments taking precedence

- **Package structure**: Added `internal/config.go` for configuration management -## [1.2.0] - 2026-04-16 +## [0.4.0] - 2026-04-16 ### Added

@@ -143,7 +167,7 @@ - **Terminal detection**: Non-TTY environments show formatted output with ANSI

colours - **Dependencies**: Added `bubbletea` and `lipgloss` for TUI functionality -## [1.1.0] - 2026-04-16 +## [0.3.0] - 2026-04-16 ### Added

@@ -151,7 +175,7 @@ - Argument parsing using `go-arg` library

- `-p/--plain` flag to output plain text without chapter name or verse numbers - Program description and help text -## [1.0.0] - 2026-04-16 +## [0.2.0] - 2026-04-16 ### Added
M README.mdREADME.md

@@ -17,6 +17,10 @@ - **Interactive Controls**: Press `q` to quit (MOTD-like behaviour)

- **Yearly Cycle**: Progresses through all four Gospels each year, restarting on 1 January - **Multiple Output Modes**: Interactive TUI, formatted ANSI, or plain text +- **Verse Numbering**: Option to print each verse on a numbered line with verse +numbers +- **Paragraph Handling**: Option to render pilcrows (¶) in source JSON Bible +file as paragraph breaks with blank lines ## Installation

@@ -35,6 +39,34 @@ 1. Calculating today's date and day of year

2. Determining Bible position: day of year * 12 verses 3. Find corresponding verses in the Gospels +### Usage Examples + +```bash +# Default TUI mode with interactive display +./bibel + +# Plain text output +./bibel --plain + +# Formatted output with ANSI colours +./bibel --formatted + +# Numbered verses (each verse on its own line with verse number) +./bibel --numbered + +# Paragraph mode (pilcrows render as blank lines) +./bibel --paragraphs + +# Combine numbered and paragraph modes +./bibel --numbered --paragraphs + +# Plain output with numbered verses +./bibel --plain --numbered + +# Generate configuration file +./bibel --generate-config +``` + This is default behaviour. Alternative configurations are available (see below).

@@ -68,6 +100,10 @@ - **use_colours**: Whether to use ANSI colours in formatted output (default:

true) - **header_format**: Header format template with variables: {book}, {chapter}, {first_verse}, {second_verse} +- **numbered**: Whether to print each verse on a numbered line corresponding to +the verse number (default: false) +- **paragraphs**: Whether to render pilcrows (¶) as blank lines instead of +ignoring them (default: false) #### Date Progression Settings (`[date_progression]` section) - **verses_per_day**: Number of verses to read per day (default: 12)

@@ -96,6 +132,8 @@ - `-p, --plain`: Output plain text without formatting or TUI

- `-f, --formatted`: Output formatted text with ANSI colours (no TUI) - `-g, --generate-config`: Generate a default configuration file and exit - `-c, --config`: Path to configuration file (not yet implemented) +- `-n, --numbered`: Print each verse on a numbered line corresponding to the verse number +- `-g, --paragraphs`: Render pilcrows (¶) as blank lines instead of ignoring them ## Data Format
M cmd/bibel.gocmd/bibel.go

@@ -16,8 +16,10 @@ type Args struct {

Reading string `arg:"-r,--reading" help:"reading mode: evangelion (Gospels), new_testament, old_testament, bible (default: evangelion)"` Plain bool `arg:"-p,--plain" help:"output plain text without formatting or TUI"` Formatted bool `arg:"-f,--formatted" help:"output formatted text with ANSI colours (no TUI)"` - GenerateConfig bool `arg:"-g,--generate-config" help:"generate a default configuration file and exit"` + GenerateConfig bool `arg:"--generate-config" help:"generate a default configuration file and exit"` ConfigPath string `arg:"-c,--config" help:"path to configuration file (default: $XDG_CONFIG_HOME/bibel/config.toml)"` + Numbered bool `arg:"-n,--numbered" help:"print each verse on a numbered line with verse number"` + Paragraphs bool `arg:"-g,--paragraphs" help:"render pilcrows (¶) as blank lines instead of ignoring them"` } // Description returns a description of the program

@@ -58,6 +60,12 @@ cfg.OutputMode = "plain"

} else if args.Formatted { cfg.OutputMode = "formatted" } + if args.Numbered { + cfg.Formatter.Numbered = args.Numbered + } + if args.Paragraphs { + cfg.Formatter.Paragraphs = args.Paragraphs + } if args.ConfigPath != "" { // Note: This would require modifying LoadConfig to accept a path // For now, we'll just use the default XDG location

@@ -97,12 +105,12 @@

// Handle different output modes switch outputMode { case "plain": - formatter := bible.NewFormatterWithConfig(bibleData, false, cfg.Formatter.HeaderFormat) + formatter := bible.NewFormatterWithFullConfig(bibleData, false, cfg.Formatter.HeaderFormat, cfg.Formatter.Numbered, cfg.Formatter.Paragraphs) // In plain mode, we don't print the header fmt.Println(formatter.ExtractAndFormat(bibleData, todayBookmark)) case "formatted": - formatter := bible.NewFormatterWithConfig(bibleData, cfg.Formatter.UseColours, cfg.Formatter.HeaderFormat) + formatter := bible.NewFormatterWithFullConfig(bibleData, cfg.Formatter.UseColours, cfg.Formatter.HeaderFormat, cfg.Formatter.Numbered, cfg.Formatter.Paragraphs) fmt.Println(formatter.FormatHeader(todayBookmark)) fmt.Println(formatter.ExtractAndFormat(bibleData, todayBookmark))
M configs/config.example.tomlconfigs/config.example.toml

@@ -33,6 +33,12 @@

# Header format template # Available variables: {book}, {chapter}, {first_verse}, {second_verse} header_format = "{book} {chapter}\nw. {first_verse}-{second_verse}" + + # Whether to print each verse on a numbered line with verse number + numbered = false + + # Whether to render pilcrows (¶) as blank lines instead of ignoring them + paragraphs = false [date_progression] # Number of verses to read per day
M internal/config.gointernal/config.go

@@ -73,6 +73,12 @@ UseColours bool `toml:"use_colours" mapstructure:"use_colours"`

// Header format template (default: "{book} {chapter}\nw. {first_verse}-{second_verse}") HeaderFormat string `toml:"header_format" mapstructure:"header_format"` + + // Whether to print each verse on a numbered line (default: false) + Numbered bool `toml:"numbered" mapstructure:"numbered"` + + // Whether to render pilcrows (¶) as blank lines instead of ignoring them (default: false) + Paragraphs bool `toml:"paragraphs" mapstructure:"paragraphs"` } `toml:"formatter" mapstructure:"formatter"` // Date progression settings

@@ -102,6 +108,8 @@ cfg.TUI.QuitColour = "" // Empty means adaptive

cfg.Formatter.UseColours = true cfg.Formatter.HeaderFormat = "{book} {chapter}\nw. {first_verse}-{second_verse}" + cfg.Formatter.Numbered = false + cfg.Formatter.Paragraphs = false cfg.DateProgression.VersesPerDay = 12 cfg.DateProgression.StartDate = "" // Empty means 1 January of current year

@@ -135,6 +143,8 @@ viper.SetDefault("tui.text_colour", defaultCfg.TUI.TextColour)

viper.SetDefault("tui.quit_colour", defaultCfg.TUI.QuitColour) viper.SetDefault("formatter.use_colours", defaultCfg.Formatter.UseColours) viper.SetDefault("formatter.header_format", defaultCfg.Formatter.HeaderFormat) + viper.SetDefault("formatter.numbered", defaultCfg.Formatter.Numbered) + viper.SetDefault("formatter.paragraphs", defaultCfg.Formatter.Paragraphs) viper.SetDefault("date_progression.verses_per_day", defaultCfg.DateProgression.VersesPerDay) viper.SetDefault("date_progression.start_date", defaultCfg.DateProgression.StartDate)

@@ -206,6 +216,8 @@ viper.Set("tui.text_colour", cfg.TUI.TextColour)

viper.Set("tui.quit_colour", cfg.TUI.QuitColour) viper.Set("formatter.use_colours", cfg.Formatter.UseColours) viper.Set("formatter.header_format", cfg.Formatter.HeaderFormat) + viper.Set("formatter.numbered", cfg.Formatter.Numbered) + viper.Set("formatter.paragraphs", cfg.Formatter.Paragraphs) viper.Set("date_progression.verses_per_day", cfg.DateProgression.VersesPerDay) viper.Set("date_progression.start_date", cfg.DateProgression.StartDate)
M internal/formatter.gointernal/formatter.go

@@ -3,6 +3,8 @@

import ( "fmt" "strings" + + "github.com/charmbracelet/lipgloss" ) // Formatter handles formatting Bible text for display

@@ -10,6 +12,8 @@ type Formatter struct {

bible *Bible useColours bool headerFormat string + numbered bool + paragraphs bool } // NewFormatter creates a new formatter with default settings

@@ -18,16 +22,25 @@ return &Formatter{

bible: bible, useColours: true, headerFormat: "{book} {chapter}\nv. {first_verse}-{second_verse}", + numbered: false, + paragraphs: false, } } -// NewFormatterWithConfig creates a new formatter with configuration -func NewFormatterWithConfig(bible *Bible, useColours bool, headerFormat string) *Formatter { +// NewFormatterWithFullConfig creates a new formatter with full configuration +func NewFormatterWithFullConfig(bible *Bible, useColours bool, headerFormat string, numbered bool, paragraphs bool) *Formatter { return &Formatter{ bible: bible, useColours: useColours, headerFormat: headerFormat, + numbered: numbered, + paragraphs: paragraphs, } +} + +// NewFormatterWithConfig creates a new formatter with configuration +func NewFormatterWithConfig(bible *Bible, useColours bool, headerFormat string) *Formatter { + return NewFormatterWithFullConfig(bible, useColours, headerFormat, false, false) } // FormatHeader formats the header for a bookmark using the configured template

@@ -93,28 +106,125 @@ }

// FormatSnippet formats the verse range text func (f *Formatter) FormatSnippet(verses []*Verse) string { + // Use the new options-aware method + return f.FormatSnippetWithOptions(verses) +} + +// FormatSnippetWithOptions formats the verse range text with configuration options +func (f *Formatter) FormatSnippetWithOptions(verses []*Verse) string { if len(verses) == 0 { return "Error: No text matched" } var sb strings.Builder for i, verse := range verses { - if i > 0 { - sb.WriteString(" ") - } - // Remove any leading pilcrow (¶) and trim + // Check for pilcrow to handle paragraph breaks text := strings.TrimSpace(verse.Text) + hasPilcrow := strings.HasPrefix(text, "¶ ") || strings.HasPrefix(text, "¶") + + // Remove pilcrow markers if present if strings.HasPrefix(text, "¶ ") { text = text[2:] } else if strings.HasPrefix(text, "¶") { text = text[1:] } - sb.WriteString(text) + + // Handle paragraph breaks (blank line before verse with pilcrow) + if f.paragraphs && hasPilcrow && i > 0 { + // Add blank line before this verse (paragraph break) + // If numbered mode, we'll add a newline anyway, so just add one more + if f.numbered { + sb.WriteString("\n\n") + } else { + // In non-numbered mode, need space then blank line + if i > 0 { + sb.WriteString(" \n\n") + } else { + sb.WriteString("\n\n") + } + } + } + + // Handle numbered output + if f.numbered { + if i > 0 && !(f.paragraphs && hasPilcrow) { + // Don't add newline if we already added one for paragraph + sb.WriteString("\n") + } + sb.WriteString(fmt.Sprintf("%d. %s", verse.VerseNum, text)) + } else { + // Regular mode - join with spaces + // Don't add space if we just added paragraph break + if i > 0 && !(f.paragraphs && hasPilcrow) { + sb.WriteString(" ") + } + sb.WriteString(text) + } } return sb.String() } +// FormatSnippetForTUI formats verse text for TUI with lipgloss styling +func (f *Formatter) FormatSnippetForTUI(verses []*Verse, style lipgloss.Style, numberStyle lipgloss.Style, contentWidth int) string { + if len(verses) == 0 { + return "Error: No text matched" + } + + var resultLines []string + + for i, verse := range verses { + // Process verse text + text := strings.TrimSpace(verse.Text) + hasPilcrow := strings.HasPrefix(text, "¶ ") || strings.HasPrefix(text, "¶") + + // Remove pilcrow markers if present + if strings.HasPrefix(text, "¶ ") { + text = text[2:] + } else if strings.HasPrefix(text, "¶") { + text = text[1:] + } + + // Handle paragraph breaks (blank line before verse with pilcrow) + if f.paragraphs && hasPilcrow && i > 0 { + // Add blank line before this verse (paragraph break) + resultLines = append(resultLines, "") + } + + if f.numbered { + // For numbered mode: "1. text" + // Estimate number takes about 4 characters (for verse numbers up to 3 digits + ". ") + const numberWidthEstimate = 4 + remainingWidth := max(1, contentWidth - numberWidthEstimate) + + numberPart := numberStyle.Render(fmt.Sprintf("%d. ", verse.VerseNum)) + textPart := style.Width(remainingWidth).Render(text) + resultLines = append(resultLines, numberPart + textPart) + } else { + // For non-numbered mode, we'll handle text accumulation separately + // Store plain text for now, will join and render later + if len(resultLines) == 0 || resultLines[len(resultLines)-1] == "" { + // Start new line + resultLines = append(resultLines, text) + } else { + // Continue current line with space + resultLines[len(resultLines)-1] = resultLines[len(resultLines)-1] + " " + text + } + } + } + + // Apply styling to non-numbered lines + if !f.numbered { + for i, line := range resultLines { + if line != "" { // Keep empty lines (paragraph breaks) as is + resultLines[i] = style.Width(contentWidth).Render(line) + } + } + } + + return strings.Join(resultLines, "\n") +} + // ExtractAndFormat extracts verses for a bookmark and formats them func (f *Formatter) ExtractAndFormat(bible *Bible, bookmark *Bookmark) string { verses := bible.GetVerseRange(int(bookmark.Book), bookmark.Chapter,

@@ -128,4 +238,10 @@ header := f.FormatHeader(bookmark)

content := f.ExtractAndFormat(bible, bookmark) return header + "\n" + content } - +// max returns the larger of two integers +func max(a, b int) int { + if a > b { + return a + } + return b +}
M internal/tui/model.gointernal/tui/model.go

@@ -26,6 +26,7 @@ type Styles struct {

box lipgloss.Style header lipgloss.Style content lipgloss.Style + numberStyle lipgloss.Style // For numbered verse output quitMessage lipgloss.Style }

@@ -36,7 +37,7 @@ bibleData: bibleData,

bookmark: bookmark, outputMode: config.OutputMode, styles: createStyles(config), - formatter: bible.NewFormatterWithConfig(bibleData, config.Formatter.UseColours, config.Formatter.HeaderFormat), + formatter: bible.NewFormatterWithFullConfig(bibleData, config.Formatter.UseColours, config.Formatter.HeaderFormat, config.Formatter.Numbered, config.Formatter.Paragraphs), config: config, } return m

@@ -116,6 +117,10 @@

content: lipgloss.NewStyle(). Foreground(textColour), + numberStyle: lipgloss.NewStyle(). + Foreground(lightDark(lipgloss.Color("#444444"), lipgloss.Color("#AAAAAA"))). + Bold(false), + quitMessage: lipgloss.NewStyle(). Foreground(quitColour). Italic(true).

@@ -157,6 +162,10 @@ // Get formatted content using the formatter

header := m.formatter.FormatHeader(m.bookmark) content := m.formatter.ExtractAndFormat(m.bibleData, m.bookmark) + // For TUI mode, use special formatting + verses := m.bibleData.GetVerseRange(int(m.bookmark.Book), m.bookmark.Chapter, + m.bookmark.FirstVerse, m.bookmark.SecondVerse) + // In plain or formatted mode, just return the original formatted text if m.outputMode == "plain" || m.outputMode == "formatted" { var output strings.Builder

@@ -191,9 +200,20 @@ // Minimum reasonable width

30) contentWidth := availableWidth - 6 // Account for box frame - // Apply width constraint for text wrapping - wrappedContent := m.styles.content.Width(contentWidth).Render(content) - boxContent.WriteString(wrappedContent) + // Format verses with appropriate styling + var verseContent string + if m.config.Formatter.Numbered || m.config.Formatter.Paragraphs { + // Use TUI-aware formatting for numbered or paragraph mode + verseContent = m.formatter.FormatSnippetForTUI(verses, + m.styles.content, + m.styles.numberStyle, + contentWidth) + } else { + // Use regular formatting + wrappedContent := m.styles.content.Width(contentWidth).Render(content) + verseContent = wrappedContent + } + boxContent.WriteString(verseContent) // Create the box with max width constraint box := m.styles.box.MaxWidth(availableWidth).Render(boxContent.String())

@@ -240,4 +260,12 @@ result.WriteByte(str[i])

} return result.String() +} + +// max returns the larger of two integers +func max(a, b int) int { + if a > b { + return a + } + return b }