all repos — bibel @ 01c758ad65f39049a6b57f85c0e09c74b5f1f1ea

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

feat: add TOML configuration system with flexible output modes and customisation

- Add configuration system using Viper and go-toml (`internal/config.go`)
  - Support XDG base directory spec (`~/.config/bibel/config.toml`)
  - Implement `LoadConfig()`, `SaveConfig()`, `GenerateDefaultConfig()`
  - Validate output modes, border styles, verses per day
- Introduce three output modes: `tui` (default), `formatted` (ANSI), `plain`
  - Add `--formatted` and `--plain` CLI flags, plus `--generate-config`
  - Auto-detect terminal and fallback from `tui` to `formatted` when not a TTY
- Make verses per day configurable (`date_progression.verses_per_day`)
  - Update `DateProgression` to use configurable value in offset calculation
  - Replace all hardcoded `12` references with `dp.versesPerDay`
  - Adjust lookahead rule to use configured verses per day
- Add configurable TUI styling (`[tui]` section)
  - Border style: `rounded`, `double`, `single`, `hidden`
  - Custom colours for border, header, text, quit message (hex or named)
  - Option to hide the "Press q to quit..." message (`show_quit_message`)
- Make formatter header template configurable (`[formatter]` section)
  - Support variables: `{book}`, `{chapter}`, `{first_verse}`, `{second_verse}`
  - Allow disabling ANSI colours (`use_colours`)
- Update dependencies:
  - Add `github.com/spf13/viper`, `github.com/adrg/xdg`, `github.com/pelletier/go-toml/v2`
  - Remove unused indirect dependencies (e.g., `charmbracelet/colorprofile`, `x/term`)
- Add `configs/config.example.toml` as reference configuration
- Update `README.md` with full configuration documentation and new CLI usage
- Refactor code to pass `*Config` struct instead of separate flags
  - Remove `plainMode` boolean from TUI model, use `outputMode` field
  - Add `NewFormatterWithConfig`, `NewDateProgressionWithConfig`
Maxwell Jensen 85795372+maxwelljens@users.noreply.github.com
Fri, 17 Apr 2026 11:47:22 +0200
commit

01c758ad65f39049a6b57f85c0e09c74b5f1f1ea

parent

d229be0e52779e06632c53ce0a1115b2c2249c25

M CHANGELOG.mdCHANGELOG.md

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

## [Unreleased] +## [1.3.0] - 2026-04-17 + +### Added + +- **TOML configuration file** system with XDG Base Directory Specification +compliance + - Configuration file location: `$XDG_CONFIG_HOME/bibel/config.toml` (default: + `~/.config/bibel/config.toml`). macOS and Windows also supported. + - Uses `xdg` for XDG compliance and `viper` for configuration management +- **Configuration generation**: `--generate-config` flag creates default +configuration file +- **Comprehensive TUI aesthetic configuration**: + - `border_style`: "rounded", "double", "single", or "hidden" + - `border_colour`: Custom hex colour for box borders + - `header_colour`: Custom hex colour for header text + - `text_colour`: Custom hex colour for verse content + - `quit_colour`: Custom hex colour for quit message + - `show_quit_message`: Boolean to show/hide "Press q to quit..." message +- **Formatter configuration**: + - `header_format`: Customisable header template with variables: `{book}`, + `{chapter}`, `{first_verse}`, `{second_verse}` + - `use_colours`: Enable/disable ANSI colour output in formatted mode +- **Date progression configuration**: + - `verses_per_day`: Customisable number of verses per day (default: 12) +- **Command-line override**: CLI flags (`--plain`, `--formatted`) override +configuration settings +- **Configuration validation**: Ensures valid output modes, border styles, and +positive verse counts + +### Changed + +- **Formatter behaviour**: `FormatHeader()` now reads `header_format` from +configuration instead of hardcoded format +- **TUI styling**: All aesthetic properties (colours, border style) now +configurable via TOML file +- **Date progression**: `NewDateProgressionWithConfig()` accepts custom +`verses_per_day` parameter +- **Application flow**: Configuration loaded before processing, with +command-line arguments taking precedence +- **Package structure**: Added `internal/config.go` for configuration +management + ## [1.2.0] - 2026-04-16 ### Added
M README.mdREADME.md

@@ -6,7 +6,8 @@ per day through the four Gospels.

