docs: clarify manual clipboard dependencies

This commit is contained in:
thePR0M3TH3AN
2025-08-03 08:12:25 -04:00
parent 36061493ac
commit ccca399b09
11 changed files with 166 additions and 139 deletions

View File

@@ -205,12 +205,14 @@ After reinstalling, run `which seedpass` on Linux/macOS or `where seedpass` on W
#### 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:
On Linux, `pyperclip` relies on external utilities like `xclip` or `xsel`. SeedPass no longer installs these tools automatically. To enable clipboard features such as secret mode, install **xclip** manually:
```bash
sudo apt-get install xclip
sudo apt install xclip
```
After installing `xclip`, restart SeedPass to enable clipboard support.
## Quick Start
After installing dependencies and activating your virtual environment, install the package in editable mode so the `seedpass` command is available:

View File

@@ -192,13 +192,15 @@ python -m pip install -e .
#### 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:
SeedPass does not install these tools automatically. To use clipboard features
such as secret mode, install **xclip** manually:
```bash
sudo apt-get install xclip
sudo apt install xclip
```
After installing `xclip`, restart SeedPass to enable clipboard support.
## Quick Start
After installing dependencies, activate your virtual environment and install

View File

@@ -26,26 +26,27 @@ print_error() { echo -e "\033[1;31m[ERROR]\033[0m $1" >&2; exit 1; }
install_dependencies() {
print_info "Installing system packages required for Gtk bindings..."
if command -v apt-get &>/dev/null; then
sudo apt-get update && sudo apt-get install -y \
build-essential pkg-config libcairo2 libcairo2-dev \
libgirepository1.0-dev gobject-introspection \
gir1.2-gtk-3.0 python3-dev libffi-dev libssl-dev xclip
sudo apt-get update && sudo apt-get install -y \\
build-essential pkg-config libcairo2 libcairo2-dev \\
libgirepository1.0-dev gobject-introspection \\
gir1.2-gtk-3.0 python3-dev libffi-dev libssl-dev
elif command -v yum &>/dev/null; then
sudo yum install -y @'Development Tools' cairo cairo-devel \
gobject-introspection-devel gtk3-devel python3-devel \
libffi-devel openssl-devel xclip
sudo yum install -y @'Development Tools' cairo cairo-devel \\
gobject-introspection-devel gtk3-devel python3-devel \\
libffi-devel openssl-devel
elif command -v dnf &>/dev/null; then
sudo dnf groupinstall -y "Development Tools" && sudo dnf install -y \
cairo cairo-devel gobject-introspection-devel gtk3-devel \
python3-devel libffi-devel openssl-devel xclip
sudo dnf groupinstall -y "Development Tools" && sudo dnf install -y \\
cairo cairo-devel gobject-introspection-devel gtk3-devel \\
python3-devel libffi-devel openssl-devel
elif command -v pacman &>/dev/null; then
sudo pacman -Syu --noconfirm base-devel pkgconf cairo \
gobject-introspection gtk3 python xclip
sudo pacman -Syu --noconfirm base-devel pkgconf cairo \\
gobject-introspection gtk3 python
elif command -v brew &>/dev/null; then
brew install pkg-config cairo gobject-introspection gtk+3
else
print_warning "Unsupported package manager. Please install Gtk/GObject dependencies manually."
fi
print_warning "Install 'xclip' manually to enable clipboard features in secret mode."
}
usage() {
echo "Usage: $0 [-b | --branch <branch_name>] [-h | --help]"

View File

@@ -1251,11 +1251,8 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in
idx, password_manager.parent_seed
)
print(code)
try:
copy_to_clipboard(code, password_manager.clipboard_clear_delay)
if 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}")
return 0
def signal_handler(sig, _frame):

View File

