import pyperclip
import keyboard
import json
import os
import time
import threading
from datetime import datetime
from pathlib import Path
# ============================================================
# CONFIGURATION
# ============================================================
HISTORY_FILE = "clipboard_history.json"
MAX_ENTRIES = 50 # Maximum clipboard entries to store
POLL_INTERVAL = 1.0 # Seconds between clipboard checks
PREVIEW_LEN = 60 # Characters to show in preview
# ============================================================
# LOAD & SAVE HISTORY
# ============================================================
def load_history():
if Path(HISTORY_FILE).exists():
try:
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except:
return []
return []
def save_history(history):
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump(history, f, indent=2, ensure_ascii=False)
# ============================================================
# ADD ENTRY
# ============================================================
def add_entry(history, text):
# Avoid duplicate consecutive entries
if history and history[-1]["text"] == text:
return history
# Avoid duplicates anywhere in history — move to top instead
history = [h for h in history if h["text"] != text]
entry = {
"text": text,
"timestamp": datetime.now().strftime("%d-%m-%Y %H:%M:%S"),
"length": len(text)
}
history.append(entry)
# Keep only last MAX_ENTRIES
if len(history) > MAX_ENTRIES:
history = history[-MAX_ENTRIES:]
save_history(history)
return history
# ============================================================
# MONITOR CLIPBOARD (Background Thread)
# ============================================================
class ClipboardMonitor(threading.Thread):
def __init__(self):
super().__init__(daemon=True)
self.history = load_history()
self.last_text = ""
self.running = True
self.entry_count = len(self.history)
def run(self):
print(" Clipboard monitor started (background)...")
while self.running:
try:
current = pyperclip.paste()
if current and current != self.last_text:
self.history = add_entry(self.history, current)
self.last_text = current
new_count = len(self.history)
if new_count != self.entry_count:
preview = current[:PREVIEW_LEN].replace("\n", " ")
print(f"\n Captured: \"{preview}{'...' if len(current) > PREVIEW_LEN else ''}\"")
print(" > ", end="", flush=True)
self.entry_count = new_count
except:
pass
time.sleep(POLL_INTERVAL)
def stop(self):
self.running = False
# ============================================================
# DISPLAY HISTORY
# ============================================================
def display_history(history):
if not history:
print("\n No clipboard history yet.")
return
print("\n" + "="*60)
print(f" CLIPBOARD HISTORY ({len(history)} entries)")
print("="*60)
for i, entry in enumerate(reversed(history), 1):
preview = entry["text"][:PREVIEW_LEN].replace("\n", " ")
dots = "..." if len(entry["text"]) > PREVIEW_LEN else ""
print(f"\n [{i:02d}] {entry['timestamp']} | {entry['length']} chars")
print(f" \"{preview}{dots}\"")
print("\n" + "="*60)
# ============================================================
# SEARCH HISTORY
# ============================================================
def search_history(history, keyword):
keyword_lower = keyword.lower()
results = [
(i, h) for i, h in enumerate(history)
if keyword_lower in h["text"].lower()
]
if not results:
print(f"\n No results found for: \"{keyword}\"")
return
print(f"\n Found {len(results)} result(s) for \"{keyword}\":\n")
print("-"*60)
for rank, (idx, entry) in enumerate(results, 1):
preview = entry["text"][:PREVIEW_LEN].replace("\n", " ")
dots = "..." if len(entry["text"]) > PREVIEW_LEN else ""
print(f" [{rank}] 🕒 {entry['timestamp']} (index {idx+1})")
print(f" \"{preview}{dots}\"")
print()
# ============================================================
# RESTORE TO CLIPBOARD
# ============================================================
def restore_entry(history, index):
"""Copy a history entry back to clipboard by display index (1-based, newest first)."""
if index < 1 or index > len(history):
print(" Invalid index.")
return
# Display is reversed so index 1 = last item
real_index = len(history) - index
entry = history[real_index]
pyperclip.copy(entry["text"])
preview = entry["text"][:PREVIEW_LEN].replace("\n", " ")
print(f"\n Restored to clipboard: \"{preview}{'...' if len(entry['text']) > PREVIEW_LEN else ''}\"")
# ============================================================
# DELETE ENTRY
# ============================================================
def delete_entry(history, index):
if index < 1 or index > len(history):
print(" Invalid index.")
return history
real_index = len(history) - index
removed = history.pop(real_index)
save_history(history)
preview = removed["text"][:PREVIEW_LEN].replace("\n", " ")
print(f"\n Deleted: \"{preview}{'...' if len(removed['text']) > PREVIEW_LEN else ''}\"")
return history
# ============================================================
# CLEAR ALL HISTORY
# ============================================================
def clear_history():
confirm = input("\n ⚠ Clear ALL clipboard history? (yes/no): ").strip().lower()
if confirm == "yes":
save_history([])
print(" Clipboard history cleared.")
return []
print(" Cancelled.")
return load_history()
# ============================================================
# EXPORT HISTORY TO TXT
# ============================================================
def export_history(history):
if not history:
print(" Nothing to export.")
return
filename = f"clipboard_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
with open(filename, "w", encoding="utf-8") as f:
f.write("CLIPBOARD HISTORY EXPORT\n")
f.write(f"Exported: {datetime.now().strftime('%d-%m-%Y %H:%M:%S')}\n")
f.write(f"Total entries: {len(history)}\n")
f.write("="*60 + "\n\n")
for i, entry in enumerate(reversed(history), 1):
f.write(f"[{i:02d}] {entry['timestamp']} ({entry['length']} chars)\n")
f.write(entry["text"] + "\n")
f.write("-"*60 + "\n\n")
print(f"\n Exported to: {filename}")
# ============================================================
# MAIN MENU
# ============================================================
def print_menu():
print("\n" + "-"*40)
print(" CLIPBOARD HISTORY MANAGER")
print("-"*40)
print(" 1. View history")
print(" 2. Search history")
print(" 3. Restore entry to clipboard")
print(" 4. Delete an entry")
print(" 5. Export history to .txt")
print(" 6. Clear all history")
print(" 0. Exit")
print("-"*40)
def main():
print("\n" + "="*55)
print(" CLIPBOARD HISTORY MANAGER")
print("="*55)
print(f"\n Max entries : {MAX_ENTRIES}")
print(f" History file: {HISTORY_FILE}")
print(f" Poll interval: every {POLL_INTERVAL}s")
# Start background monitor
monitor = ClipboardMonitor()
monitor.start()
time.sleep(0.5)
while True:
print_menu()
choice = input(" > ").strip()
if choice == "1":
display_history(monitor.history)
elif choice == "2":
keyword = input("\n Search keyword: ").strip()
if keyword:
search_history(monitor.history, keyword)
elif choice == "3":
display_history(monitor.history)
try:
idx = int(input("\n Enter entry number to restore: ").strip())
restore_entry(monitor.history, idx)
except ValueError:
print(" Invalid number.")
elif choice == "4":
display_history(monitor.history)
try:
idx = int(input("\n Enter entry number to delete: ").strip())
monitor.history = delete_entry(monitor.history, idx)
except ValueError:
print(" Invalid number.")
elif choice == "5":
export_history(monitor.history)
elif choice == "6":
monitor.history = clear_history()
elif choice == "0":
monitor.stop()
print("\n Clipboard History Manager closed. Goodbye!\n")
break
else:
print(" Invalid choice. Try again.")
# ============================================================
# RUN
# ============================================================
if __name__ == "__main__":
main()
No comments:
Post a Comment