Clipboard History Manager

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: