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

@@ -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