Skip to content

Add native KWin support (libei pointer + ScreenShot2 target detection) - #97

Open
mcdax wants to merge 20 commits into
moverest:mainfrom
mcdax:feat/kde-pointer-support
Open

Add native KWin support (libei pointer + ScreenShot2 target detection)#97
mcdax wants to merge 20 commits into
moverest:mainfrom
mcdax:feat/kde-pointer-support

Conversation

@mcdax

@mcdax mcdax commented Jun 14, 2026

Copy link
Copy Markdown

Implements #44 — native KWin support, gated behind a new optional kde Meson feature so the wlroots path and minimal builds are unchanged.

Pointer movement & clicks

KWin doesn't implement wlr-virtual-pointer, so the pointer is driven through libei over an EIS socket obtained from KWin's org.kde.KWin.EIS.RemoteDesktop.connectToEIS D-Bus method — the approach described by @Gliese852 in #44 (thanks!). move_pointer() is now a small dispatcher that picks the wlroots backend when wl_virtual_pointer is present, otherwise the KDE backend.

A couple of things that were needed to make it reliable on KWin:

  • the D-Bus connection that called connectToEIS must stay alive for the lifetime of the EIS session (KWin tears the session down otherwise);
  • ei_dispatch() has to run before poll() so the initial libei handshake is driven, and an ei_ping() round-trip flushes the final click before teardown.

Target auto-detection (floating + detect)

Since KWin also lacks wlr-screencopy, the screen is captured via KWin's org.kde.KWin.ScreenShot2 D-Bus interface (CaptureArea) and fed into the existing OpenCV pipeline unchanged. This path is built only when both opencv and kde are enabled. Because ScreenShot2 is a restricted interface, the installed desktop file now carries X-KDE-DBUS-Restricted-Interfaces=org.kde.KWin.ScreenShot2 and uses an absolute Exec (generated via configure_file); capture only works for an installed binary, not from the build tree.

Build

meson setup build -Dkde=enabled            # pointer support
meson setup build -Dkde=enabled -Dopencv=enabled   # + auto-detection

New dependencies (only with -Dkde): libei and an sd-bus implementation (libsystemd, or basu on non-systemd systems). wl-kbptr --version prints (kde).

Testing

Tested on KWin Plasma 6 (Wayland):

  • pointer jumps to exact global coordinates including on a second monitor; left/middle/right clicks work;
  • auto-detection captures via ScreenShot2 and the OpenCV pipeline returns sensible targets;
  • a build without -Dkde and a wlroots compositor behave exactly as before;
  • added a unit test for the global→region coordinate mapper.

Known limitations

  • ScreenShot2 auto-detection requires the program to be installed (allow-list entry), so it can't be exercised from the build tree.

Happy to adjust naming, the feature split, or anything else to fit the project's conventions.

mcdax added 12 commits June 14, 2026 10:06
… poll

Two defects found via live testing on KWin (Plasma 6, Wayland):

- KWin ties the EIS session to the D-Bus connection that called
  connectToEIS. eis_dbus_connect() unref'd the bus immediately, so KWin
  tore the session down ("Disconnected by EIS") before any input could
  be sent. eis_dbus now returns an eis_connection {bus, fd} the caller
  keeps open until after ei_unref(), via eis_dbus_disconnect().

- The handshake pump poll()ed before the first ei_dispatch(), so the
  initial libei connection handshake (and queued capability binding) was
  never driven and the device never resumed (2s timeout). Dispatch now
  runs at the top of both pump loops, before poll().

Verified: cursor moves to exact global coordinates incl. a second
monitor (e.g. 3000,500), no disconnect, click path flushes via ei_ping.
@mcdax mcdax mentioned this pull request Jun 14, 2026
@moverest

moverest commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Thank your for your PR, this is an interesting addition. I don't use KDE thus why I didn't actively worked on supporting it, however it's still something I'm interested in supporting.

I like the fact that this is all optional since it inevitably brings new dependencies. In fact, that's the reason why the detection feature is optional.

