//! Keyboard input on `Forge`: raw key events, the compositor keybinding table
//! (`keyboard_action`), user-defined bindings, virtual key injection and the
//! xkb config. Operates on `Forge`.

use super::*;

impl Forge {
    pub fn set_xkb_config(&mut self, config: XkbConfig) {
        let Some(keyboard) = self.seat.get_keyboard() else { return };

        let num_lock = keyboard.modifier_state().num_lock;

        if let Err(e) = keyboard.set_xkb_config(self, config) {
            error!("XKB config update failed: {e}");
            return;
        }

        let mut mods = keyboard.modifier_state();
        if mods.num_lock != num_lock {
            mods.num_lock = num_lock;
            keyboard.set_modifier_state(mods);
        }
    }

    pub(super) fn on_keyboard_input(&mut self, code: Keycode, state: KeyState, time: u32) {
        let Some(keyboard) = self.seat.get_keyboard() else { return };
        let serial = SERIAL_COUNTER.next_serial();

        let action = keyboard.input(self, code, state, serial, time, |forge, mods, keysym| {
            Self::keyboard_action(forge, mods, keysym, state)
        });

        if let Some(InputAction::ChangeVt(vt)) = action {
            self.backend.change_vt(vt);
        }
    }

    pub(super) fn send_virtual_key(&mut self, keysym: u32, mods: Modifiers) {
        let Some(keyboard) = self.seat.get_keyboard() else { return };

        let keycodes = self.keysym_to_keycode(keysym);
        let Some(keycode) = keycodes.first() else { return };

        let old_mods = keyboard.with_pressed_keysyms(|keysyms| {
            keysyms
                .iter()
                .filter(|k| {
                    let sym = k.modified_sym().raw();
                    (!mods.shift && (sym | 1 == keysyms::KEY_Shift_L))
                        || (!mods.alt && (sym | 1 == keysyms::KEY_Alt_L))
                        || (!mods.logo && (sym | 1 == keysyms::KEY_Super_L))
                        || (!mods.control && (sym | 1 == keysyms::KEY_Control_L))
                })
                .map(|k| k.raw_code().raw())
                .collect::<Vec<_>>()
        });

        let mut new_mods = Vec::new();
        let current = keyboard.modifier_state();
        if mods.shift && !current.shift {
            new_mods.push(50);
        }
        if mods.control && !current.ctrl {
            new_mods.push(37);
        }
        if mods.alt && !current.alt {
            new_mods.push(64);
        }
        if mods.logo && !current.logo {
            new_mods.push(133);
        }

        for &m in &old_mods {
            self.on_keyboard_input(Keycode::new(m), KeyState::Released, 0);
        }
        for &m in &new_mods {
            self.on_keyboard_input(Keycode::new(m), KeyState::Pressed, 0);
        }

        let raw = keycode.raw();
        self.on_keyboard_input(Keycode::new(raw), KeyState::Pressed, 0);
        self.on_keyboard_input(Keycode::new(raw), KeyState::Released, 0);

        for &m in &new_mods {
            self.on_keyboard_input(Keycode::new(m), KeyState::Released, 0);
        }
        for &m in &old_mods {
            self.on_keyboard_input(Keycode::new(m), KeyState::Pressed, 0);
        }
    }

