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

commitba67419e0e029a79f35799123877b8f264a00f36
parentea400ee95d
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-01 17:01
Make key repeat configurable, default delay 250 ms

The repeat delay was hardcoded at 400 ms in two places. Both now share
keyboard::DEFAULT_REPEAT_{RATE,DELAY} (40/s, 250 ms), and hardware
keyboards take `input { repeat_rate repeat_delay }` over them: at hotplug
in Keyboard::create, and for keyboards already present on every config
load via apply_input_config (which regroups only on an actual change).
Virtual and synthetic keyboards keep the defaults.

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

 src/server/config.rs         | 56 ++++++++++++++++++++++++++++++++++++++++++++
 src/server/keyboard.rs       | 17 ++++++++++++--
 src/server/seat.rs           |  4 ++--
 src/server/window_manager.rs | 13 ++++++++++
 4 files changed, 86 insertions(+), 4 deletions(-)

diff --git a/src/server/config.rs b/src/server/config.rs
index 2de35047..6758e87b 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -559,11 +559,28 @@ pub struct InputConfig {
     pub kinetic_scroll: Option<bool>,
     /// `scroll_friction`: coast decay, 1/s (default 6).
     pub scroll_friction: Option<f64>,
+    /// `repeat_rate`: key repeats per second once repeating starts
+    /// (default `keyboard::DEFAULT_REPEAT_RATE`; 0 disables repeat).
+    pub repeat_rate: Option<i64>,
+    /// `repeat_delay`: ms a key is held before it starts repeating
+    /// (default `keyboard::DEFAULT_REPEAT_DELAY`).
+    pub repeat_delay: Option<i64>,
     pub mouse: Option<MouseConfig>,
     pub touchpad: Option<TouchpadConfig>,
     pub trackpoint: Option<TrackpointConfig>,
 }
 
+impl InputConfig {
+    /// `(rate, delay)` for hardware keyboards, defaults filled in. Negative
+    /// values are clamped to 0, which `wl_keyboard.repeat_info` reads as
+    /// "no repeat" for the rate; the protocol rejects negatives outright.
+    pub fn repeat_info(&self) -> (i32, i32) {
+        let rate = self.repeat_rate.map_or(crate::keyboard::DEFAULT_REPEAT_RATE, |v| v.clamp(0, i32::MAX as i64) as i32);
+        let delay = self.repeat_delay.map_or(crate::keyboard::DEFAULT_REPEAT_DELAY, |v| v.clamp(0, i32::MAX as i64) as i32);
+        (rate, delay)
+    }
+}
+
 #[derive(Debug, Deserialize, Clone, Default)]
 pub struct TransparencyConfig {
     pub opacity: Option<f64>,
@@ -1318,6 +1335,19 @@ fn get_child_arg_i64(node: &kdl::KdlNode, child_name: &str, default: i64) -> i64
     default
 }
 
