Skip to content

Commit e92c36e

Browse files
Bundle ydotool in Flatpak so Cut/Paste/Backspace work out of the box on KDE Wayland
- Build ydotool v1.0.4 (pure C99, no external deps) in the Flatpak manifest. - Add --device=all so ydotoold can write to /dev/uinput inside the sandbox. - Start ydotoold from the Flatpak wrapper when running under Wayland. - Prefer the bundled /app/bin/ydotool in the Wayland backend; keep the flatpak-spawn --host fallback for custom builds. - Add one-time desktop notifications when key injection fails so the buttons no longer fail silently. - Fix indentation bug in publish-flatpak-repo.sh. - Bump version to 0.9.7.
1 parent 77a7e81 commit e92c36e

10 files changed

Lines changed: 111 additions & 19 deletions

main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from settings import get_settings
4242
from xdg_paths import CACHE_DIR, CONFIG_DIR
4343

44-
__version__ = "0.9.6"
44+
__version__ = "0.9.7"
4545

4646
LOG_FILE = CACHE_DIR / "linuxpop.log"
4747
LOCK_FILE = CACHE_DIR / "linuxpop.lock"

mcp_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747

4848
PROTOCOL_VERSION = "2024-11-05"
4949
SERVER_NAME = "linuxpop"
50-
SERVER_VERSION = "0.9.6"
50+
SERVER_VERSION = "0.9.7"
5151

5252
# Log to a file so the user can debug without stdout-noise corrupting
5353
# the JSON-RPC stream the MCP client is reading.

packaging/flatpak/io.github.GaimsDevSoftware.LinuxPop.yml

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,12 @@
2222
# - gtk-layer-shell: positions the popup as a layer-shell surface
2323
# (a plain GTK window can't be placed on Wayland). Built with
2424
# introspection so the GtkLayerShell-0.1 typelib is loadable by gi.
25+
# - ydotool + ydotoold: key injection on Wayland. ydotoold writes to
26+
# /dev/uinput, which needs the --device=all finish-arg. Bundling it
27+
# means users don't have to install a host tool for Cut/Paste/Backspace.
2528
# Neither ships in GNOME Platform 46, so both are built from source
2629
# below with real upstream URL + sha256.
2730
#
28-
# NOT bundled - ydotool (key injection / paste chords):
29-
# ydotool needs /dev/uinput + the ydotoold daemon, which a Flatpak
30-
# sandbox cannot provide, so the binary would be inert here. wtype is
31-
# also dead on KWin (no zwp_virtual_keyboard_v1). In-sandbox paste
32-
# therefore needs the libei / XDG RemoteDesktop portal route (future
33-
# work); until then the Wayland paste path degrades to "copy + press
34-
# Ctrl+V yourself". Outside Flatpak the host's ydotool is used.
35-
#
3631
# RUNTIME VERSION: org.gnome.Platform//49 (GNOME 46 is EOL). 49 was
3732
# verified to ship GTK3, the Handy-1 typelib, and python3-gi on Python
3833
# 3.13 - the bundled Python wheels are cp313 to match. Re-check the
@@ -86,6 +81,7 @@ finish-args:
8681
# exactly what the feature does, and LinuxPop is self-distributed (not Flathub).
8782
- --talk-name=org.freedesktop.Flatpak
8883
- --device=dri # GTK rendering
84+
- --device=all # ydotoold needs /dev/uinput for key injection
8985

9086
cleanup:
9187
- /include
@@ -235,6 +231,23 @@ modules:
235231
url: https://github.com/bugaevc/wl-clipboard/archive/refs/tags/v2.3.0.tar.gz
236232
sha256: b4dc560973f0cd74e02f817ffa2fd44ba645a4f1ea94b7b9614dacc9f895f402
237233

