Skip to content

GNOME/Mutter Wayland support (XWayland hybrid backend); popup anchors above selection#4

Open
GaimsDevSoftware wants to merge 10 commits into
mainfrom
feature/gnome-wayland-support
Open

GNOME/Mutter Wayland support (XWayland hybrid backend); popup anchors above selection#4
GaimsDevSoftware wants to merge 10 commits into
mainfrom
feature/gnome-wayland-support

Conversation

@GaimsDevSoftware

Copy link
Copy Markdown
Owner

Adds Fedora GNOME (Mutter, Wayland) support so one build auto-detects and runs
correctly on KDE Plasma (Wayland), GNOME (Wayland), and Mint/Cinnamon (X11).

How it works

platform_backend.detect() now routes a GNOME Wayland session to a new
xwayland_gnome backend (KDE stays wayland_kde, non-Wayland stays x11).
Mutter has no wlr-layer-shell and no external cursor API, so the popup runs
under XWayland and reuses the proven X11 positioning/pointer path, swapping only
the I/O that X11 tools can't do for native Wayland apps:

  • Popup placement + pointer: inherited X11 backend (Gtk.Window.move, XQueryPointer) under XWayland
  • Selection watch: wl-paste --primary --watch (sees native Wayland apps)
  • Clipboard + key injection: delegated to the Wayland backend (wl-clipboard, ydotool)
  • Global hotkey: new EvdevHotkey (/dev/input) so it fires even when a native Wayland window is focused
  • Active-window detection: AT-SPI + XWayland WM_CLASS

main.py forces GDK_BACKEND=x11 before importing GTK, but only for the
xwayland_gnome backend. See docs/gnome-wayland.md for the full design.

Popup anchors above the selected text

Enables the selection-rect anchoring on GNOME: _atspi_environment_safe() now
accepts GNOME's org.a11y.Bus (not just the XDG socket KDE/X11 use), and
focused_selection_rect() is re-gated on the user-facing
popup_anchor_to_selection (default on) instead of the heavy focus listener.
Falls back to the mouse pointer when no selection geometry is available.

Status / verification

Verified on real Fedora 44 GNOME/Wayland: detection → xwayland_gnome, selection
read, pointer, popup render, plugin load, no crashes; the fractional-scaling
pointer math is correct.

Needs verification on real GNOME hardware before relying on it:

  • Selection-rect coordinate space under fractional scaling (pointer fallback is safe meanwhile)
  • Global hotkey / double-click / ydotool paste require the user in the input group

Mutter implements neither wlr-layer-shell (no absolute surface placement)
nor an external cursor-position API (no KWin workspace.cursorPos equivalent)
the two things the KDE backend relies on for popup positioning. So GNOME
gets a hybrid backend that reuses the proven X11 path for the hard parts and
swaps only the I/O that X11 tools can't do on Wayland.

New: platform_backend/xwayland_gnome.py (XWaylandGnomeBackend)
  - inherits X11Backend for pointer (XQueryPointer) and popup placement
    (Gtk.Window.move) - both work for an XWayland toplevel and return global
    coordinates, no layer-shell needed
  - overrides selection/clipboard (wl-clipboard) and key injection (ydotool)
    by delegating to the KDE backend's compositor-agnostic helpers, because
    xclip/XFixes/xdotool only see XWayland apps, not native Wayland ones
  - selection watch via wl-paste --primary --watch (sees native Wayland apps)
  - double-click via the existing kernel-level evdev watcher
  - active window via AT-SPI, merged with XWayland WM_CLASS

New: platform_backend/evdev_hotkey.py (EvdevHotkey)
  - global hotkey read from /dev/input at kernel level, so it fires whatever
    app has focus (an X11 grab via XWayland misses native-Wayland-focused
    windows). Modifier state via libxkbcommon, so ctrl:swap_lalt_lctl and
    other xkb options are honoured. Needs the 'input' group (same as ydotool).

