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

commit5fd1e33da66bfee7f1afeebfd02f70add625647b
parentba67419e0e
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-01 17:03
Implement `ccectl repeat`

The command was listed in ccectl's usage but the compositor had no
handler for it. `repeat <rate> <delay>` now sets the key repeat of every
hardware keyboard live, until the next config load restores
`input { repeat_rate repeat_delay }` (the same contract as `idle
timeouts`); with no args it prints `rate= delay= keyboards=`. The
per-keyboard push moved out of apply_input_config into apply_key_repeat
so both paths share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/cce_ctl.rs               |  2 +-
 src/server/window_manager.rs | 41 ++++++++++++++++++++++++++++++++++++++---
 2 files changed, 39 insertions(+), 4 deletions(-)

diff --git a/src/cce_ctl.rs b/src/cce_ctl.rs
index 45004157..eb2bb731 100644
--- a/src/cce_ctl.rs
+++ b/src/cce_ctl.rs
@@ -73,7 +73,7 @@ fn usage(name: &str, to_stderr: bool) {
     print("  restart");
     print("  restart-compositor        # exit cleanly; cce-display-manager relaunches the session");
     print("  reload");
-    print("  repeat <rate> <delay>");
+    print("  repeat [<rate> <delay>]    # key repeat for hardware keyboards: repeats/s (0 = off), ms before the first; until the next config load. No args prints it");
     print("  input <device_name|*> scroll-factor <value>");
     print("  config-done");
     print("  spawn <command>");
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index cb5831e0..666e8df7 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -5942,6 +5942,23 @@ impl WindowManager {
                 "ok\n".to_string()
             }
             "idle" => unsafe { (*self.server).idle.ipc(&parts[1..]) },
+            // Live key repeat for every hardware keyboard, like `idle
+            // timeouts`: it lasts until the next config load, which puts
+            // `input { repeat_rate repeat_delay }` back.
+            "repeat" => {
+                if parts.len() == 3 {
+                    let (Ok(rate), Ok(delay)) = (parts[1].parse::<u32>(), parts[2].parse::<u32>()) else {
+                        return "error: rate and delay must be non-negative integers\n".to_string();
+                    };
+                    self.input_config.repeat_rate = Some(rate as i64);
+                    self.input_config.repeat_delay = Some(delay as i64);
+                } else if parts.len() != 1 {
+                    return "error: usage: repeat [<rate> <delay>]\n".to_string();
+                }
+                let keyboards = unsafe { self.apply_key_repeat() };
+                let (rate, delay) = self.input_config.repeat_info();
+                format!("rate={} delay={} keyboards={}\n", rate, delay, keyboards)
+            }
             "outputs" => {
                 // One line per output: the figures a client's `units::Metric`
                 // is built from (mode, scale, logical size, physical mm) and
@@ -6616,14 +6633,31 @@ impl WindowManager {
             if let Some(ref mut libinput) = (*device).libinput {
                 libinput.apply_config(&self.input_config);
             }
-            // Key repeat: a hardware keyboard regroups on a change (groups
-            // are keyed by repeat info), so only touch it when it differs.
+            curr = next;
+        }
+        self.apply_key_repeat();
+    }
+
+    /// Push `input_config`'s repeat rate/delay to every hardware keyboard and
+    /// return how many there are. A keyboard regroups on a change (groups are
+    /// keyed by repeat info), so one already matching is left alone.
+    pub unsafe fn apply_key_repeat(&mut self) -> usize {
+        if self.server.is_null() {
+            return 0;
+        }
+        let (rate, delay) = self.input_config.repeat_info();
+        let mut count = 0;
+        let devices_head = &mut (*self.server).input_manager.devices as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*devices_head).next;
+        while curr != devices_head {
+            let next = (*curr).next;
+            let device = crate::container_of!(curr, crate::input_device::InputDevice, link);
             if !(*device).virtual_device
                 && ffi::river_wlr_input_device_get_type((*device).wlr_device) == ffi::wlr_input_device_type_WLR_INPUT_DEVICE_KEYBOARD
             {
                 let keyboard = (*device).destroy_data as *mut crate::keyboard::Keyboard;
                 if !keyboard.is_null() {
-                    let (rate, delay) = self.input_config.repeat_info();
+                    count += 1;
                     if (*keyboard).config.repeat_rate != rate || (*keyboard).config.repeat_delay != delay {
                         (*keyboard).set_repeat_info(rate, delay);
                     }
@@ -6631,6 +6665,7 @@ impl WindowManager {
             }
             curr = next;
         }
+        count
     }
 
     pub unsafe fn spawn_startup_program(&mut self, prog: crate::config::StartupConfig) {