## Features -- **Interactive TUI**: Terminal User Interface using `bubbletea` with styled boxes +- **Interactive TUI**: Terminal User Interface using `bubbletea` with styled +boxes - **Date-Based Progression**: Automatically calculates position based on day of year (1 January = Matthew 1:1-12) - **Smart Sizing**: Default snippet size is 12 verses, extends to end of

@@ -29,18 +30,73 @@ ```bash

./bibel ``` -The program will: -1. Calculate today's date and day of year -2. Determine Bible position: day_of_year × 12 verses +By default, the verses the program picks are done by: +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 -4. Display the Bible snippet with colored headers + +This is default behaviour. Alternative configurations are available (see +below). + +## Configuration + +`bibel` supports configuration via a TOML file located at +`$XDG_CONFIG_HOME/bibel/config.toml` (default: `~/.config/bibel/config.toml`). +Default macOS and Windows paths are also supported. + +### Configuration Options + +- **output_mode**: Output mode: "tui" (interactive terminal UI), "formatted" +(ANSI-coloured text), or "plain" (plain text) +- **bible_path**: Path to Bible data file (default: "books/pol_nbg.json") + +#### TUI Settings (`[tui]` section) +- **show_quit_message**: Whether to show "Press q to quit..." message (default: +true) +- **border_style**: Box border style: "rounded", "double", "single", or +"hidden" (default: "rounded") +- **border_colour**: Box border colour (hex or named colour, empty for +adaptive) +- **header_colour**: Header text colour (empty for adaptive) +- **text_colour**: Bible text colour (empty for adaptive) +- **quit_colour**: Quit message colour (empty for adaptive) + +#### Formatter Settings (`[formatter]` section) +- **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} + +#### Date Progression Settings (`[date_progression]` section) +- **verses_per_day**: Number of verses to read per day (default: 12) +- **start_date**: Start date for yearly progression (format: "1 January", empty +for current year) + +### Example Configuration + +See `configs/config.example.toml` in the project directory for a complete example. + +### Generating Configuration + +Generate a default configuration file: + +```bash +./bibel --generate-config +``` + +### Command Line Arguments -No bookmark file is needed or created - the position is calculated from the -date alone. +Command line arguments override configuration file settings: + +- `-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) ## Data Format -The Bible data is in JSON format (`books/pol_nbg.json`) with the following structure: +The Bible data is in JSON format (see `books/pol_nbg.json`) with the following +structure: ```json { "metadata": { ... },

@@ -68,8 +124,10 @@

The program calculates reading position as follows: 1. **Day of Year**: Get current day number (1-366) -2. **Verse Offset**: Multiply by 12 verses per day: `offset = (day_of_year - 1) × 12` -3. **Modulo Wrap**: Apply modulo with total Gospel verses (3779) to cycle yearly +2. **Verse Offset**: Multiply by 12 verses per day: `offset = (day_of_year - 1) + × 12` +3. **Modulo Wrap**: Apply modulo with total Gospel verses (3779) to cycle + yearly 4. **Position Mapping**: Walk through Gospels to find corresponding verses 5. **Lookahead Rule**: Extend to chapter end if less than 12 verses remain

@@ -77,18 +135,21 @@ ## Project Structure

``` . -├── cmd/bibel.go # Main CLI entry point -├── internal/ -│ ├── bible/ # Core Bible functionality -│ ├── verse.go # Data structures -│ ├── loader.go # JSON loading and indexing -│ ├── dateprogression.go # Date-based position calculation -│ ├── formatter.go # Output formatting -│ └── tui/ # Terminal User Interface -│ └── model.go # bubbletea TUI model and styling -├── books/pol_nbg.json # Bible data -├── go.mod # Go module dependencies -└── go.sum # Go dependency checksums +├── cmd/bibel.go # Main CLI entry point +├── configs/ # Default configuration files +│ └── config.example.toml # The default configuration file +├── internal/ +│ ├── bible/ # Core Bible functionality +│ ├── config.go # Reading and writing to config +│ ├── dateprogression.go # Date-based position calculation +│ ├── verse.go # Data structures +│ ├── loader.go # JSON loading and indexing +│ ├── formatter.go # Output formatting +│ └── tui/ # Terminal User Interface +│ └── model.go # bubbletea TUI model and styling +├── books/pol_nbg.json # Bible data +├── go.mod # Go module dependencies +└── go.sum # Go dependency checksums ``` ## Examples
M cmd/bibel.gocmd/bibel.go

