//! The `srd` Lua scripting API: config values, keybindings, layout/theme //! setup, and window/workspace actions, all callable from `.lua` files. //! //! This targets the API surface documented in the legacy project's //! `docs/DEFAULTS.md` (see docs/PRIOR_ART.md for the full comparison), which //! is richer than what the C++ `lua_manager.cc` actually registered: that //! engine's `srd.window.focused()` returned a hardcoded placeholder table //! with no methods, even though the shipped example `keybindings.lua` called //! `window:close()` on it - a call that would have errored at runtime. Here, //! `srd.window.close()` / `.minimize()` / `.maximize()` / `.focus(direction)` //! act on the real focused window via a shared [`srdwm_core::WindowManager`], //! and `srd.bind` stores the actual Lua closure (via the registry) rather //! than just the key-combo string. use crate::value::ConfigValue; use support::{default_config, do_reload}; use mlua::{Lua, RegistryKey, Table, Value}; use srdwm_core::{Direction, Rect, WindowManager, WindowMatch, WindowRule, WindowRuleActions}; use std::cell::RefCell; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::rc::Rc; struct SharedState { wm: Rc>, values: HashMap, key_bindings: HashMap, /// Human-readable description for a binding, from `srd.bind`'s optional /// third argument. Separate from `key_bindings` so a binding without one /// costs nothing, and cleared alongside it on reload. /// /// Exists so a launcher or cheat-sheet can list what the keys actually /// do - a bare combo is close to useless in a UI. Published to /// `WindowManager` by `main.rs` and served over IPC as /// `srd keybindings`. key_descriptions: HashMap, /// Every key the *config* set via `srd.set`, as opposed to a seeded /// default. Cleared and rebuilt on each load, so it always describes /// the config currently in force. /// /// `values` alone cannot answer this: defaults are seeded into it /// before any script runs, so every key looks set. The distinction /// matters because a config file stating a value must be able to /// override a live `srd set` when it is reloaded - otherwise a /// setting changed at runtime can never be corrected from the config /// again. config_set_keys: std::collections::HashSet, /// Combos registered with `srd.bind_repeat`, which fire repeatedly while /// held (Hyprland's `binde`). A subset of `key_bindings`. repeat_keys: std::collections::HashSet, /// Handlers for non-key events (currently the lid switch), registered /// via `srd.on(...)`. Kept separate from `key_bindings` because the /// backends use that map to decide which *keypresses* to withhold from /// clients - a pseudo-entry there would be grabbed as if it were a key. event_handlers: HashMap, config_dir: PathBuf, log: Vec, running: Rc>, profile_start: Option, } /// Owns the Lua interpreter and the `srd` module state. Cheap to keep around /// for the process lifetime; `reload` re-executes `init.lua` from scratch /// against a fresh Lua state so stale closures/globals can't linger. pub struct Engine { lua: Lua, state: Rc>, } #[derive(thiserror::Error, Debug)] pub enum ConfigError { #[error("lua error: {0}")] Lua(#[from] mlua::Error), #[error("io error reading {path}: {source}")] Io { path: PathBuf, source: std::io::Error }, } pub type Result = std::result::Result; impl Engine { /// Builds a fresh interpreter wired to `wm` and loads defaults; call /// [`Engine::load_init`] to run the user's `init.lua`. pub fn new(wm: Rc>, config_dir: impl Into) -> Result { let lua = Lua::new(); let state = Rc::new(RefCell::new(SharedState { wm, values: default_config(), key_bindings: HashMap::new(), key_descriptions: HashMap::new(), config_set_keys: std::collections::HashSet::new(), repeat_keys: std::collections::HashSet::new(), event_handlers: HashMap::new(), config_dir: config_dir.into(), log: Vec::new(), running: Rc::new(std::cell::Cell::new(true)), profile_start: None, })); let engine = Self { lua, state }; engine.register_srd_module()?; Ok(engine) } /// Shared flag `srd.quit()` clears; the main loop polls this to know /// when to stop. pub fn running_flag(&self) -> Rc> { self.state.borrow().running.clone() } pub fn config_dir(&self) -> PathBuf { self.state.borrow().config_dir.clone() } /// Loads and executes `init.lua` from the config directory. pub fn load_init(&self) -> Result<()> { let path = self.config_dir().join("init.lua"); self.exec_file(&path) } /// Re-executes `init.lua` from scratch: clears keybindings, event /// handlers and the repeat-key set first, so a binding or handler /// removed from the edited config doesn't linger from the previous /// load. `values` (`srd.set` keys) are deliberately left alone -- /// `platform.backend`/`platform.os` are published once by `main.rs` /// before the *first* `load_init` and nothing in Lua ever re-sets them, /// so clearing `values` here would silently break every /// `if srd.get("platform.backend") == ...` branch in the reloaded /// config. /// /// Does *not* re-grab/re-register the reloaded key set with the /// platform backend - `main.rs` reads `bound_keys()` once, before /// connecting, to build the X11 `XGrabKey` list / Wayland intercept /// set. A binding whose *combo* is unchanged from startup picks up a /// reload immediately; a config that adds a brand new combo needs a /// real restart before the backend will ever hand that keypress to /// srdwm instead of the focused client. pub fn reload(&self) -> Result<()> { do_reload(&self.lua, &self.state) } pub fn exec_file(&self, path: &Path) -> Result<()> { let src = std::fs::read_to_string(path).map_err(|source| ConfigError::Io { path: path.to_path_buf(), source })?; self.lua.load(&src).set_name(path.to_string_lossy().as_ref()).exec()?; Ok(()) } pub fn get(&self, key: &str) -> Option { self.state.borrow().values.get(key).cloned() } pub fn get_string(&self, key: &str, default: &str) -> String { self.get(key).and_then(|v| v.as_str().map(str::to_string)).unwrap_or_else(|| default.to_string()) } pub fn get_bool(&self, key: &str, default: bool) -> bool { self.get(key).and_then(|v| v.as_bool()).unwrap_or(default) } pub fn get_f64(&self, key: &str, default: f64) -> f64 { self.get(key).and_then(|v| v.as_f64()).unwrap_or(default) } /// Sets a config value from Rust rather than Lua - used by `main.rs` /// to publish facts the *host* determined (which backend was picked, /// which OS this is) before `load_init` runs `init.lua`, so config /// files can read them back via `srd.get(key)` and branch on them /// (`if srd.get("platform.backend") == "wayland" then ... end`). /// Writing straight into `values` is the same thing `srd.set` does from /// the Lua side, just without going through the interpreter. pub fn set_string(&self, key: &str, value: impl Into) { self.state.borrow_mut().values.insert(key.to_string(), ConfigValue::String(value.into())); } /// Runs the Lua function bound to `combo` (e.g. `"Mod4+Return"`), if any. /// Returns `true` if a binding existed and ran without erroring. /// Runs the `srd.on(name, ...)` handler for a non-key event, if any. /// Returns false when nothing is registered, so callers can log it. pub fn dispatch_event(&self, name: &str) -> bool { let func = { let state = self.state.borrow(); state.event_handlers.get(name).and_then(|key| self.lua.registry_value::(key).ok()) }; match func { Some(f) => { if let Err(e) = f.call::<_, ()>(()) { log::error!("event handler '{name}' errored: {e}"); } true } None => false, } } pub fn dispatch_keybinding(&self, combo: &str) -> bool { let func = { let state = self.state.borrow(); state.key_bindings.get(combo).and_then(|key| self.lua.registry_value::(key).ok()) }; match func { Some(f) => { if let Err(e) = f.call::<_, ()>(()) { log::error!("keybinding '{combo}' errored: {e}"); } true } None => false, } } /// Keys the loaded config explicitly set - see `config_set_keys`. pub fn config_set_keys(&self) -> Vec { self.state.borrow().config_set_keys.iter().cloned().collect() } pub fn bound_keys(&self) -> Vec { self.state.borrow().key_bindings.keys().cloned().collect() } /// Every binding as `(combo, description)`, sorted by combo so the list /// is stable between calls - a UI listing them should not reshuffle on /// every refresh. `description` is empty when the binding did not give /// one. pub fn bound_keys_with_descriptions(&self) -> Vec<(String, String)> { let state = self.state.borrow(); let mut out: Vec<(String, String)> = state.key_bindings.keys().map(|combo| (combo.clone(), state.key_descriptions.get(combo).cloned().unwrap_or_default())).collect(); out.sort(); out } /// Combos that should auto-repeat while held. pub fn repeat_keys(&self) -> Vec { self.state.borrow().repeat_keys.iter().cloned().collect() } } mod general; mod layout; mod register; mod support; mod theme; mod window; mod workspace; #[cfg(test)] mod tests;