+fn get_child_arg_i64_opt(node: &kdl::KdlNode, child_name: &str) -> Option<i64> {
+    if let Some(children) = node.children() {
+        for child in children.nodes() {
+            if child.name().value() == child_name {
+                if let Some(entry) = child.entries().first() {
+                    return entry.value().as_i64();
+                }
+            }
+        }
+    }
+    None
+}
+
 fn get_child_arg_f64(node: &kdl::KdlNode, child_name: &str, default: f64) -> f64 {
     if let Some(children) = node.children() {
         for child in children.nodes() {
@@ -1990,6 +2020,8 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
         let scroll_ease = get_child_arg_f64_opt(node, "scroll_ease");
         let kinetic_scroll = get_child_arg_bool_opt(node, "kinetic_scroll");
         let scroll_friction = get_child_arg_f64_opt(node, "scroll_friction");
+        let repeat_rate = get_child_arg_i64_opt(node, "repeat_rate");
+        let repeat_delay = get_child_arg_i64_opt(node, "repeat_delay");
 
         let mut touchpad = None;
         if let Some(children) = node.children() {
@@ -2059,6 +2091,8 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
             scroll_ease,
             kinetic_scroll,
             scroll_friction,
+            repeat_rate,
+            repeat_delay,
             mouse,
             touchpad,
             trackpoint,
@@ -3310,6 +3344,28 @@ style {
         assert_eq!(tpoint.scroll_method, Some("none".to_string()));
     }
 
+    #[test]
+    fn test_kdl_input_key_repeat() {
+        let input = parse_kdl_config("input {\n repeat_rate 30\n repeat_delay 200\n}\n")
+            .unwrap()
+            .input
+            .unwrap();
+        assert_eq!(input.repeat_rate, Some(30));
+        assert_eq!(input.repeat_delay, Some(200));
+        assert_eq!(input.repeat_info(), (30, 200));
+
+        // Unset keys fall back to the defaults; negatives clamp to 0.
+        let input = parse_kdl_config("input {\n repeat_delay -5\n}\n").unwrap().input.unwrap();
+        assert_eq!(
+            input.repeat_info(),
+            (crate::keyboard::DEFAULT_REPEAT_RATE, 0)
+        );
+        assert_eq!(
+            InputConfig::default().repeat_info(),
+            (crate::keyboard::DEFAULT_REPEAT_RATE, crate::keyboard::DEFAULT_REPEAT_DELAY)
+        );
+    }
+
     #[test]
     fn test_kdl_touchpad_hscroll_shift_apps() {
         let content = r#"
diff --git a/src/server/keyboard.rs b/src/server/keyboard.rs
index 67b56d0b..7234ebab 100644
--- a/src/server/keyboard.rs
+++ b/src/server/keyboard.rs
@@ -7,6 +7,14 @@ use crate::server::{WlListener, wl_listener_remove, wl_signal_add};
 use crate::keyboard_group::KeyboardGroup;
 use std::collections::HashSet;
 
+/// Key repeat defaults (keys/s, ms before the first repeat). A hardware
+/// keyboard takes `input { repeat_rate repeat_delay }` over these
+/// (`InputConfig::repeat_info`); the synthetic and virtual keyboards keep
+/// them. A client can still change a device's via river's
+/// `input_device.set_repeat_info`, until the next config load.
+pub const DEFAULT_REPEAT_RATE: i32 = 40;
+pub const DEFAULT_REPEAT_DELAY: i32 = 250;
+
 #[derive(Clone, Copy)]
 pub struct KeyboardConfig {
     pub keymap: *mut ffi::xkb_keymap,
@@ -34,6 +42,11 @@ impl Keyboard {
         let wlr_keyboard = ffi::wlr_keyboard_from_input_device((*device).wlr_device);
         
         let virtual_device = (*device).virtual_device;
+        let (repeat_rate, repeat_delay) = if virtual_device {
+            (DEFAULT_REPEAT_RATE, DEFAULT_REPEAT_DELAY)
+        } else {
+            (*(*(*device).seat).server).wm.input_config.repeat_info()
+        };
         let mut keymap = std::ptr::null_mut();
         if virtual_device {
             let kbd_keymap = ffi::river_wlr_keyboard_get_keymap(wlr_keyboard);
@@ -60,8 +73,8 @@ impl Keyboard {
             group_link: std::mem::zeroed(),
             config: KeyboardConfig {
                 keymap,
-                repeat_rate: 40,
-                repeat_delay: 400,
+                repeat_rate,
+                repeat_delay,
             },
         }));
 
diff --git a/src/server/seat.rs b/src/server/seat.rs
index f25fe1a5..38705cfe 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -761,8 +761,8 @@ impl Seat {
         // `keyboard_notify_enter` its pressed-key tracking.
         let config = crate::keyboard::KeyboardConfig {
             keymap,
-            repeat_rate: 40,
-            repeat_delay: 400,
+            repeat_rate: crate::keyboard::DEFAULT_REPEAT_RATE,
+            repeat_delay: crate::keyboard::DEFAULT_REPEAT_DELAY,
         };
         match crate::keyboard_group::KeyboardGroup::create(self, config, true) {
             Ok(group) => {
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index d1a3556c..cb5831e0 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -6616,6 +6616,19 @@ 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.
+            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();
+                    if (*keyboard).config.repeat_rate != rate || (*keyboard).config.repeat_delay != delay {
+                        (*keyboard).set_repeat_info(rate, delay);
+                    }
+                }
+            }
             curr = next;
         }
     }