From 2c044de38f6adcfbd3cc64eb9fa55e67d61e6f94 Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 2 Nov 2026 13:22:44 +0100 Subject: config: parse the config once and reuse it until the file changes Every command, status line and notification went through Config::load, which reads *and* rewrites config.toml, so `rsp status` alone touched the file on each call. The daemon now keeps a parsed copy and only stats the file to notice outside edits, so `rsp settings set` still takes effect immediately. Config::save refreshes the cached copy too, and Config::config_path is shared by the loader and the cache. --- src/config.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 17 deletions(-) (limited to 'src/config.rs') diff --git a/src/config.rs b/src/config.rs index f20fb3a..a08f01f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,8 +9,10 @@ use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; +use std::sync::{LazyLock, RwLock}; +use std::time::SystemTime; -#[derive(Debug, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Config { #[serde(skip)] pub config_path: PathBuf, @@ -25,24 +27,29 @@ pub struct Config { pub display_format: Option, } +/// Identity of the config file on disk, used to tell whether a cached copy is +/// still current. `modified` alone is not enough: several filesystems only +/// store whole seconds, so a quick edit could keep the same timestamp. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct FileStamp { + modified: Option, + len: u64, +} + +struct CachedConfig { + config: Config, + stamp: FileStamp, +} + +static CACHE: LazyLock>> = LazyLock::new(|| RwLock::new(None)); + impl Config { pub fn load() -> Result> { - let config_dir = if cfg!(debug_assertions) { - std::env::current_exe()? - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| PathBuf::from(".")) - } else { - let proj_dirs = ProjectDirs::from("com", "rsplayer", "rsplayer") - .ok_or("Failed to get project directories")?; - if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { - PathBuf::from(xdg).join("rsplayer") - } else { - proj_dirs.config_dir().to_path_buf() - } - }; - - let config_path = config_dir.join("config.toml"); + let config_path = Self::config_path()?; + let config_dir = config_path + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| PathBuf::from(".")); if !config_path.exists() { fs::create_dir_all(&config_dir)?; @@ -76,6 +83,64 @@ impl Config { Ok(config) } + /// Where the config lives: next to the binary for debug builds (so a + /// checkout does not touch the installed config), XDG config otherwise. + pub fn config_path() -> Result> { + if cfg!(debug_assertions) { + return Ok(std::env::current_exe()? + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| PathBuf::from(".")) + .join("config.toml")); + } + let proj_dirs = ProjectDirs::from("com", "rsplayer", "rsplayer") + .ok_or("Failed to get project directories")?; + let config_dir = if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { + PathBuf::from(xdg).join("rsplayer") + } else { + proj_dirs.config_dir().to_path_buf() + }; + Ok(config_dir.join("config.toml")) + } + + fn file_stamp() -> FileStamp { + let stamp = Self::config_path() + .ok() + .and_then(|path| fs::metadata(path).ok()); + FileStamp { + modified: stamp.as_ref().and_then(|m| m.modified().ok()), + len: stamp.as_ref().map(|m| m.len()).unwrap_or(0), + } + } + + /// The config, parsed once and reused until the file changes on disk. + /// + /// The daemon reads the config for every command, status line and + /// notification, and [`Config::load`] parses the TOML *and* rewrites the + /// file. Checking a stat instead keeps that off the hot paths while + /// `rsp settings set` still takes effect immediately. + pub fn cached() -> Result> { + let stamp = Self::file_stamp(); + if let Ok(guard) = CACHE.read() + && let Some(cached) = guard.as_ref() + && cached.stamp == stamp + { + return Ok(cached.config.clone()); + } + + let config = Self::load()?; + // `load` normalizes and rewrites the file, so take the stamp afterwards: + // reading it before would make our own write look like an outside edit. + let stamp = Self::file_stamp(); + if let Ok(mut guard) = CACHE.write() { + *guard = Some(CachedConfig { + config: config.clone(), + stamp, + }); + } + Ok(config) + } + pub fn save(&self) -> Result<(), Box> { let mut output = String::new(); let paths_str = self @@ -131,6 +196,14 @@ impl Config { fs::create_dir_all(parent)?; } fs::write(&self.config_path, output)?; + // Keep the parsed cache in step with what was just written, so this + // write does not make the next `cached()` re-read the file. + if let Ok(mut guard) = CACHE.write() + && let Some(cached) = guard.as_mut() + { + cached.stamp = Self::file_stamp(); + cached.config = self.clone(); + } Ok(()) } -- cgit v1.2.3