diff options
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | packaging/arch/PKGBUILD | 2 | ||||
| -rw-r--r-- | src/parley/desktop.py | 35 | ||||
| -rw-r--r-- | tests/test_desktop.py | 33 |
4 files changed, 53 insertions, 19 deletions
@@ -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() |