234+
# ----- ydotool -----
235+
# Key injection on Wayland. ydotool v1.x is pure C99 with no external
236+
# dependencies and only needs CMake. The companion ydotoold daemon holds
237+
# a persistent uinput device; the wrapper script starts it at launch.
238+
# --device=all grants /dev/uinput access inside the sandbox.
239+
# We patch out the manpage subdir so we don't need scdoc in the SDK.
240+
- name: ydotool
241+
buildsystem: cmake-ninja
242+
config-opts:
243+
- -DCMAKE_POLICY_VERSION_MINIMUM=3.5
244+
sources:
245+
- type: archive
246+
url: https://github.com/ReimuNotMoe/ydotool/archive/refs/tags/v1.0.4.tar.gz
247+
sha256: ba075a43aa6ead51940e892ecffa4d0b8b40c241e4e2bc4bd9bd26b61fde23bd
248+
- type: patch
249+
path: ydotool-no-docs.patch
250+
238251
# ----- python3-dbus (dbus-python) -----
239252
# The Wayland/KDE backend uses dbus-python for KGlobalAccel (global
240253
# hotkeys) and KWin scripting over D-Bus; it is NOT in the GNOME runtime,

packaging/flatpak/linuxpop.wrapper

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
#!/bin/sh
22
# Wrapper that launches LinuxPop from inside the Flatpak sandbox.
33
# /app/share/linuxpop is where the build step deposits the Python sources.
4+
5+
# Start the bundled ydotool daemon on Wayland if present. It needs
6+
# /dev/uinput (granted via --device=all) so key injection works out of
7+
# the box without a host-installed ydotool. If a daemon is already
8+
# running it will quietly fail and the existing socket is reused.
9+
if [ -n "$WAYLAND_DISPLAY" ] && [ -x /app/bin/ydotoold ]; then
10+
nohup /app/bin/ydotoold >/dev/null 2>&1 &
11+
fi
12+
413
exec python3 /app/share/linuxpop/main.py "$@"

packaging/flatpak/publish-flatpak-repo.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ touch "$PAGES/.nojekyll"
3636

3737
echo "[4/5] commit + push Pages ..."
3838
git -C "$PAGES" config user.name "$(git -C "$HOME/src/linuxpop" config user.name)"
39-
git -C "$PAGES" config user.email "$(git -C "$HOME/src/linuxpop" config user.email)"
40-
git -C "$PAGES" add -A
39+
git -C "$PAGES" config user.email "$(git -C "$HOME/src/linuxpop" config user.email)"
40+
git -C "$PAGES" add -A
4141
git -C "$PAGES" commit -m "Update Flatpak repo ($(git -C "$(dirname "$MANIFEST")/../.." describe --tags --always 2>/dev/null || echo update))" \
4242
&& git -C "$PAGES" push || echo " (nothing to push)"
4343

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
--- a/CMakeLists.txt
2+
+++ b/CMakeLists.txt
3+
@@ -37,4 +37,3 @@ add_executable(ydotool ${SOURCE_FILES_CLIENT})
4+
install(TARGETS ydotool DESTINATION ${CMAKE_INSTALL_BINDIR})
5+
6+
add_subdirectory(Daemon)
7+
-add_subdirectory(manpage)

packaging/io.github.GaimsDevSoftware.LinuxPop.metainfo.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@
9696
<content_rating type="oars-1.1" />
9797

9898
<releases>
99+
<release version="0.9.7" date="2026-06-16">
100+
<description>
101+
<p>Cut, Paste, Backspace and the other editing buttons now work out
102+
of the box in the Flatpak build on KDE Plasma Wayland. ydotool and
103+
its ydotoold daemon are bundled and started automatically, so users
104+
no longer need to install a host key-injection tool. If injection
105+
ever fails, the app now shows a warning instead of failing silently.</p>
106+
</description>
107+
</release>
99108
<release version="0.9.6" date="2026-06-15">
100109
<description>
101110
<p>The editing buttons work in the Flatpak again. Select all, Cut,

platform_backend/base.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,28 @@
2121
from __future__ import annotations
2222

2323
import abc
24+
import subprocess
2425
from typing import Callable, Optional, Protocol
2526

27+
# One-time notification per session when a key-injection backend is missing.
28+
# Keeps Cut/Paste/Backspace from failing completely silently.
29+
_missing_injector_warned: set[str] = set()
30+
31+
32+
def _warn_missing_injector(backend_name: str, message: str) -> None:
33+
"""Show a desktop notification once per session if key injection fails."""
34+
if backend_name in _missing_injector_warned:
35+
return
36+
_missing_injector_warned.add(backend_name)
37+
try:
38+
subprocess.run(
39+
["notify-send", "--hint=byte:transient:1", "-t", "6000",
40+
"-i", "dialog-warning", "LinuxPop", message],
41+
check=False,
42+
)
43+
except Exception:
44+
pass
45+
2646

