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 // TUI settings
49 TUI struct {
50 // Whether to show quit message (default: true)
51 ShowQuitMessage bool `toml:"show_quit_message" mapstructure:"show_quit_message"`
52
53 // Box border style: "rounded", "double", "single", "hidden"
54 BorderStyle string `toml:"border_style" mapstructure:"border_style"`
55
56 // Box border colour (default: adaptive to terminal)
57 BorderColour string `toml:"border_colour" mapstructure:"border_colour"`
58
59 // Header colour (default: adaptive to terminal)
60 HeaderColour string `toml:"header_colour" mapstructure:"header_colour"`
61
62 // Text colour (default: adaptive to terminal)
63 TextColour string `toml:"text_colour" mapstructure:"text_colour"`
64
65 // Quit message colour (default: adaptive to terminal)
66 QuitColour string `toml:"quit_colour" mapstructure:"quit_colour"`
67 } `toml:"tui" mapstructure:"tui"`
68
69 // Formatter settings
70 Formatter struct {
71 // Whether to use ANSI colours in formatted mode (default: true)
72 UseColours bool `toml:"use_colours" mapstructure:"use_colours"`
73
74 // Header format template (default: "{book} {chapter}\nw. {first_verse}-{second_verse}")
75 HeaderFormat string `toml:"header_format" mapstructure:"header_format"`
76 } `toml:"formatter" mapstructure:"formatter"`
77
78 // Date progression settings
79 DateProgression struct {
80 // Verses per day (default: 12)
81 VersesPerDay int `toml:"verses_per_day" mapstructure:"verses_per_day"`
82
83 // Start date for progression (default: "1 January" of current year)
84 StartDate string `toml:"start_date" mapstructure:"start_date"`
85 } `toml:"date_progression" mapstructure:"date_progression"`
86}
87
88// DefaultConfig returns a configuration with default values
89func DefaultConfig() *Config {
90 cfg := &Config{
91 ReadingMode: "evangelion",
92 OutputMode: "tui",
93 BiblePath: "", // Empty means use XDG data directory
94 }
95
96 cfg.TUI.ShowQuitMessage = true
97 cfg.TUI.BorderStyle = "rounded"
98 cfg.TUI.BorderColour = "" // Empty means adaptive
99 cfg.TUI.HeaderColour = "" // Empty means adaptive
100 cfg.TUI.TextColour = "" // Empty means adaptive
101 cfg.TUI.QuitColour = "" // Empty means adaptive
102
103 cfg.Formatter.UseColours = true
104 cfg.Formatter.HeaderFormat = "{book} {chapter}\nw. {first_verse}-{second_verse}"
105
106 cfg.DateProgression.VersesPerDay = 12
107 cfg.DateProgression.StartDate = "" // Empty means 1 January of current year
108
109 return cfg
110}
111
112// LoadConfig loads configuration from file and environment
113func LoadConfig() (*Config, error) {
114 // Set up viper
115 viper.SetConfigName("config") // Name of config file (without extension)
116 viper.SetConfigType("toml") // REQUIRED if the config file does not have the extension in the name
117
118 // Add XDG config paths
119 configPath := filepath.Join(xdg.ConfigHome, "bibel")
120 viper.AddConfigPath(configPath)
121
122 // Also check current directory for local config
123 viper.AddConfigPath(".")
124
125 // Set defaults
126 defaultCfg := DefaultConfig()
127 viper.SetDefault("reading_mode", defaultCfg.ReadingMode)
128 viper.SetDefault("output_mode", defaultCfg.OutputMode)
129 viper.SetDefault("bible_path", defaultCfg.BiblePath)
130 viper.SetDefault("tui.show_quit_message", defaultCfg.TUI.ShowQuitMessage)
131 viper.SetDefault("tui.border_style", defaultCfg.TUI.BorderStyle)
132 viper.SetDefault("tui.border_colour", defaultCfg.TUI.BorderColour)
133 viper.SetDefault("tui.header_colour", defaultCfg.TUI.HeaderColour)
134 viper.SetDefault("tui.text_colour", defaultCfg.TUI.TextColour)
135 viper.SetDefault("tui.quit_colour", defaultCfg.TUI.QuitColour)
136 viper.SetDefault("formatter.use_colours", defaultCfg.Formatter.UseColours)
137 viper.SetDefault("formatter.header_format", defaultCfg.Formatter.HeaderFormat)
138 viper.SetDefault("date_progression.verses_per_day", defaultCfg.DateProgression.VersesPerDay)
139 viper.SetDefault("date_progression.start_date", defaultCfg.DateProgression.StartDate)
140
141 // Read in config file (if it exists)
142 if err := viper.ReadInConfig(); err != nil {
143 if _, ok := err.(viper.ConfigFileNotFoundError); ok {
144 // Config file not found; we'll use defaults
145 fmt.Fprintf(os.Stderr, "Note: No configuration file found at %s/config.toml\n", configPath)
146 fmt.Fprintf(os.Stderr, "Using default configuration. Run 'bibel --generate-config' to create a default config file.\n")
147 } else {
148 // Config file was found but another error was produced
149 return nil, fmt.Errorf("error reading config file: %w", err)
150 }
151 }
152
153 // Unmarshal config into struct
154 var cfg Config
155 if err := viper.Unmarshal(&cfg); err != nil {
156 return nil, fmt.Errorf("error unmarshalling config: %w", err)
157 }
158
159 // Apply defaults for any empty fields
160 defaultCfg = DefaultConfig()
161 if cfg.ReadingMode == "" {
162 cfg.ReadingMode = defaultCfg.ReadingMode
163 }
164 if cfg.OutputMode == "" {
165 cfg.OutputMode = defaultCfg.OutputMode
166 }
167 if cfg.BiblePath == "" {
168 cfg.BiblePath = defaultCfg.BiblePath
169 }
170 if cfg.TUI.BorderStyle == "" {
171 cfg.TUI.BorderStyle = defaultCfg.TUI.BorderStyle
172 }
173 if cfg.DateProgression.VersesPerDay == 0 {
174 cfg.DateProgression.VersesPerDay = defaultCfg.DateProgression.VersesPerDay
175 }
176
177 // If BiblePath is still empty (default or from config), try to find a Bible file in XDG data directory
178 if cfg.BiblePath == "" {
179 biblePath, err := findBibleFileInDataDir()
180 if err != nil {
181 return nil, fmt.Errorf("failed to find Bible data file: %w", err)
182 }
183 cfg.BiblePath = biblePath
184 fmt.Fprintf(os.Stderr, "Using Bible file from XDG data directory: %s\n", cfg.BiblePath)
185 }
186
187 // Validate configuration
188 if err := cfg.Validate(); err != nil {
189 return nil, fmt.Errorf("invalid configuration: %w", err)
190 }
191
192 return &cfg, nil
193}
194
195// SaveConfig saves the configuration to file
196func SaveConfig(cfg *Config) error {
197 // Set up viper with current config
198 viper.Set("reading_mode", cfg.ReadingMode)
199 viper.Set("output_mode", cfg.OutputMode)
200 viper.Set("bible_path", cfg.BiblePath)
201 viper.Set("tui.show_quit_message", cfg.TUI.ShowQuitMessage)
202 viper.Set("tui.border_style", cfg.TUI.BorderStyle)
203 viper.Set("tui.border_colour", cfg.TUI.BorderColour)
204 viper.Set("tui.header_colour", cfg.TUI.HeaderColour)
205 viper.Set("tui.text_colour", cfg.TUI.TextColour)
206 viper.Set("tui.quit_colour", cfg.TUI.QuitColour)
207 viper.Set("formatter.use_colours", cfg.Formatter.UseColours)
208 viper.Set("formatter.header_format", cfg.Formatter.HeaderFormat)
209 viper.Set("date_progression.verses_per_day", cfg.DateProgression.VersesPerDay)
210 viper.Set("date_progression.start_date", cfg.DateProgression.StartDate)
211
212 // Ensure config directory exists
213 configPath := filepath.Join(xdg.ConfigHome, "bibel")
214 if err := os.MkdirAll(configPath, 0755); err != nil {
215 return fmt.Errorf("error creating config directory: %w", err)
216 }
217
218 // Write config file
219 configFile := filepath.Join(configPath, "config.toml")
220 if err := viper.WriteConfigAs(configFile); err != nil {
221 return fmt.Errorf("error writing config file: %w", err)
222 }
223
224 return nil
225}
226
227// GenerateDefaultConfig creates a default configuration file
228func GenerateDefaultConfig() error {
229 return SaveConfig(DefaultConfig())
230}
231
232// GetConfigPath returns the path to the configuration file
233
234// Validate checks if configuration values are valid
235func (c *Config) Validate() error {
236 // Validate reading mode
237 validReadingModes := map[string]bool{
238 "evangelion": true,
239 "new_testament": true,
240 "old_testament": true,
241 "bible": true,
242 }
243 // Allow empty reading mode (will use default)
244 if c.ReadingMode != "" && !validReadingModes[c.ReadingMode] {
245 return fmt.Errorf("invalid reading mode: %s, must be one of: evangelion, new_testament, old_testament, bible", c.ReadingMode)
246 }
247
248 // Validate output mode
249 validOutputModes := map[string]bool{
250 "tui": true,
251 "formatted": true,
252 "plain": true,
253 }
254 if !validOutputModes[c.OutputMode] {
255 return fmt.Errorf("invalid output mode: %s, must be one of: tui, formatted, plain", c.OutputMode)
256 }
257
258 // Validate TUI border style
259 validBorderStyles := map[string]bool{
260 "rounded": true,
261 "double": true,
262 "single": true,
263 "hidden": true,
264 }
265 if !validBorderStyles[c.TUI.BorderStyle] {
266 return fmt.Errorf("invalid border style: %s, must be one of: rounded, double, single, hidden", c.TUI.BorderStyle)
267 }
268
269 // Validate verses per day
270 if c.DateProgression.VersesPerDay <= 0 {
271 return fmt.Errorf("verses per day must be positive, got: %d", c.DateProgression.VersesPerDay)
272 }
273
274 return nil
275}
276func GetConfigPath() string {
277 return filepath.Join(xdg.ConfigHome, "bibel", "config.toml")
278}