mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Update settings menu test for new option
This commit is contained in:
@@ -266,9 +266,10 @@ Back in the Settings menu you can:
|
||||
* Select `8` to export all 2FA codes.
|
||||
* Choose `9` to set an additional backup location.
|
||||
* Select `10` to change the inactivity timeout.
|
||||
* Choose `11` to lock the vault and require re-entry of your password.
|
||||
* Select `12` to return to the main menu.
|
||||
* Choose `13` to view seed profile stats.
|
||||
* Choose `11` to toggle Secret Mode and set the clipboard clear delay.
|
||||
* Select `12` to lock the vault and require re-entry of your password.
|
||||
* Choose `13` to return to the main menu.
|
||||
* Select `14` to view seed profile stats.
|
||||
|
||||
## Running Tests
|
||||
|
||||
@@ -440,6 +441,7 @@ The SeedPass roadmap outlines a structured development plan divided into distinc
|
||||
- **Toggle Setting:** Allow users to enable or disable "secret" mode.
|
||||
- **Clipboard Integration:** Ensure passwords are copied securely to the clipboard when "secret" mode is active.
|
||||
- **User Feedback:** Notify users that the password has been copied to the clipboard.
|
||||
- **Settings Menu:** Toggle this mode under `Settings -> Toggle Secret Mode` and set how long the clipboard is retained.
|
||||
- **Two-Factor Security Model with Random Index Generation**
|
||||
- **Description:** Create a robust two-factor security system using a master seed and master password combination, enhanced with random index generation for additional security.
|
||||
- **Key Features:**
|
||||
|
@@ -494,6 +494,8 @@ seedpass set-secret --disable
|
||||
- `--enable`: Activates "secret" mode.
|
||||
- `--disable`: Deactivates "secret" mode.
|
||||
|
||||
You can also enable or disable secret mode from the interactive Settings menu by selecting **Toggle Secret Mode**.
|
||||
|
||||
---
|
||||
|
||||
### 21. Batch Post Snapshot Deltas to Nostr
|
||||
|
54
src/main.py
54
src/main.py
@@ -499,6 +499,47 @@ def handle_set_additional_backup_location(pm: PasswordManager) -> None:
|
||||
print(colored(f"Error: {e}", "red"))
|
||||
|
||||
|
||||
def handle_toggle_secret_mode(pm: PasswordManager) -> None:
|
||||
"""Toggle secret mode and adjust clipboard delay."""
|
||||
cfg = pm.config_manager
|
||||
if cfg is None:
|
||||
print(colored("Configuration manager unavailable.", "red"))
|
||||
return
|
||||
try:
|
||||
enabled = cfg.get_secret_mode_enabled()
|
||||
delay = cfg.get_clipboard_clear_delay()
|
||||
except Exception as exc:
|
||||
logging.error(f"Error loading secret mode settings: {exc}")
|
||||
print(colored(f"Error loading settings: {exc}", "red"))
|
||||
return
|
||||
print(colored(f"Secret mode is currently {'ON' if enabled else 'OFF'}", "cyan"))
|
||||
value = input("Enable secret mode? (y/n, blank to keep): ").strip().lower()
|
||||
if value in ("y", "yes"):
|
||||
enabled = True
|
||||
elif value in ("n", "no"):
|
||||
enabled = False
|
||||
dur = input(f"Clipboard clear delay in seconds [{delay}]: ").strip()
|
||||
if dur:
|
||||
try:
|
||||
delay = int(dur)
|
||||
if delay <= 0:
|
||||
print(colored("Delay must be positive.", "red"))
|
||||
return
|
||||
except ValueError:
|
||||
print(colored("Invalid number.", "red"))
|
||||
return
|
||||
try:
|
||||
cfg.set_secret_mode_enabled(enabled)
|
||||
cfg.set_clipboard_clear_delay(delay)
|
||||
pm.secret_mode_enabled = enabled
|
||||
pm.clipboard_clear_delay = delay
|
||||
status = "enabled" if enabled else "disabled"
|
||||
print(colored(f"Secret mode {status}.", "green"))
|
||||
except Exception as exc:
|
||||
logging.error(f"Error saving secret mode: {exc}")
|
||||
print(colored(f"Error: {exc}", "red"))
|
||||
|
||||
|
||||
def handle_profiles_menu(password_manager: PasswordManager) -> None:
|
||||
"""Submenu for managing seed profiles."""
|
||||
while True:
|
||||
@@ -583,9 +624,10 @@ def handle_settings(password_manager: PasswordManager) -> None:
|
||||
print("8. Export 2FA codes")
|
||||
print("9. Set additional backup location")
|
||||
print("10. Set inactivity timeout")
|
||||
print("11. Lock Vault")
|
||||
print("12. Stats")
|
||||
print("13. Back")
|
||||
print("11. Toggle Secret Mode")
|
||||
print("12. Lock Vault")
|
||||
print("13. Stats")
|
||||
print("14. Back")
|
||||
choice = input("Select an option: ").strip()
|
||||
if choice == "1":
|
||||
handle_profiles_menu(password_manager)
|
||||
@@ -610,12 +652,14 @@ def handle_settings(password_manager: PasswordManager) -> None:
|
||||
elif choice == "10":
|
||||
handle_set_inactivity_timeout(password_manager)
|
||||
elif choice == "11":
|
||||
handle_toggle_secret_mode(password_manager)
|
||||
elif choice == "12":
|
||||
password_manager.lock_vault()
|
||||
print(colored("Vault locked. Please re-enter your password.", "yellow"))
|
||||
password_manager.unlock_vault()
|
||||
elif choice == "12":
|
||||
handle_display_stats(password_manager)
|
||||
elif choice == "13":
|
||||
handle_display_stats(password_manager)
|
||||
elif choice == "14":
|
||||
break
|
||||
else:
|
||||
print(colored("Invalid choice.", "red"))
|
||||
|
@@ -45,6 +45,8 @@ class ConfigManager:
|
||||
"password_hash": "",
|
||||
"inactivity_timeout": INACTIVITY_TIMEOUT,
|
||||
"additional_backup_path": "",
|
||||
"secret_mode_enabled": False,
|
||||
"clipboard_clear_delay": 45,
|
||||
}
|
||||
try:
|
||||
data = self.vault.load_config()
|
||||
@@ -56,6 +58,8 @@ class ConfigManager:
|
||||
data.setdefault("password_hash", "")
|
||||
data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT)
|
||||
data.setdefault("additional_backup_path", "")
|
||||
data.setdefault("secret_mode_enabled", False)
|
||||
data.setdefault("clipboard_clear_delay", 45)
|
||||
|
||||
# Migrate legacy hashed_password.enc if present and password_hash is missing
|
||||
legacy_file = self.fingerprint_dir / "hashed_password.enc"
|
||||
@@ -144,3 +148,27 @@ class ConfigManager:
|
||||
config = self.load_config(require_pin=False)
|
||||
value = config.get("additional_backup_path", "")
|
||||
return value or None
|
||||
|
||||
def set_secret_mode_enabled(self, enabled: bool) -> None:
|
||||
"""Persist the secret mode toggle."""
|
||||
config = self.load_config(require_pin=False)
|
||||
config["secret_mode_enabled"] = bool(enabled)
|
||||
self.save_config(config)
|
||||
|
||||
def get_secret_mode_enabled(self) -> bool:
|
||||
"""Retrieve whether secret mode is enabled."""
|
||||
config = self.load_config(require_pin=False)
|
||||
return bool(config.get("secret_mode_enabled", False))
|
||||
|
||||
def set_clipboard_clear_delay(self, delay: int) -> None:
|
||||
"""Persist clipboard clear timeout in seconds."""
|
||||
if delay <= 0:
|
||||
raise ValueError("Delay must be positive")
|
||||
config = self.load_config(require_pin=False)
|
||||
config["clipboard_clear_delay"] = int(delay)
|
||||
self.save_config(config)
|
||||
|
||||
def get_clipboard_clear_delay(self) -> int:
|
||||
"""Retrieve clipboard clear delay in seconds."""
|
||||
config = self.load_config(require_pin=False)
|
||||
return int(config.get("clipboard_clear_delay", 45))
|
||||
|
@@ -42,6 +42,7 @@ from utils.password_prompt import (
|
||||
confirm_action,
|
||||
)
|
||||
from utils.memory_protection import InMemorySecret
|
||||
from utils.clipboard import copy_to_clipboard
|
||||
from constants import MIN_HEALTHY_RELAYS
|
||||
|
||||
from constants import (
|
||||
@@ -106,6 +107,8 @@ class PasswordManager:
|
||||
self.last_activity: float = time.time()
|
||||
self.locked: bool = False
|
||||
self.inactivity_timeout: float = INACTIVITY_TIMEOUT
|
||||
self.secret_mode_enabled: bool = False
|
||||
self.clipboard_clear_delay: int = 45
|
||||
|
||||
# Initialize the fingerprint manager first
|
||||
self.initialize_fingerprint_manager()
|
||||
@@ -776,6 +779,8 @@ class PasswordManager:
|
||||
self.inactivity_timeout = config.get(
|
||||
"inactivity_timeout", INACTIVITY_TIMEOUT
|
||||
)
|
||||
self.secret_mode_enabled = bool(config.get("secret_mode_enabled", False))
|
||||
self.clipboard_clear_delay = int(config.get("clipboard_clear_delay", 45))
|
||||
|
||||
self.nostr_client = NostrClient(
|
||||
encryption_manager=self.encryption_manager,
|
||||
@@ -1021,9 +1026,18 @@ class PasswordManager:
|
||||
try:
|
||||
while True:
|
||||
code = self.entry_manager.get_totp_code(index, self.parent_seed)
|
||||
print(colored("\n[+] Retrieved 2FA Code:\n", "green"))
|
||||
print(colored(f"Label: {label}", "cyan"))
|
||||
print(colored(f"Code: {code}", "yellow"))
|
||||
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",
|
||||
)
|
||||
)
|
||||
else:
|
||||
print(colored("\n[+] Retrieved 2FA Code:\n", "green"))
|
||||
print(colored(f"Label: {label}", "cyan"))
|
||||
print(colored(f"Code: {code}", "yellow"))
|
||||
if notes:
|
||||
print(colored(f"Notes: {notes}", "cyan"))
|
||||
remaining = self.entry_manager.get_totp_time_remaining(index)
|
||||
@@ -1084,18 +1098,30 @@ class PasswordManager:
|
||||
password = self.password_generator.generate_password(length, index)
|
||||
|
||||
if password:
|
||||
print(
|
||||
colored(f"\n[+] Retrieved Password for {website_name}:\n", "green")
|
||||
)
|
||||
print(colored(f"Password: {password}", "yellow"))
|
||||
print(colored(f"Associated Username: {username or 'N/A'}", "cyan"))
|
||||
print(colored(f"Associated URL: {url or 'N/A'}", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f"Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}",
|
||||
"cyan",
|
||||
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",
|
||||
)
|
||||
)
|
||||
else:
|
||||
print(
|
||||
colored(
|
||||
f"\n[+] Retrieved Password for {website_name}:\n",
|
||||
"green",
|
||||
)
|
||||
)
|
||||
print(colored(f"Password: {password}", "yellow"))
|
||||
print(colored(f"Associated Username: {username or 'N/A'}", "cyan"))
|
||||
print(colored(f"Associated URL: {url or 'N/A'}", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f"Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}",
|
||||
"cyan",
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
print(colored("Error: Failed to retrieve the password.", "red"))
|
||||
except Exception as e:
|
||||
@@ -1402,7 +1428,13 @@ class PasswordManager:
|
||||
remaining = self.entry_manager.get_totp_time_remaining(idx)
|
||||
filled = int(20 * (period - remaining) / period)
|
||||
bar = "[" + "#" * filled + "-" * (20 - filled) + "]"
|
||||
print(f"[{idx}] {label}: {code} {bar} {remaining:2d}s")
|
||||
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"
|
||||
)
|
||||
else:
|
||||
print(f"[{idx}] {label}: {code} {bar} {remaining:2d}s")
|
||||
if imported_list:
|
||||
print(colored("\nImported 2FA Codes:", "green"))
|
||||
for label, idx, period, _ in imported_list:
|
||||
@@ -1410,7 +1442,13 @@ class PasswordManager:
|
||||
remaining = self.entry_manager.get_totp_time_remaining(idx)
|
||||
filled = int(20 * (period - remaining) / period)
|
||||
bar = "[" + "#" * filled + "-" * (20 - filled) + "]"
|
||||
print(f"[{idx}] {label}: {code} {bar} {remaining:2d}s")
|
||||
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"
|
||||
)
|
||||
else:
|
||||
print(f"[{idx}] {label}: {code} {bar} {remaining:2d}s")
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
if sys.stdin in select.select([sys.stdin], [], [], 1)[0]:
|
||||
|
68
src/tests/test_clipboard_utils.py
Normal file
68
src/tests/test_clipboard_utils.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from pathlib import Path
|
||||
import pyperclip
|
||||
import threading
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from utils.clipboard import copy_to_clipboard
|
||||
|
||||
|
||||
def test_copy_to_clipboard_clears(monkeypatch):
|
||||
clipboard = {"text": ""}
|
||||
|
||||
def fake_copy(val):
|
||||
clipboard["text"] = val
|
||||
|
||||
def fake_paste():
|
||||
return clipboard["text"]
|
||||
|
||||
callbacks = {}
|
||||
|
||||
class DummyTimer:
|
||||
def __init__(self, delay, func):
|
||||
callbacks["delay"] = delay
|
||||
callbacks["func"] = func
|
||||
|
||||
def start(self):
|
||||
callbacks["started"] = True
|
||||
|
||||
monkeypatch.setattr(pyperclip, "copy", fake_copy)
|
||||
monkeypatch.setattr(pyperclip, "paste", fake_paste)
|
||||
monkeypatch.setattr(threading, "Timer", DummyTimer)
|
||||
|
||||
copy_to_clipboard("secret", 2)
|
||||
assert clipboard["text"] == "secret"
|
||||
assert callbacks["delay"] == 2
|
||||
assert callbacks["started"]
|
||||
callbacks["func"]()
|
||||
assert clipboard["text"] == ""
|
||||
|
||||
|
||||
def test_copy_to_clipboard_does_not_clear_if_changed(monkeypatch):
|
||||
clipboard = {"text": ""}
|
||||
|
||||
def fake_copy(val):
|
||||
clipboard["text"] = val
|
||||
|
||||
def fake_paste():
|
||||
return clipboard["text"]
|
||||
|
||||
callbacks = {}
|
||||
|
||||
class DummyTimer:
|
||||
def __init__(self, delay, func):
|
||||
callbacks["func"] = func
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(pyperclip, "copy", fake_copy)
|
||||
monkeypatch.setattr(pyperclip, "paste", fake_paste)
|
||||
monkeypatch.setattr(threading, "Timer", DummyTimer)
|
||||
|
||||
copy_to_clipboard("secret", 1)
|
||||
fake_copy("other")
|
||||
callbacks["func"]()
|
||||
assert clipboard["text"] == "other"
|
@@ -130,3 +130,19 @@ def test_additional_backup_path_round_trip():
|
||||
cfg_mgr.set_additional_backup_path(None)
|
||||
cfg2 = cfg_mgr.load_config(require_pin=False)
|
||||
assert cfg2["additional_backup_path"] == ""
|
||||
|
||||
|
||||
def test_secret_mode_round_trip():
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
vault, enc_mgr = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
|
||||
cfg_mgr = ConfigManager(vault, Path(tmpdir))
|
||||
|
||||
cfg = cfg_mgr.load_config(require_pin=False)
|
||||
assert cfg["secret_mode_enabled"] is False
|
||||
assert cfg["clipboard_clear_delay"] == 45
|
||||
|
||||
cfg_mgr.set_secret_mode_enabled(True)
|
||||
cfg_mgr.set_clipboard_clear_delay(99)
|
||||
cfg2 = cfg_mgr.load_config(require_pin=False)
|
||||
assert cfg2["secret_mode_enabled"] is True
|
||||
assert cfg2["clipboard_clear_delay"] == 99
|
||||
|
@@ -39,6 +39,7 @@ def test_handle_display_totp_codes(monkeypatch, capsys):
|
||||
pm.nostr_client = FakeNostrClient()
|
||||
pm.fingerprint_dir = tmp_path
|
||||
pm.is_dirty = False
|
||||
pm.secret_mode_enabled = False
|
||||
|
||||
entry_mgr.add_totp("Example", TEST_SEED)
|
||||
|
||||
@@ -78,6 +79,7 @@ def test_display_totp_codes_excludes_blacklisted(monkeypatch, capsys):
|
||||
pm.nostr_client = FakeNostrClient()
|
||||
pm.fingerprint_dir = tmp_path
|
||||
pm.is_dirty = False
|
||||
pm.secret_mode_enabled = False
|
||||
|
||||
entry_mgr.add_totp("Visible", TEST_SEED)
|
||||
entry_mgr.add_totp("Hidden", TEST_SEED)
|
||||
|
@@ -39,6 +39,7 @@ def test_handle_retrieve_totp_entry(monkeypatch, capsys):
|
||||
pm.nostr_client = FakeNostrClient()
|
||||
pm.fingerprint_dir = tmp_path
|
||||
pm.is_dirty = False
|
||||
pm.secret_mode_enabled = False
|
||||
|
||||
entry_mgr.add_totp("Example", TEST_SEED)
|
||||
|
||||
|
82
src/tests/test_secret_mode.py
Normal file
82
src/tests/test_secret_mode.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from types import SimpleNamespace
|
||||
|
||||
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from password_manager.entry_management import EntryManager
|
||||
from password_manager.backup import BackupManager
|
||||
from password_manager.manager import PasswordManager, EncryptionMode
|
||||
from password_manager.config_manager import ConfigManager
|
||||
|
||||
|
||||
def setup_pm(tmp_path):
|
||||
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
|
||||
cfg_mgr = ConfigManager(vault, tmp_path)
|
||||
backup_mgr = BackupManager(tmp_path, cfg_mgr)
|
||||
entry_mgr = EntryManager(vault, backup_mgr)
|
||||
pm = PasswordManager.__new__(PasswordManager)
|
||||
pm.encryption_mode = EncryptionMode.SEED_ONLY
|
||||
pm.encryption_manager = enc_mgr
|
||||
pm.vault = vault
|
||||
pm.entry_manager = entry_mgr
|
||||
pm.backup_manager = backup_mgr
|
||||
pm.password_generator = SimpleNamespace(generate_password=lambda l, i: "pw")
|
||||
pm.parent_seed = TEST_SEED
|
||||
pm.nostr_client = SimpleNamespace()
|
||||
pm.fingerprint_dir = tmp_path
|
||||
pm.config_manager = cfg_mgr
|
||||
pm.secret_mode_enabled = True
|
||||
pm.clipboard_clear_delay = 5
|
||||
return pm, entry_mgr
|
||||
|
||||
|
||||
def test_password_retrieve_secret_mode(monkeypatch, capsys):
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
tmp = Path(tmpdir)
|
||||
pm, entry_mgr = setup_pm(tmp)
|
||||
entry_mgr.add_entry("example", 8)
|
||||
|
||||
monkeypatch.setattr("builtins.input", lambda *a, **k: "0")
|
||||
called = []
|
||||
monkeypatch.setattr(
|
||||
"password_manager.manager.copy_to_clipboard",
|
||||
lambda text, t: called.append((text, t)),
|
||||
)
|
||||
|
||||
pm.handle_retrieve_entry()
|
||||
out = capsys.readouterr().out
|
||||
assert "Password:" not in out
|
||||
assert "copied to clipboard" in out
|
||||
assert called == [("pw", 5)]
|
||||
|
||||
|
||||
def test_totp_display_secret_mode(monkeypatch, capsys):
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
tmp = Path(tmpdir)
|
||||
pm, entry_mgr = setup_pm(tmp)
|
||||
entry_mgr.add_totp("Example", TEST_SEED)
|
||||
|
||||
monkeypatch.setattr(pm.entry_manager, "get_totp_code", lambda *a, **k: "123456")
|
||||
monkeypatch.setattr(
|
||||
pm.entry_manager, "get_totp_time_remaining", lambda *a, **k: 30
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"password_manager.manager.select.select",
|
||||
lambda *a, **k: (_ for _ in ()).throw(KeyboardInterrupt()),
|
||||
)
|
||||
called = []
|
||||
monkeypatch.setattr(
|
||||
"password_manager.manager.copy_to_clipboard",
|
||||
lambda text, t: called.append((text, t)),
|
||||
)
|
||||
|
||||
pm.handle_display_totp_codes()
|
||||
out = capsys.readouterr().out
|
||||
assert "123456" not in out
|
||||
assert "copied to clipboard" in out
|
||||
assert called == [("123456", 5)]
|
@@ -93,7 +93,7 @@ def test_settings_menu_additional_backup(monkeypatch):
|
||||
tmp_path = Path(tmpdir)
|
||||
pm, cfg_mgr, fp_mgr = setup_pm(tmp_path, monkeypatch)
|
||||
|
||||
inputs = iter(["9", "13"])
|
||||
inputs = iter(["9", "14"])
|
||||
with patch("main.handle_set_additional_backup_location") as handler:
|
||||
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
|
||||
main.handle_settings(pm)
|
||||
|
@@ -25,6 +25,7 @@ try:
|
||||
from .password_prompt import prompt_for_password
|
||||
from .input_utils import timed_input
|
||||
from .memory_protection import InMemorySecret
|
||||
from .clipboard import copy_to_clipboard
|
||||
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.info("Modules imported successfully.")
|
||||
@@ -49,4 +50,5 @@ __all__ = [
|
||||
"prompt_for_password",
|
||||
"timed_input",
|
||||
"InMemorySecret",
|
||||
"copy_to_clipboard",
|
||||
]
|
||||
|
16
src/utils/clipboard.py
Normal file
16
src/utils/clipboard.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import threading
|
||||
import pyperclip
|
||||
|
||||
|
||||
def copy_to_clipboard(text: str, timeout: int) -> None:
|
||||
"""Copy text to the clipboard and clear after timeout seconds if unchanged."""
|
||||
|
||||
pyperclip.copy(text)
|
||||
|
||||
def clear_clipboard() -> None:
|
||||
if pyperclip.paste() == text:
|
||||
pyperclip.copy("")
|
||||
|
||||
timer = threading.Timer(timeout, clear_clipboard)
|
||||
timer.daemon = True
|
||||
timer.start()
|
Reference in New Issue
Block a user