Allow temporary VM keyboard capture with user consent

Original forum link
https://forum.qubes-os.org/t/36062
Original poster
MellowPoison
Created at
2025-09-11 15:40:06
Posts count
1
Likes count
1

This is useful when using remote-desktop applications (e.g. VNC/RDP client). By following this guide, you'll be able to use the keyboard shortcut Left Ctrl + Right Ctrl to capture/release the dom0 keyboard by the qube to which the currently focused window belongs to.

Install qubes-input-proxy-receiver package in the template of a qube that you want to be able to capture the dom0 keyboard.

In dom0 install python3-evdev package:

sudo qubes-dom0-update python3-evdev

Create a python program in dom0 that will capture and release the keyboard:

mkdir -p /home/user/bin
cat << 'EOF' | tee /home/user/bin/kbcapture.py > /dev/null
#!/usr/bin/python3
# First argument is a path to a keyboard input device e.g. /dev/input/eventX

import evdev
import subprocess
import re
import sys

def notify_send(summary, body):
    display = ':'+subprocess.check_output(['ls', '/tmp/.X11-unix/']).decode('utf-8').replace('X', '')[0]
    who = subprocess.check_output(['who']).decode('utf-8')
    user = [line for line in who.split('\n') if f'({display})' in line][0].split(None, 1)[0]
    uid = subprocess.check_output(['id', '-u', user]).decode('utf-8').rstrip()
    subprocess.Popen(['sudo', '-u', user, f'DISPLAY={display}', f'DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{uid}/bus', 'notify-send', summary, body])

if len(sys.argv) == 1:
    notify_send('Keyboard capture ERROR', f"Specify the path to a keyboard input device as an argument.")
    sys.exit(1)

window = subprocess.check_output(['xdotool', 'getwindowfocus'])
try:
    qube_name = re.findall('"([^"]*)"', subprocess.check_output(['xprop', '_QUBES_VMNAME', '-id', window]).decode('utf-8'))[0]
except:
    notify_send('Keyboard capture ERROR', f"Couldn't get the qube's name from the focused window.")
    sys.exit(1)

try:
    kbd = evdev.InputDevice(sys.argv[1])
except:
    notify_send('Keyboard capture ERROR', f"Couldn't open the keyboard input device. Make sure that you passed the correct path as an argument.")
    sys.exit(1)

kbd.grab()

RCtrl = False
LCtrl = False

with evdev.UInput.from_device(kbd, name='kbdcapture') as ui:
    p=subprocess.Popen(['qvm-run', '-u', 'root', '--pass-io', f'--localcmd=input-proxy-sender {ui.device.path}', qube_name, 'input-proxy-receiver --keyboard'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
    notify_send('Keyboard capture', f'dom0 keyboard is captured by the qube {qube_name}')
    for ev in kbd.read_loop():
        if ev.type == evdev.ecodes.EV_KEY:
            if (ev.code == evdev.ecodes.KEY_LEFTCTRL and ev.value == 1 and RCtrl or
                ev.code == evdev.ecodes.KEY_RIGHTCTRL and ev.value == 1 and LCtrl):
                p.terminate()
                notify_send('Keyboard capture', f'dom0 keyboard is released from the qube {qube_name}')
                kbd.ungrab()
                break
            else:
                ui.write(evdev.ecodes.EV_KEY, ev.code, ev.value)
            LCtrl = (ev.code == evdev.ecodes.KEY_LEFTCTRL and ev.value)
            RCtrl = (ev.code == evdev.ecodes.KEY_RIGHTCTRL and ev.value)
        else:
            ui.write(ev.type, ev.code, ev.value)
EOF
chmod +x /home/user/bin/kbcapture.py

Determine the path to the keyboard input device in dom0 (/dev/input/eventX). Run this command in dom0 to list all available input devices:

sudo libinput list-devices
Find the input device with Capabilities: keyboard corresponding to your keyboard, for example:
...

Device:           AT Translated Set 2 keyboard
Kernel:           /dev/input/event2
Group:            3
Seat:             seat0, default
Capabilities:     keyboard 
Tap-to-click:     n/a
Tap-and-drag:     n/a
Tap drag lock:    n/a
Left-handed:      n/a
Nat.scrolling:    n/a
Middle emulation: n/a
Calibration:      n/a
Scroll methods:   none
Click methods:    none
Disable-w-typing: n/a
Disable-w-trackpointing: n/a
Accel profiles:   n/a
Rotation:         n/a

...
dev/input/event2 here is the path that you'll need.

Configure keyboard shortcut to run this program: Open Qubes App Menu Q -> Gear icon -> System Settings -> Keyboard -> Application Shortcuts -> Add button Set command to this one, but change /dev/input/eventX to the correct path of your keyboard input device:

sudo /home/user/bin/kbcapture.py /dev/input/eventX
Press OK button and in the next window that will ask you to "Press keyboard keys to trigger the command" press Left Ctrl + Right Ctrl.

Related github feature request: https://github.com/QubesOS/qubes-issues/issues/9785

If you need to allow the temporary VM keyboard capture for HVM Windows qubes or qubes without qubes tools installed, you can use this solution instead: https://forum.qubes-os.org/t/mouse-and-keyboard-passthrough-to-windows-hvm/16905