I've tested it on my machine and I have a few observations:

  • When the program doesn't have access to query the org.kde.KWin.ScreenShot2 API it will run into a segmentation fault when trying to do the detection.
  • It doesn't reliably click on my machine. I'm not sure if it's because the overlay surface doesn't disappear fast enough thus gets the click event or if it's something else. I tried adding a one second delay but it didn't seem to reliably work either (but maybe it help?). I need to investigate further. Is it doing this on your machine? I've ran it with Arch's plasma-desktop 6.6.5-1.
  • Setup:
    • Since I don't use KDE, I'm not sure what would be the best way to configure the software. From my understanding, it has to run through the wl-kbptr.desktop to get the necessary permissions. I've been able to set a short cut in the config panel, but I'm not sure how we can pass arguments to it (e.g. to change the modes or just use a different configuration file). The way I've ran it is by creating a secondary file with the options I needed. That can't be the only way, is it?
    • I'm also asking so that this could be put into the README with a sub-section in the Setting the bindings section with instructions on how to configure it for KDE. Though, this doesn't have to be done in this PR.
    • The following is a bit of a nitpick: The terminology is a bit fuzzy, we're talking about KWin but the option is kde. I think it would make sense to just say everywhere kwin, i.e. the compositor (and only mention KDE in the README) as it's really what we're targeting.

I haven't looked into the code in detail yet. I also haven't tested a multiple monitor setup either. I will do that a bit later.

@mcdax

mcdax commented Jun 18, 2026

Copy link
Copy Markdown
Author

Thank you very much for your input! I'll analyse the issues over the next few days.

Interestingly, I don't experience any delay or click issues at all (KDE Plasma 6.6.4 on Kubuntu 26.04). I'm going to test on a few more machines.

▎ The way I've ran it is by creating a secondary file with the options I needed. That can't be the only way, is it?

That's exactly how I tested it too, since I can't think of another way that wouldn't make things even more complex. I'll give it more thought. There's also the issue that you can start the binary multiple times, which leads to strange effects.

Multi-monitor is still a limitation: it currently only works on the active output.

mcdax added 2 commits June 18, 2026 14:35
query_screenshot() can return NULL on the KWin backend when the
compositor denies the org.kde.KWin.ScreenShot2 request (the wlroots
backend never returned NULL). Detection dereferenced the buffer
unconditionally, segfaulting. Bail out with an empty area list instead.
The feature targets the KWin compositor specifically, so name it after
the compositor everywhere in the code and build (option, KWIN_ENABLED
macro, file/symbol names, --version marker). 'KDE' is kept only in
user-facing prose in the README and in the actual D-Bus interface names
(org.kde.KWin.*, X-KDE-DBUS-Restricted-Interfaces).
mcdax pushed a commit to mcdax/wl-kbptr that referenced this pull request Jun 23, 2026
Open the EIS/libei session once and reuse it for every pointer move
instead of reconnecting -- plus running pointer_kwin_available()'s extra
connect/close probe -- on every keystroke. Per-keystroke session churn
made KWin repeatedly build and tear down a virtual pointer device, and
left the final click racing against the teardown of the preview move's
session: a likely cause of the unreliable clicks reported in moverest#97.

Also inject motion, press and release as separate frames with a
flush_until_pong() round-trip between each, mirroring the working
wlr_virtual_pointer path. Without it KWin received the whole burst at
once with near-identical ei_now() timestamps, producing a zero-duration
click. Reusing the session also requires a monotonically increasing
start_emulating() sequence number.

Adds temporary diagnostic logging (device capabilities, regions, click
timestamps) to compare behaviour across machines; to be removed before
merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mcdax

mcdax commented Jun 23, 2026

Copy link
Copy Markdown
Author

Hi @moverest,

I can't reproduce it on my other machine either, unfortunately. (Arch with recent plasma-desktop)

I did a small refactor around the roundtrips and the EIS connection handling. It now opens one connection and reuses it instead of reconnecting on every keystroke and I also added some extra debug logging.

