all repos — bibel @ 15d6e631ba47d5dd17683aa9b6802fdf66083d05

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	// 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}