srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/srd/keybindings.lua56
-rw-r--r--crates/config/src/engine/general.rs8
-rw-r--r--crates/config/src/engine/mod.rs22
-rw-r--r--crates/config/src/engine/support.rs4
-rw-r--r--crates/core/src/manager/mod.rs12
-rw-r--r--crates/ctl/src/main.rs8
-rw-r--r--crates/platform/src/ipc/dispatch.rs11
-rw-r--r--crates/platform/src/ipc/types.rs14
-rw-r--r--crates/srdwm/src/main.rs14
-rw-r--r--docs/DEFAULTS.md27
10 files changed, 144 insertions, 32 deletions
diff --git a/config/srd/keybindings.lua b/config/srd/keybindings.lua
index aa3e425..dc315e9 100644
--- a/config/srd/keybindings.lua
+++ b/config/srd/keybindings.lua
@@ -21,25 +21,25 @@ srd.bind("Mod4+3", function()
end)
- Window management (act on the focused window)
-srd.bind("Mod4+q", function() srd.window.close() end)
-srd.bind("Mod4+m", function() srd.window.minimize() end)
-srd.bind("Mod4+f", function() srd.window.maximize() end)
-srd.bind("Mod4+Shift+space", function() srd.window.toggle_floating() end)
+srd.bind("Mod4+q", function() srd.window.close() end, "Close the focused window")
+srd.bind("Mod4+m", function() srd.window.minimize() end, "Minimize the focused window")
+srd.bind("Mod4+f", function() srd.window.maximize() end, "Maximize or restore the focused window")
+srd.bind("Mod4+Shift+space", function() srd.window.toggle_floating() end, "Float or unfloat the focused window")
- Window navigation (vim-style directional focus)
-srd.bind("Mod4+h", function() srd.window.focus("left") end)
-srd.bind("Mod4+j", function() srd.window.focus("down") end)
-srd.bind("Mod4+k", function() srd.window.focus("up") end)
-srd.bind("Mod4+l", function() srd.window.focus("right") end)
+srd.bind("Mod4+h", function() srd.window.focus("left") end, "Focus the window to the left")
+srd.bind("Mod4+j", function() srd.window.focus("down") end, "Focus the window to the down")
+srd.bind("Mod4+k", function() srd.window.focus("up") end, "Focus the window to the up")
+srd.bind("Mod4+l", function() srd.window.focus("right") end, "Focus the window to the right")
- Move the focused window in a direction, mirroring the focus keys above.
- `srd.window.move` has existed for a long time and simply had no default
- binding, so "move window to absolute directions" was unreachable without
- writing your own config.
-srd.bind("Mod4+Shift+h", function() srd.window.move("left") end)
-srd.bind("Mod4+Shift+j", function() srd.window.move("down") end)
-srd.bind("Mod4+Shift+k", function() srd.window.move("up") end)
-srd.bind("Mod4+Shift+l", function() srd.window.move("right") end)
+srd.bind("Mod4+Shift+h", function() srd.window.move("left") end, "Move the window left")
+srd.bind("Mod4+Shift+j", function() srd.window.move("down") end, "Move the window down")
+srd.bind("Mod4+Shift+k", function() srd.window.move("up") end, "Move the window up")
+srd.bind("Mod4+Shift+l", function() srd.window.move("right") end, "Move the window right")
- Layout switching. srdwm is dynamic-first: "dynamic" is free placement,
- the default, and tiling is one opt-in layout among several. Mod4+s
@@ -50,30 +50,30 @@ srd.bind("Mod4+s", function()
else
srd.layout.set("tiling")
end
-end)
+end, "Toggle tiling and dynamic layout")
- Lock the session using srdwm's own built-in lock screen. `srd.lock()`
- talks to the compositor directly rather than shelling out to the control
- CLI, so it still works when that binary is not on PATH.
-srd.bind("Mod4+Ctrl+l", function() srd.lock() end)
+srd.bind("Mod4+Ctrl+l", function() srd.lock() end, "Lock the session")
- Workspace management
-srd.bind("Mod4+Tab", function() srd.workspace.next() end)
-srd.bind("Mod4+Shift+Tab", function() srd.workspace.prev() end)
+srd.bind("Mod4+Tab", function() srd.workspace.next() end, "Next workspace")
+srd.bind("Mod4+Shift+Tab", function() srd.workspace.prev() end, "Previous workspace")
- Workspace switching with number keys (0 doubles as "workspace 10")
for i = 0, 9 do
- srd.bind("Mod4+" .. i, function() srd.workspace.switch(i) end)
- srd.bind("Mod4+Shift+" .. i, function() srd.workspace.move_window(i) end)
+ srd.bind("Mod4+" .. i, function() srd.workspace.switch(i) end, "Switch to workspace " .. i)
+ srd.bind("Mod4+Shift+" .. i, function() srd.workspace.move_window(i) end, "Move window to workspace " .. i)
end
- Quick actions
-srd.bind("Mod4+d", function() srd.spawn("rofi -show drun") end)
-srd.bind("Mod4+Return", function() srd.spawn("alacritty") end)
-srd.bind("Mod4+r", function() srd.spawn("rofi -show run") end)
+srd.bind("Mod4+d", function() srd.spawn("rofi -show drun") end, "Application launcher")
+srd.bind("Mod4+Return", function() srd.spawn("alacritty") end, "Open a terminal")
+srd.bind("Mod4+r", function() srd.spawn("rofi -show run") end, "Run a command")
- System
-srd.bind("Mod4+Shift+q", function() srd.quit() end)
+srd.bind("Mod4+Shift+q", function() srd.quit() end, "Quit srdwm")
- Custom function example: toggle window gaps at runtime
local function toggle_gaps()
@@ -85,13 +85,13 @@ local function toggle_gaps()
srd.notify("Gaps: On", "info")
end
end
-srd.bind("Mod4+g", toggle_gaps)
+srd.bind("Mod4+g", toggle_gaps, "Toggle window gaps")
- Media keys
-srd.bind("XF86AudioRaiseVolume", function() srd.spawn("pactl set-sink-volume @DEFAULT_SINK@ +5%") end)
-srd.bind("XF86AudioLowerVolume", function() srd.spawn("pactl set-sink-volume @DEFAULT_SINK@ -5%") end)
-srd.bind("XF86AudioMute", function() srd.spawn("pactl set-sink-mute @DEFAULT_SINK@ toggle") end)
-srd.bind("XF86MonBrightnessUp", function() srd.spawn("brightnessctl set +5%") end)
-srd.bind("XF86MonBrightnessDown", function() srd.spawn("brightnessctl set 5%-") end)
+srd.bind("XF86AudioRaiseVolume", function() srd.spawn("pactl set-sink-volume @DEFAULT_SINK@ +5%") end, "Volume up")
+srd.bind("XF86AudioLowerVolume", function() srd.spawn("pactl set-sink-volume @DEFAULT_SINK@ -5%") end, "Volume down")
+srd.bind("XF86AudioMute", function() srd.spawn("pactl set-sink-mute @DEFAULT_SINK@ toggle") end, "Mute")
+srd.bind("XF86MonBrightnessUp", function() srd.spawn("brightnessctl set +5%") end, "Brightness up")
+srd.bind("XF86MonBrightnessDown", function() srd.spawn("brightnessctl set 5%-") end, "Brightness down")
print("Key bindings configuration loaded")
diff --git a/crates/config/src/engine/general.rs b/crates/config/src/engine/general.rs
index c8a136f..68b441e 100644
--- a/crates/config/src/engine/general.rs
+++ b/crates/config/src/engine/general.rs
@@ -109,7 +109,7 @@ impl Engine {
pub(super) fn fn_bind(&self) -> Result<mlua::Function<'_>> {
let state = self.state.clone();
- Ok(self.lua.create_function(move |lua, (combo, f): (String, mlua::Function)| {
+ Ok(self.lua.create_function(move |lua, (combo, f, description): (String, mlua::Function, Option<String>)| {
// `key_bindings` is keyed by whatever string dispatch builds
// from a real keypress (`srdwm_core::key_combo_string`, fixed
// Ctrl/Shift/Alt/Mod4 order) - storing the config's own
@@ -119,7 +119,11 @@ impl Engine {
// grabbed/intercepted. See `parse_key_combo`'s doc comment.
let combo = srdwm_core::canonicalize_key_combo(&combo);
let key = lua.create_registry_value(f)?;
- state.borrow_mut().key_bindings.insert(combo, key);
+ let mut s = state.borrow_mut();
+ if let Some(description) = description.filter(|d| !d.trim().is_empty()) {
+ s.key_descriptions.insert(combo.clone(), description);
+ }
+ s.key_bindings.insert(combo, key);
Ok(())
})?)
}
diff --git a/crates/config/src/engine/mod.rs b/crates/config/src/engine/mod.rs
index ac7bdb8..8facd56 100644
--- a/crates/config/src/engine/mod.rs
+++ b/crates/config/src/engine/mod.rs
@@ -26,6 +26,15 @@ 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>,
@@ -67,6 +76,7 @@ impl Engine {
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(),
@@ -189,6 +199,18 @@ impl Engine {
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()
diff --git a/crates/config/src/engine/support.rs b/crates/config/src/engine/support.rs
index 76bcf5e..9d81035 100644
--- a/crates/config/src/engine/support.rs
+++ b/crates/config/src/engine/support.rs
@@ -33,16 +33,18 @@ pub(super) fn do_reload(lua: &Lua, state: &Rc<RefCell<SharedState>>) -> Result<(
std::mem::take(&mut s.key_bindings),
std::mem::take(&mut s.event_handlers),
std::mem::take(&mut s.repeat_keys),
+ std::mem::take(&mut s.key_descriptions),
);
(s.config_dir.clone(), previous)
};
- let restore = |state: &Rc<RefCell<SharedState>>, 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;
+ s.key_descriptions = previous.3;
};
let path = config_dir.join("init.lua");
let src = match std::fs::read_to_string(&path) {
diff --git a/crates/core/src/manager/mod.rs b/crates/core/src/manager/mod.rs
index 2ca2bf9..a225220 100644
--- a/crates/core/src/manager/mod.rs
+++ b/crates/core/src/manager/mod.rs
@@ -439,6 +439,17 @@ pub struct WindowManager {
/// above it. A top bar's own reservation is always honoured either
/// way. Default `true` - see `input::layers::maximize_geometry_for`.
pub maximize_covers_dock: bool,
+ /// Every key binding as `(combo, description)`, published by the main
+ /// loop after the config loads and after every reload.
+ ///
+ /// Core neither owns nor interprets these - it has no Lua state and
+ /// never dispatches a key itself. It holds them so the IPC layer, which
+ /// is handed a `WindowManager` and nothing else, can serve them to a
+ /// panel or launcher that wants to show the user what their keys do.
+ /// Asked for as whether "our bindings show in ags's launcher": they
+ /// could not, because nothing published them anywhere a client could
+ /// read.
+ pub keybindings: Vec<(String, String)>,
/// Set by `request_refresh`, drained by the main loop. Same
/// cross-boundary queued-request shape as `lock_requested`.
refresh_requested: bool,
@@ -628,6 +639,7 @@ impl WindowManager {
auto_raise: false,
theme: ThemeConfig::default(),
lock: LockConfig::default(),
+ keybindings: Vec::new(),
maximize_covers_dock: true,
refresh_requested: false,
live_settings: std::collections::BTreeMap::new(),
diff --git a/crates/ctl/src/main.rs b/crates/ctl/src/main.rs
index 4689660..1153611 100644
--- a/crates/ctl/src/main.rs
+++ b/crates/ctl/src/main.rs
@@ -7,6 +7,7 @@
//! Usage:
//! srd clients list windows, one JSON object
//! srd workspaces list workspaces, one JSON object
+//! srd keybindings list key bindings and descriptions
//! srd settings current shadows/rounded_corners/
//! animations/night_light/reading_mode
//! state, one JSON object
@@ -144,6 +145,10 @@ fn build_request(args: &[String]) -> Result<String, String> {
Some("clients") => Ok(r#"{"cmd":"clients"}"#.to_string()),
Some("monitors") => Ok(r#"{"cmd":"monitors"}"#.to_string()),
Some("workspaces") => Ok(r#"{"cmd":"workspaces"}"#.to_string()),
+ // Every binding the loaded config registered, with its description
+ // if it gave one - what a launcher or cheat-sheet needs to show
+ // the user their own keys.
+ Some("keybindings") => Ok(r#"{"cmd":"keybindings"}"#.to_string()),
Some("settings") => Ok(r#"{"cmd":"settings"}"#.to_string()),
Some("pinned") if args.get(1).map(String::as_str) == Some("inputs") => Ok(r#"{"cmd":"pinned_inputs"}"#.to_string()),
Some("pinned") => Err("did you mean 'srd pinned inputs'?".to_string()),
@@ -221,7 +226,7 @@ fn build_request(args: &[String]) -> Result<String, String> {
Ok(format!(r#"{{"cmd":"set","key":"{key}","value":{value}}}"#))
}
_ => Err(
- "expected 'clients', 'monitors', 'workspaces', 'settings', 'keyboard layout', 'subscribe', 'dispatch <action> <id>', 'capture workspace <id> <path>', or 'set <key> <value>'"
+ "expected 'clients', 'monitors', 'workspaces', 'keybindings', 'settings', 'keyboard layout', 'subscribe', 'dispatch <action> <id>', 'capture workspace <id> <path>', or 'set <key> <value>'"
.to_string(),
),
}
@@ -409,6 +414,7 @@ fn print_usage() {
eprintln!(" srd clients");
eprintln!(" srd monitors");
eprintln!(" srd workspaces");
+ eprintln!(" srd keybindings");
eprintln!(" srd settings");
eprintln!(" srd pinned inputs");
eprintln!(" srd keyboard layout");
diff --git a/crates/platform/src/ipc/dispatch.rs b/crates/platform/src/ipc/dispatch.rs
index 749b2aa..ad79b98 100644
--- a/crates/platform/src/ipc/dispatch.rs
+++ b/crates/platform/src/ipc/dispatch.rs
@@ -417,6 +417,17 @@ pub(crate) fn handle_request(line: &[u8], wm: &std::rc::Rc<std::cell::RefCell<Wi
// `redraw_decoration`/`apply_geometry` for every visible window
// unconditionally - this only has to mutate the right field and
// let that existing machinery do the rest.
+ // `{"cmd":"keybindings"}` - every binding the loaded config
+ // registered, as `(combo, description)`. Read-only, and the one
+ // way a panel or launcher can show the user what their own keys
+ // do: `srd.bind` lives entirely in the Lua engine, so nothing
+ // outside this process could see a binding at all before this.
+ "keybindings" => {
+ let wm = wm.borrow();
+ let list: Vec<KeybindingInfo> =
+ wm.keybindings.iter().map(|(combo, description)| KeybindingInfo { combo: combo.clone(), description: description.clone() }).collect();
+ (serde_json::to_vec(&KeybindingsResponse { keybindings: list }).unwrap_or_default(), false)
+ }
"set" => handle_set(&req, wm),
_ => (err("unknown command"), false),
}
diff --git a/crates/platform/src/ipc/types.rs b/crates/platform/src/ipc/types.rs
index f9af26c..286df20 100644
--- a/crates/platform/src/ipc/types.rs
+++ b/crates/platform/src/ipc/types.rs
@@ -122,6 +122,20 @@ pub(crate) struct MonitorsResponse {
pub(crate) monitors: Vec<MonitorInfo>,
}
+/// One key binding, for `srd keybindings`. `description` is empty when the
+/// binding did not give one - an empty string rather than `null`, so a
+/// consumer can render it without a branch.
+#[derive(Serialize)]
+pub(crate) struct KeybindingInfo {
+ pub(crate) combo: String,
+ pub(crate) description: String,
+}
+
+#[derive(Serialize)]
+pub(crate) struct KeybindingsResponse {
+ pub(crate) keybindings: Vec<KeybindingInfo>,
+}
+
#[derive(Serialize)]
pub(crate) struct WorkspacesResponse {
pub(crate) workspaces: Vec<WorkspaceInfo>,
diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs
index 5de7ace..85e5427 100644
--- a/crates/srdwm/src/main.rs
+++ b/crates/srdwm/src/main.rs
@@ -104,6 +104,16 @@ const RELOAD_COMBO_LITERAL: &str = "Mod4+Ctrl+r";
/// `general.config_reload_on_write` is on. See `config_mtime`.
const CONFIG_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
+/// Copies the loaded config's key bindings into the `WindowManager`, where
+/// the IPC layer can serve them (`srd keybindings`).
+///
+/// Called after the initial load and after every reload, so a rebound key
+/// shows up without a restart - unlike the *grab*, which `main.rs` only
+/// registers once at startup (see `Engine::reload`'s own doc comment).
+fn publish_keybindings(engine: &Engine, wm: &Rc<RefCell<WindowManager>>) {
+ wm.borrow_mut().keybindings = engine.bound_keys_with_descriptions();
+}
+
/// Puts a config error in front of the user instead of only in a log they
/// have no reason to be reading.
///
@@ -698,6 +708,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
report_config_error(&format!("Config failed to load, using built-in defaults.\n{e}"));
}
}
+ publish_keybindings(&engine, &wm);
apply_workspace_count(&engine, &wm);
apply_general_settings(&engine, &wm);
apply_default_layout(&engine, &wm);
@@ -803,6 +814,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Err(e) => report_config_error(&format!("Config edit not applied, keeping the last working one.\n{e}")),
}
apply_general_settings(&engine, &wm);
+ publish_keybindings(&engine, &wm);
// After `apply_general_settings`, which rebuilds the
// theme from the config file - see
// `WindowManager::live_settings` for why a hand-made
@@ -825,6 +837,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Err(e) => report_config_error(&format!("Config reload failed, keeping the last working one.\n{e}")),
}
apply_general_settings(&engine, &wm);
+ publish_keybindings(&engine, &wm);
srdwm_platform::replay_live_settings(&wm);
// After the reload, so a handler edited in the config since
// startup is the one that runs.
@@ -842,6 +855,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Err(e) => report_config_error(&format!("Config reload failed, keeping the last working one.\n{e}")),
}
apply_general_settings(&engine, &wm);
+ publish_keybindings(&engine, &wm);
srdwm_platform::replay_live_settings(&wm);
} else if !engine.dispatch_keybinding(&combo) {
log::debug!("no binding for '{combo}'");
diff --git a/docs/DEFAULTS.md b/docs/DEFAULTS.md
index 2cc1614..abcdad1 100644
--- a/docs/DEFAULTS.md
+++ b/docs/DEFAULTS.md
@@ -935,3 +935,30 @@ make an app forget where it lives.
Dialogs are centred instead, and take neither a remembered position nor a
remembered size - window memory is keyed by `app_id`, which a dialog shares
with the window that spawned it.
+
+## Listing key bindings
+
+`srd keybindings` returns every binding the loaded config registered, with a
+description if it gave one:
+
+```
+{"keybindings":[{"combo":"Mod4+Return","description":"Open a terminal"}, ...]}
+```
+
+`srd.bind` takes an optional third argument for that description:
+
+```lua
+srd.bind("Mod4+Return", function() srd.spawn("alacritty") end, "Open a terminal")
+```
+
+Bindings live entirely inside the Lua engine, so before this nothing outside
+the compositor could see that a binding existed at all - a launcher or
+cheat-sheet had no way to show the user their own keys. The list is
+republished after every config reload, so a rebound key appears without a
+restart. Note that a brand new *combination* still needs a restart before the
+compositor grabs that key at all; an existing combination picks up its new
+action immediately.
+
+Combos are reported in canonical order (`Ctrl+Mod4+l`), which is what the
+compositor matches a real keypress against - not necessarily how the combo
+was written in the config.