Could you pull the latest commit, build with -Dkwin=enabled, run it again, and check if the problem still happens? If you paste the logs (run it from a terminal, the info: lines), that'd really help to see where it breaks on your machine.

Btw, I don't think the overlay is stealing the click: its input region is empty, so clicks pass straight through to the window underneath. That probably also explains why the 1s delay didn't help.

Thanks!

PS: Most of the code is AI generated, I still have to revise esp. the comments.

Open the EIS/libei session once and reuse it for every pointer move
instead of reconnecting -- plus running pointer_kwin_available()'s extra
connect/close probe -- on every keystroke. Per-keystroke session churn
made KWin repeatedly build and tear down a virtual pointer device, and
left the final click racing against the teardown of the preview move's
session: a likely cause of the unreliable clicks reported in moverest#97.

Also inject motion, press and release as separate frames with a
flush_until_pong() round-trip between each, mirroring the working
wlr_virtual_pointer path. Without it KWin received the whole burst at
once with near-identical ei_now() timestamps, producing a zero-duration
click. Reusing the session also requires a monotonically increasing
start_emulating() sequence number.

Adds temporary diagnostic logging (device capabilities, regions, click
timestamps) to compare behaviour across machines; to be removed before
merge.
@mcdax
mcdax force-pushed the feat/kde-pointer-support branch from a9d3bd3 to cbbb0a9 Compare June 23, 2026 10:25
@mcdax

mcdax commented Jun 23, 2026

Copy link
Copy Markdown
Author

btw. Most of the code can also be used for GNOME support. (In the long run)

@bybv

bybv commented Jul 22, 2026

Copy link
Copy Markdown

Tested this branch at cbbb0a9 on Arch Linux with Plasma 6.7 (kwin_wayland, libei 1.6.0), built with -Dkwin=enabled -Dopencv=enabled, single monitor.

Results:

  • Pointer motion and clicks work reliably. The instrumentation logs show the motion/press/release frames each round-tripping (press_to_release ~27–35 µs between send timestamps), and every click registered in real use across tile,click, tile,bisect (g/h/b clicks), and floating,click with source=detect. We could not reproduce the unreliable/delayed click with this revision.
  • Target auto-detection via ScreenShot2 also works with the installed desktop file carrying the X-KDE-DBUS-Restricted-Interfaces entry.

Two small build issues on current Arch, where OpenCV is now version 5 (pkg-config name opencv5):

  1. meson.build looks up dependency('opencv4') — needs dependency('opencv5', 'opencv4') (meson tries them in order).

  2. OpenCV 5 moved cv::boundingRect into the new geometry module, so src/target_detection.cpp fails to compile without:

    #if CV_VERSION_MAJOR >= 5
    #include <opencv2/geometry.hpp>
    #endif

With those two changes it builds and runs cleanly. Happy to provide logs or test further revisions.


Transparency note: this comment was written by Claude Code (an AI assistant), which set up and tested the build together with this account's owner on their machine; the owner reviewed and approved it before posting.

@bybv

bybv commented Jul 29, 2026

Copy link
Copy Markdown

Follow-up on the unreliable-click issue @moverest reported: we can now
reproduce it, and it turns out to be toolkit-dependent, not a race with the
overlay.

Symptom: with the branch at cbbb0a9 (Arch, Plasma 6.7.3, libei 1.6.0),
the injected click registers in Qt apps (Dolphin) but is silently dropped in
Chromium-based apps (tested Brave running as a native Wayland client — no
XWayland involved). The cursor moves, the instrumentation logs show the
press/release frames acknowledged by KWin, but the app never reacts.

Cause (as far as we can tell): the per-frame round-trips order the events
correctly, but press and release still go out only ~30–40 µs apart, so the
client sees a button press and release carrying the same millisecond
timestamp — a zero-duration click. Qt accepts that; Chromium's input pipeline
discards it. This would also explain why a delay before the click sequence
(the 1 s you tried) didn't help: the gap that matters is between press and
release.

Fix that works for us: sleeping ~60 ms before the press and again before
the release makes clicks register reliably in Brave as well as Qt apps.
Locally we made it tunable via an env var:

