aboutsummaryrefslogtreecommitdiffstats
path: root/src/config.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-02 13:22:44 +0100
committersalmonik <salmonik@tuta.io>2026-11-02 13:22:44 +0100
commit2c044de38f6adcfbd3cc64eb9fa55e67d61e6f94 (patch)
tree24a7a55a70b2235d115d9861d24385a61725e7b8 /src/config.rs
parentseek: jump within the current track from the CLI and over MPRIS (diff)
downloadrsplayer-2c044de38f6adcfbd3cc64eb9fa55e67d61e6f94.tar.gz
rsplayer-2c044de38f6adcfbd3cc64eb9fa55e67d61e6f94.zip
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.
Diffstat (limited to 'src/config.rs')
-rw-r--r--src/config.rs107
1 files changed, 90 insertions, 17 deletions
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;
9use serde::{Deserialize, Serialize}; 9use serde::{Deserialize, Serialize};
10use std::fs; 10use std::fs;
11use std::path::PathBuf; 11use std::path::PathBuf;
12use std::sync::{LazyLock, RwLock};
13use std::time::SystemTime;
12 14
13#[derive(Debug, Serialize, Deserialize, Default)] 15#[derive(Debug, Clone, Serialize, Deserialize, Default)]
14pub struct Config { 16pub struct Config {
15 #[serde(skip)] 17 #[serde(skip)]
16 pub config_path: PathBuf, 18 pub config_path: PathBuf,
@@ -25,24 +27,29 @@ pub struct Config {
25 pub display_format: Option<String>, 27 pub display_format: Option<String>,
26} 28}
27 29
30/// Identity of the config file on disk, used to tell whether a cached copy is
31/// still current. `modified` alone is not enough: several filesystems only
32/// store whole seconds, so a quick edit could keep the same timestamp.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34struct FileStamp {
35 modified: Option<SystemTime>,
36 len: u64,
37}
38
39struct CachedConfig {
40 config: Config,
41 stamp: FileStamp,
42}
43
44static CACHE: LazyLock<RwLock<Option<CachedConfig>>> = LazyLock::new(|| RwLock::new(None));
45
28impl Config { 46impl Config {
29 pub fn load() -> Result<Self, Box<dyn std::error::Error>> { 47 pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
30 let config_dir = if cfg!(debug_assertions) { 48 let config_path = Self::config_path()?;
31 std::env::current_exe()? 49 let config_dir = config_path
32 .parent() 50 .parent()
33 .map(|p| p.to_path_buf()) 51 .map(|p| p.to_path_buf())
34 .unwrap_or_else(|| PathBuf::from(".")) 52 .unwrap_or_else(|| PathBuf::from("."));
35 } else {
36 let proj_dirs = ProjectDirs::from("com", "rsplayer", "rsplayer")
37 .ok_or("Failed to get project directories")?;
38 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
39 PathBuf::from(xdg).join("rsplayer")
40 } else {
41 proj_dirs.config_dir().to_path_buf()
42 }
43 };
44
45 let config_path = config_dir.join("config.toml");
46 53
47 if !config_path.exists() { 54 if !config_path.exists() {
48 fs::create_dir_all(&config_dir)?; 55 fs::create_dir_all(&config_dir)?;
@@ -76,6 +83,64 @@ impl Config {
76 Ok(config) 83 Ok(config)
77 } 84 }
78 85
86 /// Where the config lives: next to the binary for debug builds (so a
87 /// checkout does not touch the installed config), XDG config otherwise.
88 pub fn config_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
89 if cfg!(debug_assertions) {
90 return Ok(std::env::current_exe()?
91 .parent()
92 .map(|p| p.to_path_buf())
93 .unwrap_or_else(|| PathBuf::from("."))
94 .join("config.toml"));
95 }
96 let proj_dirs = ProjectDirs::from("com", "rsplayer", "rsplayer")
97 .ok_or("Failed to get project directories")?;
98 let config_dir = if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
99 PathBuf::from(xdg).join("rsplayer")
100 } else {
101 proj_dirs.config_dir().to_path_buf()
102 };
103 Ok(config_dir.join("config.toml"))
104 }
105
106 fn file_stamp() -> FileStamp {
107 let stamp = Self::config_path()
108 .ok()
109 .and_then(|path| fs::metadata(path).ok());
110 FileStamp {
111 modified: stamp.as_ref().and_then(|m| m.modified().ok()),
112 len: stamp.as_ref().map(|m| m.len()).unwrap_or(0),
113 }
114 }
115
116 /// The config, parsed once and reused until the file changes on disk.
117 ///
118 /// The daemon reads the config for every command, status line and
119 /// notification, and [`Config::load`] parses the TOML *and* rewrites the
120 /// file. Checking a stat instead keeps that off the hot paths while
121 /// `rsp settings set` still takes effect immediately.
122 pub fn cached() -> Result<Self, Box<dyn std::error::Error>> {
123 let stamp = Self::file_stamp();
124 if let Ok(guard) = CACHE.read()
125 && let Some(cached) = guard.as_ref()
126 && cached.stamp == stamp
127 {
128 return Ok(cached.config.clone());
129 }
130
131 let config = Self::load()?;
132 // `load` normalizes and rewrites the file, so take the stamp afterwards:
133 // reading it before would make our own write look like an outside edit.
134 let stamp = Self::file_stamp();
135 if let Ok(mut guard) = CACHE.write() {
136 *guard = Some(CachedConfig {
137 config: config.clone(),
138 stamp,
139 });
140 }
141 Ok(config)
142 }
143
79 pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> { 144 pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
80 let mut output = String::new(); 145 let mut output = String::new();
81 let paths_str = self 146 let paths_str = self
@@ -131,6 +196,14 @@ impl Config {
131 fs::create_dir_all(parent)?; 196 fs::create_dir_all(parent)?;
132 } 197 }
133 fs::write(&self.config_path, output)?; 198 fs::write(&self.config_path, output)?;
199 // Keep the parsed cache in step with what was just written, so this
200 // write does not make the next `cached()` re-read the file.
201 if let Ok(mut guard) = CACHE.write()
202 && let Some(cached) = guard.as_mut()
203 {
204 cached.stamp = Self::file_stamp();
205 cached.config = self.clone();
206 }
134 Ok(()) 207 Ok(())
135 } 208 }
136 209