// Name: Clipboard Diff Widget // Description: Opens a widget showing a colorful diff between the two most recent clipboard entries. // Author: tayiorbeii // GitHub: tayiorbeii import "@johnlindquist/kit" import { diffWords, diffLines } from "diff" const history = await getClipboardHistory().catch(() => []) const texts = (Array.isArray(history) ? history : []) .map((item: any) => { if (typeof item === "string") return item if (typeof item?.text === "string") return item.text if (typeof item?.content === "string") return item.content if (typeof item?.value === "string") return item.value return "" }) .filter(Boolean) const html = await buildHtml(texts) await widget(html, { width: 900, height: 600, }) async function buildHtml(texts: string[]) { if (texts.length < 2) { return ` <div class="h-screen w-screen flex items-center justify-center bg-slate-900 text-slate-100"> <div class="text-center max-w-xl p-8"> <h1 class="text-2xl font-semibold mb-2">Clipboard Diff Widget</h1> <p class="text-slate-300">Need at least two text clipboard entries to show a diff.</p> <p class="text-slate-400 mt-3">Copy some text twice, then re-run.</p> </div> </div>` } const current = texts[0] || "" const previous = texts[1] || "" const isMultiLine = current.includes("\n") || previous.includes("\n") || Math.max(current.length, previous.length) > 240 const parts = isMultiLine ? diffLines(previous, current) : diffWords(previous, current) const diffHtml = parts .map(part => { const text = escapeHtml(part.value) if (part.added) return `<span class="bg-emerald-500/20 text-emerald-200 underline decoration-emerald-400">${text}</span>` if (part.removed) return `<span class="bg-rose-500/20 text-rose-200 line-through decoration-rose-400">${text}</span>` return `<span class="text-slate-200">${text}</span>` }) .join("") const summary = buildSummary(previous, current) return ` <div class="h-screen w-screen bg-slate-900 text-slate-100"> <div class="max-w-5xl mx-auto p-6 space-y-4"> <header class="flex items-center justify-between"> <h1 class="text-xl font-semibold">Clipboard Diff</h1> <div class="text-xs text-slate-400">${summary}</div> </header> <section class="grid grid-cols-1 md:grid-cols-2 gap-3"> <div class="rounded-lg bg-slate-800/70 border border-slate-700 p-3"> <div class="text-xs uppercase tracking-wide text-slate-400 mb-2">Previous</div> <pre class="whitespace-pre-wrap text-slate-300 text-sm max-h-56 overflow-auto">${escapeHtml(trimPreview(previous))}</pre> </div> <div class="rounded-lg bg-slate-800/70 border border-slate-700 p-3"> <div class="text-xs uppercase tracking-wide text-slate-400 mb-2">Current</div> <pre class="whitespace-pre-wrap text-slate-300 text-sm max-h-56 overflow-auto">${escapeHtml(trimPreview(current))}</pre> </div> </section> <section class="rounded-lg bg-slate-800/70 border border-slate-700 p-4"> <div class="text-xs uppercase tracking-wide text-slate-400 mb-2">Diff</div> <pre class="whitespace-pre-wrap text-sm leading-6">${diffHtml}</pre> </section> <footer class="text-xs text-slate-500 pt-2"> Green = added, Red = removed. Unified view${isMultiLine ? " (line-based)" : " (word-based)"}. </footer> </div> </div>` } function escapeHtml(s: string) { return s .replaceAll("&", "&amp;") .replaceAll("<", "&lt;") .replaceAll(">", "&gt;") .replaceAll('"', "&quot;") .replaceAll("'", "&#039;") } function trimPreview(s: string, max = 800) { if (s.length <= max) return s return s.slice(0, max) + "\n…" } function buildSummary(prev: string, curr: string) { const added = Math.max(0, curr.length - prev.length) const removed = Math.max(0, prev.length - curr.length) const diffLen = Math.abs(curr.length - prev.length) return [ `Len(prev: ${prev.length})`, `Len(curr: ${curr.length})`, diffLen ? `Δ ${diffLen} (${added ? "+" + added : ""}${removed ? (added ? " / -" : "-") + removed : ""})` : "no length change", ].join(" • ") }