    fn keyboard_action(
        forge: &mut Forge,
        mods: &ModifiersState,
        keysym: KeysymHandle,
        state: KeyState,
    ) -> FilterResult<InputAction> {
        let sym = keysym.modified_sym().raw();
        let raw_sym = keysym.raw_syms().first().map(|s| s.raw()).unwrap_or(sym);

        // Polkit auth dialog — modal, intercepts all keyboard
        if forge.auth_active() {
            if state == KeyState::Pressed {
                let intent = match sym {
                    keysyms::KEY_Return | keysyms::KEY_KP_Enter => Some(UiIntent::Authenticate),
                    keysyms::KEY_Escape => Some(UiIntent::CancelAuthentication),
                    keysyms::KEY_BackSpace => Some(UiIntent::AuthBackspace),
                    s => char::from_u32(s)
                        .filter(|c| !c.is_control())
                        .map(UiIntent::AuthTextInput),
                };
                if let Some(intent) = intent {
                    forge.dispatch(intent);
                }
            }
            return InputAction::None.into();
        }

        // Portal consent dialog — modal, intercepts all keyboard. Granting
        // needs a deliberate click/tap on Allow; a stray Enter (e.g. typed
        // into a terminal without seeing the dialog) must not consent, so the
        // keyboard can only deny.
        if forge.windows.confirm.visible {
            if state == KeyState::Pressed && sym == keysyms::KEY_Escape {
                forge.dispatch(UiIntent::PortalConsent(false));
            }
            return InputAction::None.into();
        }

        // Ctrl+Alt+Backspace: quit compositor (return to login screen)
        if mods.ctrl && mods.alt && state == KeyState::Pressed
            && raw_sym == keysyms::KEY_BackSpace
        {
            forge.terminated = true;
            return InputAction::None.into();
        }

        // Ctrl+Alt+F<N>: VT switch. Mirrors the XF86Switch_VT_<N> match in the
        // big keysym table below, but kicks in even when the xkb layout does
        // not translate Ctrl+Alt+F<N> to the symbolic keysym (depends on
        // model/options). Pressed-only to avoid a duplicate switch on release.
        if mods.ctrl && mods.alt && state == KeyState::Pressed
            && matches!(raw_sym, keysyms::KEY_F1..=keysyms::KEY_F12)
        {
            return InputAction::ChangeVt(
                (raw_sym - keysyms::KEY_F1 + 1) as i32,
            ).into();
        }

        // Auto-dismiss OSD on any keypress (it'll also expire naturally)
        if forge.shell.osd.visible {
            forge.shell.osd.maybe_expire();
        }

        match (raw_sym, state) {
            // Volume/brightness keys -> built-in OSD
            (keysyms::KEY_XF86AudioRaiseVolume, KeyState::Pressed) => {
                let (percent, muted) = crate::osd::adjust_volume(true);
                forge.shell.osd.show(crate::osd::OsdKind::Volume, percent, muted);
                forge.shell.sys_state.volume.percent = percent;
                forge.shell.sys_state.volume.muted = muted;
                forge.request_redraw();
                InputAction::None.into()
            },
            (keysyms::KEY_XF86AudioLowerVolume, KeyState::Pressed) => {
                let (percent, muted) = crate::osd::adjust_volume(false);
                forge.shell.osd.show(crate::osd::OsdKind::Volume, percent, muted);
                forge.shell.sys_state.volume.percent = percent;
                forge.shell.sys_state.volume.muted = muted;
                forge.request_redraw();
                InputAction::None.into()
            },
            (keysyms::KEY_XF86AudioMute, KeyState::Pressed) => {
                let (percent, muted) = crate::osd::toggle_mute();
                forge.shell.osd.show(crate::osd::OsdKind::Volume, percent, muted);
                forge.shell.sys_state.volume.percent = percent;
                forge.shell.sys_state.volume.muted = muted;
                forge.request_redraw();
                InputAction::None.into()
            },
            (keysyms::KEY_XF86MonBrightnessUp, KeyState::Pressed) => {
                let percent = crate::osd::adjust_brightness(true);
                forge.shell.osd.show(crate::osd::OsdKind::Brightness, percent, false);
                forge.request_redraw();
                InputAction::None.into()
            },
            (keysyms::KEY_XF86MonBrightnessDown, KeyState::Pressed) => {
                let percent = crate::osd::adjust_brightness(false);
                forge.shell.osd.show(crate::osd::OsdKind::Brightness, percent, false);
                forge.request_redraw();
                InputAction::None.into()
            },
            // Print Screen: take screenshot
            (keysyms::KEY_Print, KeyState::Pressed) if !mods.shift => {
                forge.take_screenshot();
                InputAction::None.into()
            },
            // Shift+Print Screen: toggle screencast recording
            (keysyms::KEY_Print, KeyState::Pressed) if mods.shift => {
                forge.toggle_screencast();
                InputAction::None.into()
            },
            (keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12, KeyState::Pressed) => {
                InputAction::ChangeVt((raw_sym - keysyms::KEY_XF86Switch_VT_1 + 1) as i32).into()
            },
            // Power button: short press = lock + display off, long press = power menu
            (keysyms::KEY_XF86PowerOff, KeyState::Pressed) => {
                forge.session.power_key_pressed_at = Some(Instant::now());
                InputAction::None.into()
            },
            (keysyms::KEY_XF86PowerOff, KeyState::Released) => {
                if let Some(pressed_at) = forge.session.power_key_pressed_at.take() {
                    if pressed_at.elapsed() >= Duration::from_millis(800) {
                        if !Self::send_to_crustace(r#"{"action":"show_power_menu"}"#) {
                            forge.show_power_off_dialog();
                        }
                    } else {
                        info!("power key short press: locking and turning display off");
                        forge.lock_session();
                        forge.set_display_status(false);
                    }
                }
                forge.request_redraw();
                InputAction::None.into()
            },
            // The screenshot selector is modal over the app switcher, so its keys go first.
            (keysyms::KEY_Return | keysyms::KEY_KP_Enter, KeyState::Pressed)
                if forge.shell.screenshot.is_active() =>
            {
                if let Some(region) = forge.shell.screenshot.confirm() {
                    forge.screenshot_region_to_clipboard(region);
                } else {
                    forge.request_redraw();
                }
                InputAction::None.into()
            },
            // Esc while the screenshot selector is up: cancel it.
            (keysyms::KEY_Escape, KeyState::Pressed) if forge.shell.screenshot.is_active() => {
                forge.shell.screenshot.cancel();
                forge.request_redraw();
                InputAction::None.into()
            },
            // Once the app switcher is visible, plain Tab/arrows cycle
            // the selection and Enter activates it — modifier-less so the
            // user can navigate after the corner-gesture / mouse-driven
            // open.
            (keysyms::KEY_Tab | keysyms::KEY_ISO_Left_Tab, KeyState::Pressed)
                if forge.shell.app_switcher.is_active() =>
            {
                let step = if mods.shift { -1 } else { 1 };
                forge.shell.app_switcher.step(step);
                forge.windows.set_dirty();
                forge.unstall();
                InputAction::None.into()
            },
            // Shift+arrow in the switcher moves the selected window to the
            // next / previous screen instead of moving the selection: the
            // tile travels to that screen's panel, so the result is visible
            // where the window now lives.
            (keysyms::KEY_Right | keysyms::KEY_Left, KeyState::Pressed)
                if forge.shell.app_switcher.is_active() && mods.shift =>
            {
                let forward = sym == keysyms::KEY_Right;
                if let Some(oid) = forge.shell.app_switcher.selected_object_id() {
                    if forge.windows.move_window_to_next_output(&oid, forward) {
                        let entries = forge.windows.app_switcher_entries();
                        forge.shell.app_switcher.refresh_entries(entries);
                    }
                }
                forge.windows.set_dirty();
                forge.unstall();
                InputAction::None.into()
            },
            (keysyms::KEY_Right, KeyState::Pressed) if forge.shell.app_switcher.is_active() => {
                forge.shell.app_switcher.step(1);
                forge.windows.set_dirty();
                forge.unstall();
                InputAction::None.into()
            },
            (keysyms::KEY_Left, KeyState::Pressed) if forge.shell.app_switcher.is_active() => {
                forge.shell.app_switcher.step(-1);
                forge.windows.set_dirty();
                forge.unstall();
                InputAction::None.into()
            },
            (keysyms::KEY_Down, KeyState::Pressed) if forge.shell.app_switcher.is_active() => {
                let per_row = forge.shell.app_switcher.per_row(forge.windows.canvas().is_mobile()) as i32;
                forge.shell.app_switcher.step(per_row);
                forge.windows.set_dirty();
                forge.unstall();
                InputAction::None.into()
            },
            (keysyms::KEY_Up, KeyState::Pressed) if forge.shell.app_switcher.is_active() => {
                let per_row = forge.shell.app_switcher.per_row(forge.windows.canvas().is_mobile()) as i32;
                forge.shell.app_switcher.step(-per_row);
                forge.windows.set_dirty();
                forge.unstall();
                InputAction::None.into()
            },
            (keysyms::KEY_Return | keysyms::KEY_KP_Enter, KeyState::Pressed)
                if forge.shell.app_switcher.is_active() =>
            {
                if let Some(oid) = forge.shell.app_switcher.commit_object_id() {
                    forge.windows.activate_by_object_id(&oid);
                    if let Some(s) = forge.windows.activate_x11_by_object_id(&oid) {
                        if let Some(xwm) = forge.xwm.as_mut() {
                            let _ = xwm.raise_window(&s);
                        }
                        let _ = s.set_activated(true);
                    }
                }
                forge.request_redraw();
                InputAction::None.into()
            },
            // Alt+Tab / Alt+Shift+Tab: Windows-style window switcher.
            // First press shows the overlay and selects next/prev window;
            // subsequent presses while Alt is held cycle the selection.
            (keysyms::KEY_Tab | keysyms::KEY_ISO_Left_Tab, KeyState::Pressed)
                if mods.alt && !forge.windows.is_locked() =>
            {
                let step = if mods.shift { -1 } else { 1 };
                if forge.shell.app_switcher.is_active() {
                    forge.shell.app_switcher.step(step);
                } else {
                    let entries = forge.windows.app_switcher_entries();
                    let focused = forge.windows.focused_object_id();
                    forge.shell.app_switcher.begin(entries, step, focused);
                    forge.hide_osk();
                }
                forge.windows.set_dirty();
                forge.unstall();
                InputAction::None.into()
            },
            // Super+Tab / Super+Shift+Tab: alias for Alt+Tab so the
            // switcher is reachable in nested winit sessions even when the
            // host compositor swallows Alt+Tab.
            (keysyms::KEY_Tab | keysyms::KEY_ISO_Left_Tab, KeyState::Pressed)
                if mods.logo && !forge.windows.is_locked() =>
            {
                let step = if mods.shift { -1 } else { 1 };
                if forge.shell.app_switcher.is_active() {
                    forge.shell.app_switcher.step(step);
                } else {
                    let entries = forge.windows.app_switcher_entries();
                    let focused = forge.windows.focused_object_id();
                    forge.shell.app_switcher.begin(entries, step, focused);
                    forge.hide_osk();
                }
                forge.windows.set_dirty();
                forge.unstall();
                InputAction::None.into()
            },
            // Super release while switcher visible: commit selection.
            (keysyms::KEY_Super_L | keysyms::KEY_Super_R, KeyState::Released)
                if forge.shell.app_switcher.is_active() =>
            {
                if let Some(oid) = forge.shell.app_switcher.commit_object_id() {
                    forge.windows.activate_by_object_id(&oid);
                    if let Some(s) = forge.windows.activate_x11_by_object_id(&oid) {
                        if let Some(xwm) = forge.xwm.as_mut() {
                            let _ = xwm.raise_window(&s);
                        }
                        let _ = s.set_activated(true);
                    }
                }
                forge.request_redraw();
                InputAction::None.into()
            },
            // Esc while switcher visible: cancel.
            (keysyms::KEY_Escape, KeyState::Pressed) if forge.shell.app_switcher.is_active() => {
                forge.shell.app_switcher.cancel();
                forge.request_redraw();
                InputAction::None.into()
            },
            // Type-to-search: a printable keystroke while the switcher is up
            // dismisses it and opens the launcher seeded with that character,
            // so the user can filter apps by just starting to type. The
            // launcher grabs keyboard focus on reveal; keys typed before that
            // happens are accumulated by the arm below.
            (_, KeyState::Pressed)
                if forge.shell.app_switcher.is_active()
                    && !mods.ctrl && !mods.alt && !mods.logo
                    && keysym.modified_sym().key_char().is_some_and(|c| !c.is_control()) =>
            {
                let c = keysym.modified_sym().key_char().unwrap();
                forge.shell.app_switcher.cancel();
                forge.shell.pending_launcher_query = Some((c.to_string(), Instant::now()));
                Self::send_launcher_query(&c.to_string());
                forge.request_redraw();
                InputAction::None.into()
            },
            // Launcher-reveal race: after the seed, keys routed to the
            // launcher over Wayland are lost until its interactive panel is
            // actually revealed — the always-mounted host grabs the keyboard
            // within a frame or two of opening, but during the slide-in the
            // panel is a raster with no text field mounted, so focus is not
            // readiness. While the pending window is armed, text keys go over
            // IPC instead, resending the whole query — crustace replaces it
            // on every message, so resends are idempotent.
            (_, KeyState::Pressed)
                if forge.shell.pending_launcher_query.is_some()
                    && !mods.ctrl && !mods.alt && !mods.logo
                    && (sym == keysyms::KEY_BackSpace
                        || sym == keysyms::KEY_Escape
                        || sym == keysyms::KEY_Return
                        || sym == keysyms::KEY_KP_Enter
                        || keysym.modified_sym().key_char().is_some_and(|c| !c.is_control())) =>
            {
                let expired = forge
                    .shell
                    .pending_launcher_query
                    .as_ref()
                    .is_some_and(|(_, seeded)| seeded.elapsed() > Duration::from_millis(1500));
                if expired {
                    forge.shell.pending_launcher_query = None;
                    return FilterResult::Forward;
                }
                match sym {
                    keysyms::KEY_Escape => {
                        forge.shell.pending_launcher_query = None;
                        return InputAction::None.into();
                    }
                    keysyms::KEY_Return | keysyms::KEY_KP_Enter => {
                        forge.shell.pending_launcher_query = None;
                        Self::send_to_crustace(r#"{"action":"launcher_search_submit"}"#);
                        return InputAction::None.into();
                    }
                    _ => {}
                }
                if let Some((query, _)) = forge.shell.pending_launcher_query.as_mut() {
                    if sym == keysyms::KEY_BackSpace {
                        query.pop();
                    } else if let Some(c) = keysym.modified_sym().key_char() {
                        query.push(c);
                    }
                    let query = query.clone();
                    Self::send_launcher_query(&query);
                }
                InputAction::None.into()
            },
            // Alt release while switcher visible: commit selection.
            (keysyms::KEY_Alt_L | keysyms::KEY_Alt_R, KeyState::Released)
                if forge.shell.app_switcher.is_active() =>
            {
                if let Some(oid) = forge.shell.app_switcher.commit_object_id() {
                    forge.windows.activate_by_object_id(&oid);
                    if let Some(s) = forge.windows.activate_x11_by_object_id(&oid) {
                        if let Some(xwm) = forge.xwm.as_mut() {
                            let _ = xwm.raise_window(&s);
                        }
                        let _ = s.set_activated(true);
                    }
                }
                forge.request_redraw();
                InputAction::None.into()
            },
            // Super+Up: maximize/unmaximize focused window (desktop)
            (keysyms::KEY_Up, KeyState::Pressed) if mods.logo => {
                forge.windows.toggle_maximize_focused();
                forge.request_redraw();
                InputAction::None.into()
            },
            // Super+Down: minimize focused window (desktop)
            (keysyms::KEY_Down, KeyState::Pressed) if mods.logo => {
                forge.windows.minimize_focused();
                forge.request_redraw();
                InputAction::None.into()
            },
            // Super+L: lock screen
            (keysyms::KEY_l, KeyState::Pressed) if mods.logo => {
                forge.lock_session();
                forge.request_redraw();
                InputAction::None.into()
            },
            // Super+D: show desktop (minimize all)
            (keysyms::KEY_d, KeyState::Pressed) if mods.logo => {
                forge.windows.minimize_all();
                forge.hide_osk();
                Self::send_to_crustace(r#"{"action":"show_homescreen"}"#);
                forge.request_redraw();
                InputAction::None.into()
            },
            // Super+1..9: switch to workspace by number
            (keysyms::KEY_1..=keysyms::KEY_9, KeyState::Pressed) if mods.logo => {
                let index = (raw_sym - keysyms::KEY_1) as usize;
                forge.windows.switch_workspace(index);
                forge.unstall();
                InputAction::None.into()
            },
            // Alt+F4: close focused window
            (keysyms::KEY_F4, KeyState::Pressed) if mods.alt => {
                forge.windows.close_focused();
                forge.unstall();
                InputAction::None.into()
            },
            // F11 while the screenshot selector is up: toggle the selection
            // between the full screen under the pointer and the previous
            // rectangle.
            (keysyms::KEY_F11, KeyState::Pressed) if forge.shell.screenshot.is_active() => {
                let pos = forge.input.touch_state.position;
                let full = forge
                    .windows
                    .extra_outputs()
                    .iter()
                    .map(|o| o.logical_rect())
                    .find(|r| r.to_f64().contains(pos))
                    .unwrap_or_else(|| forge.windows.primary_output().logical_rect());
                forge.shell.screenshot.toggle_fullscreen(full);
                forge.request_redraw();
                InputAction::None.into()
            },
            // F11: toggle fullscreen
            (keysyms::KEY_F11, KeyState::Pressed) => {
                forge.windows.toggle_fullscreen();
                forge.unstall();
                InputAction::None.into()
            },
            // Ctrl+L: lock screen
            (keysyms::KEY_l, KeyState::Pressed) if mods.ctrl && !mods.alt => {
                forge.lock_session();
                forge.request_redraw();
                InputAction::None.into()
            },
            // Ctrl+Return: launch terminal (try multiple terminals)
            (keysyms::KEY_Return, KeyState::Pressed) if mods.ctrl && !mods.alt => {
                Self::launch_first(
                    &["foot", "ptyxis", "konsole", "kitty", "alacritty", "gnome-terminal", "xfce4-terminal", "xterm"],
                    "Terminal",
                );
                InputAction::None.into()
            },
            // Ctrl+S: (quick settings removed)
            (keysyms::KEY_s, KeyState::Pressed) if mods.ctrl && !mods.alt => {
                InputAction::None.into()
            },
            // Ctrl+Space: cycle keyboard layout
            (keysyms::KEY_space, KeyState::Pressed) if mods.ctrl && !mods.alt => {
                forge.cycle_keyboard_layout();
                forge.unstall();
                InputAction::None.into()
            },
            // Super+Return: launch terminal
            (keysyms::KEY_Return, KeyState::Pressed) if mods.logo => {
                Self::launch_first(
                    &["foot", "ptyxis", "konsole", "kitty", "alacritty", "gnome-terminal", "xfce4-terminal", "xterm"],
                    "Terminal",
                );
                InputAction::None.into()
            },
            // Super+E: file manager
            (keysyms::KEY_e, KeyState::Pressed) if mods.logo => {
                Self::launch_first(
                    &["nautilus", "dolphin", "thunar", "nemo", "pcmanfm"],
                    "File manager",
                );
                InputAction::None.into()
            },
            // Super+B: browser
            (keysyms::KEY_b, KeyState::Pressed) if mods.logo => {
                Self::launch_first(
                    &["firefox", "firefox-esr", "chromium", "chromium-browser", "google-chrome-stable", "epiphany"],
                    "Browser",
                );
                InputAction::None.into()
            },
            // Super+A: toggle app launcher (handled by crustace via IPC)
            (keysyms::KEY_a, KeyState::Pressed) if mods.logo => {
                forge.unstall();
                InputAction::None.into()
            },
            // Super+P: power menu
            (keysyms::KEY_p, KeyState::Pressed) if mods.logo => {
                Self::send_to_crustace(r#"{"action":"show_power_menu"}"#);
                InputAction::None.into()
            },
            _ => Self::handle_user_binding(forge, mods, raw_sym, state),
        }
    }

    fn handle_user_binding(
        forge: &mut Forge,
        mods: impl Into<Modifiers>,
        raw_keysym: u32,
        state: KeyState,
    ) -> FilterResult<InputAction> {
        forge.input.touch_state.cancel_key_repeat();

        let inhibits = forge.last_focus().is_some_and(|surface| {
            compositor::with_states(surface, |states| {
                states.data_map.get::<ForgeSurfaceData>().is_some_and(|d| d.inhibits_shortcuts)
            })
        });

        if inhibits {
            return FilterResult::Forward;
        }

        let mods = mods.into();
        let active_app = forge.windows.focus().and_then(|(_, app_id)| app_id);
        let pressed = state == KeyState::Pressed;

        if pressed {
            forge.input.active_keys.insert(Keysym::Xkb(raw_keysym));
        }

        let mut result = FilterResult::Forward;

        for binding in &forge.input.key_bindings {
            let trigger_matches = match binding.trigger {
                KeyTrigger::Release => !pressed,
                _ => pressed,
            };

            if *binding.keys == forge.input.active_keys
                && binding.mods == mods
                && binding.app_id.matches(active_app.as_ref())
                && trigger_matches
            {
                let _ = daemon(&binding.program, &binding.arguments)
                    .inspect_err(|e| error!("Keybinding failed: {e}"));

                if binding.trigger == KeyTrigger::Repeat {
                    let program = binding.program.clone();
                    let arguments = binding.arguments.clone();
                    forge.input.touch_state.start_key_repeat(move || {
                        let _ = daemon(&program, &arguments)
                            .inspect_err(|e| error!("Keybinding repeat failed: {e}"));
                    });
                }

                result = InputAction::None.into();
            }
        }

        if !pressed {
            forge.input.active_keys.remove(&Keysym::Xkb(raw_keysym));
        }

        result
    }

    fn keysym_to_keycode(&mut self, keysym: u32) -> Vec<Keycode> {
        let Some(keyboard) = self.seat.get_keyboard() else {
            return Vec::new();
        };

        let mut codes = Vec::new();

        keyboard.with_xkb_state(self, |ctx| unsafe {
            let xkb = ctx.xkb().lock().unwrap();
            let layout = xkb.active_layout();
            xkb.keymap().key_for_each(|_, keycode| {
                if xkb.raw_syms_for_key_in_layout(keycode, layout).iter().any(|s| s.raw() == keysym)
                {
                    codes.push(keycode);
                }
            });
        });

        codes
    }
}
