use anyhow::anyhow; use std::{fmt, fs::{self, File}, io::{Read, Write}, path::PathBuf, time::Duration}; use toml::Value; use crate::desktop::NewLoginCommand; const CONFIG_FILE_RELATIVE_PATH: &str = "bscreensaver/bscreensaver.toml"; const LOCK_TIMEOUT: &str = "lock-timeout"; const BLANK_BEFORE_LOCKING: &str = "blank-before-locking"; const DIALOG_BACKEND: &str = "dialog-backend"; const NEW_LOGIN_COMMAND: &str = "new-login-command"; #[derive(Debug, Clone, Copy, PartialEq)] pub enum DialogBackend { Gtk3, } impl DialogBackend { pub fn binary_name(&self) -> &str { match self { Self::Gtk3 => "bscreensaver-dialog-gtk3", } } pub fn display_name(&self) -> &str { match self { Self::Gtk3 => "GTK3", } } } impl fmt::Display for DialogBackend { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Gtk3 => write!(f, "gtk3"), } } } impl TryFrom<&str> for DialogBackend { type Error = anyhow::Error; fn try_from(value: &str) -> Result { match value { "gtk3" => Ok(Self::Gtk3), other => Err(anyhow!("'{}' is not a valid dialog backend (valid: 'gtk3')", other)), } } } #[derive(Debug, Clone, PartialEq)] pub struct Configuration { pub lock_timeout: Duration, pub blank_before_locking: Duration, pub dialog_backend: DialogBackend, pub new_login_command: NewLoginCommand, } impl Configuration { pub fn load() -> anyhow::Result { use humantime::parse_duration; let config_tomls = xdg::BaseDirectories::new()? .find_config_files(CONFIG_FILE_RELATIVE_PATH) .map(|config_path| parse_config_toml(&config_path)) .collect::, _>>()?; let mut config = Configuration::default(); for config_toml in config_tomls { config.lock_timeout = match config_toml.get("lock-timeout") { None => config.lock_timeout, Some(val) => parse_duration(val.as_str().ok_or(anyhow!("'lock-timeout' must be a duration string like '10m' or '90s'"))?)?, }; config.blank_before_locking = match config_toml.get("blank-before-locking") { None => config.blank_before_locking, Some(val) => parse_duration(val.as_str().ok_or(anyhow!("'blank-before-locking' must be a duration string like '10m' or '90s'"))?)?, }; config.dialog_backend = match config_toml.get("dialog-backend") { None => config.dialog_backend, Some(val) => DialogBackend::try_from(val.as_str().ok_or(anyhow!("'dialog-backend' must be a string"))?)?, }; config.new_login_command = match config_toml.get("new-login-command") { None => config.new_login_command, Some(val) => val.try_into().unwrap_or(NewLoginCommand::Auto), }; } if config.blank_before_locking >= config.lock_timeout { Err(anyhow!("'blank-before-locking' cannot be greater than 'lock-timeout'")) } else { Ok(config) } } pub fn save(&self) -> anyhow::Result<()> { use humantime::format_duration; use toml::map::Map; let mut config_map = Map::new(); config_map.insert(LOCK_TIMEOUT.to_string(), Value::String(format_duration(self.lock_timeout).to_string())); config_map.insert(BLANK_BEFORE_LOCKING.to_string(), Value::String(format_duration(self.blank_before_locking).to_string())); config_map.insert(DIALOG_BACKEND.to_string(), Value::String(self.dialog_backend.to_string())); config_map.insert(NEW_LOGIN_COMMAND.to_string(), self.new_login_command.clone().try_into()?); let config_path = xdg::BaseDirectories::new()?.place_config_file(CONFIG_FILE_RELATIVE_PATH)?; let mut tmp_filename = config_path.file_name().unwrap().to_os_string(); tmp_filename.push(".new"); let tmp_path = config_path.with_file_name(tmp_filename); let mut f = File::create(&tmp_path)?; f.write_all(Value::Table(config_map).to_string().as_bytes())?; drop(f); fs::rename(tmp_path, config_path)?; Ok(()) } } impl Default for Configuration { fn default() -> Self { Self { lock_timeout: Duration::from_secs(60 * 10), blank_before_locking: Duration::ZERO, dialog_backend: DialogBackend::Gtk3, new_login_command: NewLoginCommand::Auto, } } } fn parse_config_toml(config_path: &PathBuf) -> anyhow::Result { let mut f = File::open(config_path)?; let mut config = String::new(); f.read_to_string(&mut config)?; drop(f); let config_toml = config.parse::()?; Ok(config_toml) }