Use xclip directly if available

This gives better ergonomics because we can terminate xclip ourselves

.. instead of using paste to check whether the copy buffer is the same
as our previous TOTP code
This commit is contained in:
Jeff Epler 2025-04-24 18:01:57 +02:00
parent 552849e2ad
commit 3cff298395

View file

@ -6,6 +6,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
import signal
import time import time
import hashlib import hashlib
import pathlib import pathlib
@ -35,6 +36,13 @@ from typing import TypeGuard # use `typing_extensions` for Python 3.9 and below
# workaround for pyperclip being un-typed # workaround for pyperclip being un-typed
if TYPE_CHECKING: if TYPE_CHECKING:
class CopyProcessor:
def do_copy(self, data: str) -> None:
...
def do_clear_copy(self) -> bool:
...
def pyperclip_paste() -> str: def pyperclip_paste() -> str:
... ...
@ -45,6 +53,63 @@ else:
from pyperclip import copy as pyperclip_copy from pyperclip import copy as pyperclip_copy
def command_exists(s: str) -> bool:
status = subprocess.run(
["which", s],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL,
)
return status.returncode == 0
@dataclass
class PyperclipCopyProcessor:
copied: str = ""
def do_copy(self, data: str) -> None:
self.copied = data
def do_clear_copy(self) -> bool:
if self.copied and pyperclip_paste() == self.copied:
pyperclip_copy("")
return True
return False
@dataclass
class XClipCopyProcessor:
process: subprocess.Popen[bytes] | None = None
def do_copy(self, data: str) -> None:
self.do_clear_copy()
self.process = subprocess.Popen(
["xclip", "-verbose", "-sel", "c"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
stdin=subprocess.PIPE,
)
assert self.process.stdin is not None # mypy worries about this at night
self.process.stdin.write(data.encode("utf-8"))
self.process.stdin.close()
def do_clear_copy(self) -> bool:
if self.process is None:
return False
self.process.send_signal(signal.SIGINT)
returncode = self.process.wait(0.1)
if returncode is None:
self.process.send_signal(signal.SIGKILL)
returncode = self.process.wait(0.1)
self.process = None
return True
copy_processor = (
XClipCopyProcessor() if command_exists("xclip") else PyperclipCopyProcessor()
)
def is_str_list(val: Any) -> TypeGuard[list[str]]: def is_str_list(val: Any) -> TypeGuard[list[str]]:
"""Determines whether all objects in the list are strings""" """Determines whether all objects in the list are strings"""
if not isinstance(val, list): if not isinstance(val, list):
@ -319,9 +384,8 @@ class TTOTP(App[None]):
self.notify("Will exit soon due to inactivity", title="Auto-exit") self.notify("Will exit soon due to inactivity", title="Auto-exit")
def clear_clipboard_func(self) -> None: def clear_clipboard_func(self) -> None:
if pyperclip_paste() == self.copied: if copy_processor.do_clear_copy():
self.notify("Clipboard cleared", title="") self.notify("Clipboard cleared", title="")
pyperclip_copy("")
def timer_func(self) -> None: def timer_func(self) -> None:
now = time.time() now = time.time()
@ -350,8 +414,7 @@ class TTOTP(App[None]):
if widget is not None: if widget is not None:
otp = cast(TOTPLabel, widget).otp otp = cast(TOTPLabel, widget).otp
code = otp.totp.now() code = otp.totp.now()
pyperclip_copy(code) copy_processor.do_copy(code)
self.copied = code
if self.clear_clipboard_timer is not None: if self.clear_clipboard_timer is not None:
self.clear_clipboard_timer.pause() self.clear_clipboard_timer.pause()