diff --git a/README.md b/README.md index f1dce1e..9cc5e96 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,16 @@ pip install --upgrade pip pip install -r src/requirements.txt ``` +#### Linux Clipboard Support + +On Linux, `pyperclip` relies on external utilities like `xclip` or `xsel`. +SeedPass will attempt to install **xclip** automatically if neither tool is +available. If the automatic installation fails, you can install it manually: + +```bash +sudo apt-get install xclip +``` + ## Quick Start After installing dependencies and activating your virtual environment, launch diff --git a/src/main.py b/src/main.py index f1dde23..011255a 100644 --- a/src/main.py +++ b/src/main.py @@ -19,9 +19,8 @@ from nostr.client import NostrClient from password_manager.entry_types import EntryType from constants import INACTIVITY_TIMEOUT, initialize_app from utils.password_prompt import PasswordPromptError -from utils import timed_input +from utils import timed_input, copy_to_clipboard from local_bip85.bip85 import Bip85Error -import pyperclip colorama_init() @@ -852,7 +851,7 @@ def main(argv: list[str] | None = None) -> int: ) print(code) try: - pyperclip.copy(code) + copy_to_clipboard(code, password_manager.clipboard_clear_delay) print(colored("Code copied to clipboard", "green")) except Exception as exc: logging.warning(f"Clipboard copy failed: {exc}") diff --git a/src/tests/test_cli_subcommands.py b/src/tests/test_cli_subcommands.py index 2f5b7e3..e62cf57 100644 --- a/src/tests/test_cli_subcommands.py +++ b/src/tests/test_cli_subcommands.py @@ -21,6 +21,7 @@ def make_pm(search_results, entry=None, totp_code="123456"): nostr_client=SimpleNamespace(close_client_pool=lambda: None), parent_seed="seed", inactivity_timeout=1, + clipboard_clear_delay=45, ) return pm @@ -58,7 +59,9 @@ def test_totp_command(monkeypatch, capsys): monkeypatch.setattr(main, "configure_logging", lambda: None) monkeypatch.setattr(main, "initialize_app", lambda: None) monkeypatch.setattr(main.signal, "signal", lambda *a, **k: None) - monkeypatch.setattr(main.pyperclip, "copy", lambda v: called.setdefault("val", v)) + monkeypatch.setattr( + main, "copy_to_clipboard", lambda v, d: called.setdefault("val", v) + ) rc = main.main(["totp", "ex"]) assert rc == 0 out = capsys.readouterr().out diff --git a/src/utils/clipboard.py b/src/utils/clipboard.py index e576419..b5b5e60 100644 --- a/src/utils/clipboard.py +++ b/src/utils/clipboard.py @@ -1,10 +1,40 @@ import threading +import logging +import subprocess +import shutil +import sys + import pyperclip +logger = logging.getLogger(__name__) + + +def _ensure_clipboard() -> None: + """Attempt to ensure a clipboard mechanism is available.""" + try: + pyperclip.copy("") + except pyperclip.PyperclipException as exc: + if sys.platform.startswith("linux"): + if shutil.which("xclip") is None and shutil.which("xsel") is None: + apt = shutil.which("apt-get") or shutil.which("apt") + if apt: + try: + subprocess.run( + ["sudo", apt, "install", "-y", "xclip"], check=True + ) + pyperclip.copy("") + return + except Exception as install_exc: # pragma: no cover - system dep + logger.warning( + "Automatic xclip installation failed: %s", install_exc + ) + raise exc + def copy_to_clipboard(text: str, timeout: int) -> None: """Copy text to the clipboard and clear after timeout seconds if unchanged.""" + _ensure_clipboard() pyperclip.copy(text) def clear_clipboard() -> None: