| 1 | //! Configuration loading |
| 2 | //! |
| 3 | //! Supports: |
| 4 | //! - Lua configuration via ~/.config/gar/init.lua (gar.calculator table) |
| 5 | //! - TOML fallback via ~/.config/garcalc/config.toml |
| 6 | |
| 7 | use anyhow::Result; |
| 8 | use mlua::Lua; |
| 9 | use serde::{Deserialize, Serialize}; |
| 10 | use std::path::PathBuf; |
| 11 | |
| 12 | /// Calculator configuration |
| 13 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 14 | #[serde(default)] |
| 15 | pub struct Config { |
| 16 | pub general: GeneralConfig, |
| 17 | pub popup: PopupConfig, |
| 18 | pub graph: GraphConfig, |
| 19 | pub appearance: AppearanceConfig, |
| 20 | } |
| 21 | |
| 22 | impl Default for Config { |
| 23 | fn default() -> Self { |
| 24 | Self { |
| 25 | general: GeneralConfig::default(), |
| 26 | popup: PopupConfig::default(), |
| 27 | graph: GraphConfig::default(), |
| 28 | appearance: AppearanceConfig::default(), |
| 29 | } |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 34 | #[serde(default)] |
| 35 | pub struct GeneralConfig { |
| 36 | pub default_mode: String, |
| 37 | pub precision: u32, |
| 38 | pub angle_mode: String, |
| 39 | pub exact_mode: bool, |
| 40 | } |
| 41 | |
| 42 | impl Default for GeneralConfig { |
| 43 | fn default() -> Self { |
| 44 | Self { |
| 45 | default_mode: "calculator".to_string(), |
| 46 | precision: 15, |
| 47 | angle_mode: "radians".to_string(), |
| 48 | exact_mode: false, |
| 49 | } |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 54 | #[serde(default)] |
| 55 | pub struct PopupConfig { |
| 56 | pub width: u32, |
| 57 | pub height: u32, |
| 58 | pub position: String, |
| 59 | } |
| 60 | |
| 61 | impl Default for PopupConfig { |
| 62 | fn default() -> Self { |
| 63 | Self { |
| 64 | width: 700, |
| 65 | height: 500, |
| 66 | position: "center".to_string(), |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 72 | #[serde(default)] |
| 73 | pub struct GraphConfig { |
| 74 | pub default_x_range: (f64, f64), |
| 75 | pub default_y_range: (f64, f64), |
| 76 | pub grid_enabled: bool, |
| 77 | } |
| 78 | |
| 79 | impl Default for GraphConfig { |
| 80 | fn default() -> Self { |
| 81 | Self { |
| 82 | default_x_range: (-10.0, 10.0), |
| 83 | default_y_range: (-10.0, 10.0), |
| 84 | grid_enabled: true, |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 90 | #[serde(default)] |
| 91 | pub struct AppearanceConfig { |
| 92 | pub font_family: String, |
| 93 | pub font_size: u32, |
| 94 | pub button_panel_visible: bool, |
| 95 | } |
| 96 | |
| 97 | impl Default for AppearanceConfig { |
| 98 | fn default() -> Self { |
| 99 | Self { |
| 100 | font_family: "monospace".to_string(), |
| 101 | font_size: 14, |
| 102 | button_panel_visible: false, |
| 103 | } |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | impl Config { |
| 108 | /// Load configuration |
| 109 | /// Tries Lua config first (~/.config/gar/init.lua), then falls back to TOML |
| 110 | pub fn load() -> Result<Self> { |
| 111 | // Try Lua config first (shared with other gar components) |
| 112 | if let Ok(config) = Self::load_from_lua() { |
| 113 | return Ok(config); |
| 114 | } |
| 115 | |
| 116 | // Fall back to TOML config |
| 117 | let path = toml_config_path(); |
| 118 | if path.exists() { |
| 119 | let content = std::fs::read_to_string(&path)?; |
| 120 | let config: Config = toml::from_str(&content)?; |
| 121 | Ok(config) |
| 122 | } else { |
| 123 | Ok(Config::default()) |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | /// Load configuration from Lua (~/.config/gar/init.lua) |
| 128 | fn load_from_lua() -> Result<Self> { |
| 129 | let lua_path = lua_config_path(); |
| 130 | if !lua_path.exists() { |
| 131 | return Err(anyhow::anyhow!("Lua config not found")); |
| 132 | } |
| 133 | |
| 134 | let lua = Lua::new(); |
| 135 | let content = std::fs::read_to_string(&lua_path)?; |
| 136 | |
| 137 | // Create gar table if it doesn't exist |
| 138 | lua.scope(|_scope| { |
| 139 | let globals = lua.globals(); |
| 140 | |
| 141 | // Initialize gar table |
| 142 | let gar: mlua::Table = lua.create_table()?; |
| 143 | globals.set("gar", gar)?; |
| 144 | |
| 145 | // Execute the config file |
| 146 | lua.load(&content).exec()?; |
| 147 | |
| 148 | // Get gar.calculator table |
| 149 | let gar: mlua::Table = globals.get("gar")?; |
| 150 | let calc: Option<mlua::Table> = gar.get("calculator").ok(); |
| 151 | |
| 152 | if let Some(calc) = calc { |
| 153 | let config = Self::from_lua_table(&calc)?; |
| 154 | Ok(config) |
| 155 | } else { |
| 156 | Err(mlua::Error::RuntimeError( |
| 157 | "gar.calculator not found".to_string(), |
| 158 | )) |
| 159 | } |
| 160 | }) |
| 161 | .map_err(|e| anyhow::anyhow!("Lua error: {}", e)) |
| 162 | } |
| 163 | |
| 164 | /// Parse Config from Lua table |
| 165 | fn from_lua_table(table: &mlua::Table) -> mlua::Result<Self> { |
| 166 | let mut config = Config::default(); |
| 167 | |
| 168 | // General settings |
| 169 | if let Ok(general) = table.get::<mlua::Table>("general") { |
| 170 | if let Ok(mode) = general.get::<String>("default_mode") { |
| 171 | config.general.default_mode = mode; |
| 172 | } |
| 173 | if let Ok(precision) = general.get::<u32>("precision") { |
| 174 | config.general.precision = precision; |
| 175 | } |
| 176 | if let Ok(angle) = general.get::<String>("angle_mode") { |
| 177 | config.general.angle_mode = angle; |
| 178 | } |
| 179 | if let Ok(exact) = general.get::<bool>("exact_mode") { |
| 180 | config.general.exact_mode = exact; |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | // Popup settings |
| 185 | if let Ok(popup) = table.get::<mlua::Table>("popup") { |
| 186 | if let Ok(w) = popup.get::<u32>("width") { |
| 187 | config.popup.width = w; |
| 188 | } |
| 189 | if let Ok(h) = popup.get::<u32>("height") { |
| 190 | config.popup.height = h; |
| 191 | } |
| 192 | if let Ok(pos) = popup.get::<String>("position") { |
| 193 | config.popup.position = pos; |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | // Graph settings |
| 198 | if let Ok(graph) = table.get::<mlua::Table>("graph") { |
| 199 | if let Ok(x_range) = graph.get::<mlua::Table>("x_range") { |
| 200 | if let (Ok(min), Ok(max)) = (x_range.get::<f64>(1), x_range.get::<f64>(2)) { |
| 201 | config.graph.default_x_range = (min, max); |
| 202 | } |
| 203 | } |
| 204 | if let Ok(y_range) = graph.get::<mlua::Table>("y_range") { |
| 205 | if let (Ok(min), Ok(max)) = (y_range.get::<f64>(1), y_range.get::<f64>(2)) { |
| 206 | config.graph.default_y_range = (min, max); |
| 207 | } |
| 208 | } |
| 209 | if let Ok(grid) = graph.get::<bool>("grid") { |
| 210 | config.graph.grid_enabled = grid; |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | // Appearance settings |
| 215 | if let Ok(appearance) = table.get::<mlua::Table>("appearance") { |
| 216 | if let Ok(font) = appearance.get::<String>("font_family") { |
| 217 | config.appearance.font_family = font; |
| 218 | } |
| 219 | if let Ok(size) = appearance.get::<u32>("font_size") { |
| 220 | config.appearance.font_size = size; |
| 221 | } |
| 222 | if let Ok(panel) = appearance.get::<bool>("button_panel") { |
| 223 | config.appearance.button_panel_visible = panel; |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | Ok(config) |
| 228 | } |
| 229 | |
| 230 | /// Save configuration to TOML file |
| 231 | #[allow(dead_code)] |
| 232 | pub fn save(&self) -> Result<()> { |
| 233 | let path = toml_config_path(); |
| 234 | if let Some(parent) = path.parent() { |
| 235 | std::fs::create_dir_all(parent)?; |
| 236 | } |
| 237 | let content = toml::to_string_pretty(self)?; |
| 238 | std::fs::write(path, content)?; |
| 239 | Ok(()) |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | fn lua_config_path() -> PathBuf { |
| 244 | dirs::config_dir() |
| 245 | .unwrap_or_else(|| PathBuf::from(".")) |
| 246 | .join("gar") |
| 247 | .join("init.lua") |
| 248 | } |
| 249 | |
| 250 | fn toml_config_path() -> PathBuf { |
| 251 | dirs::config_dir() |
| 252 | .unwrap_or_else(|| PathBuf::from(".")) |
| 253 | .join("garcalc") |
| 254 | .join("config.toml") |
| 255 | } |
| 256 |