all repos — bibel @ 24fb7ab0d94eef6e9a9dab5d2553b861096d5889

Unnamed repository; edit this file 'description' to name the repository.

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}