diff options
| author | cashmere <cashmere@autistici.org> | 2026-08-16 08:21:37 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-08-16 08:21:37 +0200 |
| commit | 4901b5d77d73db59f0c1545719ec6d1404aad979 (patch) | |
| tree | 0b3233e82387ffe7a9cd5c0934971a665e0575a9 /src/config.rs | |
| parent | 91b84d1f1bfbc5de2eb140a48185a5252b1b592e (diff) | |
| download | i3bar-mango-main.tar.gz | |
Diffstat (limited to 'src/config.rs')
| -rw-r--r-- | src/config.rs | 98 |
1 files changed, 84 insertions, 14 deletions
diff --git a/src/config.rs b/src/config.rs index 4e41069..d354839 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,15 @@ use crate::color::Color; use crate::protocol::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; use anyhow::{Context, Result}; +use inotify::{Inotify, WatchMask}; use pangocairo::pango::FontDescription; use serde::{Deserialize, de}; use std::collections::HashMap; +use std::ffi::OsString; use std::fs::read_to_string; +use std::io; use std::ops::Deref; +use std::os::fd::{AsRawFd, RawFd}; use std::path::{Path, PathBuf}; use std::{env, fmt}; @@ -114,27 +118,21 @@ impl Default for Config { impl Config { pub fn new(path: Option<&Path>) -> Result<Self> { - let buf; - - let path = match path { - Some(path) => Some(path), - None => { - buf = config_path(); - buf.as_deref() - } - }; - - Ok(match path { + match Self::path(path) { Some(config_path) => { let config = read_to_string(config_path).context("Failed to read configuration")?; - toml::from_str(&config).context("Failed to deserialize configuration")? + toml::from_str(&config).context("Failed to deserialize configuration") } None => { eprintln!("Could not find the configuration path"); eprintln!("Using default configuration"); - Self::default() + Ok(Self::default()) } - }) + } + } + + pub fn path(path: Option<&Path>) -> Option<PathBuf> { + path.map(Path::to_path_buf).or_else(config_path) } pub fn output_enabled(&self, output: &str) -> bool { @@ -145,6 +143,47 @@ impl Config { } } +pub struct ConfigWatcher { + inotify: Inotify, + buffer: Vec<u8>, + filename: OsString, +} + +impl ConfigWatcher { + pub fn new(path: &Path) -> io::Result<Self> { + let directory = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")); + let filename = path + .file_name() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid config path"))? + .to_os_string(); + let inotify = Inotify::init()?; + inotify.watches().add( + directory, + WatchMask::CLOSE_WRITE | WatchMask::CREATE | WatchMask::MOVED_TO, + )?; + Ok(Self { + inotify, + buffer: vec![0; 4096], + filename, + }) + } + + pub fn fd(&self) -> RawFd { + self.inotify.as_raw_fd() + } + + pub fn changed(&mut self) -> io::Result<bool> { + let mut changed = false; + for event in self.inotify.read_events(&mut self.buffer)? { + changed |= event.name.is_some_and(|name| name == self.filename); + } + Ok(changed) + } +} + fn config_dir() -> Option<PathBuf> { env::var_os("XDG_CONFIG_HOME") .map(PathBuf::from) @@ -296,3 +335,34 @@ impl<'de> de::Deserialize<'de> for Font { deserializer.deserialize_str(FontVisitor) } } + +#[cfg(test)] +mod tests { + use super::ConfigWatcher; + use std::fs; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[test] + fn watcher_detects_overwrite_and_atomic_replacement() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let directory = + std::env::temp_dir().join(format!("i3bar-mango-{unique}-{}", std::process::id())); + let config_path = directory.join("config.toml"); + let replacement_path = directory.join("config.toml.new"); + fs::create_dir(&directory).unwrap(); + fs::write(&config_path, "height = 24").unwrap(); + + let mut watcher = ConfigWatcher::new(&config_path).unwrap(); + fs::write(&config_path, "height = 25").unwrap(); + assert!(watcher.changed().unwrap()); + + fs::write(&replacement_path, "height = 26").unwrap(); + fs::rename(&replacement_path, &config_path).unwrap(); + assert!(watcher.changed().unwrap()); + + fs::remove_dir_all(directory).unwrap(); + } +} |