detect(): GNOME Wayland -> xwayland_gnome; KDE and X11 paths unchanged.
main.py: force GDK_BACKEND=x11 before GTK import, but only for the gnome
  backend (KDE keeps native Wayland for gtk-layer-shell). LINUXPOP_GDK_BACKEND
  opts out.
editable_detect.py: add active_window_atspi_haystacks() for blocklist matching
  on Wayland where WM_CLASS is unavailable for native apps.

Verified on KDE-under-XWayland: detection, pointer, selection, paste,
positioning (test_popup), and evdev hotkey arming (honouring the user's
ctrl:swap_lalt_lctl). KDE backend confirmed unchanged. Visual verification
on a real GNOME session (placement accuracy, fractional scaling) still
pending; Flatpak packaging for GNOME deferred until then.
Self-contained state-of-play so the GNOME work can be continued on a fresh
Fedora GNOME machine (or by a fresh Claude session) without the original chat
transcript: design rationale, file map, what's verified vs pending, how to run
and test on GNOME, and the deferred work (Flatpak packaging, fractional-scaling
check, docs).
Replace the branch handoff note with a standard architecture doc covering
the hybrid XWayland approach, the capability split vs the KDE backend, the
fractional-scaling coordinate handling, and setup on Fedora GNOME.
…on rect)

The popup is meant to open directly above the selected text, falling back to
the mouse pointer only when that geometry isn't available. The rect path
already existed (focused_selection_rect -> popup.show_for rect anchoring) but
was unreachable on GNOME for two reasons, both fixed here:

- editable_detect._atspi_environment_safe() only accepted AT-SPI when a socket
  existed at $XDG_RUNTIME_DIR/at-spi/bus_0. GNOME drops no socket there and
  exposes the bus via org.a11y.Bus on the session bus instead, so the probe
  returned False and _HAS_ATSPI was False. The probe now runs the foreign-uid
  at-spi-bus-launcher crash guard first (unchanged), then accepts either the
  XDG socket (KDE / X11) or a reachable org.a11y.Bus (GNOME).

- focused_selection_rect() was gated behind editable_atspi_listener_enabled
  (the heavy always-on focus listener, off by default). It does a one-shot
  bounded walk and never needs that listener, so it's re-gated on the
  user-facing popup_anchor_to_selection toggle (default on). The focus
  listener and is_focus_editable's synchronous walk stay gated on
  editable_atspi_listener_enabled, so this does not turn those on.

Requires desktop accessibility (toolkit-accessibility) so apps expose AT-SPI.
When the rect is unavailable the popup falls back to the mouse pointer exactly
as before, so this is safe when AT-SPI is off or an app exposes no geometry.
The selection-rect anchoring enabled in the previous commit runs a one-shot
AT-SPI walk by default (gated on popup_anchor_to_selection). On Cinnamon
(Linux Mint's default) activating AT-SPI was correlated with a desktop-panel
segfault - the exact reason the focus listener defaults off. Guard
focused_selection_rect with a Cinnamon check so Mint keeps its proven
mouse-pointer placement; power users who set editable_atspi_listener_enabled
still opt in. GNOME/KDE are unaffected.
…ice)

Native (non-Flatpak) installs had no working keystroke injection on Wayland:
the distro ydotool package ships only a *system* ydotoold service that runs as
root on a root-only socket the app can't reach, and /dev/uinput is root-only.
So Cut/Paste/Backspace silently no-op'd on GNOME/KDE Wayland.

Add packaging/wayland/ with the standard fix: a udev rule giving the 'input'
group access to /dev/uinput, a per-user ydotoold.service on the socket path the
Wayland backend expects ($XDG_RUNTIME_DIR/.ydotool_socket), and a README with
the one-time setup. The Flatpak path is unchanged (it bundles ydotool itself).
Fix the stale gnome-wayland.md instruction that referenced a non-existent user
unit.

