@@ -0,0 +1,325 @@ |
| 1 | +//! Configuration loading and management for garlock |
| 2 | +//! |
| 3 | +//! Loads configuration from TOML file with sensible defaults. |
| 4 | + |
| 5 | +use anyhow::{Context, Result}; |
| 6 | +use serde::{Deserialize, Serialize}; |
| 7 | +use std::path::Path; |
| 8 | + |
| 9 | +/// Main configuration structure |
| 10 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 11 | +#[serde(default)] |
| 12 | +pub struct Config { |
| 13 | + /// General settings |
| 14 | + pub general: GeneralConfig, |
| 15 | + |
| 16 | + /// Background settings |
| 17 | + pub background: BackgroundConfig, |
| 18 | + |
| 19 | + /// Ring indicator settings |
| 20 | + pub ring: RingConfig, |
| 21 | + |
| 22 | + /// Indicator settings (caps lock, attempts, etc.) |
| 23 | + pub indicator: IndicatorConfig, |
| 24 | + |
| 25 | + /// Font settings |
| 26 | + pub font: FontConfig, |
| 27 | +} |
| 28 | + |
| 29 | +impl Default for Config { |
| 30 | + fn default() -> Self { |
| 31 | + Self { |
| 32 | + general: GeneralConfig::default(), |
| 33 | + background: BackgroundConfig::default(), |
| 34 | + ring: RingConfig::default(), |
| 35 | + indicator: IndicatorConfig::default(), |
| 36 | + font: FontConfig::default(), |
| 37 | + } |
| 38 | + } |
| 39 | +} |
| 40 | + |
| 41 | +impl Config { |
| 42 | + /// Load configuration from file or use defaults |
| 43 | + /// |
| 44 | + /// If no config file exists, creates one with default values. |
| 45 | + pub fn load(path: Option<&Path>) -> Result<Self> { |
| 46 | + let config_path = path |
| 47 | + .map(|p| p.to_path_buf()) |
| 48 | + .or_else(|| dirs::config_dir().map(|d| d.join("garlock/config.toml"))); |
| 49 | + |
| 50 | + if let Some(path) = &config_path { |
| 51 | + if path.exists() { |
| 52 | + let content = std::fs::read_to_string(path) |
| 53 | + .with_context(|| format!("Failed to read config file: {:?}", path))?; |
| 54 | + let config: Config = toml::from_str(&content) |
| 55 | + .with_context(|| format!("Failed to parse config file: {:?}", path))?; |
| 56 | + tracing::info!(?path, "Loaded configuration"); |
| 57 | + return Ok(config); |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + // Create default config file if it doesn't exist |
| 62 | + let config = Config::default(); |
| 63 | + if let Some(path) = config_path { |
| 64 | + if let Err(e) = config.write_default(&path) { |
| 65 | + tracing::warn!(?path, "Failed to create default config: {}", e); |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + tracing::debug!("Using default configuration"); |
| 70 | + Ok(config) |
| 71 | + } |
| 72 | + |
| 73 | + /// Write the default configuration file with comments |
| 74 | + fn write_default(&self, path: &Path) -> Result<()> { |
| 75 | + // Create parent directory if needed |
| 76 | + if let Some(parent) = path.parent() { |
| 77 | + std::fs::create_dir_all(parent) |
| 78 | + .with_context(|| format!("Failed to create config directory: {:?}", parent))?; |
| 79 | + } |
| 80 | + |
| 81 | + let content = Self::default_config_content(); |
| 82 | + std::fs::write(path, content) |
| 83 | + .with_context(|| format!("Failed to write config file: {:?}", path))?; |
| 84 | + |
| 85 | + tracing::info!(?path, "Created default configuration file"); |
| 86 | + Ok(()) |
| 87 | + } |
| 88 | + |
| 89 | + /// Generate default config file content with documentation comments |
| 90 | + fn default_config_content() -> String { |
| 91 | + r##"# garlock configuration |
| 92 | +# Screen locker for the gar desktop suite |
| 93 | +# |
| 94 | +# This file is auto-generated with default values. |
| 95 | +# Uncomment and modify options as needed. |
| 96 | + |
| 97 | +[general] |
| 98 | +# Grace period in seconds before password is required (0 to disable) |
| 99 | +grace_period = 0 |
| 100 | + |
| 101 | +# PAM service name (must have corresponding /etc/pam.d/garlock file) |
| 102 | +pam_service = "garlock" |
| 103 | + |
| 104 | +# Maximum failed attempts before cooldown kicks in |
| 105 | +max_attempts = 3 |
| 106 | + |
| 107 | +# Cooldown duration in seconds (multiplied by attempts over max) |
| 108 | +cooldown_seconds = 5 |
| 109 | + |
| 110 | +[background] |
| 111 | +# Gaussian blur radius (higher = more blur, 0 to disable) |
| 112 | +blur_radius = 25.0 |
| 113 | + |
| 114 | +# Brightness adjustment (0.0 = black, 1.0 = original brightness) |
| 115 | +brightness = 0.6 |
| 116 | + |
| 117 | +# Fallback solid color if screenshot capture fails (hex format) |
| 118 | +fallback_color = "#1a1a2e" |
| 119 | + |
| 120 | +[ring] |
| 121 | +# Ring geometry |
| 122 | +radius_outer = 90.0 |
| 123 | +radius_inner = 75.0 |
| 124 | +line_width = 6.0 |
| 125 | + |
| 126 | +# State colors (hex format with optional alpha: #RRGGBB or #RRGGBBAA) |
| 127 | +# Idle: waiting for input |
| 128 | +color_idle = "#1e90ffcc" |
| 129 | + |
| 130 | +# Typing: receiving password input |
| 131 | +color_typing = "#00ff00cc" |
| 132 | + |
| 133 | +# Verifying: checking password with PAM |
| 134 | +color_verifying = "#ffa500cc" |
| 135 | + |
| 136 | +# Wrong: authentication failed |
| 137 | +color_wrong = "#ff0000cc" |
| 138 | + |
| 139 | +# Clear: backspace pressed / clearing input |
| 140 | +color_clear = "#ffff00cc" |
| 141 | + |
| 142 | +# Inner circle fill color |
| 143 | +color_inside = "#00000088" |
| 144 | + |
| 145 | +# Ring track background color |
| 146 | +color_ring_bg = "#00000055" |
| 147 | + |
| 148 | +[indicator] |
| 149 | +# Show Caps Lock warning when active |
| 150 | +show_caps_lock = true |
| 151 | +caps_lock_text = "Caps Lock" |
| 152 | + |
| 153 | +# Show number of failed attempts |
| 154 | +show_failed_attempts = true |
| 155 | + |
| 156 | +# Show current time on lock screen |
| 157 | +show_time = false |
| 158 | +time_format = "%H:%M" |
| 159 | + |
| 160 | +[font] |
| 161 | +# Font family for text elements |
| 162 | +family = "Sans" |
| 163 | + |
| 164 | +# Font size in points |
| 165 | +size = 14 |
| 166 | +"## |
| 167 | + .to_string() |
| 168 | + } |
| 169 | +} |
| 170 | + |
| 171 | +/// General settings |
| 172 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 173 | +#[serde(default)] |
| 174 | +pub struct GeneralConfig { |
| 175 | + /// Grace period in seconds before password is required (0 to disable) |
| 176 | + pub grace_period: u32, |
| 177 | + |
| 178 | + /// PAM service name |
| 179 | + pub pam_service: String, |
| 180 | + |
| 181 | + /// Maximum failed attempts before cooldown |
| 182 | + pub max_attempts: u32, |
| 183 | + |
| 184 | + /// Cooldown duration in seconds per attempt over max |
| 185 | + pub cooldown_seconds: u32, |
| 186 | +} |
| 187 | + |
| 188 | +impl Default for GeneralConfig { |
| 189 | + fn default() -> Self { |
| 190 | + Self { |
| 191 | + grace_period: 0, |
| 192 | + pam_service: "garlock".to_string(), |
| 193 | + max_attempts: 3, |
| 194 | + cooldown_seconds: 5, |
| 195 | + } |
| 196 | + } |
| 197 | +} |
| 198 | + |
| 199 | +/// Background settings |
| 200 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 201 | +#[serde(default)] |
| 202 | +pub struct BackgroundConfig { |
| 203 | + /// Blur radius (0 to disable) |
| 204 | + pub blur_radius: f32, |
| 205 | + |
| 206 | + /// Brightness adjustment (0.0-1.0, lower is darker) |
| 207 | + pub brightness: f32, |
| 208 | + |
| 209 | + /// Fallback color if screenshot fails (hex format) |
| 210 | + pub fallback_color: String, |
| 211 | +} |
| 212 | + |
| 213 | +impl Default for BackgroundConfig { |
| 214 | + fn default() -> Self { |
| 215 | + Self { |
| 216 | + blur_radius: 25.0, |
| 217 | + brightness: 0.6, |
| 218 | + fallback_color: "#1a1a2e".to_string(), |
| 219 | + } |
| 220 | + } |
| 221 | +} |
| 222 | + |
| 223 | +/// Ring indicator settings |
| 224 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 225 | +#[serde(default)] |
| 226 | +pub struct RingConfig { |
| 227 | + /// Outer ring radius |
| 228 | + pub radius_outer: f64, |
| 229 | + |
| 230 | + /// Inner ring radius |
| 231 | + pub radius_inner: f64, |
| 232 | + |
| 233 | + /// Ring line width |
| 234 | + pub line_width: f64, |
| 235 | + |
| 236 | + /// Color when idle (hex format with alpha) |
| 237 | + pub color_idle: String, |
| 238 | + |
| 239 | + /// Color when typing (hex format with alpha) |
| 240 | + pub color_typing: String, |
| 241 | + |
| 242 | + /// Color when verifying (hex format with alpha) |
| 243 | + pub color_verifying: String, |
| 244 | + |
| 245 | + /// Color when wrong password (hex format with alpha) |
| 246 | + pub color_wrong: String, |
| 247 | + |
| 248 | + /// Color when cleared (hex format with alpha) |
| 249 | + pub color_clear: String, |
| 250 | + |
| 251 | + /// Inner circle color (hex format with alpha) |
| 252 | + pub color_inside: String, |
| 253 | + |
| 254 | + /// Ring background color (hex format with alpha) |
| 255 | + pub color_ring_bg: String, |
| 256 | +} |
| 257 | + |
| 258 | +impl Default for RingConfig { |
| 259 | + fn default() -> Self { |
| 260 | + Self { |
| 261 | + radius_outer: 90.0, |
| 262 | + radius_inner: 75.0, |
| 263 | + line_width: 6.0, |
| 264 | + color_idle: "#1e90ffcc".to_string(), |
| 265 | + color_typing: "#00ff00cc".to_string(), |
| 266 | + color_verifying: "#ffa500cc".to_string(), |
| 267 | + color_wrong: "#ff0000cc".to_string(), |
| 268 | + color_clear: "#ffff00cc".to_string(), |
| 269 | + color_inside: "#00000088".to_string(), |
| 270 | + color_ring_bg: "#00000055".to_string(), |
| 271 | + } |
| 272 | + } |
| 273 | +} |
| 274 | + |
| 275 | +/// Indicator settings |
| 276 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 277 | +#[serde(default)] |
| 278 | +pub struct IndicatorConfig { |
| 279 | + /// Show Caps Lock warning |
| 280 | + pub show_caps_lock: bool, |
| 281 | + |
| 282 | + /// Caps Lock warning text |
| 283 | + pub caps_lock_text: String, |
| 284 | + |
| 285 | + /// Show failed attempt count |
| 286 | + pub show_failed_attempts: bool, |
| 287 | + |
| 288 | + /// Show time on lock screen |
| 289 | + pub show_time: bool, |
| 290 | + |
| 291 | + /// Time format (strftime) |
| 292 | + pub time_format: String, |
| 293 | +} |
| 294 | + |
| 295 | +impl Default for IndicatorConfig { |
| 296 | + fn default() -> Self { |
| 297 | + Self { |
| 298 | + show_caps_lock: true, |
| 299 | + caps_lock_text: "Caps Lock".to_string(), |
| 300 | + show_failed_attempts: true, |
| 301 | + show_time: false, |
| 302 | + time_format: "%H:%M".to_string(), |
| 303 | + } |
| 304 | + } |
| 305 | +} |
| 306 | + |
| 307 | +/// Font settings |
| 308 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 309 | +#[serde(default)] |
| 310 | +pub struct FontConfig { |
| 311 | + /// Font family |
| 312 | + pub family: String, |
| 313 | + |
| 314 | + /// Font size |
| 315 | + pub size: u32, |
| 316 | +} |
| 317 | + |
| 318 | +impl Default for FontConfig { |
| 319 | + fn default() -> Self { |
| 320 | + Self { |
| 321 | + family: "Sans".to_string(), |
| 322 | + size: 14, |
| 323 | + } |
| 324 | + } |
| 325 | +} |