feat: add multiple reading modes and dynamic book name support
- Add `-r/--reading` command line option for reading scope selection
- Support four modes: `evangelion` (default, Gospels only),
`new_testament`, `old_testament`, `bible`
- Overrides `reading_mode` setting in configuration file
- Enhance date progression to work with any Bible book range
- `NewDateProgressionWithReadingMode()` accepts `ReadingMode`
parameter
- `getBookRange()` returns start/end books for each mode
- Wrap correctly at boundaries of selected reading scope
- Remove hardcoded Polish book names from `BookIndex.String()`
- Book names now read directly from JSON `book_name` field
- `Formatter` requires `*Bible` parameter to retrieve book names
dynamically
- Update all formatter calls in `cmd/bibel.go` and `tui/model.go`
- Add `reading_mode` to TOML configuration (`config.toml`)
- Validate against allowed values in `Config.Validate()`
- Apply default `"evangelion"` for empty or missing values
- Fix configuration unmarshalling: apply defaults for empty fields
- Handle empty `ReadingMode`, `OutputMode`, `BiblePath`,
`BorderStyle`, `VersesPerDay`
- Update `README.md` and example config to document new reading modes
- Deprecate `GetTotalGospelVerses()` in favour of
`GetTotalVersesForMode()`
@@ -8,6 +8,44 @@ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] +## [1.4.0] - 2026-04-17 + +### Added + +- **Multiple reading modes** via new `-r/--reading` command line option: + - `evangelion` (default): Gospels only (Matthew, Mark, Luke, John) + - `new_testament`: Entire New Testament (books 40-66) + - `old_testament`: Old Testament only (books 1-39) + - `bible`: The entire Bible (books 1-66) +- **Flexible Bible source support**: Can now work with any Bible translation + - Removed hardcoded Polish book names + - Reads `book_name` field directly from JSON data + +### Changed + +- **Date progression system enhanced**: + - `NewDateProgressionWithReadingMode()` accepts reading mode parameter + - Calculates verse positions based on selected reading scope + - Proper wrapping at boundaries of each reading mode +- **Formatter now requires Bible reference**: + - `NewFormatterWithConfig()` now accepts `*Bible` parameter + - Formatter retrieves book names dynamically from loaded Bible data + - Updated all formatter initialization calls in codebase +- **Book name handling refactored**: + - Removed `polishBookNames` map from `verse.go` + - `BookIndex.String()` returns numeric fallback representation + - Actual book names sourced from Bible JSON metadata +- **Configuration system enhanced**: + - Added `reading_mode` configuration option with validation + - Command line `-r/--reading` overrides config setting + - Improved default value handling for empty configuration fields + +### Fixed + +- **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 ### Added@@ -100,9 +138,8 @@
### Changed - **BREAKING**: Changed from bookmark-based progression to date-based progression - - Previous versions used `bookmark.toml` to track reading position - - New version calculates position from current date - - Backward incompatible with bookmark-based usage + - Previous versions used `bookmark.toml` to track reading position, new + version calculates position from current date - Restructured codebase with modular Go packages: - `internal/loader.go`: JSON Bible data loading and indexing - `internal/dateprogression.go`: Date-to-verse position calculation
@@ -46,6 +46,8 @@ Default macOS and Windows paths are also supported.
### Configuration Options +- **reading_mode**: Reading mode: "evangelion" (four Gospels; *default*), +"new_testament", "old_testament", "bible" (default: "evangelion") - **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")@@ -88,6 +90,8 @@ ### Command Line Arguments
Command line arguments override configuration file settings: +- `-r, --reading`: "evangelion" (four Gospels; *default*), "new_testament", +"old_testament", "bible" (default: "evangelion") - `-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@@ -139,7 +143,6 @@ ├── 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
@@ -13,6 +13,7 @@ )
// Args defines the command line arguments 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"`@@ -21,8 +22,9 @@ }
// 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" + + return "Display today's Bible reading based on selected reading mode.\n" + "Reads 12 verses per day based on the current date.\n" + + "Reading modes: evangelion (four Gospels), new_testament, old_testament, bible\n" + "Configuration file: $XDG_CONFIG_HOME/bibel/config.toml" }@@ -48,6 +50,9 @@ os.Exit(1)
} // Override config with command line arguments + if args.Reading != "" { + cfg.ReadingMode = args.Reading + } if args.Plain { cfg.OutputMode = "plain" } else if args.Formatted {@@ -68,7 +73,7 @@ os.Exit(1)
} // Initialise date progression with configured verses per day - dateProg := bible.NewDateProgressionWithConfig(bibleData, cfg.DateProgression.VersesPerDay) + dateProg := bible.NewDateProgressionWithReadingMode(bibleData, cfg.DateProgression.VersesPerDay, bible.ReadingMode(cfg.ReadingMode)) // Get current date currentDate := time.Now()@@ -92,12 +97,12 @@
// Handle different output modes switch outputMode { case "plain": - formatter := bible.NewFormatterWithConfig(false, cfg.Formatter.HeaderFormat) + formatter := bible.NewFormatterWithConfig(bibleData, 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) + formatter := bible.NewFormatterWithConfig(bibleData, cfg.Formatter.UseColours, cfg.Formatter.HeaderFormat) fmt.Println(formatter.FormatHeader(todayBookmark)) fmt.Println(formatter.ExtractAndFormat(bibleData, todayBookmark))
@@ -8,6 +8,9 @@
# Path to Bible data file (relative to executable or absolute path) bible_path = "books/pol_nbg.json" +# Reading mode: "evangelion" (four Gospels), "new_testament", "old_testament", "bible" +reading_mode = "evangelion" + [tui] # Whether to show the quit message "Press q to quit..." show_quit_message = true
@@ -11,6 +11,8 @@ )
// Config represents the application configuration type Config struct { + // Reading mode: "evangelion", "new_testament", "old_testament", "bible" + ReadingMode string `toml:"reading_mode" mapstructure:"reading_mode"` // Output mode: "tui", "formatted", "plain" OutputMode string `toml:"output_mode" mapstructure:"output_mode"`@@ -60,6 +62,7 @@
// DefaultConfig returns a configuration with default values func DefaultConfig() *Config { cfg := &Config{ + ReadingMode: "evangelion", OutputMode: "tui", BiblePath: "books/pol_nbg.json", }@@ -95,6 +98,7 @@ viper.AddConfigPath(".")
// Set defaults defaultCfg := DefaultConfig() + viper.SetDefault("reading_mode", defaultCfg.ReadingMode) viper.SetDefault("output_mode", defaultCfg.OutputMode) viper.SetDefault("bible_path", defaultCfg.BiblePath) viper.SetDefault("tui.show_quit_message", defaultCfg.TUI.ShowQuitMessage)@@ -126,6 +130,24 @@ if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("error unmarshalling config: %w", err) } + // Apply defaults for any empty fields + defaultCfg = DefaultConfig() + if cfg.ReadingMode == "" { + cfg.ReadingMode = defaultCfg.ReadingMode + } + if cfg.OutputMode == "" { + cfg.OutputMode = defaultCfg.OutputMode + } + if cfg.BiblePath == "" { + cfg.BiblePath = defaultCfg.BiblePath + } + if cfg.TUI.BorderStyle == "" { + cfg.TUI.BorderStyle = defaultCfg.TUI.BorderStyle + } + if cfg.DateProgression.VersesPerDay == 0 { + cfg.DateProgression.VersesPerDay = defaultCfg.DateProgression.VersesPerDay + } + // Validate configuration if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("invalid configuration: %w", err)@@ -137,6 +159,7 @@
// SaveConfig saves the configuration to file func SaveConfig(cfg *Config) error { // Set up viper with current config + viper.Set("reading_mode", cfg.ReadingMode) viper.Set("output_mode", cfg.OutputMode) viper.Set("bible_path", cfg.BiblePath) viper.Set("tui.show_quit_message", cfg.TUI.ShowQuitMessage)@@ -174,6 +197,18 @@ // GetConfigPath returns the path to the configuration file
// Validate checks if configuration values are valid func (c *Config) Validate() error { + // Validate reading mode + validReadingModes := map[string]bool{ + "evangelion": true, + "new_testament": true, + "old_testament": true, + "bible": true, + } + // Allow empty reading mode (will use default) + if c.ReadingMode != "" && !validReadingModes[c.ReadingMode] { + return fmt.Errorf("invalid reading mode: %s, must be one of: evangelion, new_testament, old_testament, bible", c.ReadingMode) + } + // Validate output mode validOutputModes := map[string]bool{ "tui": true,
@@ -9,11 +9,12 @@ // DateProgression handles calculating Bible position based on date
type DateProgression struct { bible *Bible versesPerDay int + readingMode ReadingMode } // NewDateProgression creates a new date progression calculator func NewDateProgression(bible *Bible) *DateProgression { - return &DateProgression{bible: bible, versesPerDay: 12} + return &DateProgression{bible: bible, versesPerDay: 12, readingMode: ReadingModeEvangelion} } // NewDateProgressionWithConfig creates a new date progression calculator with config@@ -21,7 +22,15 @@ func NewDateProgressionWithConfig(bible *Bible, versesPerDay int) *DateProgression {
if versesPerDay <= 0 { versesPerDay = 12 } - return &DateProgression{bible: bible, versesPerDay: versesPerDay} + return &DateProgression{bible: bible, versesPerDay: versesPerDay, readingMode: ReadingModeEvangelion} +} + +// NewDateProgressionWithReadingMode creates a new date progression calculator with reading mode +func NewDateProgressionWithReadingMode(bible *Bible, versesPerDay int, readingMode ReadingMode) *DateProgression { + if versesPerDay <= 0 { + versesPerDay = 12 + } + return &DateProgression{bible: bible, versesPerDay: versesPerDay, readingMode: readingMode} } // GetPositionForDate calculates the verse range for a given date@@ -32,14 +41,17 @@
// Each day shows configured number of verses targetVerseOffset := (dayOfYear - 1) * dp.versesPerDay // Zero-indexed - // Walk through Gospels to find the position + // Walk through appropriate books based on reading mode return dp.findPositionForOffset(targetVerseOffset) } // findPositionForOffset finds the bookmark for a given cumulative verse offset func (dp *DateProgression) findPositionForOffset(offset int) (*Bookmark, error) { - // Iterate through books 40-43 (Matthew, Mark, Luke, John) - for book := 40; book <= 43; book++ { + // Determine which books to iterate through based on reading mode + startBook, endBook := dp.getBookRange() + + // Iterate through books in the range + for book := startBook; book <= endBook; book++ { // Find last chapter in this book maxChapter := 0 for chapter := 1; ; chapter++ {@@ -47,6 +59,9 @@ if dp.bible.CountVersesInChapter(book, chapter) == 0 {
maxChapter = chapter - 1 break } + } + if maxChapter == 0 { + continue // Book has no chapters? Shouldn't happen } // Iterate through chapters@@ -76,10 +91,9 @@ offset -= versesInChapter
} } - // If we've gone through all Gospels and offset is still positive, wrap - // around to beginning (start over) Calculate modulo offset within total - // Gospel verses - totalVerses := dp.GetTotalGospelVerses() + // If we've gone through all books and offset is still positive, wrap + // around to beginning (start over) Calculate modulo offset within total verses + totalVerses := dp.GetTotalVersesForMode() if offset >= 0 { adjustedOffset := offset % totalVerses // Recursively find position for adjusted offset@@ -89,6 +103,22 @@
return nil, fmt.Errorf("could not find position for offset %d", offset) } +// getBookRange returns the start and end book numbers for the current reading mode +func (dp *DateProgression) getBookRange() (startBook, endBook int) { + switch dp.readingMode { + case ReadingModeEvangelion: + return 40, 43 // Matthew, Mark, Luke, John + case ReadingModeNewTestament: + return 40, 66 // Matthew through Revelation + case ReadingModeOldTestament: + return 1, 39 // Genesis through Malachi + case ReadingModeBible: + return 1, 66 // Entire Bible + default: + return 40, 43 // Default to Evangelion + } +} + // applyLookahead applies the lookahead rule (same as BookmarkManager.AdjustForLookahead) func (dp *DateProgression) applyLookahead(bookmark *Bookmark) *Bookmark { versesInChapter := dp.bible.CountVersesInChapter(int(bookmark.Book), bookmark.Chapter)@@ -108,8 +138,27 @@ // Otherwise return the original bookmark
return bookmark } +// GetTotalVersesForMode returns the total number of verses for the current reading mode +func (dp *DateProgression) GetTotalVersesForMode() int { + total := 0 + startBook, endBook := dp.getBookRange() + + for book := startBook; book <= endBook; book++ { + for chapter := 1; ; chapter++ { + verses := dp.bible.CountVersesInChapter(book, chapter) + if verses == 0 { + break + } + total += verses + } + } + return total +} + // GetTotalGospelVerses returns the total number of verses in all four Gospels +// Deprecated: Use GetTotalVersesForMode instead func (dp *DateProgression) GetTotalGospelVerses() int { + // Calculate Gospels total (books 40-43) total := 0 for book := 40; book <= 43; book++ { for chapter := 1; ; chapter++ {
@@ -7,21 +7,24 @@ )
// Formatter handles formatting Bible text for display type Formatter struct { + bible *Bible useColours bool headerFormat string } // NewFormatter creates a new formatter with default settings -func NewFormatter() *Formatter { +func NewFormatter(bible *Bible) *Formatter { return &Formatter{ + bible: bible, useColours: true, - headerFormat: "{book} {chapter}\nw. {first_verse}-{second_verse}", + headerFormat: "{book} {chapter}\nv. {first_verse}-{second_verse}", } } // NewFormatterWithConfig creates a new formatter with configuration -func NewFormatterWithConfig(useColours bool, headerFormat string) *Formatter { +func NewFormatterWithConfig(bible *Bible, useColours bool, headerFormat string) *Formatter { return &Formatter{ + bible: bible, useColours: useColours, headerFormat: headerFormat, }@@ -32,20 +35,23 @@ func (f *Formatter) FormatHeader(bookmark *Bookmark) string {
template := f.headerFormat if template == "" { // Fall back to default template - template = "{book} {chapter}\nw. {first_verse}-{second_verse}" + template = "{book} {chapter}\nv. {first_verse}-{second_verse}" } - + + // Get book name from Bible data + bookName := f.getBookName(bookmark.Book) + // Replace template variables result := template - result = strings.ReplaceAll(result, "{book}", bookmark.Book.String()) + result = strings.ReplaceAll(result, "{book}", bookName) 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 (@@ -53,7 +59,7 @@ 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 {@@ -63,11 +69,24 @@ if len(lines) >= 2 {
lines[1] = yellow + lines[1] + reset } result = strings.Join(lines, "\n") - + return result } -// FormatHeaderWithTemplate is deprecated - use FormatHeader instead +// getBookName retrieves the book name from Bible data +func (f *Formatter) getBookName(book BookIndex) string { + // Try to find any verse from this book to get the book name + // Start from chapter 1, verse 1 and work forward + for chapter := 1; chapter <= 150; chapter++ { + for verse := 1; verse <= 200; verse++ { + if v, ok := f.bible.GetVerse(int(book), chapter, verse); ok { + return v.BookName + } + } + } + // If we can't find the book in the data, return a generic name + return fmt.Sprintf("Book %d", book) +} // FormatHeaderWithTemplate is deprecated - use FormatHeader instead func (f *Formatter) FormatHeaderWithTemplate(bookmark *Bookmark) string { return f.FormatHeader(bookmark) }@@ -108,4 +127,5 @@ func (f *Formatter) ExtractAndFormatWithHeader(bible *Bible, bookmark *Bookmark) string {
header := f.FormatHeader(bookmark) content := f.ExtractAndFormat(bible, bookmark) return header + "\n" + content -}+} +
@@ -36,7 +36,7 @@ bibleData: bibleData,
bookmark: bookmark, outputMode: config.OutputMode, styles: createStyles(config), - formatter: bible.NewFormatterWithConfig(config.Formatter.UseColours, config.Formatter.HeaderFormat), + formatter: bible.NewFormatterWithConfig(bibleData, config.Formatter.UseColours, config.Formatter.HeaderFormat), config: config, } return m
@@ -1,5 +1,9 @@
package bible +import ( + "fmt" +) + // Verse represents a single Bible verse type Verse struct { BookName string `json:"book_name"`@@ -17,36 +21,31 @@ FirstVerse int
LastVerse int } -// BookIndex represents the four Gospels +// BookIndex represents Bible book numbers type BookIndex int +// BookIndex.String returns the string representation of the book index. +// Note: The actual book name should be retrieved from Bible data using the Formatter. +// This is kept for backward compatibility but returns a simple numeric representation. +func (b BookIndex) String() string { + // Return numeric representation - actual book names should come from Bible data + return fmt.Sprintf("Book %d", b) +} + +// ReadingMode defines the scope of Bible reading +type ReadingMode string + const ( - Matthew BookIndex = iota + 40 - Mark - Luke - John + ReadingModeEvangelion ReadingMode = "evangelion" // Books 40-43 (Matthew, Mark, Luke, John) + ReadingModeNewTestament ReadingMode = "new_testament" // Books 40-66 + ReadingModeOldTestament ReadingMode = "old_testament" // Books 1-39 + ReadingModeBible ReadingMode = "bible" // Books 1-66 ) -func (b BookIndex) String() string { - switch b { - case Matthew: - return "Ewangelia wg. Mateusza" - case Mark: - return "Ewangelia wg. Marka" - case Luke: - return "Ewangelia wg. Łukasza" - case John: - return "Ewangelia wg. Jana" - default: - return "Unknown" - } -} - // Bookmark represents the current reading position type Bookmark struct { Book BookIndex `toml:"book"` Chapter int `toml:"chapter"` FirstVerse int `toml:"first_verse"` SecondVerse int `toml:"second_verse"` -} - +}