internal/config.go (view raw)
1package bible
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7
8 "github.com/adrg/xdg"
9 "github.com/spf13/viper"
10)
11
12// findBibleFileInDataDir looks for Bible JSON files in the XDG data directory
13// It returns the path to the first JSON file found, or an error if none found
14func findBibleFileInDataDir() (string, error) {
15 dataDir := filepath.Join(xdg.DataHome, "bibel")
16
17 // Check if the directory exists
18 if _, err := os.Stat(dataDir); os.IsNotExist(err) {
19 return "", fmt.Errorf("Bible data directory not found: %s\nPlease create this directory and add Bible JSON files to it", dataDir)
20 }
21
22 // Read the directory
23 entries, err := os.ReadDir(dataDir)
24 if err != nil {
25 return "", fmt.Errorf("error reading Bible data directory %s: %w", dataDir, err)
26 }
27
28 // Look for JSON files
29 for _, entry := range entries {
30 if !entry.IsDir() && filepath.Ext(entry.Name()) == ".json" {
31 return filepath.Join(dataDir, entry.Name()), nil
32 }
33 }
34
35 return "", fmt.Errorf("no JSON Bible files found in %s\nPlease add Bible JSON files to this directory", dataDir)
36}
37
38// Config represents the application configuration
39type Config struct {
40 // Reading mode: "evangelion", "new_testament", "old_testament", "bible"
41 ReadingMode string `toml:"reading_mode" mapstructure:"reading_mode"`
42 // Output mode: "tui", "formatted", "plain"
43 OutputMode string `toml:"output_mode" mapstructure:"output_mode"`
44
45 // Bible data file path (default: "books/pol_nbg.json")
46 BiblePath string `toml:"bible_path" mapstructure:"bible_path"`
47
48 // Easter type: "orthodox" (default), "latin"
49 EasterType string `toml:"easter_type" mapstructure:"easter_type"`
50
51 // TUI settings
52 TUI struct {
53 // Whether to show quit message (default: true)
54 ShowQuitMessage bool `toml:"show_quit_message" mapstructure:"show_quit_message"`
55
56 // Box border style: "rounded", "double", "single", "hidden"
57 BorderStyle string `toml:"border_style" mapstructure:"border_style"`
58 } `toml:"tui" mapstructure:"tui"`
59
60 // Formatter settings
61 Formatter struct {
62 // Whether to use ANSI colours in formatted mode (default: true)
63 UseColours bool `toml:"use_colours" mapstructure:"use_colours"`
64
65 // Header format template (default: "{book} {chapter}\nw. {first_verse}-{second_verse}")
66 HeaderFormat string `toml:"header_format" mapstructure:"header_format"`
67
68 // Whether to print each verse on a numbered line (default: false)
69 Numbered bool `toml:"numbered" mapstructure:"numbered"`
70
71 // Whether to render pilcrows (ΒΆ) as blank lines instead of ignoring them (default: false)
72 Paragraphs bool `toml:"paragraphs" mapstructure:"paragraphs"`
73 } `toml:"formatter" mapstructure:"formatter"`
74
75 // Date progression settings
76 DateProgression struct {
77 // Verses per day (default: 12)
78 VersesPerDay int `toml:"verses_per_day" mapstructure:"verses_per_day"`
79
80 // Start date for progression (default: "1 January" of current year)
81 StartDate string `toml:"start_date" mapstructure:"start_date"`
82 } `toml:"date_progression" mapstructure:"date_progression"`
83}
84
85// DefaultConfig returns a configuration with default values
86func DefaultConfig() *Config {
87 cfg := &Config{
88 ReadingMode: "evangelion",
89 OutputMode: "tui",
90 BiblePath: "", // Empty means use XDG data directory
91 EasterType: "orthodox", // Default to Orthodox Easter
92 }
93
94 cfg.TUI.ShowQuitMessage = true
95 cfg.TUI.BorderStyle = "rounded"
96
97 cfg.Formatter.UseColours = true
98 cfg.Formatter.HeaderFormat = "{book} {chapter}\nw. {first_verse}-{second_verse}"
99 cfg.Formatter.Numbered = false
100 cfg.Formatter.Paragraphs = false
101
102 cfg.DateProgression.VersesPerDay = 12
103 cfg.DateProgression.StartDate = "" // Empty means 1 January of current year
104
105 return cfg
106}
107
108// LoadConfig loads configuration from file and environment
109// If configPath is empty, uses default XDG config location
110// LoadConfig loads configuration from file and environment
111// If configPath is empty, uses default XDG config location
112func LoadConfig(configPath string, verbose bool) (*Config, error) {
113 logger := NewVerboseLogger(verbose)
114 logger.Section("Loading Configuration")
115 // Set up viper
116 viper.SetConfigName("config") // Name of config file (without extension)
117 viper.SetConfigType("toml") // REQUIRED if the config file does not have the extension in the name
118
119 // If a specific config file path was provided, use it
120 if configPath != "" {
121 // Use the provided config file directly
122 viper.SetConfigFile(configPath)
123 } else {
124 // Use default XDG config path
125 xdgConfigPath := filepath.Join(xdg.ConfigHome, "bibel")
126 viper.AddConfigPath(xdgConfigPath)
127 // Also check current directory for local config
128 viper.AddConfigPath(".")
129 }
130
131 // Set defaults
132 defaultCfg := DefaultConfig()
133 viper.SetDefault("reading_mode", defaultCfg.ReadingMode)
134 viper.SetDefault("output_mode", defaultCfg.OutputMode)
135 viper.SetDefault("bible_path", defaultCfg.BiblePath)
136 viper.SetDefault("easter_type", defaultCfg.EasterType)
137 viper.SetDefault("tui.show_quit_message", defaultCfg.TUI.ShowQuitMessage)
138 viper.SetDefault("tui.border_style", defaultCfg.TUI.BorderStyle)
139 viper.SetDefault("formatter.use_colours", defaultCfg.Formatter.UseColours)
140 viper.SetDefault("formatter.header_format", defaultCfg.Formatter.HeaderFormat)
141 viper.SetDefault("formatter.numbered", defaultCfg.Formatter.Numbered)
142 viper.SetDefault("formatter.paragraphs", defaultCfg.Formatter.Paragraphs)
143 viper.SetDefault("date_progression.verses_per_day", defaultCfg.DateProgression.VersesPerDay)
144 viper.SetDefault("date_progression.start_date", defaultCfg.DateProgression.StartDate)
145
146 // Read in config file (if it exists)
147 if err := viper.ReadInConfig(); err != nil {
148 if _, ok := err.(viper.ConfigFileNotFoundError); ok {
149 // Config file not found; we'll use defaults
150 if configPath != "" {
151 logger.Info("No configuration file found at %s", configPath)
152 } else {
153 xdgConfigPath := filepath.Join(xdg.ConfigHome, "bibel")
154 logger.Info("No configuration file found at %s/config.toml", xdgConfigPath)
155 }
156 logger.Info("Using default configuration. Run 'bibel --generate-config' to create a default config file.")
157 } else {
158 // Config file was found but another error was produced
159 return nil, fmt.Errorf("error reading config file: %w", err)
160 }
161 }
162
163 // Unmarshal config into struct
164 var cfg Config
165 if err := viper.Unmarshal(&cfg); err != nil {
166 return nil, fmt.Errorf("error unmarshalling config: %w", err)
167 }
168
169 // Apply defaults for any empty fields
170 defaultCfg = DefaultConfig()
171 if cfg.ReadingMode == "" {
172 cfg.ReadingMode = defaultCfg.ReadingMode
173 }
174 if cfg.OutputMode == "" {
175 cfg.OutputMode = defaultCfg.OutputMode
176 }
177 if cfg.BiblePath == "" {
178 cfg.BiblePath = defaultCfg.BiblePath
179 }
180 if cfg.EasterType == "" {
181 cfg.EasterType = defaultCfg.EasterType
182 }
183 if cfg.TUI.BorderStyle == "" {
184 cfg.TUI.BorderStyle = defaultCfg.TUI.BorderStyle
185 }
186 if cfg.DateProgression.VersesPerDay == 0 {
187 cfg.DateProgression.VersesPerDay = defaultCfg.DateProgression.VersesPerDay
188 }
189
190 // If BiblePath is still empty (default or from config), try to find a Bible file in XDG data directory
191 if cfg.BiblePath == "" {
192 biblePath, err := findBibleFileInDataDir()
193 if err != nil {
194 logger.Warning("Failed to find Bible data file: %v", err)
195 return nil, fmt.Errorf("failed to find Bible data file: %w", err)
196 }
197 cfg.BiblePath = biblePath
198 logger.Info("Using Bible file from XDG data directory")
199 logger.Path(cfg.BiblePath)
200 }
201
202 // Validate configuration
203 logger.Section("Validating Configuration")
204 if err := cfg.Validate(); err != nil {
205 logger.Warning("Configuration validation failed: %v", err)
206 return nil, fmt.Errorf("invalid configuration: %w", err)
207 }
208 logger.Success("Configuration validated successfully")
209
210 return &cfg, nil
211}
212
213// SaveConfig saves the configuration to file
214// SaveConfig saves the configuration to file
215func SaveConfig(cfg *Config) error {
216 // Set up viper with current config
217 viper.Set("reading_mode", cfg.ReadingMode)
218 viper.Set("output_mode", cfg.OutputMode)
219 viper.Set("bible_path", cfg.BiblePath)
220 viper.Set("easter_type", cfg.EasterType)
221 viper.Set("tui.show_quit_message", cfg.TUI.ShowQuitMessage)
222 viper.Set("tui.border_style", cfg.TUI.BorderStyle)
223 viper.Set("formatter.use_colours", cfg.Formatter.UseColours)
224 viper.Set("formatter.header_format", cfg.Formatter.HeaderFormat)
225 viper.Set("formatter.numbered", cfg.Formatter.Numbered)
226 viper.Set("formatter.paragraphs", cfg.Formatter.Paragraphs)
227 viper.Set("date_progression.verses_per_day", cfg.DateProgression.VersesPerDay)
228 viper.Set("date_progression.start_date", cfg.DateProgression.StartDate)
229
230 // Ensure config directory exists
231 xdgConfigPath := filepath.Join(xdg.ConfigHome, "bibel")
232 if err := os.MkdirAll(xdgConfigPath, 0755); err != nil {
233 return fmt.Errorf("error creating config directory: %w", err)
234 }
235
236 // Write config file
237 configFile := filepath.Join(xdgConfigPath, "config.toml")
238 if err := viper.WriteConfigAs(configFile); err != nil {
239 return fmt.Errorf("error writing config file: %w", err)
240 }
241
242 return nil
243}
244
245// GenerateDefaultConfig creates a default configuration file
246func GenerateDefaultConfig() error {
247 return SaveConfig(DefaultConfig())
248}
249
250// GetConfigPath returns the path to the configuration file
251
252// Validate checks if configuration values are valid
253func (c *Config) Validate() error {
254 // Validate reading mode
255 validReadingModes := map[string]bool{
256 "evangelion": true,
257 "new_testament": true,
258 "old_testament": true,
259 "bible": true,
260 }
261 // Allow empty reading mode (will use default)
262 if c.ReadingMode != "" && !validReadingModes[c.ReadingMode] {
263 return fmt.Errorf("invalid reading mode: %s, must be one of: evangelion, new_testament, old_testament, bible", c.ReadingMode)
264 }
265
266 // Validate output mode
267 validOutputModes := map[string]bool{
268 "tui": true,
269 "formatted": true,
270 "plain": true,
271 }
272 if !validOutputModes[c.OutputMode] {
273 return fmt.Errorf("invalid output mode: %s, must be one of: tui, formatted, plain", c.OutputMode)
274 }
275
276 // Validate TUI border style
277 validBorderStyles := map[string]bool{
278 "rounded": true,
279 "double": true,
280 "single": true,
281 "hidden": true,
282 }
283 if !validBorderStyles[c.TUI.BorderStyle] {
284 return fmt.Errorf("invalid border style: %s, must be one of: rounded, double, single, hidden", c.TUI.BorderStyle)
285 }
286
287 // Validate Easter type
288 validEasterTypes := map[string]bool{
289 "orthodox": true,
290 "latin": true,
291 }
292 // Allow empty Easter type (will use default)
293 if c.EasterType != "" && !validEasterTypes[c.EasterType] {
294 return fmt.Errorf("invalid easter type: %s, must be one of: orthodox, latin", c.EasterType)
295 }
296
297 // Validate verses per day
298 if c.DateProgression.VersesPerDay <= 0 {
299 return fmt.Errorf("verses per day must be positive, got: %d", c.DateProgression.VersesPerDay)
300 }
301
302 return nil
303}
304func GetConfigPath() string {
305 return filepath.Join(xdg.ConfigHome, "bibel", "config.toml")
306}