@@ pointer_kwin_move()
+    const char *delay_env = getenv("WL_KBPTR_KWIN_CLICK_DELAY_MS");
+    long click_delay_us = (delay_env ? atol(delay_env) : 60) * 1000;
+
     uint64_t t_motion = ei_now(s->ei);
     ei_device_pointer_motion_absolute(device, tx, ty);
     ei_device_frame(device, t_motion);
     flush_until_pong(s);
@@
+        if (click_delay_us > 0) {
+            usleep(click_delay_us);
+        }
         t_press = ei_now(s->ei);
         ei_device_button_button(device, btn, true);
         ei_device_frame(device, t_press);
         flush_until_pong(s);

+        if (click_delay_us > 0) {
+            usleep(click_delay_us);
+        }
         t_release = ei_now(s->ei);

For upstream it probably makes more sense as a proper config option (or just a
sane hardcoded gap) than an env var — happy to test whatever form you prefer.
A press→release gap on the order of tens of ms is what real hardware produces,
so it seems safe to always apply.

@mcdax

mcdax commented Jul 29, 2026

Copy link
Copy Markdown
Author

Thanks a lot for the analysis and recommendations! Sadly, I hadn't much time to look into it yet. I'll try go though all your points in detail this week.

@bybv

bybv commented Jul 29, 2026 via email

Copy link
Copy Markdown

mcdax added 3 commits August 2, 2026 16:48
OpenCV 5 ships under the pkg-config name opencv5 and moved
cv::boundingRect into the new geometry module. Look up opencv5 first
then fall back to opencv4, and include opencv2/geometry.hpp when
CV_VERSION_MAJOR >= 5 so target_detection.cpp compiles on both.
The per-frame flush_until_pong() round-trips order the press and release
frames correctly, but do not advance the ei_now() clock fast enough to
separate them: press and release still go out only tens of microseconds
apart, carrying the same-millisecond ei_now() timestamp, i.e. a
zero-duration click. Qt accepts that, but Chromium-based Wayland clients
discard it, so the click is silently dropped there.

Sleep 60 ms before the press and again before the release so the
press->release gap resembles real hardware and every toolkit sees a
distinct press and release.
Document the user-friendly way to bind wl-kbptr on KDE Plasma: a plain
custom shortcut with no arguments (the XDG config file is picked up
automatically), and clarify that the installed desktop file is only
needed for ScreenShot2-based auto-detection, not for pointer/click or
stdin-driven floating mode.
@mcdax
mcdax force-pushed the feat/kde-pointer-support branch from 8909007 to d688ca8 Compare August 4, 2026 16:08
Remove the temporary per-click LOG_INFO diagnostics and the device_count
field from the KWin pointer backend (flagged "remove before merge"), and
extract the duplicated 60ms click-pacing sleep into a named
KWIN_CLICK_GAP_US constant carrying the rationale.

Also add the missing NULL check on the screenshot result allocation in
the ScreenShot2 backend, and fix the README pointer flag (--only-print,
not --print-only).
@mcdax
mcdax force-pushed the feat/kde-pointer-support branch from 12502bc to e270d61 Compare August 4, 2026 19:55
read_metadata() ignored every sd_bus_message_read() return value. sd_bus
leaves the destination untouched on failure, so a malformed reply left
`value` uninitialized while still setting the corresponding have_* flag,
propagating a garbage width/height/stride into the buffer arithmetic and
malloc() below. The unchecked key read could also hand strcmp() an
uninitialized pointer. Check each read and treat any failure as an
unusable reply.

Also validate the reported geometry before allocating: every format we
accept is 4 bytes per pixel, so a zero-sized image or a stride too small
for one row means the size arithmetic would not describe the pixel data.

While here, trace the EIS region-mapping fallback (the one path that can
silently place the pointer somewhere unexpected, including when a layout
exceeds MAX_REGIONS) and correct the move_pointer() doc comment, which
claimed the click came from state->click rather than the parameter.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants