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

commit6c9f39718d031f20914e1905528d25a61e35b118
parentaa60a537dd
authorLucas Galante <[email protected]>
date2026-05-27 23:30
Implement XKB Keyboard Status Broadcasting (river_xkb_keyboard_v1)

 src/input_device.rs   |  11 ++
 src/keyboard_group.rs |  21 ++-
 src/main.rs           |   1 +
 src/xkb_config.rs     |  11 ++
 src/xkb_keyboard.rs   | 497 ++++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 536 insertions(+), 5 deletions(-)

diff --git a/src/input_device.rs b/src/input_device.rs
index 1ca38f8..15c6c41 100644
--- a/src/input_device.rs
+++ b/src/input_device.rs
@@ -23,6 +23,7 @@ pub struct InputDevice {
     pub objects: ffi::wl_list, // list of InputDeviceObject
     pub link: ffi::wl_list,    // link inside InputManager::devices
     pub libinput: Option<Box<crate::libinput_device::LibinputDevice>>,
+    pub xkb_keyboard: Option<Box<crate::xkb_keyboard::XkbKeyboard>>,
 }
 
 pub struct InputDeviceObject {
@@ -54,6 +55,7 @@ impl InputDevice {
             objects: std::mem::zeroed(),
             link: std::mem::zeroed(),
             libinput: None,
+            xkb_keyboard: None,
         }));
 
         ffi::wl_list_init(&mut (*device).objects);
@@ -85,6 +87,11 @@ impl InputDevice {
             if !handle.is_null() {
                 (*device).libinput = Some(crate::libinput_device::LibinputDevice::init(device, handle));
             }
+
+            let dev_type = ffi::river_wlr_input_device_get_type(wlr_device);
+            if dev_type == ffi::wlr_input_device_type_WLR_INPUT_DEVICE_KEYBOARD {
+                (*device).xkb_keyboard = Some(crate::xkb_keyboard::XkbKeyboard::init(device));
+            }
         }
 
         // Output mapping logic for pointers and touch screens (as done in Zig version)
@@ -272,6 +279,10 @@ unsafe extern "C" fn handle_device_destroy(listener: *mut ffi::wl_listener, _dat
         libinput.deinit();
     }
 
+    if let Some(mut xkb_kbd) = device.xkb_keyboard.take() {
+        xkb_kbd.deinit();
+    }
+
     // Call custom destroy callback if set (e.g. to clean up Tablet/Keyboard wrappers)
     if let Some(destroy_fn) = device.destroy_fn {
         destroy_fn(device.destroy_data);
diff --git a/src/keyboard_group.rs b/src/keyboard_group.rs
index 7d885e7..b92621d 100644
--- a/src/keyboard_group.rs
+++ b/src/keyboard_group.rs
@@ -243,24 +243,35 @@ impl KeyboardGroup {
             return;
         }
         let layout_index = self.wlr_keyboard.modifiers.group;
-        let _layout_name = ffi::xkb_keymap_layout_get_name(keymap, layout_index);
+        let layout_name = ffi::xkb_keymap_layout_get_name(keymap, layout_index);
         let caps_idx = ffi::xkb_keymap_mod_get_index(keymap, b"Caps Lock\0".as_ptr() as *const _);
-        let _capslock = if caps_idx != ffi::XKB_MOD_INVALID {
+        let capslock = if caps_idx != ffi::XKB_MOD_INVALID {
             let caps_mask = 1 << caps_idx;
             (self.wlr_keyboard.modifiers.locked & caps_mask) != 0
         } else {
             false
         };
         let num_idx = ffi::xkb_keymap_mod_get_index(keymap, b"Num Lock\0".as_ptr() as *const _);
-        let _numlock = if num_idx != ffi::XKB_MOD_INVALID {
+        let numlock = if num_idx != ffi::XKB_MOD_INVALID {
             let num_mask = 1 << num_idx;
             (self.wlr_keyboard.modifiers.locked & num_mask) != 0
         } else {
             false
         };
 
-        // Note: Layout configurations broadcasting (river_xkb_keyboard_v1) is stubbed out
-        // in xkb_config.rs and will be fully ported in a later phase.
+        let server = (*self.seat).server;
+        let keyboards_head = &mut (*server).xkb_config.keyboards as *mut ffi::wl_list as *mut crate::server::WlList;
+        let mut curr = (*keyboards_head).next;
+        while curr != keyboards_head {
+            let next = (*curr).next;
+            let xkb_kbd = crate::container_of!(curr, crate::xkb_keyboard::XkbKeyboard, link);
+            let parent_dev = (*xkb_kbd).parent_device;
+            let kbd = (*parent_dev).destroy_data as *mut crate::keyboard::Keyboard;
+            if !kbd.is_null() && (*kbd).group == self as *mut KeyboardGroup {
+                (*xkb_kbd).send_state(layout_index, layout_name, capslock, numlock);
+            }
+            curr = next;
+        }
     }
 }
 
diff --git a/src/main.rs b/src/main.rs
index 58be2a4..5acbd4d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -17,6 +17,7 @@ mod input_manager;
 mod libinput_config;
 pub mod libinput_device;
 pub mod libinput_accel_config;
+pub mod xkb_keyboard;
 mod xkb_config;
 mod idle_inhibit_manager;
 mod lock_manager;
diff --git a/src/xkb_config.rs b/src/xkb_config.rs
index 49d940d..f814215 100644
--- a/src/xkb_config.rs
+++ b/src/xkb_config.rs
@@ -10,6 +10,7 @@ pub struct XkbConfig {
     pub context: *mut ffi::xkb_context,
     pub default_keymap: *mut ffi::xkb_keymap,
     pub objects: ffi::wl_list,
+    pub keyboards: ffi::wl_list,
 }
 
 pub struct XkbConfigObject {
@@ -28,6 +29,7 @@ impl XkbConfig {
     pub unsafe fn init(&mut self, server: *mut Server) -> Result<(), &'static str> {
         self.server = server;
         ffi::wl_list_init(&mut self.objects);
+        ffi::wl_list_init(&mut self.keyboards);
 
         let context = ffi::xkb_context_new(ffi::xkb_context_flags_XKB_CONTEXT_NO_FLAGS);
         if context.is_null() {
@@ -119,6 +121,15 @@ unsafe extern "C" fn bind_xkb_config(
 
     let list_head = &mut (*config).objects as *mut ffi::wl_list as *mut WlList;
     wl_list_insert((*list_head).prev, &mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+
+    let keyboards_head = &mut (*config).keyboards as *mut ffi::wl_list as *mut WlList;
+    let mut curr = (*keyboards_head).next;
+    while curr != keyboards_head {
+        let next = (*curr).next;
+        let xkb_kbd = crate::container_of!(curr, crate::xkb_keyboard::XkbKeyboard, link);
+        (*xkb_kbd).create_object(resource);
+        curr = next;
+    }
 }
 
 unsafe extern "C" fn handle_object_destroy(resource: *mut ffi::wl_resource) {
diff --git a/src/xkb_keyboard.rs b/src/xkb_keyboard.rs
new file mode 100644
index 0000000..682df0f
--- /dev/null
+++ b/src/xkb_keyboard.rs
@@ -0,0 +1,497 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::input_device::InputDevice;
+use crate::server::WlList;
+
+pub struct XkbKeyboardState {
+    pub layout_index: Option<u32>,
+    pub layout_name: Option<*const std::os::raw::c_char>,
+    pub capslock: Option<bool>,
+    pub numlock: Option<bool>,
+}
+
+pub struct XkbKeyboard {
+    pub parent_device: *mut InputDevice,
+    pub objects: ffi::wl_list, // list of XkbKeyboardObject
+    pub link: ffi::wl_list,    // link inside XkbConfig::keyboards
+    pub sent: XkbKeyboardState,
+}
+
+pub struct XkbKeyboardObject {
+    pub keyboard: *mut XkbKeyboard,
+    pub resource: *mut ffi::wl_resource,
+    pub link: ffi::wl_list,
+}
+
+impl XkbKeyboard {
+    pub unsafe fn init(
+        parent_device: *mut InputDevice,
+    ) -> Box<Self> {
+        let mut xkb_kbd = Box::new(Self {
+            parent_device,
+            objects: std::mem::zeroed(),
+            link: std::mem::zeroed(),
+            sent: XkbKeyboardState {
+                layout_index: None,
+                layout_name: None,
+                capslock: None,
+                numlock: None,
+            },
+        });
+
+        ffi::wl_list_init(&mut xkb_kbd.objects);
+        ffi::wl_list_init(&mut xkb_kbd.link);
+
+        let server = (*(*parent_device).seat).server;
+        let keyboards_head = &mut (*server).xkb_config.keyboards as *mut ffi::wl_list as *mut WlList;
+        crate::server::wl_list_insert((*keyboards_head).prev, &mut xkb_kbd.link as *mut ffi::wl_list as *mut WlList);
+
+        let config_objects = &mut (*server).xkb_config.objects as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*config_objects).next;
+        while curr != config_objects {
+            let next = (*curr).next;
+            let config_obj = crate::container_of!(curr, crate::xkb_config::XkbConfigObject, link);
+            xkb_kbd.create_object((*config_obj).resource);
+            curr = next;
+        }
+
+        xkb_kbd
+    }
+
+    pub unsafe fn deinit(&mut self) {
+        let objects_head = &mut self.objects as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*objects_head).next;
+        while curr != objects_head {
+            let next = (*curr).next;
+            let obj = crate::container_of!(curr, XkbKeyboardObject, link);
+
+            crate::server::wl_list_remove(curr);
+            ffi::wl_list_init(curr as *mut ffi::wl_list);
+
+            ffi::wl_resource_post_event((*obj).resource, 0); // removed event
+
+            ffi::wl_resource_set_implementation(
+                (*obj).resource,
+                &XKB_KEYBOARD_INERT_INTERFACE as *const _ as *const _,
+                obj as *mut _,
+                Some(handle_xkb_keyboard_object_destroy),
+            );
+
+            curr = next;
+        }
+
+        crate::server::wl_list_remove(&mut self.link as *mut ffi::wl_list as *mut WlList);
+    }
+
+    pub unsafe fn create_object(&mut self, config_v1_resource: *mut ffi::wl_resource) {
+        let client = ffi::wl_resource_get_client(config_v1_resource);
+        let version = ffi::wl_resource_get_version(config_v1_resource);
+
+        let resource = ffi::wl_resource_create(
+            client,
+            &ffi::river_xkb_keyboard_v1_interface,
+            version,
+            0,
+        );
+        if resource.is_null() {
+            log::error!("out of memory creating river_xkb_keyboard_v1");
+            ffi::wl_client_post_no_memory(client);
+            return;
+        }
+
+        let obj = Box::into_raw(Box::new(XkbKeyboardObject {
+            keyboard: self,
+            resource,
+            link: std::mem::zeroed(),
+        }));
+
+        ffi::wl_resource_set_implementation(
+            resource,
+            &XKB_KEYBOARD_INTERFACE as *const _ as *const _,
+            obj as *mut _,
+            Some(handle_xkb_keyboard_object_destroy),
+        );
+
+        let list_head = &mut self.objects as *mut ffi::wl_list as *mut WlList;
+        crate::server::wl_list_insert(list_head, &mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+
+        // Send xkb_keyboard event to config_v1
+        ffi::wl_resource_post_event(config_v1_resource, 1, resource); // opcode 1: xkb_keyboard
+
+        // Pair with input device resource
+        let parent_dev = self.parent_device;
+        let input_dev_objects = &mut (*parent_dev).objects as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*input_dev_objects).next;
+        while curr != input_dev_objects {
+            let next = (*curr).next;
+            let input_dev_obj = crate::container_of!(curr, crate::input_device::InputDeviceObject, link);
+            if ffi::wl_resource_get_client((*input_dev_obj).resource) == client {
+                ffi::wl_resource_post_event(resource, 1, (*input_dev_obj).resource); // input_device event (opcode 1)
+            }
+            curr = next;
+        }
+
+        // Send current cached state to client
+        let sent = &self.sent;
+        if let Some(layout_index) = sent.layout_index {
+            let name_ptr = sent.layout_name.unwrap_or(std::ptr::null());
+            ffi::wl_resource_post_event(resource, 2, layout_index, name_ptr); // layout event (opcode 2)
+        }
+        if let Some(capslock) = sent.capslock {
+            if capslock {
+                ffi::wl_resource_post_event(resource, 3); // capslock_enabled (opcode 3)
+            } else {
+                ffi::wl_resource_post_event(resource, 4); // capslock_disabled (opcode 4)
+            }
+        }
+        if let Some(numlock) = sent.numlock {
+            if numlock {
+                ffi::wl_resource_post_event(resource, 5); // numlock_enabled (opcode 5)
+            } else {
+                ffi::wl_resource_post_event(resource, 6); // numlock_disabled (opcode 6)
+            }
+        }
+
+        if version >= 2 {
+            ffi::wl_resource_post_event(resource, 7); // done event (opcode 7)
+        }
+    }
+
+    pub unsafe fn send_state(
+        &mut self,
+        layout_index: u32,
+        layout_name: *const std::os::raw::c_char,
+        capslock: bool,
+        numlock: bool,
+    ) {
+        let sent = &mut self.sent;
+        let objects_head = &mut self.objects as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*objects_head).next;
+        while curr != objects_head {
+            let next = (*curr).next;
+            let obj = crate::container_of!(curr, XkbKeyboardObject, link);
+            let resource = (*obj).resource;
+            let version = ffi::wl_resource_get_version(resource);
+
+            let mut send_done = false;
+
+            // Check layout changed
+            let layout_changed = match sent.layout_index {
+                None => true,
+                Some(idx) => idx != layout_index,
+            } || match (sent.layout_name, layout_name.is_null()) {
+                (None, false) => true,
+                (Some(_), true) => true,
+                (Some(old_ptr), false) => {
+                    libc::strcmp(old_ptr, layout_name) != 0
+                }
+                (None, true) => false,
+            };
+
+            if layout_changed {
+                ffi::wl_resource_post_event(resource, 2, layout_index, layout_name); // layout event (opcode 2)
+                send_done = true;
+            }
+
+            // Check capslock changed
+            let capslock_changed = match sent.capslock {
+                None => true,
+                Some(state) => state != capslock,
+            };
+            if capslock_changed {
+                if capslock {
+                    ffi::wl_resource_post_event(resource, 3); // capslock_enabled
+                } else {
+                    ffi::wl_resource_post_event(resource, 4); // capslock_disabled
+                }
+                send_done = true;
+            }
+
+            // Check numlock changed
+            let numlock_changed = match sent.numlock {
+                None => true,
+                Some(state) => state != numlock,
+            };
+            if numlock_changed {
+                if numlock {
+                    ffi::wl_resource_post_event(resource, 5); // numlock_enabled
+                } else {
+                    ffi::wl_resource_post_event(resource, 6); // numlock_disabled
+                }
+                send_done = true;
+            }
+
+            if send_done && version >= 2 {
+                ffi::wl_resource_post_event(resource, 7); // done event
+            }
+
+            curr = next;
+        }
+
+        sent.layout_index = Some(layout_index);
+        sent.layout_name = if layout_name.is_null() { None } else { Some(layout_name) };
+        sent.capslock = Some(capslock);
+        sent.numlock = Some(numlock);
+    }
+}
+
+unsafe extern "C" fn handle_xkb_keyboard_object_destroy(resource: *mut ffi::wl_resource) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut XkbKeyboardObject;
+    if !obj.is_null() {
+        crate::server::wl_list_remove(&mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+        let _ = Box::from_raw(obj);
+    }
+}
+
+static XKB_KEYBOARD_INTERFACE: ffi::river_xkb_keyboard_v1_interface = ffi::river_xkb_keyboard_v1_interface {
+    destroy: Some(xkb_keyboard_destroy),
+    set_keymap: Some(xkb_keyboard_set_keymap),
+    set_layout_by_index: Some(xkb_keyboard_set_layout_by_index),
+    set_layout_by_name: Some(xkb_keyboard_set_layout_by_name),
+    capslock_enable: Some(xkb_keyboard_capslock_enable),
+    capslock_disable: Some(xkb_keyboard_capslock_disable),
+    numlock_enable: Some(xkb_keyboard_numlock_enable),
+    numlock_disable: Some(xkb_keyboard_numlock_disable),
+};
+
+static XKB_KEYBOARD_INERT_INTERFACE: ffi::river_xkb_keyboard_v1_interface = ffi::river_xkb_keyboard_v1_interface {
+    destroy: Some(xkb_keyboard_destroy),
+    set_keymap: None,
+    set_layout_by_index: None,
+    set_layout_by_name: None,
+    capslock_enable: None,
+    capslock_disable: None,
+    numlock_enable: None,
+    numlock_disable: None,
+};
+
+unsafe extern "C" fn xkb_keyboard_destroy(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn xkb_keyboard_set_keymap(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    keymap_resource: *mut ffi::wl_resource,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut XkbKeyboardObject;
+    if obj.is_null() {
+        return;
+    }
+    let keyboard = (*(*obj).keyboard).parent_device;
+    let keymap_struct = ffi::wl_resource_get_user_data(keymap_resource) as *mut crate::xkb_config::XkbKeymap;
+    if keymap_struct.is_null() {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_xkb_keyboard_v1_error_RIVER_XKB_KEYBOARD_V1_ERROR_INVALID_KEYMAP,
+            b"client set invalid keymap\0".as_ptr() as *const _,
+        );
+        return;
+    }
+
+    let kbd = (*keyboard).destroy_data as *mut crate::keyboard::Keyboard;
+    if !kbd.is_null() {
+        (*kbd).set_keymap((*keymap_struct).xkb_keymap);
+    }
+}
+
+unsafe extern "C" fn xkb_keyboard_set_layout_by_index(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    index: i32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut XkbKeyboardObject;
+    if obj.is_null() {
+        return;
+    }
+    let keyboard = (*(*obj).keyboard).parent_device;
+    let kbd = (*keyboard).destroy_data as *mut crate::keyboard::Keyboard;
+    if kbd.is_null() {
+        return;
+    }
+    let group = (*kbd).group;
+    if group.is_null() {
+        return;
+    }
+
+    let keymap = (*group).config.keymap;
+    if keymap.is_null() {
+        return;
+    }
+
+    let num_layouts = ffi::xkb_keymap_num_layouts(keymap);
+    if index < 0 || (index as u32) >= num_layouts {
+        return;
+    }
+
+    let mut modifiers = (*group).wlr_keyboard.modifiers;
+    modifiers.group = index as u32;
+    (*group).process_modifiers(modifiers);
+}
+
+unsafe extern "C" fn xkb_keyboard_set_layout_by_name(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    name: *const std::os::raw::c_char,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut XkbKeyboardObject;
+    if obj.is_null() || name.is_null() {
+        return;
+    }
+    let keyboard = (*(*obj).keyboard).parent_device;
+    let kbd = (*keyboard).destroy_data as *mut crate::keyboard::Keyboard;
+    if kbd.is_null() {
+        return;
+    }
+    let group = (*kbd).group;
+    if group.is_null() {
+        return;
+    }
+
+    let keymap = (*group).config.keymap;
+    if keymap.is_null() {
+        return;
+    }
+
+    let index = ffi::xkb_keymap_layout_get_index(keymap, name);
+    if index == ffi::XKB_LAYOUT_INVALID {
+        return;
+    }
+
+    let mut modifiers = (*group).wlr_keyboard.modifiers;
+    modifiers.group = index;
+    (*group).process_modifiers(modifiers);
+}
+
+unsafe extern "C" fn xkb_keyboard_capslock_enable(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut XkbKeyboardObject;
+    if obj.is_null() {
+        return;
+    }
+    let keyboard = (*(*obj).keyboard).parent_device;
+    let kbd = (*keyboard).destroy_data as *mut crate::keyboard::Keyboard;
+    if kbd.is_null() {
+        return;
+    }
+    let group = (*kbd).group;
+    if group.is_null() {
+        return;
+    }
+
+    let keymap = (*group).config.keymap;
+    if keymap.is_null() {
+        return;
+    }
+
+    let caps_idx = ffi::xkb_keymap_mod_get_index(keymap, b"Caps Lock\0".as_ptr() as *const _);
+    if caps_idx != ffi::XKB_MOD_INVALID {
+        let mask = 1 << caps_idx;
+        let mut modifiers = (*group).wlr_keyboard.modifiers;
+        modifiers.locked |= mask;
+        (*group).process_modifiers(modifiers);
+    }
+}
+
+unsafe extern "C" fn xkb_keyboard_capslock_disable(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut XkbKeyboardObject;
+    if obj.is_null() {
+        return;
+    }
+    let keyboard = (*(*obj).keyboard).parent_device;
+    let kbd = (*keyboard).destroy_data as *mut crate::keyboard::Keyboard;
+    if kbd.is_null() {
+        return;
+    }
+    let group = (*kbd).group;
+    if group.is_null() {
+        return;
+    }
+
+    let keymap = (*group).config.keymap;
+    if keymap.is_null() {
+        return;
+    }
+
+    let caps_idx = ffi::xkb_keymap_mod_get_index(keymap, b"Caps Lock\0".as_ptr() as *const _);
+    if caps_idx != ffi::XKB_MOD_INVALID {
+        let mask = 1 << caps_idx;
+        let mut modifiers = (*group).wlr_keyboard.modifiers;
+        modifiers.locked &= !mask;
+        (*group).process_modifiers(modifiers);
+    }
+}
+
+unsafe extern "C" fn xkb_keyboard_numlock_enable(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut XkbKeyboardObject;
+    if obj.is_null() {
+        return;
+    }
+    let keyboard = (*(*obj).keyboard).parent_device;
+    let kbd = (*keyboard).destroy_data as *mut crate::keyboard::Keyboard;
+    if kbd.is_null() {
+        return;
+    }
+    let group = (*kbd).group;
+    if group.is_null() {
+        return;
+    }
+
+    let keymap = (*group).config.keymap;
+    if keymap.is_null() {
+        return;
+    }
+
+    let num_idx = ffi::xkb_keymap_mod_get_index(keymap, b"Num Lock\0".as_ptr() as *const _);
+    if num_idx != ffi::XKB_MOD_INVALID {
+        let mask = 1 << num_idx;
+        let mut modifiers = (*group).wlr_keyboard.modifiers;
+        modifiers.locked |= mask;
+        (*group).process_modifiers(modifiers);
+    }
+}
+
+unsafe extern "C" fn xkb_keyboard_numlock_disable(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut XkbKeyboardObject;
+    if obj.is_null() {
+        return;
+    }
+    let keyboard = (*(*obj).keyboard).parent_device;
+    let kbd = (*keyboard).destroy_data as *mut crate::keyboard::Keyboard;
+    if kbd.is_null() {
+        return;
+    }
+    let group = (*kbd).group;
+    if group.is_null() {
+        return;
+    }
+
+    let keymap = (*group).config.keymap;
+    if keymap.is_null() {
+        return;
+    }
+
+    let num_idx = ffi::xkb_keymap_mod_get_index(keymap, b"Num Lock\0".as_ptr() as *const _);
+    if num_idx != ffi::XKB_MOD_INVALID {
+        let mask = 1 << num_idx;
+        let mut modifiers = (*group).wlr_keyboard.modifiers;
+        modifiers.locked &= !mask;
+        (*group).process_modifiers(modifiers);
+    }
+}