feat: add verbose logging for config and bible loading
- Add `verbose` flag to `LoadConfig` and `LoadBible` functions
- Implement `VerboseLogger` with conditional output (Info, Warning,
Section, Path, Success)
- Replace direct `fmt.Fprintf` calls in `internal/config.go` with
structured logger
- Add detailed logging for Bible metadata (name, language, verse
count) in `internal/loader.go`
- Optimise `GetChapterText` by using `strings.Builder` instead of string
concatenation
- Update `cmd/bibel.go` to pass `args.Verbose` when loading config and
Bible data
- Add missing newlines at end of `easterprogression.go` and `config.go`
(cosmetic)
- Update `CHANGELOG.md` with `0.11.0`
Maxwell Jensen 85795372+maxwelljens@users.noreply.github.com
Sat, 18 Apr 2026 15:07:19 +0200
6 files changed,
183 insertions(+),
13 deletions(-)
M
CHANGELOG.md
→
CHANGELOG.md
@@ -8,6 +8,17 @@ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] +## [0.11.0] - 2026-04-18 + +### Added + +- **New flag**: `-v/--verbose` flag to print with more information if needed. + +### Fixed + +- Add missing newlines at end of `easterprogression.go` and `config.go` + (cosmetic) + ## [0.10.0] - 2026-04-18 ### Changed
M
cmd/bibel.go
→
cmd/bibel.go
@@ -47,7 +47,7 @@ return
} // Load configuration - config, err := bible.LoadConfig() + config, err := bible.LoadConfig(args.Verbose) if err != nil { fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) os.Exit(1)@@ -64,7 +64,7 @@ config.ReadingMode = args.Reading
} // Load Bible data - bibleData, err := bible.LoadBible(config.BiblePath) + bibleData, err := bible.LoadBible(config.BiblePath, args.Verbose) if err != nil { fmt.Fprintf(os.Stderr, "Error loading Bible data: %v\n", err) os.Exit(1)@@ -125,4 +125,4 @@ if _, err := program.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err) os.Exit(1) } -} +}
M
internal/config.go
→
internal/config.go
@@ -106,7 +106,9 @@ return cfg
} // LoadConfig loads configuration from file and environment -func LoadConfig() (*Config, error) { +func LoadConfig(verbose bool) (*Config, error) { + logger := NewVerboseLogger(verbose) + logger.Section("Loading Configuration") // 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@@ -137,8 +139,8 @@ // 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") + logger.Info("No configuration file found at %s/config.toml", configPath) + logger.Info("Using default configuration. Run 'bibel --generate-config' to create a default config file.") } else { // Config file was found but another error was produced return nil, fmt.Errorf("error reading config file: %w", err)@@ -176,16 +178,21 @@ // If BiblePath is still empty (default or from config), try to find a Bible file in XDG data directory
if cfg.BiblePath == "" { biblePath, err := findBibleFileInDataDir() if err != nil { - return nil, fmt.Errorf("failed to find Bible data file: %w", err) + logger.Warning("Failed to find Bible data file: %v", err) + return nil, fmt.Errorf("failed to find Bible data file: %w", err) } cfg.BiblePath = biblePath - fmt.Fprintf(os.Stderr, "Using Bible file from XDG data directory: %s\n", cfg.BiblePath) + logger.Info("Using Bible file from XDG data directory") + logger.Path(cfg.BiblePath) } // Validate configuration + logger.Section("Validating Configuration") if err := cfg.Validate(); err != nil { + logger.Warning("Configuration validation failed: %v", err) return nil, fmt.Errorf("invalid configuration: %w", err) } + logger.Success("Configuration validated successfully") return &cfg, nil }@@ -282,4 +289,4 @@ return nil
} func GetConfigPath() string { return filepath.Join(xdg.ConfigHome, "bibel", "config.toml") -} +}
M
internal/easterprogression.go
→
internal/easterprogression.go
@@ -157,3 +157,4 @@
return elapsedDuration.Seconds() / totalDuration.Seconds(), nil } } +
M
internal/loader.go
→
internal/loader.go
@@ -3,6 +3,7 @@
import ( "encoding/json" "os" + "strings" ) // Bible represents the complete Bible data@@ -18,9 +19,14 @@ byBookChapterVerse map[int]map[int]map[int]*Verse
} // LoadBible loads Bible data from a JSON file -func LoadBible(filePath string) (*Bible, error) { +func LoadBible(filePath string, verbose bool) (*Bible, error) { + logger := NewVerboseLogger(verbose) + logger.Info("Loading Bible data from file") + logger.Path(filePath) + data, err := os.ReadFile(filePath) if err != nil { + logger.Warning("Failed to read Bible file: %v", err) return nil, err }@@ -30,6 +36,13 @@ return nil, err
} bible.buildIndex() + logger.Success("Bible data loaded successfully") + logger.WithFields(map[string]any{ + "metadata.name": bible.Metadata.Name, + "metadata.lang": bible.Metadata.Lang, + "verse_count": len(bible.Verses), + }, "Bible metadata") + return &bible, nil }@@ -68,18 +81,18 @@ // GetChapterText retrieves the full text of a chapter
func (b *Bible) GetChapterText(book, chapter int) string { if chapterMap, ok := b.byBookChapterVerse[book]; ok { if verseMap, ok := chapterMap[chapter]; ok { - var text string + var text strings.Builder // We need to get verses in order // Since we don't know the max verse number, we'll iterate // This could be optimized but works for now for i := 1; ; i++ { if verse, ok := verseMap[i]; ok { - text += verse.Text + " " + text.WriteString(verse.Text + " ") } else { break } } - return text + return text.String() } } return ""@@ -111,3 +124,4 @@ }
return verses } +
A
internal/verbose.go
@@ -0,0 +1,137 @@
+package bible + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/charmbracelet/lipgloss" +) + +// VerboseLogger handles verbose output with lipgloss styling +type VerboseLogger struct { + enabled bool + styles *VerboseStyles +} + +// VerboseStyles defines styles for verbose output +type VerboseStyles struct { + Info lipgloss.Style + Warning lipgloss.Style + Success lipgloss.Style + Path lipgloss.Style + Label lipgloss.Style + Value lipgloss.Style +} + +// NewVerboseLogger creates a new verbose logger +func NewVerboseLogger(enabled bool) *VerboseLogger { + return &VerboseLogger{ + enabled: enabled, + styles: createVerboseStyles(), + } +} + +// createVerboseStyles creates and returns the verbose output styles +func createVerboseStyles() *VerboseStyles { + return &VerboseStyles{ + Info: lipgloss.NewStyle().Foreground(lipgloss.Color("4")), + Warning: lipgloss.NewStyle().Foreground(lipgloss.Color("3")), + Success: lipgloss.NewStyle().Foreground(lipgloss.Color("2")), + Path: lipgloss.NewStyle().Foreground(lipgloss.Color("12")), + Label: lipgloss.NewStyle().Foreground(lipgloss.Color("8")), + Value: lipgloss.NewStyle().Foreground(lipgloss.Color("13")), + } +} + +// Info prints an informational message +func (vl *VerboseLogger) Info(format string, args ...any) { + if !vl.enabled { + return + } + message := fmt.Sprintf(format, args...) + styled := vl.styles.Info.Render("INFO: ") + message + fmt.Fprintln(os.Stderr, styled) +} + +// Warning prints a warning message +func (vl *VerboseLogger) Warning(format string, args ...any) { + if !vl.enabled { + return + } + message := fmt.Sprintf(format, args...) + styled := vl.styles.Warning.Render("WARN: ") + message + fmt.Fprintln(os.Stderr, styled) +} + +// Success prints a success message +func (vl *VerboseLogger) Success(format string, args ...any) { + if !vl.enabled { + return + } + message := fmt.Sprintf(format, args...) + styled := vl.styles.Success.Render("SUCCESS: ") + message + fmt.Fprintln(os.Stderr, styled) +} + +// Path prints a file path with styling +func (vl *VerboseLogger) Path(path string) { + if !vl.enabled { + return + } + // Make path absolute for clarity + absPath, err := filepath.Abs(path) + if err != nil { + absPath = path + } + styled := vl.styles.Label.Render("Path: ") + vl.styles.Path.Render(absPath) + fmt.Fprintln(os.Stderr, styled) +} + +// Config prints configuration information +func (vl *VerboseLogger) Config(format string, args ...any) { + if !vl.enabled { + return + } + message := fmt.Sprintf(format, args...) + styled := vl.styles.Label.Render("Config: ") + message + fmt.Fprintln(os.Stderr, styled) +} + +// Value prints a key-value pair +func (vl *VerboseLogger) Value(key string, value any) { + if !vl.enabled { + return + } + styled := vl.styles.Label.Render(key+": ") + vl.styles.Value.Render(fmt.Sprintf("%v", value)) + fmt.Fprintln(os.Stderr, styled) +} + +// Section prints a section header +func (vl *VerboseLogger) Section(title string) { + if !vl.enabled { + return + } + styled := vl.styles.Info.Render("=== " + title + " ===") + fmt.Fprintln(os.Stderr, styled) +} + +// WithFields creates a formatted message with key-value pairs +func (vl *VerboseLogger) WithFields(fields map[string]any, message string) { + if !vl.enabled { + return + } + + var parts []string + for key, value := range fields { + parts = append(parts, fmt.Sprintf("%s=%v", key, value)) + } + + fieldsStr := strings.Join(parts, " ") + styled := vl.styles.Label.Render("Fields: ") + message + if fieldsStr != "" { + styled += " " + vl.styles.Info.Render("["+fieldsStr+"]") + } + fmt.Fprintln(os.Stderr, styled) +}