1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
|
//! 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<RefCell<WindowManager>>,
values: HashMap<String, ConfigValue>,
key_bindings: HashMap<String, RegistryKey>,
/// 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<String, String>,
/// Combos registered with `srd.bind_repeat`, which fire repeatedly while
/// held (Hyprland's `binde`). A subset of `key_bindings`.
repeat_keys: std::collections::HashSet<String>,
/// 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<String, RegistryKey>,
config_dir: PathBuf,
log: Vec<String>,
running: Rc<std::cell::Cell<bool>>,
profile_start: Option<std::time::Instant>,
}
/// 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<RefCell<SharedState>>,
}
#[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<T> = std::result::Result<T, ConfigError>;
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<RefCell<WindowManager>>, config_dir: impl Into<PathBuf>) -> Result<Self> {
let lua = Lua::new();
let state = Rc::new(RefCell::new(SharedState {
wm,
values: default_config(),
key_bindings: HashMap::new(),
key_descriptions: HashMap::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<std::cell::Cell<bool>> {
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<ConfigValue> {
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<String>) {
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::<mlua::Function>(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::<mlua::Function>(key).ok())
};
match func {
Some(f) => {
if let Err(e) = f.call::<_, ()>(()) {
log::error!("keybinding '{combo}' errored: {e}");
}
true
}
None => false,
}
}
pub fn bound_keys(&self) -> Vec<String> {
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<String> {
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;
|