A small user-level xdg-desktop-portal RemoteDesktop backend that makes KDE
Connect remote input work on Wayland compositors that expose virtual input
protocols.
KDE Connect already uses org.freedesktop.portal.RemoteDesktop for remote
mouse and keyboard input on Wayland. Some compositor-specific portal backends
expose screenshots, screencast, and global shortcuts, but not RemoteDesktop
input. This bridge fills that one missing portal backend interface and injects
events through zwp_virtual_keyboard_manager_v1 and
zwlr_virtual_pointer_manager_v1.
This is a compatibility shim, not a compositor plugin. It was written for Hyprland, but the core requirement is protocol support; it can also work on wlroots or protocol-compatible compositors such as sway, river, Wayfire, labwc, phosh, and niri when their portal routing is configured.
Warning
This software is 99% vibe coded with OpenAI CodeX, but have been manual audited, warn in case you mind it.
Implemented:
- relative pointer motion
- absolute pointer motion
- pointer button events
- smooth and discrete scrolling
- keyboard keycode input
- keyboard keysym input through
xkbcommon - libei
ConnectToEISsender clients through a minimallibeisbridge
Not implemented:
- touchscreen events
- InputCapture/share-input-devices edge capture
- a permission dialog
KDE Connect 26.04+ prefers ConnectToEIS on Wayland. This bridge accepts that
path and translates incoming libei pointer, scroll, and keyboard events into the
same virtual-input backend used by the RemoteDesktop Notify* methods.
Build-time:
- CMake
- a C++23 compiler
- Qt 6.5+ Core/DBus
pkg-configwayland-client1.20+wayland-scannerxkbcommon1.5+libeis1.4+
Runtime:
- a Wayland compositor exposing
zwlr_virtual_pointer_manager_v1 - a Wayland compositor exposing
zwp_virtual_keyboard_manager_v1 xdg-desktop-portallibeis- KDE Connect
On Arch-like systems the useful package set is roughly:
sudo pacman -S cmake gcc pkgconf qt6-base wayland libxkbcommon libei xdg-desktop-portalArch Linux users can also install the community-maintained
hypr-kdeconnect-fix-git
AUR package.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$HOME/.local"
cmake --build build -j"$(nproc)"
ctest --test-dir build --output-on-failure
cmake --install buildThis installs:
~/.local/bin/hypr-kdeconnect-portal~/.local/share/xdg-desktop-portal/portals/hypr-kdeconnect.portal~/.local/share/dbus-1/services/org.freedesktop.impl.portal.desktop.hypr_kdeconnect.service~/.local/share/systemd/user/hypr-kdeconnect-portal.service
The default build enables compiler/linker hardening on Linux, including PIE, RELRO/BIND_NOW, stack protector, and fortify checks when the compiler supports them.
By default, the generated portal metadata is visible when XDG_CURRENT_DESKTOP
is one of wlroots, Hyprland, sway, Wayfire, river, phosh, niri, or
labwc. If your compatible compositor uses a different desktop id, set it at
configure time:
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$HOME/.local" \
-DHKCF_PORTAL_USE_IN='wlroots;Hyprland;sway;Wayfire;river;phosh;niri;labwc;your-desktop-id'Configure xdg-desktop-portal to route only RemoteDesktop to this backend, while
leaving your normal portal providers in charge of screenshot, screencast, global
shortcuts, file chooser, and other interfaces.
Hyprland example:
~/.config/xdg-desktop-portal/portals.conf:
[preferred]
default=gtk
org.freedesktop.impl.portal.ScreenCast=hyprland
org.freedesktop.impl.portal.Screenshot=hyprland
org.freedesktop.impl.portal.GlobalShortcuts=hyprland
org.freedesktop.impl.portal.RemoteDesktop=hypr-kdeconnectniri example:
[preferred]
default=gnome;gtk;
org.freedesktop.impl.portal.Access=gtk
org.freedesktop.impl.portal.Notification=gtk
org.freedesktop.impl.portal.Secret=gnome-keyring
org.freedesktop.impl.portal.RemoteDesktop=hypr-kdeconnectFor other wlroots/protocol-compatible compositors, keep your existing
portals.conf defaults and add only:
org.freedesktop.impl.portal.RemoteDesktop=hypr-kdeconnectReload user units and restart the portal frontend after changing metadata or
portals.conf:
systemctl --user daemon-reload
systemctl --user restart xdg-desktop-portalIf D-Bus already activated an older bridge process, stop the user unit first:
systemctl --user stop hypr-kdeconnect-portal.serviceIf it was started by plain D-Bus activation instead of systemd, verify the binary path before killing it:
pid="$(busctl --user status org.freedesktop.impl.portal.desktop.hypr_kdeconnect 2>/dev/null | awk -F= '/^PID=/{print $2; exit}')"
if [ -n "$pid" ] && [ "$(readlink -f "/proc/$pid/exe" 2>/dev/null)" = "$HOME/.local/bin/hypr-kdeconnect-portal" ]; then
kill "$pid"
fiCheck that the public portal frontend exposes RemoteDesktop:
busctl --user introspect \
org.freedesktop.portal.Desktop \
/org/freedesktop/portal/desktop \
org.freedesktop.portal.RemoteDesktopCheck direct virtual pointer injection. These commands will move the pointer in the current Wayland session:
hypr-kdeconnect-portal --self-test-motion 120 0
hypr-kdeconnect-portal --self-test-absolute 1440 900
hypr-kdeconnect-portal --self-test-scroll 0 120
hypr-kdeconnect-portal --self-test-scroll-discrete 0 3On Hyprland, you can also check that the compositor sees the virtual devices:
hyprctl devices | rg 'hypr-kdeconnect|virtual|unknown-device'- KDE Connect calls the public portal frontend at
org.freedesktop.portal.Desktop. xdg-desktop-portalreadsportals.confand forwards RemoteDesktop backend calls toorg.freedesktop.impl.portal.desktop.hypr_kdeconnect.- This bridge accepts KDE Connect sessions from the portal frontend and starts virtual input devices.
- If KDE Connect calls
ConnectToEIS, libei events are received throughlibeisand translated to the same virtual input path. - If a client uses the older RemoteDesktop
Notify*calls, those are handled directly. - Pointer events are sent through
zwlr_virtual_pointer_v1. - Keyboard events are sent through
zwp_virtual_keyboard_v1.
The protocol XML files are included in this repository and compiled with
wayland-scanner, so the build does not depend on a local compositor source
tree.
Compositors that expose the virtual input protocols usually expose them to trusted local Wayland clients. This bridge does not add a graphical permission prompt on top of that, so it narrows the D-Bus attack surface instead:
- RemoteDesktop and Session methods are accepted only from the current
org.freedesktop.portal.Desktopowner. - Sessions are bound to that D-Bus sender and capped in number.
- Accepted app ids are exact KDE Connect desktop ids; substring spoofing is
rejected. Empty and
surface-transientids are accepted only when the session's D-Bus caller is verified as the system KDE Connect daemon process. - Notify calls are checked against the selected device mask and bounded before being forwarded to Wayland.
Use it as a local-session compatibility bridge. Do not install it system-wide on multi-user machines unless that trust model is acceptable.
If KDE Connect still does not move the pointer:
busctl --user status org.freedesktop.impl.portal.desktop.hypr_kdeconnect
journalctl --user _COMM=hypr-kdeconnect --since '10 min ago' --no-pagerIf the RemoteDesktop interface is missing from the public frontend, check:
cat ~/.config/xdg-desktop-portal/portals.conf
ls ~/.local/share/xdg-desktop-portal/portals/hypr-kdeconnect.portal
systemctl --user restart xdg-desktop-portalIf an old binary keeps running after reinstall:
systemctl --user stop hypr-kdeconnect-portal.service
pid="$(busctl --user status org.freedesktop.impl.portal.desktop.hypr_kdeconnect 2>/dev/null | awk -F= '/^PID=/{print $2; exit}')"
if [ -n "$pid" ] && [ "$(readlink -f "/proc/$pid/exe" 2>/dev/null)" = "$HOME/.local/bin/hypr-kdeconnect-portal" ]; then
kill "$pid"
firm -f ~/.local/bin/hypr-kdeconnect-portal
rm -f ~/.local/share/xdg-desktop-portal/portals/hypr-kdeconnect.portal
rm -f ~/.local/share/dbus-1/services/org.freedesktop.impl.portal.desktop.hypr_kdeconnect.service
rm -f ~/.local/share/systemd/user/hypr-kdeconnect-portal.service
systemctl --user daemon-reload
systemctl --user restart xdg-desktop-portalAlso remove the org.freedesktop.impl.portal.RemoteDesktop=hypr-kdeconnect line
from ~/.config/xdg-desktop-portal/portals.conf.