5 min read

๐Ÿ“‹ Clipboard Vault (PowerShell 7.5)

Overview

A powerful clipboard manager for PowerShell 7.5. It captures everything you copy and provides a rich, searchable history through an interactive HTML interface. It's designed to be a "vault" for your clipboard, preventing data loss and making it easy to find previously copied items.

Functionality

  • Live Clipboard Monitoring: Continuously captures text copied to the clipboard.
  • Interactive HTML Viewer: Automatically generates a Clipboard-Viewer.html file to browse, search, and manage your history.
  • Intelligent De-duplication: When you re-copy an item, it's moved to the top of the history instead of creating a duplicate entry.
  • Powerful Search: The HTML viewer supports real-time searching of your clipboard history, including regular expressions.
  • One-Click Copy: Easily copy any item from the history back to your clipboard.
  • Database Cleanup: A feature in the viewer allows you to remove all historical duplicates, keeping only the most recent unique entries.
  • JSON Storage: History is stored in a clean, human-readable clipboard_history.json file.

Usage

  1. Ensure Clipboard-Manager.ps1 and Clipboard-Functions.ps1 are in the same directory.
  2. Open a PowerShell 7 terminal.
  3. Navigate to the script's directory.
  4. Run the script: .\Clipboard-Manager.ps1
  5. The script will start monitoring. To view your history, open the Logs\Clipboard-Viewer.html file that is created in the script's directory. The viewer updates as you copy new items.
  6. To stop the script, press Ctrl+C in the PowerShell window.

Requirements

  • PowerShell 7.5 or later
  • Windows operating system

Code Example

# Configuration
$LogDir = "$PSScriptRoot\Logs"
$ClipboardLogFile = "$LogDir\clipboard_history.json"
$MaxHistoryItems = 50

