Skip to main content

alf/config/
mod.rs

1//! Configuration management for alf.
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::PathBuf;
7
8/// Main configuration structure
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Config {
11   pub display: DisplayConfig,
12   pub general: GeneralConfig,
13   pub search: SearchConfig,
14   pub ui: UiConfig,
15}
16
17/// Controls what gets populated when Tab is pressed on an alias
18#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
19#[serde(rename_all = "lowercase")]
20pub enum AliasExpansion {
21   #[default]
22   Name,
23   Script,
24}
25
26/// General configuration options
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28#[serde(default)]
29pub struct GeneralConfig {
30   /// List of shell files to parse (supports glob patterns)
31   pub shell_files: Vec<String>,
32   pub alias_expansion: AliasExpansion,
33}
34
35/// Search behavior configuration
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct SearchConfig {
38   /// Case matching strategy
39   pub case_matching: CaseMatching,
40   /// Enable Unicode normalization
41   pub normalize: bool,
42   /// Enable regex support
43   pub enable_regex: bool,
44   /// Enable substring matching
45   pub substring_matching: bool,
46}
47
48/// Case matching options for search
49#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum CaseMatching {
52   /// Ignore case entirely
53   Ignore,
54   /// Smart case (case-insensitive unless query has uppercase)
55   Smart,
56   /// Respect case exactly
57   Respect,
58}
59
60/// UI configuration
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct UiConfig {
63   /// Selected theme name
64   pub theme: String,
65   /// Keybinding mode (currently only "vim" is supported)
66   pub keybind_mode: String,
67}
68
69/// Display preferences
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct DisplayConfig {
72   /// Show type badges (Alias/Function)
73   pub show_type_badges: bool,
74   /// Enable syntax highlighting
75   pub syntax_highlighting: bool,
76   /// Parse and display comments
77   pub parse_comments: bool,
78}
79
80impl Default for Config {
81   fn default() -> Self {
82      Self {
83         display: DisplayConfig {
84            parse_comments: true,
85            show_type_badges: true,
86            syntax_highlighting: true,
87         },
88         general: GeneralConfig::default(),
89         search: SearchConfig {
90            case_matching: CaseMatching::Smart,
91            enable_regex: true,
92            normalize: true,
93            substring_matching: true,
94         },
95         ui: UiConfig {
96            keybind_mode: "vim".to_string(),
97            theme: "default".to_string(),
98         },
99      }
100   }
101}
102
103/// Get the platform-specific configuration file path
104///
105/// - Linux/macOS: `$HOME/.config/alf/config.toml`
106/// - Windows: `%USERPROFILE%\.config\alf\config.toml`
107pub fn get_config_path() -> Result<PathBuf> {
108   let home = std::env::var("HOME")
109      .or_else(|_| std::env::var("USERPROFILE"))
110      .map_err(|_| anyhow::anyhow!("HOME or USERPROFILE environment variable not set"))?;
111
112   let config_dir = PathBuf::from(home).join(".config").join("alf");
113   Ok(config_dir.join("config.toml"))
114}
115
116/// Load configuration from disk
117pub fn load_config() -> Result<Config> {
118   let path = get_config_path()?;
119   let content = fs::read_to_string(&path)?;
120   let config: Config = toml::from_str(&content)?;
121   Ok(config)
122}
123
124/// Save configuration to disk
125pub fn save_config(config: &Config) -> Result<()> {
126   let path = get_config_path()?;
127
128   // Create the config directory if it doesn't exist
129   if let Some(parent) = path.parent() {
130      fs::create_dir_all(parent)?;
131   }
132
133   let content = toml::to_string_pretty(config)?;
134   fs::write(&path, content)?;
135   Ok(())
136}
137
138/// Check if this is the first run (config doesn't exist)
139pub fn is_first_run() -> Result<bool> {
140   let path = get_config_path()?;
141   Ok(!path.exists())
142}
143
144#[cfg(test)]
145mod config_tests;