atagen

monolith

public
Star0
Watch0

Commit ad802fd23506

tarball

atagen <boss@atagen.co>2026-10-19 07:54 UTC

4 files changed+38-16
config: report reload warnings and duplicate binds

IPC reload answered Ok even when the config had warnings, so a typo only showed up in the log. Binds that normalise to the same chord, like Mod+h and Super+h, silently resolved to whichever sorted first. Reload now replies with Answer::Warnings when there are any and plain Ok otherwise, so old clients keep working.
Modified file: crates/ipc/src/lib.rs+1 additions
@@ -277,6 +277,7 @@ pub enum Reply {
277277 pub enum Answer {
278278 State(State),
279279 Version(String),
280+ Warnings(Vec<String>),
280281 }
281282
282283 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
Modified file: crates/monolith/src/commands.rs+28 additions−15 deletions
@@ -8,29 +8,39 @@ use smithay::reexports::calloop::{Interest, Mode, PostAction};
88 use std::os::fd::{FromRawFd, OwnedFd, RawFd};
99 use std::process::{Child, Stdio};
1010
11-pub struct Binds(Vec<(Mods, Keysym, Command)>);
11+pub struct Binds {
12+ binds: Vec<(Mods, Keysym, Command)>,
13+ pub warnings: Vec<String>,
14+}
1215
1316 impl Binds {
1417 pub fn new(config: &Config) -> Self {
15- let binds = config
16- .binds
17- .iter()
18- .filter_map(|b| {
19- let sym = xkb::keysym_from_name(&b.key, xkb::KEYSYM_CASE_INSENSITIVE);
20- if sym.raw() == xkb::keysyms::KEY_NoSymbol {
21- tracing::warn!("bind: unknown key {:?}", b.key);
22- return None;
23- }
24- Some((b.mods, sym, b.command.clone()))
25- })
26- .collect();
27- Self(binds)
18+ let mut warnings = Vec::new();
19+ let mut binds: Vec<(Mods, Keysym, Command)> = Vec::new();
20+ for b in &config.binds {
21+ let sym = xkb::keysym_from_name(&b.key, xkb::KEYSYM_CASE_INSENSITIVE);
22+ if sym.raw() == xkb::keysyms::KEY_NoSymbol {
23+ warnings.push(format!("bind: unknown key {:?}", b.key));
24+ continue;
25+ }
26+ if binds.iter().any(|(m, s, _)| *m == b.mods && *s == sym) {
27+ warnings.push(format!(
28+ "bind: {:?} with {:?} duplicates an earlier bind, which wins",
29+ b.key, b.mods
30+ ));
31+ }
32+ binds.push((b.mods, sym, b.command.clone()));
33+ }
34+ for w in &warnings {
35+ tracing::warn!("{w}");
36+ }
37+ Self { binds, warnings }
2838 }
2939
3040 /// Matches on the unshifted keysyms so `Mod+Shift+1` means the 1 key.
3141 pub fn find(&self, mods: &ModifiersState, syms: &[Keysym]) -> Option<Command> {
3242 let held = held(mods);
33- self.0
43+ self.binds
3444 .iter()
3545 .find(|(m, sym, _)| *m == held && syms.contains(sym))
3646 .map(|(_, _, c)| c.clone())
@@ -216,10 +226,13 @@ impl State {
216226 /// running config untouched.
217227 pub fn reload(&mut self) -> Result<(), String> {
218228 let loaded = Config::load(self.settings.path.as_deref()).map_err(|e| e.to_string())?;
229+ let mut warnings = loaded.warnings.clone();
219230 let config = warn_config(loaded);
220231 let keyboard_changed = config.keyboard != self.settings.config.keyboard;
221232 let input_changed = config.input != self.settings.config.input;
222233 self.settings.binds = Binds::new(&config);
234+ warnings.extend(self.settings.binds.warnings.iter().cloned());
235+ self.settings.warnings = warnings;
223236 self.wm.layout.gaps = crate::state::gaps(&config);
224237 self.wm.layout.autotile = config.general.autotile;
225238 self.wm.anim.settings = config.animation.clone();
Modified file: crates/monolith/src/ipc.rs+7 additions−1 deletions
@@ -104,7 +104,12 @@ fn handle_line(state: &mut State, stream: &UnixStream, line: &[u8]) -> Option<En
104104 return Some(Envelope::new(id, Reply::Error(msg)));
105105 }
106106 let reply = match req.body {
107- Request::Run(cmd) => match state.run(cmd) {
107+ Request::Run(cmd) => match state.run(cmd.clone()) {
108+ Ok(())
109+ if cmd == monolith_ipc::Command::Reload && !state.settings.warnings.is_empty() =>
110+ {
111+ Reply::Ok(Some(Answer::Warnings(state.settings.warnings.clone())))
112+ }
108113 Ok(()) => Reply::Ok(None),
109114 Err(e) => Reply::Error(e),
110115 },
@@ -245,6 +250,7 @@ fn talk(request: Request) -> ExitCode {
245250 Reply::Ok(None) => {}
246251 Reply::Ok(Some(answer)) => match answer {
247252 Answer::Version(v) => println!("{v}"),
253+ Answer::Warnings(ws) => ws.iter().for_each(|w| eprintln!("config: {w}")),
248254 Answer::State(s) => {
249255 println!("{}", serde_json::to_string_pretty(&s).unwrap_or_default())
250256 }
Modified file: crates/monolith/src/state.rs+2 additions
@@ -97,6 +97,7 @@ pub struct Settings {
9797 pub config: Config,
9898 pub path: Option<PathBuf>,
9999 pub binds: Binds,
100+ pub warnings: Vec<String>,
100101 }
101102
102103 impl State {
@@ -171,6 +172,7 @@ impl State {
171172 binds: Binds::new(&config),
172173 config,
173174 path: config_path,
175+ warnings: Vec::new(),
174176 },
175177 renderer,
176178 wm: Wm::new(layout, Animator::new(animation)),