# Ensure log directory exists
if (!(Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir | Out-Null }

Write-Host "--- Clipboard Vault Active ---" -ForegroundColor Cyan
Write-Host "Press 'Ctrl+C' in this window to stop monitoring." -ForegroundColor Gray

function Update-HtmlViewer {
    param($History)

    $HtmlPath = "$PSScriptRoot\Logs\Clipboard-Viewer.html"

    $Header = @"
<!DOCTYPE html>
<html>
<head>
    <title>Clipboard Vault Viewer</title>
    <style>
        body { font-family: 'Segoe UI', sans-serif; background: <a href='/?search=%231a1a1a' class='obsidian-tag'>#1a1a1a</a>; color: <a href='/?search=%23e0e0e0' class='obsidian-tag'>#e0e0e0</a>; padding: 20px; }
        .entry { background: <a href='/?search=%232d2d2d' class='obsidian-tag'>#2d2d2d</a>; border-radius: 8px; padding: 15px; margin-bottom: 10px; border: 1px solid <a href='/?search=%233d3d3d' class='obsidian-tag'>#3d3d3d</a>; transition: 0.2s; }
        .entry:hover { border-color: <a href='/?search=%2300ff9d' class='obsidian-tag'>#00ff9d</a>; }
        .meta { font-size: 0.8em; color: #888; margin-bottom: 8px; display: flex; justify-content: space-between; }
        .content { font-family: 'Cascadia Code', monospace; white-space: pre-wrap; background: #121212; padding: 10px; border-radius: 4px; font-size: 0.9em; }
        .copy-btn { background: <a href='/?search=%2300ff9d' class='obsidian-tag'>#00ff9d</a>; color: <a href='/?search=%231a1a1a' class='obsidian-tag'>#1a1a1a</a>; border: none; padding: 4px 10px; border-radius: 4px; cursor: pointer; font-weight: bold; }
        .copy-btn:active { transform: scale(0.95); }
        input { width: 100%; padding: 10px; border-radius: 5px; border: 1px solid <a href='/?search=%233d3d3d' class='obsidian-tag'>#3d3d3d</a>; background: <a href='/?search=%232d2d2d' class='obsidian-tag'>#2d2d2d</a>; color: white; margin-bottom: 20px; }
    </style>
</head>
<body>
    <h2>๐Ÿ“‹ Clipboard Vault</h2>
    <div style="margin-bottom: 15px; display: flex; align-items: center; gap: 10px;">
        <input type="checkbox" id="hideDupes" style="width: auto; margin: 0;" onchange="filter()">
        <label for="hideDupes">Hide Duplicate Content</label>
    </div>
    <button class="copy-btn" style="background: <a href='/?search=%23ff4d4d' class='obsidian-tag'>#ff4d4d</a>;" onclick="requestCleanup()">๐Ÿ—‘๏ธ DELETE DUPLICATES</button>
    <input type="text" id="search" placeholder="Search clips (Regex supported)..." onkeyup="filter()">
    <div id="container">
"@

    $Body = ""
    foreach ($Item in $History) {
        $EscapedContent = [System.Web.HttpUtility]::HtmlEncode($Item.Content)
        $Body += @"
        <div class="entry" data-hash="$($Item.Hash)">
            <div class="meta">
                <span>$($Item.Timestamp)</span>
                <button class="copy-btn" onclick="copyToClipboard(this)">COPY</button>
            </div>
            <div class="content">$EscapedContent</div>
        </div>
"@
    }

    $Footer = @"
    </div>
    <script>
        function requestCleanup() {
            // We create a tiny "flag" file that the PowerShell script will see
            // This is a clever way for a browser to 'talk' to a local script
            const blob = new Blob(['cleanup'], {type: 'text/plain'});
            const anchor = document.createElement('a');
            anchor.download = 'cleanup.req'; // The extension the script looks for
            anchor.href = window.URL.createObjectURL(blob);
            anchor.click();
            alert('Cleanup request sent! PowerShell will process this in a few seconds.');
        }
    </script>
    <script>
        function copyToClipboard(btn) {
            const text = btn.parentElement.nextElementSibling.innerText;
            navigator.clipboard.writeText(text);
            const original = btn.innerText;
            btn.innerText = 'โœ… SAVED';
            setTimeout(() => btn.innerText = original, 1000);
        }

        function filter() {
            const query = document.getElementById('search').value;
            const hideDupes = document.getElementById('hideDupes').checked;
            const entries = document.querySelectorAll('.entry');
            const regex = new RegExp(query, 'i');
            const seenHashes = new Set();

            entries.forEach(entry => {
                const text = entry.querySelector('.content').innerText;
                const hash = entry.getAttribute('data-hash');

                let isMatch = regex.test(text);
                let isDuplicate = seenHashes.has(hash);

                // Mark hash as seen
                seenHashes.add(hash);

                // Logic: Show if it matches search AND (if hideDupes is on, it must not be a duplicate)
                if (isMatch && (!hideDupes || !isDuplicate)) {
                    entry.style.display = 'block';
                } else {
                    entry.style.display = 'none';
                }
            });
        }
    </script>
</body>
</html>
"@

    $Header + $Body + $Footer | Out-File $HtmlPath -Force
}

$LastContent = ""

# The "Engine"
try {
    while ($true) {
        # Fetch clipboard text (PS 7.5 compatible)
        $CurrentContent = Get-Clipboard -Raw -ErrorAction SilentlyContinue

        # Inside your while ($true) loop, at the very beginning:

        $ReqFile = "$LogDir\cleanup.req"
        if (Test-Path $ReqFile) {
            Write-Host "๐Ÿงน Cleanup Requested! Removing duplicates..." -ForegroundColor Yellow

            if (Test-Path $ClipboardLogFile) {
                $CurrentHistory = Get-Content $ClipboardLogFile | ConvertFrom-Json

                # This magic line keeps only the newest unique hash
                $CleanedHistory = $CurrentHistory | Group-Object Hash | ForEach-Object { $_.Group | Select-Object -First 1 }

                # Sort back by time (Newest first)
                $CleanedHistory = $CleanedHistory | Sort-Object Timestamp -Descending

                # Save and Update
                $CleanedHistory | ConvertTo-Json | Out-File $ClipboardLogFile -Force
                Update-HtmlViewer -History $CleanedHistory
            }

            Remove-Item $ReqFile -ErrorAction SilentlyContinue
            Write-Host "โœจ Database Cleaned." -ForegroundColor Green
        }

        # Inside your while($true) loop:
        if ($null -ne $CurrentContent -and $CurrentContent -ne $LastContent) {
            $LastContent = $CurrentContent

            $History = if (Test-Path $ClipboardLogFile) { Get-Content $ClipboardLogFile | ConvertFrom-Json } else { @() }

            # Create a unique ID based on the content so we can identify duplicates
            $ContentHash = [Convert]::ToBase64String([System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes($CurrentContent)))

            # Remove the old version if it exists (so it "jumps" to the top instead of duplicating)
            $History = $History | Where-Object { $_.Hash -ne $ContentHash }

            $NewEntry = [PSCustomObject]@{
                Timestamp = (Get-Date).ToString("HH:mm:ss")
                Hash      = $ContentHash  # Add this!
                Content   = $CurrentContent.Trim()
            }

            $History = ,$NewEntry + $History | Select-Object -First $MaxHistoryItems
            $History | ConvertTo-Json | Out-File $ClipboardLogFile -Force
            Update-HtmlViewer -History $History
        }

        # Check if it's new, not null, and not just empty whitespace
        if ($null -ne $CurrentContent -and $CurrentContent -ne $LastContent -and !([string]::IsNullOrWhiteSpace($CurrentContent))) {

            $LastContent = $CurrentContent

            # Load existing history
            $History = if (Test-Path $ClipboardLogFile) { 
                Get-Content $ClipboardLogFile | ConvertFrom-Json 
            } else { @() }

            # Create a new Entry
            $NewEntry = [PSCustomObject]@{
                Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
                Content   = $CurrentContent.Trim()
            }

            # Add to history (Newest at top)
            $History = ,$NewEntry + $History | Select-Object -First $MaxHistoryItems

            # Save to JSON
            $History | ConvertTo-Json -Depth 3 | Out-File $ClipboardLogFile -Force

            # Update HTML Viewer
            Update-HtmlViewer -History $History

            # Visual Feedback
            $Display = if ($CurrentContent.Length -gt 40) { $CurrentContent.Substring(0,40).Trim() + "..." } else { $CurrentContent.Trim() }
            Write-Host "[$(Get-Date -Format HH:mm:ss)] ๐Ÿ“‹ Captured: $Display" -ForegroundColor Green
        }

        Start-Sleep -Milliseconds 500
    }
}
catch {
    Write-Error "Vault Error: $($_.Exception.Message)"
}
finally {
    Write-Host "`nVault Closed." -ForegroundColor Yellow
}