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