How we built a Kali Linux-native, privacy-first clipboard manager featuring a floating Spotlight search overlay (Ctrl+Shift+F), smart image hashing, search input debouncing, and Master Password security.
I was working on Kali Linux, taking multiple screenshot captures of terminal error tracebacks and copying temporary API bearer tokens, when I realized two frustrating things:
- My disk was getting littered with dozens of duplicate PNG screenshots (
capture_1427.png,capture_1428.png) taking up space and cluttering my feed. - The clipboard managers available on Linux were either heavy Electron apps devouring 350MB+ RAM just sitting in the tray, or basic plain-text loggers that stored sensitive API keys unencrypted on disk.
That's why we built DotGhostBoard โ a Kali Linux-native, privacy-first, zero-trust clipboard manager built with Python 3 and PyQt6. It features AES-256-GCM encryption, local SQLite storage, global hotkey overlays, and a dark neon aesthetic โ using ~85 MB of Full Runtime RAM.
In this article, we'll dive deep into the architecture, design choices, and actual Python code behind the new features introduced in v1.5.5 (Nexus Polish & Spotlight).
๐ Key Features Introduced in v1.5.5
Here is what we engineered for this release:
- ๐ Spotlight Quick Search Overlay (
ui/spotlight.py): A frameless, floating quick-search popup accessible globally viaCtrl+Shift+F. - ๐ผ๏ธ Smart Image SHA-256 Deduplication (
core/storage.py): Automatic pixel hashing that prevents duplicate screenshots from cluttering disk space or UI feeds. - ๐ Master Password Protection for Secret Copying (
ui/dashboard.py): Zero-leak in-memory decryption for encrypted clipboard items. - โก Search Input Debouncing (
QTimer): 150msโ200ms non-blocking debounce timers to eliminate UI lag during rapid typing. - ๐ Dashboard Stats Header & Relative Time Refresh: Real-time state summary banner and relative timestamps ("just now", "5m ago", "2h ago").
1. Spotlight Quick Search Overlay (ui/spotlight.py)
A core goal of v1.5.5 was allowing users to query their clipboard history from any application without switching windows manually.
We designed SpotlightSearchDialog using PyQt6's QDialog with frameless window hints and active screen auto-centering:
๐ GitHub Source Code: ui/spotlight.py
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QLineEdit, QListWidget,
QListWidgetItem, QLabel, QHBoxLayout, QFrame,
QApplication, QGraphicsDropShadowEffect
)
from PyQt6.QtCore import Qt, pyqtSignal, QEvent, QTimer
from PyQt6.QtGui import QColor, QKeyEvent
from core import storage
class SpotlightSearchDialog(QDialog):
"""
Spotlight-style floating search overlay.
- Frameless, stays on top, centered on active monitor.
- Up / Down / Enter keyboard navigation.
- Emits sig_item_selected(item) when user picks an item.
"""
sig_item_selected = pyqtSignal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint |
Qt.WindowType.Tool
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
self.setFixedWidth(640)
self.setFixedHeight(420)
self._items = []
self._pending_query = ""
# 150ms search debounce timer
self._search_timer = QTimer(self)
self._search_timer.setSingleShot(True)
self._search_timer.setInterval(150)
self._search_timer.timeout.connect(self._do_search)
self._build_ui()
self._center_on_screen()
Keyboard Navigation & Focus Loss
To make Spotlight feel as smooth as macOS Spotlight or Raycast:
-
eventFiltercapturesUp,Down,Enter, andEsckeys regardless of whether focus is on theQLineEditorQListWidget. - Encrypted secret items (
is_secret == 1) are masked with๐ Secret Item (Encrypted)to prevent sensitive data leaks. -
changeEventlistens forActivationChangeto automatically hide Spotlight when the user clicks outside.
def eventFilter(self, obj, event):
if event.type() == QEvent.Type.KeyPress:
key_event: QKeyEvent = event
key = key_event.key()
if key == Qt.Key.Key_Escape:
self.hide()
return True
elif key == Qt.Key.Key_Down:
cur = self.results_list.currentRow()
if cur < self.results_list.count() - 1:
self.results_list.setCurrentRow(cur + 1)
return True
elif key == Qt.Key.Key_Up:
cur = self.results_list.currentRow()
if cur > 0:
self.results_list.setCurrentRow(cur - 1)
return True
elif key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
current_item = self.results_list.currentItem()
if current_item:
self._on_item_activated(current_item)
return True
return super().eventFilter(obj, event)
def changeEvent(self, event):
if event.type() == QEvent.Type.ActivationChange:
if not self.isActiveWindow():
self.hide()
super().changeEvent(event)
2. Smart Image Deduplication via SHA-256 (core/storage.py)
The Problem
When capturing screenshots or images from the system clipboard, QImage.save() creates a new temporary PNG file (e.g., capture_20260805_142748.png). Because each file gets a timestamped path, traditional string matching (WHERE content = ?) fails to recognize identical images. This led to duplicate cards filling the dashboard and wasting disk space.
The Solution: SHA-256 Image Hashing
Inside storage.add_item(), when an image is saved:
- Compute the SHA-256 checksum of the new PNG file.
- Query existing
imagerows in SQLite. - If an existing image has the exact same SHA-256 hash:
- Purge the newly generated temporary PNG file from disk (
os.remove()). - Execute
UPDATE clipboard_items SET updated_at = NOW, copy_count = copy_count + 1 WHERE id = ?. - Return the existing item ID so the UI floats the original image card to the top with an updated frequency badge (
ร2,ร3,๐ฅ ร10)!
- Purge the newly generated temporary PNG file from disk (
๐ GitHub Source Code: core/storage.py
import hashlib
def _get_file_hash(filepath: str) -> str | None:
"""Calculate SHA-256 hash of a file for image duplicate detection."""
if not filepath or not os.path.isfile(filepath):
return None
try:
h = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
except OSError:
return None
def add_item(item_type: str, content: str, preview: str = None) -> int:
"""
Add a new item to the database using a 3-stage cascade for optimal performance:
- Stage 1: Fast O(1) string match (avoids unnecessary disk IO).
- Stage 2: SHA-256 pixel hash match for images (resolves timestamped file duplicates).
- Stage 3: Clean SQL insertion for brand new unique items.
"""
# โโ STAGE 1: Direct Content Match โโ
# Check text strings or identical paths first. This is an immediate SQL lookup,
# avoiding heavy disk reads if the exact same text or path was already recorded.
existing = get_item_by_content(content)
if existing:
now = datetime.now().isoformat()
with _db() as conn:
conn.execute(
"UPDATE clipboard_items SET updated_at = ?, copy_count = copy_count + 1 WHERE id = ?",
(now, existing["id"]),
)
return existing["id"]
# โโ STAGE 2: SHA-256 Image Content Hash Match โโ
# If content paths differ (e.g. capture_1001.png vs capture_1002.png), compare actual pixel hashes.
# We filter by file size first (os.path.getsize) to avoid computing SHA-256 on non-matching sizes!
if item_type == "image" and os.path.isfile(content):
new_hash = _get_file_hash(content)
if new_hash:
new_size = os.path.getsize(content)
with _db() as conn:
rows = conn.execute(
"SELECT id, content FROM clipboard_items WHERE type = 'image' ORDER BY updated_at DESC LIMIT 100"
).fetchall()
for row in rows:
existing_path = row["content"]
if existing_path and os.path.isfile(existing_path):
# Fast pre-check: Compare file size before hashing disk bytes
if os.path.getsize(existing_path) == new_size:
if _get_file_hash(existing_path) == new_hash:
# Duplicate image pixels detected! Delete the new redundant temporary file
try:
os.remove(content)
except OSError:
pass
now = datetime.now().isoformat()
with _db() as conn:
conn.execute(
"UPDATE clipboard_items SET updated_at = ?, copy_count = copy_count + 1 WHERE id = ?",
(now, row["id"]),
)
return row["id"]
# โโ STAGE 3: New Unique Item Insertion โโ
# If neither string content nor SHA-256 hash matched, insert as a brand new item.
now = datetime.now().isoformat()
with _db() as conn:
cursor = conn.execute("""
INSERT INTO clipboard_items (type, content, preview, is_pinned, copy_count, created_at, updated_at)
VALUES (?, ?, ?, 0, 1, ?, ?)
""", (item_type, content, preview, now, now))
return cursor.lastrowid
3. Zero-Leak Master Password Verification (ui/dashboard.py)
DotGhostBoard allows users to encrypt sensitive clips (e.g., API keys, passwords) using AES-256-GCM.
When a user copies a secret item from Spotlight or the Dashboard:
-
_on_copy(item_id)checks ifitem.get("is_secret")is true. - If the session is locked (
_active_key is None), it prompts for the Master Password viaLockScreen. - The content is decrypted in-memory only for copying to the OS clipboard.
-
watcher.mark_self_paste()informs the clipboard watcher to ignore the newly pasted plaintext so it is never saved back to the database as an unencrypted card.
๐ก๏ธ Zero-Leak Security Architecture:
- Zero Disk Leak: Decryption happens strictly in-memory during copy (
storage.decrypt_item).- Watcher Interception:
watcher.mark_self_paste()intercepts the next clipboard polling tick, updating_last_contentto ignore the text and preventing the watcher thread from re-saving the unencrypted secret back into SQLite.- UI RAM Purge: When locking the session,
on_session_locked()immediately purges plaintext from Qt UI labels (setText("")) so sensitive data never lingers in widget memory.Note: These protections completely prevent secrets from leaking back into DotGhostBoard's SQLite database or UI widgets. They do not yet control how long the plaintext sits on the OS-level clipboard buffer after pasting โ which is why our upcoming v2.0 release introduces an automated 30-second clipboard wipe.
๐ GitHub Source Code: ui/dashboard.py
def _on_copy(self, item_id: int):
item = storage.get_item_by_id(item_id)
if not item:
return
if item.get("is_secret"):
# Check if session is unlocked
if self._active_key is None:
dlg = LockScreen(setup=False)
if dlg.exec() == LockScreen.DialogCode.Accepted:
self._active_key = dlg.get_key()
self._reset_auto_lock()
else:
self.statusBar().showMessage("โ Session is locked โ unlock to copy secret.")
return
# Decrypt item content in-memory for copying
plaintext = storage.decrypt_item(item_id, self._active_key)
if plaintext is None:
self.statusBar().showMessage("โ Decryption failed โ wrong key or corrupted data.")
return
item["content"] = plaintext
# Mark as self-paste so watcher updates _last_content without re-capturing
self.watcher.mark_self_paste()
self.watcher.paste_item_to_clipboard(item)
# Increment copy count in DB and refresh UI badge
new_count = storage.increment_copy_count(item_id)
card = self._cards.get(item_id)
if card:
card.update_copy_count(new_count)
self.statusBar().showMessage(f"Copied! โ (ร{new_count})")
4. Search Input Debouncing with QTimer
Connecting QLineEdit.textChanged directly to database queries causes UI freezing during rapid typing because every single keystroke executes an SQL query and clears/rebuilds PyQt6 widgets.
We implemented a single-shot QTimer debounce pattern in both the main Dashboard (200ms) and Spotlight (150ms):
๐ GitHub Source Code: ui/dashboard.py | ui/spotlight.py
# Inside Dashboard.__init__
self._search_timer = QTimer(self)
self._search_timer.setSingleShot(True)
self._search_timer.setInterval(200)
self._search_timer.timeout.connect(self._do_search)
self._pending_search_query = ""
def _on_search(self, query: str):
"""Debounce search box input by 200ms."""
self._pending_search_query = query
if not query.strip():
self._search_timer.stop()
self._do_search() # Instant clear on empty input
else:
self._search_timer.start()
def _do_search(self):
"""Execute SQL query & UI rebuild after 200ms pause in typing."""
query = getattr(self, "_pending_search_query", "")
# ... SQL search & widget pagination ...
5. Dashboard State Summary Banner (StatsHeaderCard)
At the top of the history feed, StatsHeaderCard provides a live state summary:
- ๐ Today: Total clips captured today.
- ๐ฅ Top: Most frequently copied clip created today (excluding encrypted secrets).
- ๐ Pinned: Total count of pinned clips.
๐ GitHub Source Code: core/storage.py
def get_today_stats() -> dict:
today_prefix = datetime.now().strftime("%Y-%m-%d")
with _db() as conn:
cur1 = conn.execute(
"SELECT COUNT(*) as cnt FROM clipboard_items WHERE created_at LIKE ?",
(f"{today_prefix}%",)
)
total_today = cur1.fetchone()["cnt"]
cur3 = conn.execute(
"""
SELECT preview, content, copy_count
FROM clipboard_items
WHERE copy_count > 0
AND created_at LIKE ?
AND COALESCE(is_secret, 0) = 0
ORDER BY copy_count DESC, updated_at DESC
LIMIT 1
""",
(f"{today_prefix}%",)
)
top_row = cur3.fetchone()
# ... format & return dict ...
๐งช Testing & Verification
We maintained strict TDD practices throughout development, writing 184 unit tests using pytest:
๐ GitHub Test Suite: tests/test_v155_polish.py
============================= 184 passed in 8.40s ==============================
Every component โ from relative timestamp calculation (_format_time) to SHA-256 image deduplication and reset_copy_count boolean return values โ is covered by automated unit tests in tests/test_v155_polish.py.
๐ฆ Packaging & Installation on Linux
DotGhostBoard is packaged for multiple Linux distributions including AppImage, .deb, Arch Linux (.pkg.tar.zst), and generic .tar.gz:
You can download pre-built release assets from:
- ๐ฆ GitHub Release v1.5.5: https://github.com/kareem2099/DotGhostBoard/releases/tag/v1.5.5
- ๐๏ธ OpenDesktop / Pling Store: https://www.opendesktop.org/p/2353623/
# Build DEB package locally
./scripts/build_deb.sh
# Install on Kali Linux / Debian / Ubuntu
sudo dpkg -i dotghostboard_1.5.5_amd64.deb
๐ฎ What's Next? The Roadmap to DotGhostBoard v2.x (Cerberus)
With the v1.5.x milestone complete, we are officially transitioning development to DotGhostBoard v2.x!
Here is a sneak peek at the exciting features engineered for the upcoming v2.0.0 (Cerberus) release:
- ๐ The Vault (
vault.db): An isolated, zero-knowledge encrypted database completely separate fromghost.db, protected by an independent Master Password for ultimate credential security. - ๐ง Regex Smart Secret Detection: Pattern-based heuristic detection for API Keys, AWS credentials (
AKIA...), GitHub PATs (ghp_...), JWT tokens, and high-entropy secrets โ automatically prompting users to move them into The Vault. - โณ 30-Second Auto-Wipe: Automatic clipboard buffer wiping 30 seconds after pasting sensitive tokens from the Vault.
- ๐ต๏ธ Paranoia / Zero-Logging Mode: A single-click stealth mode toggle that temporarily halts all local database logging.
๐ Conclusion & Performance Benchmarks
Building DotGhostBoard v1.5.5 proved that Linux desktop utilities don't need heavy web runtimes to deliver modern, fluid UI experiences. By choosing Python 3, PyQt6, and SQLite, we measured significant performance advantages in our local testing compared to typical Electron alternatives:
- ๐ข Full Runtime RAM: ~85 MB process RSS (GUI + SQLite DB + Spotlight dialog loaded) vs ~350 MB+ in typical Electron clipboard tools.
- โก Spotlight Query Latency: ~3.5 ms average execution time across 10 runs (min 2.23 ms, max 4.90 ms).
- ๐พ Disk Optimization: SHA-256 deduplication saved ~85% disk space during screenshot-heavy debugging sessions.
- ๐ Automated Suite: 184 / 184 tests passing in 8.32s.
Check out the full source code, download release packages, and star the repository on GitHub:
๐ GitHub Repository: https://github.com/kareem2099/DotGhostBoard
๐ GitHub Release (v1.5.5): https://github.com/kareem2099/DotGhostBoard/releases/tag/v1.5.5
๐ OpenDesktop / Pling Store: https://www.opendesktop.org/p/2353623/
๐ฌ Technical Discussion
If you were designing our v2.0 Regex Smart Secret Detection Engine, what would be the very first pattern or entropy rule you'd include to catch leaked keys? Drop your regex patterns or PR suggestions below! ๐





Top comments (0)