2747
class SelectionWatcher(Protocol):
2848
"""Fires on_selection(text, x, y) when the primary selection changes."""

platform_backend/wayland_kde.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -836,15 +836,23 @@ def _wtype_args(self, combo: str) -> Optional[list[str]]:
836836
return args
837837

838838
def _has_ydotool(self) -> bool:
839-
return _host_has("ydotool") if _in_flatpak() else bool(
840-
shutil.which("ydotool"))
839+
if _in_flatpak():
840+
# Prefer the bundled ydotool (needs --device=all for /dev/uinput);
841+
# fall back to a host install for custom Flatpak builds without it.
842+
return os.path.isfile("/app/bin/ydotool") or _host_has("ydotool")
843+
return bool(shutil.which("ydotool"))
841844

842845
def _ydotool_run(self, sub_argv: list) -> bool:
843-
"""Dispatch `ydotool <sub_argv>` to the daemon. In the Flatpak the
844-
sandbox has no /dev/uinput, so we run ydotool on the HOST via
845-
flatpak-spawn (same daemon + socket the native app uses); otherwise
846-
we run it in-process. Returns True if it was dispatched."""
847-
if _in_flatpak():
846+
"""Dispatch `ydotool <sub_argv>` to the daemon.
847+
848+
In the Flatpak we prefer the bundled ydotool binary and run it inside
849+
the sandbox (ydotoold is started by the wrapper). We keep the
850+
flatpak-spawn --host fallback so custom builds without the bundled
851+
binary can still use a host-installed ydotool."""
852+
bundled = "/app/bin/ydotool"
853+
if _in_flatpak() and os.path.isfile(bundled):
854+
argv = [bundled, *sub_argv]
855+
elif _in_flatpak():
848856
runtime = os.environ.get("XDG_RUNTIME_DIR") or f"/run/user/{os.getuid()}"
849857
sock = os.path.join(runtime, ".ydotool_socket")
850858
argv = ["flatpak-spawn", "--host",
@@ -871,6 +879,20 @@ def send_key(self, combo: str) -> None:
871879
return
872880
print(f"[wayland] no working key-injection tool for {combo!r} "
873881
"(install ydotool + ydotoold on KWin)")
882+
from .base import _warn_missing_injector
883+
if _in_flatpak():
884+
_warn_missing_injector(
885+
"wayland_kde_flatpak",
886+
"Cut/Paste/Backspace could not use ydotool. If this is a custom "
887+
"Flatpak build, install ydotool on the host; otherwise the "
888+
"bundled daemon may not have started.",
889+
)
890+
else:
891+
_warn_missing_injector(
892+
"wayland_kde_native",
893+
"Cut/Paste/Backspace needs ydotool + ydotoold. Install and "
894+
"start ydotoold to enable keystroke actions.",
895+
)
874896

875897
def type_text(self, text: str) -> None:
876898
# `--` terminates ydotool's own option parsing so text that starts

platform_backend/x11.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ def send_key(self, combo: str) -> None:
101101
import shutil
102102
if not shutil.which("xdotool"):
103103
print("[x11] xdotool missing - cannot send key")
104+
from .base import _warn_missing_injector
105+
_warn_missing_injector(
106+
"x11",
107+
"Cut/Paste/Backspace needs xdotool. Install it to enable "
108+
"keystroke actions.",
109+
)
104110
return
105111
subprocess.run(
106112
["xdotool", "key", "--clearmodifiers", combo], check=False,
@@ -114,6 +120,12 @@ def type_text(self, text: str) -> None:
114120
import shutil
115121
if not shutil.which("xdotool"):
116122
print("[x11] xdotool missing - cannot type text")
123+
from .base import _warn_missing_injector
124+
_warn_missing_injector(
125+
"x11",
126+
"Cut/Paste/Backspace needs xdotool. Install it to enable "
127+
"keystroke actions.",
128+
)
117129
return
118130
subprocess.run(
119131
["xdotool", "type", "--clearmodifiers", "--", text], check=False,

0 commit comments

Comments
 (0)