fix hot reloading /etc/niri/config.kdl (#1907)

* refactor config load logic, and properly watch the system config path

* move config creation to niri-config, and make the errors a bit nicer

notably, "error creating config" is now a cause for "error loading
config", instead of it being one error and then "error loading config:
no such file or directory". also, failure to load a config is now
printed as an error level diagnostic (because it is indeed an error, not
just a warning you can shrug off)

* refactor watcher tests; add some new ones

now they check for the file contents too! and i added some tests for
ConfigPath::Regular, including a messy one with many symlink swaps

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
This commit is contained in:
sodiboo
2025-08-05 15:27:28 +02:00
committed by GitHub
parent 5edd91d37b
commit 52c579d556
4 changed files with 585 additions and 278 deletions
+112 -6
View File
@@ -3,6 +3,8 @@ extern crate tracing;
use std::collections::HashSet;
use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::Write;
use std::ops::{Mul, MulAssign};
use std::path::{Path, PathBuf};
use std::str::FromStr;
@@ -2368,14 +2370,118 @@ pub enum PreviewRender {
ScreenCapture,
}
impl Config {
pub fn load(path: &Path) -> miette::Result<Self> {
let _span = tracy_client::span!("Config::load");
Self::load_internal(path).context("error loading config")
#[derive(Debug, Clone)]
pub enum ConfigPath {
/// Explicitly set config path.
///
/// Load the config only from this path, never create it.
Explicit(PathBuf),
/// Default config path.
///
/// Prioritize the user path, fallback to the system path, fallback to creating the user path
/// at compositor startup.
Regular {
/// User config path, usually `$XDG_CONFIG_HOME/niri/config.kdl`.
user_path: PathBuf,
/// System config path, usually `/etc/niri/config.kdl`.
system_path: PathBuf,
},
}
impl ConfigPath {
/// Load the config, or return an error if it doesn't exist.
pub fn load(&self) -> miette::Result<Config> {
let _span = tracy_client::span!("ConfigPath::load");
self.load_inner(|user_path, system_path| {
Err(miette::miette!(
"no config file found; create one at {user_path:?} or {system_path:?}",
))
})
.context("error loading config")
}
fn load_internal(path: &Path) -> miette::Result<Self> {
let contents = std::fs::read_to_string(path)
/// Load the config, or create it if it doesn't exist.
///
/// Returns a tuple containing the path that was created, if any, and the loaded config.
///
/// If the config was created, but for some reason could not be read afterwards,
/// this may return `(Some(_), Err(_))`.
pub fn load_or_create(&self) -> (Option<&Path>, miette::Result<Config>) {
let _span = tracy_client::span!("ConfigPath::load_or_create");
let mut created_at = None;
let result = self
.load_inner(|user_path, _| {
Self::create(user_path, &mut created_at)
.map(|()| user_path)
.with_context(|| format!("error creating config at {user_path:?}"))
})
.context("error loading config");
(created_at, result)
}
fn load_inner<'a>(
&'a self,
maybe_create: impl FnOnce(&'a Path, &'a Path) -> miette::Result<&'a Path>,
) -> miette::Result<Config> {
let path = match self {
ConfigPath::Explicit(path) => path.as_path(),
ConfigPath::Regular {
user_path,
system_path,
} => {
if user_path.exists() {
user_path.as_path()
} else if system_path.exists() {
system_path.as_path()
} else {
maybe_create(user_path.as_path(), system_path.as_path())?
}
}
};
Config::load(path)
}
fn create<'a>(path: &'a Path, created_at: &mut Option<&'a Path>) -> miette::Result<()> {
if let Some(default_parent) = path.parent() {
fs::create_dir_all(default_parent)
.into_diagnostic()
.with_context(|| format!("error creating config directory {default_parent:?}"))?;
}
// Create the config and fill it with the default config if it doesn't exist.
let mut new_file = match File::options()
.read(true)
.write(true)
.create_new(true)
.open(path)
{
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => return Ok(()),
res => res,
}
.into_diagnostic()
.with_context(|| format!("error opening config file at {path:?}"))?;
*created_at = Some(path);
let default = include_bytes!("../../resources/default-config.kdl");
new_file
.write_all(default)
.into_diagnostic()
.with_context(|| format!("error writing default config to {path:?}"))?;
Ok(())
}
}
impl Config {
pub fn load(path: &Path) -> miette::Result<Self> {
let contents = fs::read_to_string(path)
.into_diagnostic()
.with_context(|| format!("error reading {path:?}"))?;