Add the LoadConfigFile action (#2163)

* Add the `LoadConfigFile` action

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
This commit is contained in:
vanderlokken
2025-08-09 16:20:08 +04:00
committed by GitHub
parent f74d83dcca
commit 67361f88fd
6 changed files with 105 additions and 89 deletions
+3
View File
@@ -1924,6 +1924,8 @@ pub enum Action {
SetWindowUrgent(u64),
#[knuffel(skip)]
UnsetWindowUrgent(u64),
#[knuffel(skip)]
LoadConfigFile,
}
impl From<niri_ipc::Action> for Action {
@@ -2199,6 +2201,7 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::ToggleWindowUrgent { id } => Self::ToggleWindowUrgent(id),
niri_ipc::Action::SetWindowUrgent { id } => Self::SetWindowUrgent(id),
niri_ipc::Action::UnsetWindowUrgent { id } => Self::UnsetWindowUrgent(id),
niri_ipc::Action::LoadConfigFile {} => Self::LoadConfigFile,
}
}
}
+5
View File
@@ -840,6 +840,11 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Reload the config file.
///
/// Can be useful for scripts changing the config file, to avoid waiting the small duration for
/// niri's config file watcher to notice the changes.
LoadConfigFile {},
}
/// Change in window or column size.
+5
View File
@@ -2113,6 +2113,11 @@ impl State {
}
self.niri.queue_redraw_all();
}
Action::LoadConfigFile => {
if let Some(watcher) = &self.niri.config_file_watcher {
watcher.load_config();
}
}
}
}
+2 -23
View File
@@ -23,8 +23,7 @@ use niri::utils::spawning::{
spawn, store_and_increase_nofile_rlimit, CHILD_DISPLAY, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE,
REMOVE_ENV_RUST_LIB_BACKTRACE,
};
use niri::utils::watcher::Watcher;
use niri::utils::{cause_panic, version, xwayland, IS_SYSTEMD_SERVICE};
use niri::utils::{cause_panic, version, watcher, xwayland, IS_SYSTEMD_SERVICE};
use niri_config::ConfigPath;
use niri_ipc::socket::SOCKET_PATH_ENV;
use portable_atomic::Ordering;
@@ -230,27 +229,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
// Set up config file watcher.
let _watcher = {
// Parsing the config actually takes > 20 ms on my beefy machine, so let's do it on the
// watcher thread.
let process = |path: &ConfigPath| {
path.load().map_err(|err| {
warn!("{err:?}");
})
};
let (tx, rx) = calloop::channel::sync_channel(1);
let watcher = Watcher::new(config_path.clone(), process, tx);
event_loop
.handle()
.insert_source(rx, |event, _, state| match event {
calloop::channel::Event::Msg(config) => state.reload_config(config),
calloop::channel::Event::Closed => (),
})
.unwrap();
watcher
};
watcher::setup(&mut state, &config_path);
// Spawn commands from cli and auto-start.
spawn(cli.command, None);
+4
View File
@@ -164,6 +164,7 @@ use crate::ui::screen_transition::{self, ScreenTransition};
use crate::ui::screenshot_ui::{OutputScreenshot, ScreenshotUi, ScreenshotUiRenderElement};
use crate::utils::scale::{closest_representable_scale, guess_monitor_scale};
use crate::utils::spawning::{CHILD_DISPLAY, CHILD_ENV};
use crate::utils::watcher::Watcher;
use crate::utils::xwayland::satellite::Satellite;
use crate::utils::{
center, center_f64, expand_home, get_monotonic_time, ipc_transform_to_smithay, is_mapped,
@@ -190,6 +191,8 @@ pub struct Niri {
/// (and transient changes dropped).
pub config_file_output_config: niri_config::Outputs,
pub config_file_watcher: Option<Watcher>,
pub event_loop: LoopHandle<'static, State>,
pub scheduler: Scheduler<()>,
pub stop_signal: LoopSignal,
@@ -2528,6 +2531,7 @@ impl Niri {
let mut niri = Self {
config,
config_file_output_config,
config_file_watcher: None,
event_loop,
scheduler,
+86 -66
View File
@@ -1,22 +1,17 @@
//! File modification watcher.
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{mpsc, Arc};
use std::sync::mpsc;
use std::time::{Duration, SystemTime};
use std::{io, thread};
use niri_config::ConfigPath;
use smithay::reexports::calloop::channel::SyncSender;
pub struct Watcher {
should_stop: Arc<AtomicBool>,
}
use crate::niri::State;
impl Drop for Watcher {
fn drop(&mut self) {
self.should_stop.store(true, Ordering::SeqCst);
}
pub struct Watcher {
load_config: mpsc::Sender<()>,
}
impl Watcher {
@@ -36,79 +31,104 @@ impl Watcher {
started: Option<mpsc::SyncSender<()>>,
polling_interval: Duration,
) -> Self {
let should_stop = Arc::new(AtomicBool::new(false));
let (load_config, load_config_rx) = mpsc::channel();
{
let should_stop = should_stop.clone();
thread::Builder::new()
.name(format!("Filesystem Watcher for {config_path:?}"))
.spawn(move || {
// this "should" be as simple as storing the last seen mtime,
// and if the contents change without updating mtime, we ignore it.
//
// but that breaks if the config is a symlink, and its target
// changes but the new target and old target have identical mtimes.
// in which case we should *not* ignore it; this is an entirely different file.
//
// in practice, this edge case does not occur on systems other than nix.
// because, on nix, everything is a symlink to /nix/store
// and /nix/store keeps no mtime (= 1970-01-01)
// so, symlink targets change frequently when mtime doesn't.
//
// therefore, we must also store the canonical path, along with its mtime
thread::Builder::new()
.name(format!("Filesystem Watcher for {config_path:?}"))
.spawn(move || {
// this "should" be as simple as storing the last seen mtime,
// and if the contents change without updating mtime, we ignore it.
//
// but that breaks if the config is a symlink, and its target
// changes but the new target and old target have identical mtimes.
// in which case we should *not* ignore it; this is an entirely different file.
//
// in practice, this edge case does not occur on systems other than nix.
// because, on nix, everything is a symlink to /nix/store
// and /nix/store keeps no mtime (= 1970-01-01)
// so, symlink targets change frequently when mtime doesn't.
//
// therefore, we must also store the canonical path, along with its mtime
fn see_path(path: &Path) -> io::Result<(SystemTime, PathBuf)> {
let canon = path.canonicalize()?;
let mtime = canon.metadata()?.modified()?;
Ok((mtime, canon))
fn see_path(path: &Path) -> io::Result<(SystemTime, PathBuf)> {
let canon = path.canonicalize()?;
let mtime = canon.metadata()?.modified()?;
Ok((mtime, canon))
}
fn see(config_path: &ConfigPath) -> io::Result<(SystemTime, PathBuf)> {
match config_path {
ConfigPath::Explicit(path) => see_path(path),
ConfigPath::Regular {
user_path,
system_path,
} => see_path(user_path).or_else(|_| see_path(system_path)),
}
}
fn see(config_path: &ConfigPath) -> io::Result<(SystemTime, PathBuf)> {
match config_path {
ConfigPath::Explicit(path) => see_path(path),
ConfigPath::Regular {
user_path,
system_path,
} => see_path(user_path).or_else(|_| see_path(system_path)),
}
}
let mut last_props = see(&config_path).ok();
let mut last_props = see(&config_path).ok();
if let Some(started) = started {
let _ = started.send(());
}
if let Some(started) = started {
let _ = started.send(());
}
loop {
let mut should_load = match load_config_rx.recv_timeout(polling_interval) {
Ok(()) => true,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
Err(mpsc::RecvTimeoutError::Timeout) => false,
};
loop {
thread::sleep(polling_interval);
if should_stop.load(Ordering::SeqCst) {
break;
if let Ok(new_props) = see(&config_path) {
if last_props.as_ref() != Some(&new_props) {
last_props = Some(new_props);
trace!("config file changed");
should_load = true;
}
if let Ok(new_props) = see(&config_path) {
if last_props.as_ref() != Some(&new_props) {
trace!("config file changed");
if should_load {
let rv = process(&config_path);
let rv = process(&config_path);
if let Err(err) = changed.send(rv) {
warn!("error sending change notification: {err:?}");
break;
}
last_props = Some(new_props);
if let Err(err) = changed.send(rv) {
warn!("error sending change notification: {err:?}");
break;
}
}
}
}
debug!("exiting watcher thread for {config_path:?}");
})
.unwrap();
}
debug!("exiting watcher thread for {config_path:?}");
})
.unwrap();
Self { should_stop }
Self { load_config }
}
pub fn load_config(&self) {
let _ = self.load_config.send(());
}
}
pub fn setup(state: &mut State, config_path: &ConfigPath) {
// Parsing the config actually takes > 20 ms on my beefy machine, so let's do it on the
// watcher thread.
let process = |path: &ConfigPath| {
path.load().map_err(|err| {
warn!("{err:?}");
})
};
let (tx, rx) = calloop::channel::sync_channel(1);
state
.niri
.event_loop
.insert_source(rx, |event, _, state| match event {
calloop::channel::Event::Msg(config) => state.reload_config(config),
calloop::channel::Event::Closed => (),
})
.unwrap();
state.niri.config_file_watcher = Some(Watcher::new(config_path.clone(), process, tx));
}
#[cfg(test)]