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