diff options
Diffstat (limited to '')
| -rw-r--r-- | src/config.rs | 107 |
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; | |||
| 9 | use serde::{Deserialize, Serialize}; | 9 | use serde::{Deserialize, Serialize}; |
| 10 | use std::fs; | 10 | use std::fs; |
| 11 | use std::path::PathBuf; | 11 | use std::path::PathBuf; |
| 12 | use std::sync::{LazyLock, RwLock}; | ||
| 13 | use std::time::SystemTime; | ||
| 12 | 14 | ||
| 13 | #[derive(Debug, Serialize, Deserialize, Default)] | 15 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] |
| 14 | pub struct Config { | 16 | pub 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)] | ||
| 34 | struct FileStamp { | ||
| 35 | modified: Option<SystemTime>, | ||
| 36 | len: u64, | ||
| 37 | } | ||
| 38 | |||
| 39 | struct CachedConfig { | ||
| 40 | config: Config, | ||
| 41 | stamp: FileStamp, | ||
| 42 | } | ||
| 43 | |||
| 44 | static CACHE: LazyLock<RwLock<Option<CachedConfig>>> = LazyLock::new(|| RwLock::new(None)); | ||
| 45 | |||
| 28 | impl Config { | 46 | impl 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 | ||
