all repos — bibel @ c045b445fe953f81a929780728841400a44ce422

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

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}