Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: load keybindings from input.kdl domains; ccectl migrate-input
Keybinds now load in priority order: input.kdl's cce-window-manager
domain (names/chords resolved via the policy crate's bindings module),
then legacy config.kdl key_bindings + window_manager section, then the
policy crate's DEFAULT_BINDINGS — replacing ~95 lines of hardcoded
default pushes. Invalid chords, unknown action names, and unknown
keysyms are warned about and skipped instead of silently misbinding.
config::Keybind is now a re-export of cce_window_manager::bindings::Binding.
ccectl migrate-input (local, no compositor) extracts keybindings from
config.kdl into input.kdl: root and input-section key_bindings nodes,
the window_manager section, and the list/tree search-key props (to the
cce-ui domain). Existing input.kdl entries win, the file is backed up
first, config.kdl is never rewritten, and the run is idempotent.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PXSsCppeDFmSRRM5NAug5a
src/cce_ctl.rs | 7 ++
src/lib.rs | 3 +
src/migrate_input.rs | 329 +++++++++++++++++++++++++++++++++++++++++++++++++++
src/server/config.rs | 253 +++++++++++++--------------------------
4 files changed, 423 insertions(+), 169 deletions(-)
diff --git a/src/cce_ctl.rs b/src/cce_ctl.rs
index 052d54e..b37d27b 100644
--- a/src/cce_ctl.rs
+++ b/src/cce_ctl.rs
@@ -77,6 +77,7 @@ fn usage(name: &str, to_stderr: bool) {
print(" pointer-click [button] (left|right|middle|back|forward or evdev code)");
print(" pointer-press [button] (held until pointer-release — drives drags)");
print(" pointer-release [button]");
+ print(" migrate-input (local: move config.kdl keybindings to input.kdl)");
print(" keypress <keycode> (evdev code; press+release to the focused client)");
print(" key-down <keycode> (modifier codes — ctrl 29/97, shift 42/54,");
print(" key-up <keycode> alt 56/100, super 125/126 — update client");
@@ -93,6 +94,12 @@ pub fn run_cce_ctl(args: Vec<String>) {
usage(&args[0], false);
return;
}
+
+ // Local file operation — no compositor needed.
+ if args[1] == "migrate-input" {
+ crate::migrate_input::run();
+ return;
+ }
// Connect to IPC socket
diff --git a/src/lib.rs b/src/lib.rs
index 85876af..36e6765 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -109,3 +109,6 @@ pub use run_server::run_server;
#[path = "cce_ctl.rs"]
pub mod cce_ctl;
pub use cce_ctl::run_cce_ctl;
+
+#[path = "migrate_input.rs"]
+pub mod migrate_input;
diff --git a/src/migrate_input.rs b/src/migrate_input.rs
new file mode 100644
index 0000000..9d8d9c8
--- /dev/null
+++ b/src/migrate_input.rs
@@ -0,0 +1,329 @@
+// `ccectl migrate-input` — one-time extraction of keybindings from
+// config.kdl into the domain-scoped input.kdl.
+//
+// Reads only; config.kdl is never rewritten. Extracted bindings are merged
+// into input.kdl (existing entries win), the previous input.kdl is backed up
+// under ~/.config/cce/backups/, and a summary tells the user which config.kdl
+// entries are now redundant and can be deleted by hand.
+//
+// What gets extracted:
+// - root-level / block-form `key_bindings` nodes → `cce-window-manager`
+// - the `window_manager` section → `cce-window-manager`
+// - `style.data.list/tree` search-key props → `cce-ui`
+//
+// The brightness spawn binds synthesized from the `display` section are NOT
+// migrated — they stay derived from the display config at load time.
+
+use cce_ui::input::{BindingEntry, InputConfig, UI_DOMAIN, WINDOW_MANAGER_DOMAIN};
+use cce_window_manager::api::Action;
+use cce_window_manager::bindings::parse_chord;
+
+pub struct Extracted {
+ pub wm: Vec<BindingEntry>,
+ pub ui: Vec<BindingEntry>,
+ pub warnings: Vec<String>,
+}
+
+fn prop_string(node: &kdl::KdlNode, key: &str) -> Option<String> {
+ node.entries()
+ .iter()
+ .find(|e| e.name().map(|n| n.value()) == Some(key))
+ .and_then(|e| e.value().as_string().map(str::to_string))
+}
+
+fn first_arg_string(node: &kdl::KdlNode) -> Option<String> {
+ node.entries()
+ .iter()
+ .find(|e| e.name().is_none())
+ .and_then(|e| e.value().as_string().map(str::to_string))
+}
+
+fn child<'a>(doc: &'a kdl::KdlDocument, name: &str) -> Option<&'a kdl::KdlNode> {
+ doc.nodes().iter().find(|n| n.name().value() == name)
+}
+
+/// One legacy key_bindings entry (flat node or block child) → a wm-domain
+/// BindingEntry, validated against the policy crate's vocabulary.
+fn convert_key_binding(node: &kdl::KdlNode, out: &mut Extracted) {
+ let mods = prop_string(node, "mods").unwrap_or_default();
+ let key = prop_string(node, "key").unwrap_or_default();
+ if key.is_empty() {
+ out.warnings.push(format!("key_bindings entry without key= skipped: {}", node));
+ return;
+ }
+ let chord = if mods.is_empty() { key } else { format!("{}+{}", mods, key) };
+ let action = prop_string(node, "action").unwrap_or_default();
+
+ if Action::from_name(&action).is_none() {
+ out.warnings.push(format!("unknown action {:?} skipped (chord {:?})", action, chord));
+ return;
+ }
+ if !valid_chord(&chord) {
+ out.warnings.push(format!("invalid chord {:?} skipped (action {:?})", chord, action));
+ return;
+ }
+ out.wm.push(BindingEntry { name: action, chord, command: prop_string(node, "command") });
+}
+
+/// A chord is migratable when its modifiers parse AND its key is a real XKB
+/// keysym — this rejects gesture names like "swipe_down" that ride in
+/// keybind-typed config slots.
+fn valid_chord(chord: &str) -> bool {
+ match parse_chord(chord) {
+ Some(c) => crate::config::parse_keysym(&c.key) != 0,
+ None => false,
+ }
+}
+
+/// Pure extraction pass over config.kdl content.
+pub fn extract_from_config(content: &str) -> Result<Extracted, String> {
+ let doc: kdl::KdlDocument = content.parse().map_err(|e| format!("{}", e))?;
+ let mut out = Extracted { wm: Vec::new(), ui: Vec::new(), warnings: Vec::new() };
+
+ // key_bindings nodes live at the root or nested one level down (the
+ // `input` section) — mirror parse_kdl_config and scan both.
+ let mut kb_nodes: Vec<&kdl::KdlNode> = Vec::new();
+ for node in doc.nodes() {
+ if node.name().value() == "key_bindings" {
+ kb_nodes.push(node);
+ } else if let Some(children) = node.children() {
+ kb_nodes.extend(children.nodes().iter().filter(|n| n.name().value() == "key_bindings"));
+ }
+ }
+ for node in kb_nodes {
+ match node.children() {
+ Some(children) => {
+ for c in children.nodes() {
+ convert_key_binding(c, &mut out);
+ }
+ }
+ None => convert_key_binding(node, &mut out),
+ }
+ }
+
+ if let Some(wm_node) = child(&doc, "window_manager") {
+ if let Some(children) = wm_node.children() {
+ for (prop, name) in [
+ ("close_window", "close_window"),
+ ("toggle_fullscreen", "toggle_fullscreen"),
+ ("window_switcher", "window_switcher"),
+ ("toggle_overview", "expose"),
+ ] {
+ let Some(c) = child(children, prop) else { continue };
+ let Some(chord) = first_arg_string(c) else { continue };
+ if !valid_chord(&chord) {
+ out.warnings.push(format!("window_manager.{}: invalid chord {:?} skipped", prop, chord));
+ continue;
+ }
+ out.wm.push(BindingEntry { name: name.to_string(), chord, command: None });
+ }
+ }
+ }
+
+ // Widget search keys from style.data.{list,tree} props → cce-ui domain.
+ let data = child(&doc, "style").and_then(|n| n.children()).and_then(|c| child(c, "data"));
+ if let Some(data) = data {
+ let data_children = data.children();
+ let list = data_children.and_then(|c| child(c, "list"));
+ let tree = data_children.and_then(|c| child(c, "tree"));
+ let list_open = list.and_then(|n| prop_string(n, "open_search"));
+ let tree_open = tree.and_then(|n| prop_string(n, "open_search"));
+ let close = list.and_then(|n| prop_string(n, "close_search"));
+
+ match (&list_open, &tree_open) {
+ (Some(l), Some(t)) if l != t => out.warnings.push(format!(
+ "list.open_search ({:?}) and tree.open_search ({:?}) differ; migrating the list value — the tree keeps its config.kdl prop",
+ l, t
+ )),
+ _ => {}
+ }
+ if let Some(chord) = list_open.or(tree_open) {
+ out.ui.push(BindingEntry { name: "open_search".into(), chord, command: None });
+ }
+ if let Some(chord) = close {
+ out.ui.push(BindingEntry { name: "close_search".into(), chord, command: None });
+ }
+ }
+
+ Ok(out)
+}
+
+/// Merge extracted entries into a domain's existing list. Existing entries
+/// always win: an extracted entry is dropped when its name is already
+/// configured (or, for repeatable spawn/toggle, when the same chord is).
+pub fn merge_into(existing: &[BindingEntry], extracted: Vec<BindingEntry>) -> (Vec<BindingEntry>, usize) {
+ let mut merged = existing.to_vec();
+ let mut added = 0;
+ for e in extracted {
+ let repeatable = e.name == "spawn" || e.name == "toggle";
+ let taken = merged.iter().any(|m| {
+ if repeatable {
+ m.chord == e.chord
+ } else {
+ m.name == e.name
+ }
+ });
+ if !taken {
+ merged.push(e);
+ added += 1;
+ }
+ }
+ (merged, added)
+}
+
+fn backup(path: &std::path::Path) -> Option<std::path::PathBuf> {
+ if !path.exists() {
+ return None;
+ }
+ let backups = cce_ui::config::cce_config_dir().join("backups");
+ let _ = std::fs::create_dir_all(&backups);
+ for n in 1..1000 {
+ let candidate = backups.join(format!("input.kdl.{}.bak", n));
+ if !candidate.exists() {
+ return std::fs::copy(path, &candidate).ok().map(|_| candidate);
+ }
+ }
+ None
+}
+
+pub fn run() {
+ let config_path = cce_ui::config::get_config_path();
+ let content = match std::fs::read_to_string(&config_path) {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln!("cannot read {}: {}", config_path.display(), e);
+ std::process::exit(1);
+ }
+ };
+ let extracted = match extract_from_config(&content) {
+ Ok(x) => x,
+ Err(e) => {
+ eprintln!("cannot parse {}: {}", config_path.display(), e);
+ std::process::exit(1);
+ }
+ };
+ for w in &extracted.warnings {
+ eprintln!("warning: {}", w);
+ }
+ if extracted.wm.is_empty() && extracted.ui.is_empty() {
+ println!("nothing to migrate: no keybindings found in {}", config_path.display());
+ return;
+ }
+
+ let input_path = cce_ui::input::get_input_path();
+ let existing_content = std::fs::read_to_string(&input_path).unwrap_or_default();
+ let existing = match InputConfig::parse(&existing_content) {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln!("cannot parse existing {}: {} — fix or remove it first", input_path.display(), e);
+ std::process::exit(1);
+ }
+ };
+
+ let (wm_merged, wm_added) = merge_into(existing.domain(WINDOW_MANAGER_DOMAIN), extracted.wm);
+ let (ui_merged, ui_added) = merge_into(existing.domain(UI_DOMAIN), extracted.ui);
+ if wm_added == 0 && ui_added == 0 {
+ println!("nothing to migrate: input.kdl already covers every config.kdl binding");
+ return;
+ }
+
+ if let Some(bak) = backup(&input_path) {
+ println!("backed up {} -> {}", input_path.display(), bak.display());
+ }
+ let mut write = |domain: &str, entries: &[BindingEntry], added: usize| {
+ if added == 0 {
+ return;
+ }
+ match cce_ui::input::write_domain(&input_path, domain, entries) {
+ Ok(()) => println!("{}: migrated {} binding(s)", domain, added),
+ Err(e) => {
+ eprintln!("failed to write {}: {}", input_path.display(), e);
+ std::process::exit(1);
+ }
+ }
+ };
+ write(WINDOW_MANAGER_DOMAIN, &wm_merged, wm_added);
+ write(UI_DOMAIN, &ui_merged, ui_added);
+
+ println!();
+ println!("wrote {}", input_path.display());
+ println!("config.kdl was NOT modified. The migrated `key_bindings` nodes and the");
+ println!("`window_manager` section are now shadowed by input.kdl and can be deleted.");
+ if ui_added > 0 {
+ println!("Migrated widget search keys only take effect once the corresponding");
+ println!("style.data.list/tree props are removed from config.kdl (per-widget");
+ println!("props stay more specific than cce-ui domain defaults).");
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const CONFIG: &str = r#"
+input {
+ accel_speed (f64)1.0
+ key_bindings action="spawn" command="cce-cloud --apps" key=(keybind)"super+d"
+ key_bindings action="spawn" command="cce control keypress 69" key=(keybind)"super+slash"
+}
+key_bindings {
+ bind action="toggle" command="foot" key=(keybind)"super+t"
+ bind action="bogus" key=(keybind)"super+b"
+ bind action="spawn" command="x" key=(keybind)"hyper+x"
+}
+style {
+ data {
+ list close_search=(keybind)"escape" open_search=(keybind)"/"
+ tree open_search=(keybind)"ctrl+f"
+ }
+}
+window_manager {
+ close_window (keybind)"super+q"
+ toggle_fullscreen (keybind)"super+f"
+ window_switcher (keybind)"super+tab"
+ toggle_overview (keybind)"swipe_down"
+}
+"#;
+
+ #[test]
+ fn extracts_all_legacy_sources() {
+ let x = extract_from_config(CONFIG).unwrap();
+ // 2 nested spawns + 1 block toggle + 3 window_manager entries; the
+ // unknown action, the bad modifier, and the gesture-name chord are
+ // skipped with warnings.
+ assert_eq!(x.wm.len(), 6);
+ assert!(!x.wm.iter().any(|e| e.chord == "swipe_down"));
+ assert_eq!(x.warnings.len(), 4); // bogus action, hyper chord, swipe_down, list/tree mismatch
+ let spawn = x.wm.iter().find(|e| e.chord == "super+d").unwrap();
+ assert_eq!(spawn.name, "spawn");
+ assert_eq!(spawn.command.as_deref(), Some("cce-cloud --apps"));
+ let toggle = x.wm.iter().find(|e| e.name == "toggle").unwrap();
+ assert_eq!(toggle.chord, "super+t");
+ assert!(x.wm.iter().any(|e| e.name == "close_window" && e.chord == "super+q"));
+ assert!(x.wm.iter().any(|e| e.name == "window_switcher" && e.chord == "super+tab"));
+ // list wins the open_search mismatch; close_search comes along.
+ assert!(x.ui.iter().any(|e| e.name == "open_search" && e.chord == "/"));
+ assert!(x.ui.iter().any(|e| e.name == "close_search" && e.chord == "escape"));
+ }
+
+ #[test]
+ fn merge_never_overrides_existing() {
+ let existing = vec![
+ BindingEntry { name: "close_window".into(), chord: "super+w".into(), command: None },
+ BindingEntry { name: "spawn".into(), chord: "super+d".into(), command: Some("a".into()) },
+ ];
+ let extracted = vec![
+ // Same name, different chord: dropped (name already configured).
+ BindingEntry { name: "close_window".into(), chord: "super+q".into(), command: None },
+ // Repeatable, same chord: dropped.
+ BindingEntry { name: "spawn".into(), chord: "super+d".into(), command: Some("b".into()) },
+ // Repeatable, new chord: added.
+ BindingEntry { name: "spawn".into(), chord: "super+t".into(), command: Some("c".into()) },
+ ];
+ let (merged, added) = merge_into(&existing, extracted);
+ assert_eq!(added, 1);
+ assert_eq!(merged.len(), 3);
+ assert_eq!(merged[0].chord, "super+w");
+ assert!(merged.iter().any(|e| e.chord == "super+t"));
+ }
+}
diff --git a/src/server/config.rs b/src/server/config.rs
index b2ad7ae..1ca37c0 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -188,13 +188,9 @@ pub struct StartupConfig {
pub restart: bool,
}
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct Keybind {
- pub mods: u32,
- pub keysym: u32,
- pub action: Action,
- pub command: Option<String>,
-}
+// The compositor's resolved keybind is the policy crate's `Binding`
+// (mods, keysym, action, command), kept under its historical name here.
+pub use cce_window_manager::bindings::Binding as Keybind;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PointerBind {
@@ -680,6 +676,24 @@ pub fn parse_action(s: &str) -> Action {
}
}
+/// Warn when a spawn/toggle binding's executable can't be found (PATH lookup;
+/// absolute paths are checked directly).
+fn warn_if_command_missing(command: Option<&str>) {
+ let Some(cmd_str) = command else { return };
+ let cmd_exe = cmd_str.split_whitespace().next().unwrap_or("");
+ if cmd_exe.is_empty() {
+ return;
+ }
+ if let Ok(path_var) = std::env::var("PATH") {
+ for path_dir in std::env::split_paths(&path_var) {
+ if path_dir.join(cmd_exe).is_file() {
+ return;
+ }
+ }
+ eprintln!("[WARNING] Configured keybinding command not found in PATH: {}", cmd_exe);
+ }
+}
+
pub fn parse_keysym(key_str: &str) -> u32 {
let name = if key_str.starts_with("XKB_KEY_") {
&key_str[8..]
@@ -1609,8 +1623,20 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
let path_buf = std::path::Path::new(path);
let input_path = path_buf.parent().unwrap_or_else(|| std::path::Path::new(".")).join("input.kdl");
+ let mut wm_domain_entries: Vec<cce_ui::input::BindingEntry> = Vec::new();
if input_path.exists() {
if let Ok(input_content) = fs::read_to_string(&input_path) {
+ // New domain-scoped format: a `cce-window-manager { ... }` block
+ // of `<action_name> "<chord>"` bindings. Other domains belong to
+ // clients/widgets and are ignored here.
+ match cce_ui::input::InputConfig::parse(&input_content) {
+ Ok(ic) => {
+ wm_domain_entries = ic.domain(cce_ui::input::WINDOW_MANAGER_DOMAIN).to_vec();
+ }
+ Err(e) => eprintln!("[WARNING] {}: {}", input_path.display(), e),
+ }
+ // Legacy input.kdl contents: root-level key_bindings nodes and
+ // the input section.
if let Ok(input_config) = parse_kdl_config(&input_content) {
config.key_bindings.extend(input_config.key_bindings);
if input_config.input.is_some() {
@@ -1704,6 +1730,37 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
}
state.keybinds.clear();
+ let mut table = cce_window_manager::bindings::BindingTable::new();
+
+ // Primary source: the `cce-window-manager` domain of input.kdl.
+ for entry in &wm_domain_entries {
+ let Some(chord) = cce_window_manager::bindings::parse_chord(&entry.chord) else {
+ eprintln!("[WARNING] input.kdl: invalid chord {:?} for {}", entry.chord, entry.name);
+ continue;
+ };
+ let Some(action) = Action::from_name(&entry.name) else {
+ eprintln!("[WARNING] input.kdl: unknown window-manager action {:?}", entry.name);
+ continue;
+ };
+ let command = if action == Action::Spawn || action == Action::Toggle {
+ warn_if_command_missing(entry.command.as_deref());
+ entry.command.clone()
+ } else {
+ None
+ };
+ let keysym = parse_keysym(&chord.key);
+ if keysym == 0 {
+ eprintln!("[WARNING] input.kdl: unknown key {:?} in chord {:?}", chord.key, entry.chord);
+ continue;
+ }
+ if table.add(Keybind { mods: chord.mods, keysym, action, command }) {
+ eprintln!("[WARNING] input.kdl: {:?} is bound more than once", entry.chord);
+ }
+ }
+
+ // Legacy sources: config.kdl `key_bindings` nodes (including the
+ // synthesized brightness binds) and the `window_manager` section.
+ // input.kdl wins on chord conflicts via add_default.
let mut seen = std::collections::HashSet::new();
for kb in &config.key_bindings {
let (mods_str, key_str) = if kb.mods.is_empty() {
@@ -1717,185 +1774,43 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
};
let mods = parse_modifiers(&mods_str);
let keysym = parse_keysym(&key_str);
-
- let binding_key = (mods, keysym);
- if !seen.insert(binding_key) {
+
+ if !seen.insert((mods, keysym)) {
eprintln!("[WARNING] Keybinding conflict: multiple actions mapped to mods={:?}, key={:?}", mods_str, key_str);
}
let action = parse_action(&kb.action);
let command = if action == Action::Spawn || action == Action::Toggle {
- if let Some(ref cmd_str) = kb.command {
- let cmd_exe = cmd_str.split_whitespace().next().unwrap_or("");
- if !cmd_exe.is_empty() {
- let mut found = false;
- if let Ok(path_var) = std::env::var("PATH") {
- for path_dir in std::env::split_paths(&path_var) {
- if path_dir.join(cmd_exe).is_file() {
- found = true;
- break;
- }
- }
- }
- if !found {
- eprintln!("[WARNING] Configured keybinding command not found in PATH: {}", cmd_exe);
- }
- }
- }
+ warn_if_command_missing(kb.command.as_deref());
kb.command.clone()
} else {
None
};
- state.keybinds.push(Keybind {
- mods,
- keysym,
- action,
- command,
- });
+ table.add_default(Keybind { mods, keysym, action, command });
}
if let Some(ref wm_config) = config.window_manager {
- if let Some(ref close_win_str) = wm_config.close_window {
- let (mods_str, key_str) = if let Some(last_plus) = close_win_str.rfind('+') {
- (close_win_str[..last_plus].to_string(), close_win_str[last_plus+1..].to_string())
- } else {
- ("".to_string(), close_win_str.clone())
- };
- let mods = parse_modifiers(&mods_str);
- let keysym = parse_keysym(&key_str);
- state.keybinds.push(Keybind {
- mods,
- keysym,
- action: Action::Close,
- command: None,
- });
- }
- if let Some(ref toggle_fs_str) = wm_config.toggle_fullscreen {
- let (mods_str, key_str) = if let Some(last_plus) = toggle_fs_str.rfind('+') {
- (toggle_fs_str[..last_plus].to_string(), toggle_fs_str[last_plus+1..].to_string())
- } else {
- ("".to_string(), toggle_fs_str.clone())
- };
- let mods = parse_modifiers(&mods_str);
- let keysym = parse_keysym(&key_str);
- state.keybinds.push(Keybind {
- mods,
- keysym,
- action: Action::Fullscreen,
- command: None,
- });
- }
- if let Some(ref switcher_str) = wm_config.window_switcher {
- let (mods_str, key_str) = if let Some(last_plus) = switcher_str.rfind('+') {
- (switcher_str[..last_plus].to_string(), switcher_str[last_plus+1..].to_string())
- } else {
- ("".to_string(), switcher_str.clone())
- };
- let mods = parse_modifiers(&mods_str);
- let keysym = parse_keysym(&key_str);
- state.keybinds.push(Keybind {
- mods,
- keysym,
- action: Action::WindowSwitcher,
- command: None,
- });
+ let wm_section_binds = [
+ (&wm_config.close_window, Action::Close),
+ (&wm_config.toggle_fullscreen, Action::Fullscreen),
+ (&wm_config.window_switcher, Action::WindowSwitcher),
+ ];
+ for (chord_str, action) in wm_section_binds {
+ let Some(chord_str) = chord_str else { continue };
+ if let Some(chord) = cce_window_manager::bindings::parse_chord(chord_str) {
+ let keysym = parse_keysym(&chord.key);
+ table.add_default(Keybind { mods: chord.mods, keysym, action, command: None });
+ }
}
}
- let super_mod = parse_modifiers("super");
- let left_sym = parse_keysym("Left");
- let right_sym = parse_keysym("Right");
- if !state.keybinds.iter().any(|b| b.mods == super_mod && b.keysym == left_sym) {
- state.keybinds.push(Keybind {
- mods: super_mod,
- keysym: left_sym,
- action: Action::OverlayLeft,
- command: None,
- });
- }
- if !state.keybinds.iter().any(|b| b.mods == super_mod && b.keysym == right_sym) {
- state.keybinds.push(Keybind {
- mods: super_mod,
- keysym: right_sym,
- action: Action::OverlayRight,
- command: None,
- });
+ // Stock defaults from the policy crate; never shadow configured chords.
+ for d in cce_window_manager::bindings::DEFAULT_BINDINGS {
+ let keysym = parse_keysym(d.key);
+ table.add_default(Keybind { mods: d.mods, keysym, action: d.action, command: None });
}
- let super_ctrl_mod = parse_modifiers("super+ctrl");
- let up_sym = parse_keysym("Up");
- let down_sym = parse_keysym("Down");
- let equal_sym = parse_keysym("equal");
- let minus_sym = parse_keysym("minus");
-
- if !state.keybinds.iter().any(|b| b.mods == super_ctrl_mod && b.keysym == up_sym) {
- state.keybinds.push(Keybind {
- mods: super_ctrl_mod,
- keysym: up_sym,
- action: Action::PanUp,
- command: None,
- });
- }
- if !state.keybinds.iter().any(|b| b.mods == super_ctrl_mod && b.keysym == down_sym) {
- state.keybinds.push(Keybind {
- mods: super_ctrl_mod,
- keysym: down_sym,
- action: Action::PanDown,
- command: None,
- });
- }
- if !state.keybinds.iter().any(|b| b.mods == super_ctrl_mod && b.keysym == left_sym) {
- state.keybinds.push(Keybind {
- mods: super_ctrl_mod,
- keysym: left_sym,
- action: Action::PanLeft,
- command: None,
- });
- }
- if !state.keybinds.iter().any(|b| b.mods == super_ctrl_mod && b.keysym == right_sym) {
- state.keybinds.push(Keybind {
- mods: super_ctrl_mod,
- keysym: right_sym,
- action: Action::PanRight,
- command: None,
- });
- }
- let super_ctrl_shift_mod = parse_modifiers("super+ctrl+shift");
- if !state.keybinds.iter().any(|b| b.mods == super_ctrl_shift_mod && b.keysym == equal_sym) {
- state.keybinds.push(Keybind {
- mods: super_ctrl_shift_mod,
- keysym: equal_sym,
- action: Action::ZoomIn,
- command: None,
- });
- }
- if !state.keybinds.iter().any(|b| b.mods == super_ctrl_mod && b.keysym == minus_sym) {
- state.keybinds.push(Keybind {
- mods: super_ctrl_mod,
- keysym: minus_sym,
- action: Action::ZoomOut,
- command: None,
- });
- }
- if !state.keybinds.iter().any(|b| b.mods == super_ctrl_mod && b.keysym == equal_sym) {
- state.keybinds.push(Keybind {
- mods: super_ctrl_mod,
- keysym: equal_sym,
- action: Action::ZoomReset,
- command: None,
- });
- }
-
- let super_shift_mod = parse_modifiers("super+shift");
- let r_sym = parse_keysym("r");
- if !state.keybinds.iter().any(|b| b.mods == super_shift_mod && b.keysym == r_sym) {
- state.keybinds.push(Keybind {
- mods: super_shift_mod,
- keysym: r_sym,
- action: Action::Reload,
- command: None,
- });
- }
+ state.keybinds = table.into_bindings();
state.pointer_binds.clear();
for pb in &config.pointer_bind {