srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/config/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/config/src')
-rw-r--r--crates/config/src/engine/general.rs25
-rw-r--r--crates/config/src/engine/layout.rs19
-rw-r--r--crates/config/src/engine/register.rs2
-rw-r--r--crates/config/src/engine/support.rs58
-rw-r--r--crates/config/src/engine/tests.rs78
5 files changed, 174 insertions, 8 deletions
diff --git a/crates/config/src/engine/general.rs b/crates/config/src/engine/general.rs
index 6276854..c10ecf0 100644
--- a/crates/config/src/engine/general.rs
+++ b/crates/config/src/engine/general.rs
@@ -79,7 +79,7 @@ impl Engine {
pub(super) fn fn_on(&self) -> Result<mlua::Function<'_>> {
let state = self.state.clone();
Ok(self.lua.create_function(move |lua, (name, f): (String, mlua::Function)| {
- const KNOWN: [&str; 3] = ["lid_closed", "lid_open", "ready"];
+ const KNOWN: [&str; 4] = ["lid_closed", "lid_open", "ready", "refresh"];
if !KNOWN.contains(&name.as_str()) {
return Err(mlua::Error::RuntimeError(format!(
"srd.on: unknown event '{name}' (known: {})",
@@ -327,6 +327,29 @@ impl Engine {
})?)
}
+ /// `srd.lock()` - locks the session, the same request the control
+ /// CLI's own lock dispatch sends over IPC.
+ ///
+ /// Native rather than left to a `srd.spawn(...)` shelling out to that
+ /// CLI: a lock binding that shells out fails silently when the binary
+ /// is not on the config's own `PATH`, which is exactly the situation a
+ /// user is least able to diagnose - the screen simply does not lock.
+ /// This goes straight to `WindowManager::request_lock`, the same flag
+ /// the IPC dispatch sets, so the backend's existing drain handles it
+ /// with no second code path.
+ ///
+ /// There was no way to bind the built-in lock screen from Lua at all
+ /// before this; the shipped config's only lock key ran an external
+ /// script. Asked directly: "do we have a lockscreen binding?".
+ pub(super) fn fn_lock(&self) -> Result<mlua::Function<'_>> {
+ let state = self.state.clone();
+ Ok(self.lua.create_function(move |_, ()| {
+ let wm = state.borrow().wm.clone();
+ wm.borrow_mut().request_lock();
+ Ok(())
+ })?)
+ }
+
pub(super) fn fn_reload(&self) -> Result<mlua::Function<'_>> {
let state = self.state.clone();
// `create_function`'s closure is handed the `&Lua` it's being
diff --git a/crates/config/src/engine/layout.rs b/crates/config/src/engine/layout.rs
index ebdb317..c0d923b 100644
--- a/crates/config/src/engine/layout.rs
+++ b/crates/config/src/engine/layout.rs
@@ -16,6 +16,25 @@ impl Engine {
})?)
}
+ /// `srd.layout.get()` - the name of the layout the *current workspace*
+ /// is using right now.
+ ///
+ /// Deliberately not `srd.get("workspace.layout")`, which reads the
+ /// config values table: that holds the configured default layout for
+ /// new workspaces, not what the workspace in front of you switched to
+ /// since. A toggle written against that key works exactly once and
+ /// then sticks, because the value it reads never changes.
+ pub(super) fn fn_layout_get(&self) -> Result<mlua::Function<'_>> {
+ let state = self.state.clone();
+ Ok(self.lua.create_function(move |lua, ()| {
+ let wm = state.borrow().wm.clone();
+ let wm = wm.borrow();
+ let ws = wm.current_workspace();
+ let name = wm.workspace(ws).map(|w| w.layout.clone()).unwrap_or_default();
+ Ok(Value::String(lua.create_string(&name)?))
+ })?)
+ }
+
pub(super) fn fn_layout_configure(&self) -> Result<mlua::Function<'_>> {
let state = self.state.clone();
Ok(self.lua.create_function(move |_, (name, table): (String, Table)| {
diff --git a/crates/config/src/engine/register.rs b/crates/config/src/engine/register.rs
index 4ef6844..77aede9 100644
--- a/crates/config/src/engine/register.rs
+++ b/crates/config/src/engine/register.rs
@@ -20,6 +20,7 @@ impl Engine {
srd.set("setenv", self.fn_setenv()?)?;
srd.set("notify", self.fn_notify()?)?;
srd.set("quit", self.fn_quit()?)?;
+ srd.set("lock", self.fn_lock()?)?;
srd.set("reload", self.fn_reload()?)?;
srd.set("validate_config", self.fn_validate_config()?)?;
@@ -65,6 +66,7 @@ impl Engine {
let layout = lua.create_table()?;
layout.set("set", self.fn_layout_set()?)?;
+ layout.set("get", self.fn_layout_get()?)?;
layout.set("configure", self.fn_layout_configure()?)?;
srd.set("layout", layout)?;
diff --git a/crates/config/src/engine/support.rs b/crates/config/src/engine/support.rs
index 5574bee..a2423d2 100644
--- a/crates/config/src/engine/support.rs
+++ b/crates/config/src/engine/support.rs
@@ -4,17 +4,58 @@ use super::*;
/// the closure can't capture `&Engine` itself (it isn't `Clone`/`Rc`, and
/// `mlua::Lua::create_function` needs a `'static` closure), so both go
/// through cloned `Lua`/state handles instead of one calling the other.
+/// Re-executes `init.lua`, and puts the previous config back if that fails.
+///
+/// The clear-then-execute order is required: a binding or handler deleted
+/// from the edited file has to actually disappear, which only clearing
+/// first achieves. The bug was that nothing ever undid the clear. A Lua
+/// syntax error - the single most likely thing to go wrong with a
+/// programmable config, and the thing a user is most likely to do by
+/// accident - left the compositor with **no keybindings at all**: not the
+/// old ones, not the new ones. The only key still working was the hardcoded
+/// reload combo `main.rs` handles before consulting Lua, which is the one
+/// key nobody thinks to press when their config has just stopped working,
+/// because nothing tells them that is the situation.
+///
+/// Now the three maps are moved out rather than cleared, and moved back on
+/// any failure, so a broken edit leaves the last *working* config running.
+/// That is the behaviour every mainstream programmable config has (tmux,
+/// Neovim, Hyprland): a bad reload is a no-op with an error, not a
+/// half-applied state.
+///
+/// Answers "what happens when our config fails/user does something wrong
+/// which can be expected since lua programmable config" - asked directly,
+/// and previously answered by the code with "you lose every keybinding".
pub(super) fn do_reload(lua: &Lua, state: &Rc<RefCell<SharedState>>) -> Result<()> {
- let config_dir = {
+ let (config_dir, previous) = {
let mut s = state.borrow_mut();
- s.key_bindings.clear();
- s.event_handlers.clear();
- s.repeat_keys.clear();
- s.config_dir.clone()
+ let previous = (
+ std::mem::take(&mut s.key_bindings),
+ std::mem::take(&mut s.event_handlers),
+ std::mem::take(&mut s.repeat_keys),
+ );
+ (s.config_dir.clone(), previous)
+ };
+ let restore = |state: &Rc<RefCell<SharedState>>, previous: (_, _, _)| {
+ let mut s = state.borrow_mut();
+ // Whatever the failed run managed to register before erroring is
+ // discarded, not merged: half of a broken config is not a config.
+ s.key_bindings = previous.0;
+ s.event_handlers = previous.1;
+ s.repeat_keys = previous.2;
};
let path = config_dir.join("init.lua");
- let src = std::fs::read_to_string(&path).map_err(|source| ConfigError::Io { path: path.clone(), source })?;
- lua.load(&src).set_name(path.to_string_lossy().as_ref()).exec()?;
+ let src = match std::fs::read_to_string(&path) {
+ Ok(src) => src,
+ Err(source) => {
+ restore(state, previous);
+ return Err(ConfigError::Io { path, source });
+ }
+ };
+ if let Err(e) = lua.load(&src).set_name(path.to_string_lossy().as_ref()).exec() {
+ restore(state, previous);
+ return Err(e.into());
+ }
Ok(())
}
@@ -138,6 +179,9 @@ pub(super) fn default_config() -> HashMap<String, ConfigValue> {
m.insert(k.to_string(), v);
};
set("general.default_layout", String("dynamic".into()));
+ // Re-read `init.lua` when it changes on disk, no reload key needed.
+ // See `main.rs`'s `config_mtime`/`CONFIG_POLL_INTERVAL`.
+ set("general.config_reload_on_write", Bool(true));
set("general.smart_placement", Bool(true));
set("general.window_gap", Number(8.0));
set("general.animations", Bool(true));
diff --git a/crates/config/src/engine/tests.rs b/crates/config/src/engine/tests.rs
index f4f011b..591c4c0 100644
--- a/crates/config/src/engine/tests.rs
+++ b/crates/config/src/engine/tests.rs
@@ -101,6 +101,84 @@
}
#[test]
+ fn a_reload_that_fails_leaves_the_previous_config_running() {
+ // The single most likely thing to go wrong with a programmable
+ // config: the user saves a syntax error. Before this, the clear
+ // that precedes re-execution was never undone, so a bad edit left
+ // the compositor with no keybindings at all.
+ let dir = tempfile::tempdir().unwrap();
+ std::fs::write(dir.path().join("init.lua"), r#"srd.bind("Mod4+t", function() end)"#).unwrap();
+ let engine = engine_in(dir.path());
+ engine.load_init().unwrap();
+ assert_eq!(engine.bound_keys().len(), 1, "the good config bound one key");
+
+ std::fs::write(dir.path().join("init.lua"), "this is not lua ((").unwrap();
+ assert!(engine.reload().is_err(), "a syntax error must be reported");
+ assert_eq!(engine.bound_keys().len(), 1, "the working config must survive a failed reload");
+ assert!(engine.dispatch_keybinding(&srdwm_core::canonicalize_key_combo("Mod4+t")), "and must still actually run");
+ }
+
+ #[test]
+ fn a_reload_whose_file_has_vanished_also_keeps_the_previous_config() {
+ let dir = tempfile::tempdir().unwrap();
+ std::fs::write(dir.path().join("init.lua"), r#"srd.bind("Mod4+t", function() end)"#).unwrap();
+ let engine = engine_in(dir.path());
+ engine.load_init().unwrap();
+ std::fs::remove_file(dir.path().join("init.lua")).unwrap();
+ assert!(engine.reload().is_err());
+ assert_eq!(engine.bound_keys().len(), 1);
+ }
+
+ #[test]
+ fn a_reload_that_succeeds_still_drops_bindings_removed_from_the_file() {
+ // The restore must not turn into "bindings are never cleared".
+ let dir = tempfile::tempdir().unwrap();
+ std::fs::write(dir.path().join("init.lua"), r#"srd.bind("Mod4+t", function() end) srd.bind("Mod4+y", function() end)"#).unwrap();
+ let engine = engine_in(dir.path());
+ engine.load_init().unwrap();
+ assert_eq!(engine.bound_keys().len(), 2);
+
+ std::fs::write(dir.path().join("init.lua"), r#"srd.bind("Mod4+t", function() end)"#).unwrap();
+ engine.reload().unwrap();
+ assert_eq!(engine.bound_keys().len(), 1, "a deleted binding must really go away");
+ }
+
+ #[test]
+ fn a_half_applied_broken_config_is_discarded_not_merged() {
+ // The failing file registers a binding and *then* errors. Keeping
+ // it would leave a config that is neither the old one nor the new.
+ let dir = tempfile::tempdir().unwrap();
+ std::fs::write(dir.path().join("init.lua"), r#"srd.bind("Mod4+t", function() end)"#).unwrap();
+ let engine = engine_in(dir.path());
+ engine.load_init().unwrap();
+
+ std::fs::write(dir.path().join("init.lua"), "srd.bind(\"Mod4+z\", function() end)\nerror(\"boom\")").unwrap();
+ assert!(engine.reload().is_err());
+ let keys = engine.bound_keys();
+ assert_eq!(keys.len(), 1);
+ assert!(!keys.iter().any(|k| k.contains('z')), "the broken run's own binding must not survive: {keys:?}");
+ }
+
+ #[test]
+ fn refresh_is_a_known_event_name() {
+ let dir = tempfile::tempdir().unwrap();
+ let engine = engine_in(dir.path());
+ engine.lua.load(r#"srd.on("refresh", function() srd.set("refreshed", true) end)"#).exec().unwrap();
+ assert!(engine.dispatch_event("refresh"), "a registered refresh handler must run");
+ assert_eq!(engine.get("refreshed"), Some(ConfigValue::Bool(true)));
+ }
+
+ #[test]
+ fn srd_lock_queues_a_lock_request_on_the_window_manager() {
+ let dir = tempfile::tempdir().unwrap();
+ let wm = Rc::new(RefCell::new(WindowManager::new()));
+ let engine = Engine::new(wm.clone(), dir.path()).unwrap();
+ assert!(!wm.borrow_mut().drain_lock_request(), "nothing queued yet");
+ engine.lua.load("srd.lock()").exec().unwrap();
+ assert!(wm.borrow_mut().drain_lock_request(), "srd.lock() must queue a real request");
+ }
+
+ #[test]
fn srd_load_executes_module_relative_to_config_dir() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("extra.lua"), r#"srd.set("from.extra", "yes")"#).unwrap();