@@ -1504,13 +1504,13 @@ class PasswordManager:
)
)
if self.secret_mode_enabled:
copy_to_clipboard(password, self.clipboard_clear_delay)
print(
colored(
f"[+] Password copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(password, self.clipboard_clear_delay):
print(
colored(
f"[+] Password copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(colored(f"Password for {label}: {password}\n", "yellow"))
@@ -2017,13 +2017,13 @@ class PasswordManager:
print(colored(f"\n[+] Nostr key entry added with ID {index}.\n", "green"))
print(colored(f"npub: {npub}", "cyan"))
if self.secret_mode_enabled:
copy_to_clipboard(nsec, self.clipboard_clear_delay)
print(
colored(
f"[+] nsec copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(nsec, self.clipboard_clear_delay):
print(
colored(
f"[+] nsec copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(color_text(f"nsec: {nsec}", "deterministic"))
if confirm_action("Show QR code for npub? (Y/N): "):
@@ -2104,13 +2104,13 @@ class PasswordManager:
if notes:
print(colored(f"Notes: {notes}", "cyan"))
if self.secret_mode_enabled:
copy_to_clipboard(value, self.clipboard_clear_delay)
print(
colored(
f"[+] Value copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(value, self.clipboard_clear_delay):
print(
colored(
f"[+] Value copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(color_text(f"Value: {value}", "deterministic"))
try:
@@ -2162,13 +2162,13 @@ class PasswordManager:
)
if confirm_action("Reveal seed now? (y/N): "):
if self.secret_mode_enabled:
copy_to_clipboard(seed, self.clipboard_clear_delay)
print(
colored(
f"[+] Seed copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(seed, self.clipboard_clear_delay):
print(
colored(
f"[+] Seed copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(color_text(seed, "deterministic"))
if confirm_action("Show Compact Seed QR? (Y/N): "):
@@ -2521,13 +2521,13 @@ class PasswordManager:
while True:
code = self.entry_manager.get_totp_code(index, self.parent_seed)
if self.secret_mode_enabled:
copy_to_clipboard(code, self.clipboard_clear_delay)
print(
colored(
f"[+] 2FA code for '{label}' copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(code, self.clipboard_clear_delay):
print(
colored(
f"[+] 2FA code for '{label}' copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(colored("\n[+] Retrieved 2FA Code:\n", "green"))
print(colored(f"Label: {label}", "cyan"))
@@ -2593,13 +2593,13 @@ class PasswordManager:
print(colored("Public Key:", "cyan"))
print(color_text(pub_pem, "default"))
if self.secret_mode_enabled:
copy_to_clipboard(priv_pem, self.clipboard_clear_delay)
print(
colored(
f"[+] SSH private key copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(priv_pem, self.clipboard_clear_delay):
print(
colored(
f"[+] SSH private key copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(colored("Private Key:", "cyan"))
print(color_text(priv_pem, "deterministic"))
@@ -2628,13 +2628,13 @@ class PasswordManager:
if tags:
print(colored(f"Tags: {', '.join(tags)}", "cyan"))
if self.secret_mode_enabled:
copy_to_clipboard(phrase, self.clipboard_clear_delay)
print(
colored(
f"[+] Seed phrase copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(phrase, self.clipboard_clear_delay):
print(
colored(
f"[+] Seed phrase copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(color_text(phrase, "deterministic"))
if confirm_action("Show derived entropy as hex? (Y/N): "):
@@ -2679,13 +2679,13 @@ class PasswordManager:
print(colored(f"Tags: {', '.join(tags)}", "cyan"))
print(colored(f"Fingerprint: {fingerprint}", "cyan"))
if self.secret_mode_enabled:
copy_to_clipboard(priv_key, self.clipboard_clear_delay)
print(
colored(
f"[+] PGP key copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(priv_key, self.clipboard_clear_delay):
print(
colored(
f"[+] PGP key copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(color_text(priv_key, "deterministic"))
except Exception as e: # pragma: no cover - best effort
@@ -2704,13 +2704,13 @@ class PasswordManager:
print(colored(f"Label: {label}", "cyan"))
print(colored(f"npub: {npub}", "cyan"))
if self.secret_mode_enabled:
copy_to_clipboard(nsec, self.clipboard_clear_delay)
print(
colored(
f"[+] nsec copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(nsec, self.clipboard_clear_delay):
print(
colored(
f"[+] nsec copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(color_text(f"nsec: {nsec}", "deterministic"))
if notes:
@@ -2740,13 +2740,13 @@ class PasswordManager:
)
)
if self.secret_mode_enabled:
copy_to_clipboard(value, self.clipboard_clear_delay)
print(
colored(
f"[+] Value copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(value, self.clipboard_clear_delay):
print(
colored(
f"[+] Value copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(color_text(f"Value: {value}", "deterministic"))
@@ -2767,13 +2767,15 @@ class PasswordManager:
if show == "y":
for f_label, f_value in hidden_fields:
if self.secret_mode_enabled:
copy_to_clipboard(f_value, self.clipboard_clear_delay)
print(
colored(
f"[+] {f_label} copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(
f_value, self.clipboard_clear_delay
):
print(
colored(
f"[+] {f_label} copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(colored(f" {f_label}: {f_value}", "cyan"))
return
@@ -2808,13 +2810,13 @@ class PasswordManager:
index, self.parent_seed
)
if self.secret_mode_enabled:
copy_to_clipboard(seed, self.clipboard_clear_delay)
print(
colored(
f"[+] Seed phrase copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(seed, self.clipboard_clear_delay):
print(
colored(
f"[+] Seed phrase copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(color_text(seed, "deterministic"))
return
@@ -2852,13 +2854,13 @@ class PasswordManager:
if password:
if self.secret_mode_enabled:
copy_to_clipboard(password, self.clipboard_clear_delay)
print(
colored(
f"[+] Password for '{website_name}' copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(password, self.clipboard_clear_delay):
print(
colored(
f"[+] Password for '{website_name}' copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(
colored(
@@ -2897,13 +2899,15 @@ class PasswordManager:
if show == "y":
for label, value in hidden_fields:
if self.secret_mode_enabled:
copy_to_clipboard(value, self.clipboard_clear_delay)
print(
colored(
f"[+] {label} copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
if copy_to_clipboard(
value, self.clipboard_clear_delay
):
print(
colored(
f"[+] {label} copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
)
else:
print(colored(f" {label}: {value}", "cyan"))
else:
@@ -3847,10 +3851,10 @@ class PasswordManager:
filled = int(20 * (period - remaining) / period)
bar = "[" + "#" * filled + "-" * (20 - filled) + "]"
if self.secret_mode_enabled:
copy_to_clipboard(code, self.clipboard_clear_delay)
print(
f"[{idx}] {label}: [HIDDEN] {bar} {remaining:2d}s - copied to clipboard"
)
if copy_to_clipboard(code, self.clipboard_clear_delay):
print(
f"[{idx}] {label}: [HIDDEN] {bar} {remaining:2d}s - copied to clipboard"
)
else:
print(
f"[{idx}] {label}: {color_text(code, 'deterministic')} {bar} {remaining:2d}s"
@@ -3863,10 +3867,10 @@ class PasswordManager:
filled = int(20 * (period - remaining) / period)
bar = "[" + "#" * filled + "-" * (20 - filled) + "]"
if self.secret_mode_enabled:
copy_to_clipboard(code, self.clipboard_clear_delay)
print(
f"[{idx}] {label}: [HIDDEN] {bar} {remaining:2d}s - copied to clipboard"
)
if copy_to_clipboard(code, self.clipboard_clear_delay):
print(
f"[{idx}] {label}: [HIDDEN] {bar} {remaining:2d}s - copied to clipboard"
)
else:
print(
f"[{idx}] {label}: {color_text(code, 'imported')} {bar} {remaining:2d}s"

View File

@@ -60,7 +60,7 @@ def test_totp_command(monkeypatch, capsys):
monkeypatch.setattr(main, "initialize_app", lambda: None)
monkeypatch.setattr(main.signal, "signal", lambda *a, **k: None)
monkeypatch.setattr(
main, "copy_to_clipboard", lambda v, d: called.setdefault("val", v)
main, "copy_to_clipboard", lambda v, d: (called.setdefault("val", v), True)[1]
)
rc = main.main(["totp", "ex"])
assert rc == 0

View File

@@ -1,6 +1,7 @@
from pathlib import Path
import pyperclip
import threading
import shutil
import sys
@@ -66,3 +67,16 @@ def test_copy_to_clipboard_does_not_clear_if_changed(monkeypatch):
fake_copy("other")
callbacks["func"]()
assert clipboard["text"] == "other"
def test_copy_to_clipboard_missing_dependency(monkeypatch, capsys):
def fail_copy(*args, **kwargs):
raise pyperclip.PyperclipException("no copy")
monkeypatch.setattr(pyperclip, "copy", fail_copy)
monkeypatch.setattr(pyperclip, "paste", lambda: "")
monkeypatch.setattr(shutil, "which", lambda cmd: None)
copy_to_clipboard("secret", 1)
out = capsys.readouterr().out
assert "install xclip" in out.lower()

View File

@@ -140,7 +140,7 @@ def test_handle_add_password_secret_mode(monkeypatch, dummy_nostr_client, capsys
called = []
monkeypatch.setattr(
"seedpass.core.manager.copy_to_clipboard",
lambda text, delay: called.append((text, delay)),
lambda text, delay: (called.append((text, delay)), True)[1],
)
pm.handle_add_password()

View File

@@ -301,7 +301,7 @@ def test_show_entry_details_sensitive(monkeypatch, capsys, entry_type):
"seedpass.core.manager.confirm_action", lambda *a, **k: True
)
monkeypatch.setattr(
"seedpass.core.manager.copy_to_clipboard", lambda *a, **k: None
"seedpass.core.manager.copy_to_clipboard", lambda *a, **k: True
)
monkeypatch.setattr("seedpass.core.manager.timed_input", lambda *a, **k: "b")
monkeypatch.setattr("seedpass.core.manager.time.sleep", lambda *a, **k: None)

View File

@@ -46,7 +46,7 @@ def test_password_retrieve_secret_mode(monkeypatch, capsys):
called = []
monkeypatch.setattr(
"seedpass.core.manager.copy_to_clipboard",
lambda text, t: called.append((text, t)),
lambda text, t: (called.append((text, t)), True)[1],
)
pm.handle_retrieve_entry()
@@ -73,7 +73,7 @@ def test_totp_display_secret_mode(monkeypatch, capsys):
called = []
monkeypatch.setattr(
"seedpass.core.manager.copy_to_clipboard",
lambda text, t: called.append((text, t)),
lambda text, t: (called.append((text, t)), True)[1],
)
pm.handle_display_totp_codes()
@@ -95,7 +95,7 @@ def test_password_retrieve_no_secret_mode(monkeypatch, capsys):
called = []
monkeypatch.setattr(
"seedpass.core.manager.copy_to_clipboard",
lambda *a, **k: called.append((a, k)),
lambda *a, **k: (called.append((a, k)), True)[1],
)
pm.handle_retrieve_entry()
@@ -123,7 +123,7 @@ def test_totp_display_no_secret_mode(monkeypatch, capsys):
called = []
monkeypatch.setattr(
"seedpass.core.manager.copy_to_clipboard",
lambda *a, **k: called.append((a, k)),
lambda *a, **k: (called.append((a, k)), True)[1],
)
pm.handle_display_totp_codes()

View File

@@ -1,8 +1,7 @@
import threading
import logging
import subprocess
import shutil
import sys
import threading
import pyperclip
@@ -10,31 +9,38 @@ logger = logging.getLogger(__name__)
def _ensure_clipboard() -> None:
"""Attempt to ensure a clipboard mechanism is available."""
"""Ensure a clipboard mechanism is available or raise an informative error."""
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
raise pyperclip.PyperclipException(
"Clipboard support requires the 'xclip' package. "
"Install it with 'sudo apt install xclip' and restart SeedPass."
) from exc
raise
def copy_to_clipboard(text: str, timeout: int) -> None:
"""Copy text to the clipboard and clear after timeout seconds if unchanged."""
def copy_to_clipboard(text: str, timeout: int) -> bool:
"""Copy text to the clipboard and clear after timeout seconds if unchanged.
Returns True if the text was successfully copied, False otherwise.
"""
try:
_ensure_clipboard()
except pyperclip.PyperclipException as exc:
warning = (
"Clipboard unavailable: "
+ str(exc)
+ "\nSeedPass secret mode requires clipboard support. "
"Install xclip or disable secret mode to view secrets."
)
logger.warning(warning)
print(warning)
return False
_ensure_clipboard()
pyperclip.copy(text)
def clear_clipboard() -> None:
@@ -44,3 +50,4 @@ def copy_to_clipboard(text: str, timeout: int) -> None:
timer = threading.Timer(timeout, clear_clipboard)
timer.daemon = True
timer.start()
return True