git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitd2f2ec5458ebe2a8b875f1a3326430f9e8a5b0de
parent3b8729236e
authorLucas Galante <[email protected]>
date2026-07-16 09:45
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 {