Verified on Fedora 44 GNOME/Wayland: with the udev rule + user ydotoold,
ydotool injection lands keystrokes in the focused window and the backend
reports can_paste=True.
The selection-triggered popup - the primary way to invoke LinuxPop - never
fired on GNOME Wayland. The watcher used wl-paste --primary --watch, which
needs the wlr-data-control protocol; Mutter does not implement it, so wl-paste
exits immediately ("Watch mode requires a compositor that supports the
data-control protocol") and no selection ever reached the popup.

Add _PollingSelectionWatcher and use it from the xwayland_gnome backend: it
polls the primary selection with one-shot wl-paste reads (which DO work on
Mutter) on a 250 ms interval, debounces so a drag settles on its final value,
skips the selection already present at startup, and fires on change. Sees both
native Wayland and XWayland apps. KDE/wlroots keep the event-driven watcher
(they have data-control).

Verified on Fedora 44 GNOME/Wayland: changing the primary selection now fires
the popup callback; the pre-existing selection does not.
Swap tray_qt.py (QSystemTrayIcon, needs PySide6) for tray_dbus.py, a
hand-rolled StatusNotifierItem over dbus-python + GdkPixbuf - same
length-prefixed JSON socket protocol, no Qt dependency. PySide6 wasn't
installed, so the Qt tray subprocess failed to connect (the popup still
worked, but Settings/Plugins/Quit were unreachable from the tray). dbus-python
and GdkPixbuf are already in use, so the tray now works with no extra dep. On
GNOME the icon still needs the AppIndicator shell extension to be displayed
(GNOME shows no StatusNotifierItem host otherwise).
… Mutter)

Screen OCR never worked on GNOME/Wayland: the fullscreen grab tried
spectacle/grim/maim, but Mutter has no wlr-screencopy (grim fails) and X11
grabbers see only XWayland (maim captures black). _capture_fullscreen now
skips maim under native Wayland and falls back to the xdg-desktop-portal
Screenshot interface (interactive=false), which captures the real screen on
GNOME - and on KDE, and inside the Flatpak sandbox. The portal writes a PNG
and returns its URI; we copy it out and delete the portal's ~/Pictures copy.

Verified on Fedora 44 GNOME/Wayland: capture returns the real screen and
tesseract recognises the on-screen text end to end.
…Fixes watch

The selection popup could not be placed correctly on GNOME Wayland and the
selection watch disrupted typing. Root causes were all GNOME/Mutter Wayland
limitations with no app-level workaround, confirmed against upstream:

- Pointer: XQueryPointer freezes over native-Wayland windows and GNOME ships no
  wlr virtual-pointer / layer-shell protocol, so there is no way for an app to
  read the global cursor. Ship a tiny GNOME Shell extension
  (packaging/gnome-shell-extension/) that exposes global.get_pointer() and an
  ActivateApp() over D-Bus. The xwayland_gnome backend reads the cursor through
  it (logical coords -> pointer_is_logical=True, no scale division) and
  re-focuses the app before injecting keystrokes (clicking the popup steals
  focus on GNOME). Degrades gracefully (a short settle delay) without it.

- Selection watch: wl-paste --watch needs wlr-data-control (absent on Mutter)
  and polling wl-paste hammered the source app into re-serving the selection,
  making the highlight and caret blink. Use the X11 XFixes watcher instead -
  event-driven, reads once per change; Mutter bridges the Wayland primary to
  the X11 PRIMARY so it sees native-Wayland and XWayland apps. watcher.py gains
  a pointer_fn hook so the GNOME backend feeds it the extension-based cursor.

- AT-SPI selection rect is window-relative on Wayland (compositor security, RH
  bug 1517301), so it can't anchor a screen popup: skip it on Wayland, anchor
  to the pointer.

- popup.py: raise the window without gtk_window_present() (which requests
  activation and stole keyboard focus on Mutter, eating the user's typing), and
  trim the Wayland line-clearance so the popup sits closer to the cursor.

Verified on Fedora 44 GNOME/Wayland: popup tracks the cursor, no blink, typing
unaffected. Keystroke actions need the ActivateApp half of the extension (loads
after a re-login).
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.

1 participant