@@ -14,28 +14,61 @@

// Args defines the command line arguments type Args struct { 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"` + ConfigPath string `arg:"-c,--config" help:"path to configuration file (default: $XDG_CONFIG_HOME/bibel/config.toml)"` } // Description returns a description of the program func (Args) Description() string { return "Display today's Bible reading from the four Gospels (Matthew, Mark, Luke, John).\n" + - "Reads 12 verses per day based on the current date." + "Reads 12 verses per day based on the current date.\n" + + "Configuration file: $XDG_CONFIG_HOME/bibel/config.toml" } func main() { var args Args arg.MustParse(&args) + // Handle config generation + if args.GenerateConfig { + if err := bible.GenerateDefaultConfig(); err != nil { + fmt.Fprintf(os.Stderr, "Error generating config: %v\n", err) + os.Exit(1) + } + fmt.Printf("Default configuration generated at: %s\n", bible.GetConfigPath()) + return + } + + // Load configuration + cfg, err := bible.LoadConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading configuration: %v\n", err) + os.Exit(1) + } + + // Override config with command line arguments + if args.Plain { + cfg.OutputMode = "plain" + } else if args.Formatted { + cfg.OutputMode = "formatted" + } + if args.ConfigPath != "" { + // Note: This would require modifying LoadConfig to accept a path + // For now, we'll just use the default XDG location + fmt.Fprintf(os.Stderr, "Note: Custom config path not yet implemented, using default location\n") + } + // Load Bible data - biblePath := "books/pol_nbg.json" + biblePath := cfg.BiblePath bibleData, err := bible.LoadBible(biblePath) if err != nil { fmt.Fprintf(os.Stderr, "Error loading Bible data: %v\n", err) os.Exit(1) } - // Initialise date progression - dateProg := bible.NewDateProgression(bibleData) + // Initialise date progression with configured verses per day + dateProg := bible.NewDateProgressionWithConfig(bibleData, cfg.DateProgression.VersesPerDay) // Get current date currentDate := time.Now()

@@ -47,28 +80,38 @@ fmt.Fprintf(os.Stderr, "Error calculating date position: %v\n", err)

os.Exit(1) } - // If plain mode, just print plain text (no TUI) - if args.Plain { - formatter := bible.NewFormatter() - // In plain mode, we don't print the header - fmt.Println(formatter.ExtractAndFormat(bibleData, todayBookmark)) - return + // Determine output mode based on configuration and terminal + outputMode := cfg.OutputMode + + // If terminal detection suggests different mode, adjust + if !term.IsTerminal(int(os.Stdout.Fd())) && outputMode == "tui" { + // Not a terminal, fall back to formatted + outputMode = "formatted" } - // Check if we're running in a terminal - // If not, fall back to formatted output (similar to plain mode but with header) - if !term.IsTerminal(int(os.Stdout.Fd())) { - formatter := bible.NewFormatter() + // Handle different output modes + switch outputMode { + case "plain": + formatter := bible.NewFormatterWithConfig(false, cfg.Formatter.HeaderFormat) + // In plain mode, we don't print the header + fmt.Println(formatter.ExtractAndFormat(bibleData, todayBookmark)) + + case "formatted": + formatter := bible.NewFormatterWithConfig(cfg.Formatter.UseColours, cfg.Formatter.HeaderFormat) fmt.Println(formatter.FormatHeader(todayBookmark)) fmt.Println(formatter.ExtractAndFormat(bibleData, todayBookmark)) - return - } - - // Create and run TUI program - program := tui.CreateTUIProgram(bibleData, todayBookmark, false) - if err := program.Start(); err != nil { - fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err) + + case "tui": + // Create and run TUI program with configuration + program := tui.CreateTUIProgram(bibleData, todayBookmark, cfg) + if err := program.Start(); err != nil { + fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err) + os.Exit(1) + } + + default: + fmt.Fprintf(os.Stderr, "Unknown output mode: %s\n", outputMode) + fmt.Fprintf(os.Stderr, "Valid modes: tui, formatted, plain\n") os.Exit(1) } -} - +}
A configs/config.example.toml

@@ -0,0 +1,39 @@

+# bibel configuration file +# Location: $XDG_CONFIG_HOME/bibel/config.toml (~/.config/bibel/config.toml) + +# Output mode: "tui" (interactive terminal UI), "formatted" (ANSI-coloured text), +# or "plain" (plain text without formatting) +output_mode = "tui" + +# Path to Bible data file (relative to executable or absolute path) +bible_path = "books/pol_nbg.json" + +[tui] + # Whether to show the quit message "Press q to quit..." + show_quit_message = true + + # Box border style: "rounded", "double", "single", or "hidden" + border_style = "rounded" + + # Colours for TUI elements (empty = adaptive to terminal) + # Use hex colours like "#FF0000" or named colours like "red" + border_colour = "" # Box border colour + header_colour = "" # Header text colour + text_colour = "" # Bible text colour + quit_colour = "" # Quit message colour + +[formatter] + # Whether to use ANSI colours in formatted output mode + use_colours = true + + # Header format template + # Available variables: {book}, {chapter}, {first_verse}, {second_verse} + header_format = "{book} {chapter}\nw. {first_verse}-{second_verse}" + +[date_progression] + # Number of verses to read per day + verses_per_day = 12 + + # Start date for yearly progression (format: "1 January") + # Empty means 1 January of current year + start_date = ""
M go.modgo.mod

@@ -8,17 +8,16 @@ github.com/charmbracelet/bubbletea v0.24.0

github.com/charmbracelet/lipgloss v0.8.0 ) +require github.com/pelletier/go-toml/v2 v2.3.0 // indirect + require ( + github.com/adrg/xdg v0.5.3 github.com/alexflint/go-scalar v1.2.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.4.3 // indirect - github.com/charmbracelet/x/ansi v0.11.7 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect

@@ -28,10 +27,16 @@ github.com/muesli/cancelreader v0.2.2 // indirect

github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/sync v0.1.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 + github.com/subosito/gotenv v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.43.0 // indirect - golang.org/x/term v0.6.0 // indirect - golang.org/x/text v0.3.8 // indirect + golang.org/x/term v0.6.0 + golang.org/x/text v0.28.0 // indirect )
M go.sumgo.sum

@@ -1,3 +1,5 @@

+github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/alexflint/go-arg v1.6.1 h1:uZogJ6VDBjcuosydKgvYYRhh9sRCusjOvoOLZopBlnA= github.com/alexflint/go-arg v1.6.1/go.mod h1:nQ0LFYftLJ6njcaee0sU+G0iS2+2XJQfA8I062D0LGc= github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=

@@ -6,32 +8,26 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=

github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/bubbletea v0.24.0 h1:l8PHrft/GIeikDPCUhQe53AJrDD8xGSn0Agirh8xbe8= github.com/charmbracelet/bubbletea v0.24.0/go.mod h1:rK3g/2+T8vOSEkNHvtq40umJpeVYDn6bLaqbgzhL/hg= -github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= -github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= -github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU= github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= -github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= -github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= -github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= -github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=

@@ -49,29 +45,47 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=

github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
A internal/config.go

@@ -0,0 +1,207 @@

+package bible + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/adrg/xdg" + "github.com/spf13/viper" +) + +// Config represents the application configuration +type Config struct { + // Output mode: "tui", "formatted", "plain" + OutputMode string `toml:"output_mode" mapstructure:"output_mode"` + + // Bible data file path (default: "books/pol_nbg.json") + BiblePath string `toml:"bible_path" mapstructure:"bible_path"` + + // TUI settings + TUI struct { + // Whether to show quit message (default: true) + ShowQuitMessage bool `toml:"show_quit_message" mapstructure:"show_quit_message"` + + // Box border style: "rounded", "double", "single", "hidden" + BorderStyle string `toml:"border_style" mapstructure:"border_style"` + + // Box border colour (default: adaptive to terminal) + BorderColour string `toml:"border_colour" mapstructure:"border_colour"` + + // Header colour (default: adaptive to terminal) + HeaderColour string `toml:"header_colour" mapstructure:"header_colour"` + + // Text colour (default: adaptive to terminal) + TextColour string `toml:"text_colour" mapstructure:"text_colour"` + + // Quit message colour (default: adaptive to terminal) + QuitColour string `toml:"quit_colour" mapstructure:"quit_colour"` + } `toml:"tui" mapstructure:"tui"` + + // Formatter settings + Formatter struct { + // Whether to use ANSI colours in formatted mode (default: true) + 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"` + } `toml:"formatter" mapstructure:"formatter"` + + // Date progression settings + DateProgression struct { + // Verses per day (default: 12) + VersesPerDay int `toml:"verses_per_day" mapstructure:"verses_per_day"` + + // Start date for progression (default: "1 January" of current year) + StartDate string `toml:"start_date" mapstructure:"start_date"` + } `toml:"date_progression" mapstructure:"date_progression"` +} + +// DefaultConfig returns a configuration with default values +func DefaultConfig() *Config { + cfg := &Config{ + OutputMode: "tui", + BiblePath: "books/pol_nbg.json", + } + + cfg.TUI.ShowQuitMessage = true + cfg.TUI.BorderStyle = "rounded" + cfg.TUI.BorderColour = "" // Empty means adaptive + cfg.TUI.HeaderColour = "" // Empty means adaptive + cfg.TUI.TextColour = "" // Empty means adaptive + cfg.TUI.QuitColour = "" // Empty means adaptive + + cfg.Formatter.UseColours = true + cfg.Formatter.HeaderFormat = "{book} {chapter}\nw. {first_verse}-{second_verse}" + + cfg.DateProgression.VersesPerDay = 12 + cfg.DateProgression.StartDate = "" // Empty means 1 January of current year + + return cfg +} + +// LoadConfig loads configuration from file and environment +func LoadConfig() (*Config, error) { + // Set up viper + viper.SetConfigName("config") // Name of config file (without extension) + viper.SetConfigType("toml") // REQUIRED if the config file does not have the extension in the name + + // Add XDG config paths + configPath := filepath.Join(xdg.ConfigHome, "bibel") + viper.AddConfigPath(configPath) + + // Also check current directory for local config + viper.AddConfigPath(".") + + // Set defaults + defaultCfg := DefaultConfig() + viper.SetDefault("output_mode", defaultCfg.OutputMode) + viper.SetDefault("bible_path", defaultCfg.BiblePath) + viper.SetDefault("tui.show_quit_message", defaultCfg.TUI.ShowQuitMessage) + viper.SetDefault("tui.border_style", defaultCfg.TUI.BorderStyle) + viper.SetDefault("tui.border_colour", defaultCfg.TUI.BorderColour) + viper.SetDefault("tui.header_colour", defaultCfg.TUI.HeaderColour) + 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("date_progression.verses_per_day", defaultCfg.DateProgression.VersesPerDay) + viper.SetDefault("date_progression.start_date", defaultCfg.DateProgression.StartDate) + + // Read in config file (if it exists) + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + // Config file not found; we'll use defaults + fmt.Fprintf(os.Stderr, "Note: No configuration file found at %s/config.toml\n", configPath) + fmt.Fprintf(os.Stderr, "Using default configuration. Run 'bibel --generate-config' to create a default config file.\n") + } else { + // Config file was found but another error was produced + return nil, fmt.Errorf("error reading config file: %w", err) + } + } + + // Unmarshal config into struct + var cfg Config + if err := viper.Unmarshal(&cfg); err != nil { + return nil, fmt.Errorf("error unmarshalling config: %w", err) + } + + // Validate configuration + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("invalid configuration: %w", err) + } + + return &cfg, nil +} + +// SaveConfig saves the configuration to file +func SaveConfig(cfg *Config) error { + // Set up viper with current config + viper.Set("output_mode", cfg.OutputMode) + viper.Set("bible_path", cfg.BiblePath) + viper.Set("tui.show_quit_message", cfg.TUI.ShowQuitMessage) + viper.Set("tui.border_style", cfg.TUI.BorderStyle) + viper.Set("tui.border_colour", cfg.TUI.BorderColour) + viper.Set("tui.header_colour", cfg.TUI.HeaderColour) + 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("date_progression.verses_per_day", cfg.DateProgression.VersesPerDay) + viper.Set("date_progression.start_date", cfg.DateProgression.StartDate) + + // Ensure config directory exists + configPath := filepath.Join(xdg.ConfigHome, "bibel") + if err := os.MkdirAll(configPath, 0755); err != nil { + return fmt.Errorf("error creating config directory: %w", err) + } + + // Write config file + configFile := filepath.Join(configPath, "config.toml") + if err := viper.WriteConfigAs(configFile); err != nil { + return fmt.Errorf("error writing config file: %w", err) + } + + return nil +} + +// GenerateDefaultConfig creates a default configuration file +func GenerateDefaultConfig() error { + return SaveConfig(DefaultConfig()) +} + +// GetConfigPath returns the path to the configuration file + +// Validate checks if configuration values are valid +func (c *Config) Validate() error { + // Validate output mode + validOutputModes := map[string]bool{ + "tui": true, + "formatted": true, + "plain": true, + } + if !validOutputModes[c.OutputMode] { + return fmt.Errorf("invalid output mode: %s, must be one of: tui, formatted, plain", c.OutputMode) + } + + // Validate TUI border style + validBorderStyles := map[string]bool{ + "rounded": true, + "double": true, + "single": true, + "hidden": true, + } + if !validBorderStyles[c.TUI.BorderStyle] { + return fmt.Errorf("invalid border style: %s, must be one of: rounded, double, single, hidden", c.TUI.BorderStyle) + } + + // Validate verses per day + if c.DateProgression.VersesPerDay <= 0 { + return fmt.Errorf("verses per day must be positive, got: %d", c.DateProgression.VersesPerDay) + } + + return nil +} +func GetConfigPath() string { + return filepath.Join(xdg.ConfigHome, "bibel", "config.toml") +}
M internal/dateprogression.gointernal/dateprogression.go

@@ -7,12 +7,21 @@ )

// DateProgression handles calculating Bible position based on date type DateProgression struct { - bible *Bible + bible *Bible + versesPerDay int } // NewDateProgression creates a new date progression calculator func NewDateProgression(bible *Bible) *DateProgression { - return &DateProgression{bible: bible} + return &DateProgression{bible: bible, versesPerDay: 12} +} + +// NewDateProgressionWithConfig creates a new date progression calculator with config +func NewDateProgressionWithConfig(bible *Bible, versesPerDay int) *DateProgression { + if versesPerDay <= 0 { + versesPerDay = 12 + } + return &DateProgression{bible: bible, versesPerDay: versesPerDay} } // GetPositionForDate calculates the verse range for a given date

@@ -20,8 +29,8 @@ func (dp *DateProgression) GetPositionForDate(date time.Time) (*Bookmark, error) {

// Calculate day of year (1-366) dayOfYear := date.YearDay() - // Each day shows 12 verses - targetVerseOffset := (dayOfYear - 1) * 12 // Zero-indexed + // Each day shows configured number of verses + targetVerseOffset := (dayOfYear - 1) * dp.versesPerDay // Zero-indexed // Walk through Gospels to find the position return dp.findPositionForOffset(targetVerseOffset)

@@ -50,7 +59,7 @@ // Offset is within this chapter

firstVerse := offset + 1 // Convert from 0-indexed to 1-indexed // Create initial bookmark (like AdvanceBookmark does) - secondVerse := min(firstVerse+11, versesInChapter) + secondVerse := min(firstVerse+dp.versesPerDay-1, versesInChapter) bookmark := &Bookmark{ Book: BookIndex(book), Chapter: chapter,

@@ -85,8 +94,8 @@ func (dp *DateProgression) applyLookahead(bookmark *Bookmark) *Bookmark {

versesInChapter := dp.bible.CountVersesInChapter(int(bookmark.Book), bookmark.Chapter) versesRemaining := versesInChapter - bookmark.SecondVerse - // If we're not at the end of the chapter and less than 12 verses remain for NEXT snippet - if versesRemaining > 0 && versesRemaining < 12 { + // If we're not at the end of the chapter and less than dp.versesPerDay verses remain for NEXT snippet + if versesRemaining > 0 && versesRemaining < dp.versesPerDay { return &Bookmark{ Book: bookmark.Book, Chapter: bookmark.Chapter,

@@ -112,5 +121,4 @@ total += verses

} } return total -} - +}
M internal/formatter.gointernal/formatter.go

@@ -7,31 +7,69 @@ )

// Formatter handles formatting Bible text for display type Formatter struct { + useColours bool + headerFormat string } -// NewFormatter creates a new formatter +// NewFormatter creates a new formatter with default settings func NewFormatter() *Formatter { - return &Formatter{} + return &Formatter{ + useColours: true, + headerFormat: "{book} {chapter}\nw. {first_verse}-{second_verse}", + } } -// FormatHeader formats the header for a bookmark with ANSI colors +// NewFormatterWithConfig creates a new formatter with configuration +func NewFormatterWithConfig(useColours bool, headerFormat string) *Formatter { + return &Formatter{ + useColours: useColours, + headerFormat: headerFormat, + } +} + +// FormatHeader formats the header for a bookmark using the configured template func (f *Formatter) FormatHeader(bookmark *Bookmark) string { - // ANSI color codes + template := f.headerFormat + if template == "" { + // Fall back to default template + template = "{book} {chapter}\nw. {first_verse}-{second_verse}" + } + + // Replace template variables + result := template + result = strings.ReplaceAll(result, "{book}", bookmark.Book.String()) + result = strings.ReplaceAll(result, "{chapter}", fmt.Sprintf("%d", bookmark.Chapter)) + result = strings.ReplaceAll(result, "{first_verse}", fmt.Sprintf("%d", bookmark.FirstVerse)) + result = strings.ReplaceAll(result, "{second_verse}", fmt.Sprintf("%d", bookmark.SecondVerse)) + + if !f.useColours { + return result + } + + // Add ANSI colours - simple implementation + // For more advanced templating, we'd need a proper template engine const ( reset = "\033[0m" green = "\033[32m" yellow = "\033[33m" ) + + // Simple colouring - book and chapter in green, verse range in yellow + lines := strings.Split(result, "\n") + if len(lines) >= 1 { + lines[0] = green + lines[0] + reset + } + if len(lines) >= 2 { + lines[1] = yellow + lines[1] + reset + } + result = strings.Join(lines, "\n") + + return result +} - return fmt.Sprintf("%s%s %d%s\n%s w. %d-%d%s", - green, - bookmark.Book.String(), - bookmark.Chapter, - reset, - yellow, - bookmark.FirstVerse, - bookmark.SecondVerse, - reset) +// FormatHeaderWithTemplate is deprecated - use FormatHeader instead +func (f *Formatter) FormatHeaderWithTemplate(bookmark *Bookmark) string { + return f.FormatHeader(bookmark) } // FormatSnippet formats the verse range text

@@ -65,3 +103,9 @@ bookmark.FirstVerse, bookmark.SecondVerse)

return f.FormatSnippet(verses) } +// ExtractAndFormatWithHeader extracts verses and formats with header +func (f *Formatter) ExtractAndFormatWithHeader(bible *Bible, bookmark *Bookmark) string { + header := f.FormatHeader(bookmark) + content := f.ExtractAndFormat(bible, bookmark) + return header + "\n" + content +}
M internal/tui/model.gointernal/tui/model.go

@@ -10,14 +10,15 @@ )

// Model represents the TUI application state type Model struct { - bibleData *bible.Bible - bookmark *bible.Bookmark - width int - height int - quitting bool - plainMode bool - styles *Styles - formatter *bible.Formatter + bibleData *bible.Bible + bookmark *bible.Bookmark + width int + height int + quitting bool + outputMode string + styles *Styles + formatter *bible.Formatter + config *bible.Config } // Styles contains the lipgloss styles for the TUI

@@ -29,19 +30,20 @@ quitMessage lipgloss.Style

} // NewModel creates a new TUI model with the given Bible data and bookmark -func NewModel(bibleData *bible.Bible, bookmark *bible.Bookmark, plainMode bool) Model { +func NewModel(bibleData *bible.Bible, bookmark *bible.Bookmark, config *bible.Config) Model { m := Model{ - bibleData: bibleData, - bookmark: bookmark, - plainMode: plainMode, - styles: createStyles(), - formatter: &bible.Formatter{}, + bibleData: bibleData, + bookmark: bookmark, + outputMode: config.OutputMode, + styles: createStyles(config), + formatter: bible.NewFormatterWithConfig(config.Formatter.UseColours, config.Formatter.HeaderFormat), + config: config, } return m } -// createStyles creates the lipgloss styles, adapting to terminal background color -func createStyles() *Styles { +// createStyles creates the lipgloss styles based on configuration +func createStyles(config *bible.Config) *Styles { // Detect if terminal has dark background hasDarkBG := lipgloss.HasDarkBackground()

@@ -53,30 +55,69 @@ }

return light } - // Colors that match the existing formatter colors (green/yellow) - // but adapted for light/dark backgrounds - green := lightDark(lipgloss.Color("#00AA00"), lipgloss.Color("#00FF00")) - borderColor := lightDark(lipgloss.Color("#666666"), lipgloss.Color("#999999")) - textColor := lightDark(lipgloss.Color("#000000"), lipgloss.Color("#FFFFFF")) - quitColor := lightDark(lipgloss.Color("#555555"), lipgloss.Color("#AAAAAA")) + // Get colours from config, or use adaptive defaults + var borderColour, headerColour, textColour, quitColour lipgloss.TerminalColor + + // Border colour + if config.TUI.BorderColour != "" { + borderColour = lipgloss.Color(config.TUI.BorderColour) + } else { + borderColour = lightDark(lipgloss.Color("#666666"), lipgloss.Color("#999999")) + } + + // Header colour (matching formatter's green) + if config.TUI.HeaderColour != "" { + headerColour = lipgloss.Color(config.TUI.HeaderColour) + } else { + headerColour = lightDark(lipgloss.Color("#00AA00"), lipgloss.Color("#00FF00")) + } + + // Text colour + if config.TUI.TextColour != "" { + textColour = lipgloss.Color(config.TUI.TextColour) + } else { + textColour = lightDark(lipgloss.Color("#000000"), lipgloss.Color("#FFFFFF")) + } + + // Quit message colour + if config.TUI.QuitColour != "" { + quitColour = lipgloss.Color(config.TUI.QuitColour) + } else { + quitColour = lightDark(lipgloss.Color("#555555"), lipgloss.Color("#AAAAAA")) + } + + // Create border style based on config + var border lipgloss.Border + switch config.TUI.BorderStyle { + case "double": + border = lipgloss.DoubleBorder() + case "single": + border = lipgloss.NormalBorder() + case "hidden": + border = lipgloss.HiddenBorder() + case "rounded": + fallthrough + default: + border = lipgloss.RoundedBorder() + } return &Styles{ box: lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(borderColor). + Border(border). + BorderForeground(borderColour). Padding(1, 2). Margin(1, 0), header: lipgloss.NewStyle(). - Foreground(green). + Foreground(headerColour). Bold(true). MarginBottom(1), content: lipgloss.NewStyle(). - Foreground(textColor), + Foreground(textColour), quitMessage: lipgloss.NewStyle(). - Foreground(quitColor). + Foreground(quitColour). Italic(true). MarginTop(1), }

@@ -116,8 +157,8 @@ // Get formatted content using the formatter

header := m.formatter.FormatHeader(m.bookmark) content := m.formatter.ExtractAndFormat(m.bibleData, m.bookmark) - // In plain mode, just return the original formatted text (with ANSI colors) - if m.plainMode { + // In plain or formatted mode, just return the original formatted text + if m.outputMode == "plain" || m.outputMode == "formatted" { var output strings.Builder output.WriteString(header) output.WriteString("\n")

@@ -157,16 +198,21 @@

// Create the box with max width constraint box := m.styles.box.MaxWidth(availableWidth).Render(boxContent.String()) - // Add quit message below the box - quitMsg := m.styles.quitMessage.Render("Press q to quit...") + // Add quit message below the box if configured to show it + var fullOutput string + if m.config.TUI.ShowQuitMessage { + quitMsg := m.styles.quitMessage.Render("Press q to quit...") + fullOutput = lipgloss.JoinVertical(lipgloss.Center, box, quitMsg) + } else { + fullOutput = lipgloss.JoinVertical(lipgloss.Center, box) + } - // Combine box and quit message - return lipgloss.JoinVertical(lipgloss.Center, box, quitMsg) + return fullOutput } // CreateTUIProgram creates and returns a bubbletea program for the Bible verse -func CreateTUIProgram(bible *bible.Bible, bookmark *bible.Bookmark, plainMode bool) *tea.Program { - model := NewModel(bible, bookmark, plainMode) +func CreateTUIProgram(bible *bible.Bible, bookmark *bible.Bookmark, config *bible.Config) *tea.Program { + model := NewModel(bible, bookmark, config) return tea.NewProgram(model) }

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

} return result.String() -} +}