use anyhow::{Result, bail}; use regex::Regex; use serde::{Deserialize, Serialize}; const MIN_MEMORY_MB: u64 = 512; pub const DEFAULTS_MEMORY: &str = "16G"; pub const VALID_KEYS: &[&str] = &["memory"]; #[derive(Debug, Serialize, Deserialize, Default, Clone)] pub struct Config { #[serde(skip_serializing_if = "Option::is_none")] pub memory: Option, } fn config_dir() -> std::path::PathBuf { dirs::home_dir() .expect("cannot find home directory") .join(".config") .join("sandlot") } fn config_path() -> std::path::PathBuf { config_dir().join("config.json") } pub fn validate_memory(v: &str) -> Result { let re = Regex::new(r"^[1-9]\d*[GMgm]$").unwrap(); if !re.is_match(v) { bail!("Invalid memory value: {v} (must be a number followed by G or M, e.g. 16G)"); } let num: u64 = v[..v.len() - 1].parse().unwrap(); let unit = v.chars().last().unwrap().to_ascii_uppercase(); let mb = if unit == 'G' { num * 1024 } else { num }; if mb < MIN_MEMORY_MB { bail!("Memory too low: {v} (minimum {MIN_MEMORY_MB}M)"); } Ok(v.to_uppercase()) } pub async fn load() -> Config { let path = config_path(); match tokio::fs::read_to_string(&path).await { Ok(content) => serde_json::from_str(&content).unwrap_or_default(), Err(_) => Config::default(), } } pub async fn save(config: &Config) -> Result<()> { let dir = config_dir(); tokio::fs::create_dir_all(&dir).await?; let json = serde_json::to_string_pretty(config)? + "\n"; tokio::fs::write(config_path(), json).await?; Ok(()) } pub async fn get_memory() -> Option { load().await.memory } pub async fn set_memory(value: String) -> Result<()> { let mut config = load().await; config.memory = Some(value); save(&config).await }