summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--packaging/arch/PKGBUILD2
-rw-r--r--src/parley/desktop.py35
-rw-r--r--tests/test_desktop.py33
4 files changed, 53 insertions, 19 deletions
diff --git a/README.md b/README.md
index 6f494b3..9773ce5 100644
--- a/README.md
+++ b/README.md
@@ -108,7 +108,7 @@ systemctl --user enable --now parley.service
The package deliberately does not bundle the runtime or model. Put `transcribe-cli` on `PATH` (for example at `~/.local/bin/transcribe-cli`) and the model at `~/.local/share/parley/models/parakeet-unified-en-0.6b-Q8_0.gguf`, or set the documented environment overrides. User-manager environment overrides can be placed in `~/.config/environment.d/parley.conf`; log in again after changing them.
-The daemon uses GDK 4 for clipboard ownership and `notify-send` for prototype notifications.
+The prototype daemon uses `xclip` through GNOME's XWayland clipboard bridge and `notify-send` for notifications.
## Planned first milestone
diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD
index 8718009..a135145 100644
--- a/packaging/arch/PKGBUILD
+++ b/packaging/arch/PKGBUILD
@@ -7,10 +7,10 @@ arch=('any')
license=('GPL-3.0-or-later')
depends=(
'ffmpeg'
- 'gtk4'
'libnotify'
'python'
'python-gobject'
+ 'xclip'
'xdg-utils'
)
makedepends=(
diff --git a/src/parley/desktop.py b/src/parley/desktop.py
index 4072655..37f8a7f 100644
--- a/src/parley/desktop.py
+++ b/src/parley/desktop.py
@@ -12,24 +12,25 @@ class DesktopIntegrationError(ParleyError):
def copy_text(text: str) -> None:
- """Copy text with GDK, retaining ownership in the long-running daemon."""
+ """Copy text through XWayland's clipboard bridge on GNOME."""
+ executable = shutil.which("xclip")
+ if executable is None:
+ raise DesktopIntegrationError(
+ "xclip is not installed; transcript was saved but not copied"
+ )
try:
- import gi
-
- gi.require_version("Gdk", "4.0")
- gi.require_version("Gtk", "4.0")
- from gi.repository import Gdk, Gtk
-
- # GDK 4 treats opening a display before GTK initialization as fatal.
- Gtk.init()
- display = Gdk.Display.get_default()
- if display is None:
- raise DesktopIntegrationError("could not connect to the graphical display")
- display.get_clipboard().set(text)
- except DesktopIntegrationError:
- raise
- except Exception as error:
- raise DesktopIntegrationError(f"GDK clipboard is unavailable: {error}") from error
+ result = subprocess.run(
+ [executable, "-selection", "clipboard", "-in"],
+ input=text,
+ text=True,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ timeout=10,
+ )
+ except (OSError, subprocess.TimeoutExpired) as error:
+ raise DesktopIntegrationError(f"could not copy transcript: {error}") from error
+ if result.returncode != 0:
+ raise DesktopIntegrationError("xclip could not access the clipboard")
def notify(summary: str, body: str) -> None:
diff --git a/tests/test_desktop.py b/tests/test_desktop.py
new file mode 100644
index 0000000..a273617
--- /dev/null
+++ b/tests/test_desktop.py
@@ -0,0 +1,33 @@
+import subprocess
+import unittest
+from unittest.mock import MagicMock, patch
+
+from parley.desktop import DesktopIntegrationError, copy_text
+
+
+class ClipboardTests(unittest.TestCase):
+ @patch("parley.desktop.subprocess.run")
+ @patch("parley.desktop.shutil.which", return_value="/usr/bin/xclip")
+ def test_copies_utf8_text(self, which: MagicMock, run: MagicMock) -> None:
+ run.return_value.returncode = 0
+ run.return_value.stderr = ""
+
+ copy_text("héllo")
+
+ run.assert_called_once_with(
+ ["/usr/bin/xclip", "-selection", "clipboard", "-in"],
+ input="héllo",
+ text=True,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ timeout=10,
+ )
+
+ @patch("parley.desktop.shutil.which", return_value=None)
+ def test_reports_missing_xclip(self, which: MagicMock) -> None:
+ with self.assertRaisesRegex(DesktopIntegrationError, "xclip is not installed"):
+ copy_text("text")
+
+
+if __name__ == "__main__":
+ unittest.main()