ClipVault PyQt6 search interface filtering clipboard cards with O(1) virtualized rows

How We Built a Wayland Clipboard Manager in Python and PyQt6 Without GNOME Flicker

Writing a clipboard tool on X11 once took 40 lines of Python. On Ubuntu GNOME Wayland, that same script hits three walls fast. Hidden windows cannot see live QClipboard events, polling wl-paste makes GNOME Dash to Dock flash, and blind reads store KeePassXC secrets into plain SQLite files.

While building ClipVault (source on GitHub), we solved those snags with a three-tier loop, pre-read MIME checks, virtualized Qt rows, a UNIX socket toggle, and strict core24 Snap rules. Here is what worked in real tests and what limits we kept.

Executive Summary

  • Stop GNOME dock flicker: Prioritizing off-thread xclip and xsel ahead of wl-paste, and stripping DESKTOP_STARTUP_ID plus XDG_ACTIVATION_TOKEN, eliminates transparent surface blinks.
  • Drop secrets pre-read: ClipVault checks clipboard targets against eight password tags (including x-kde-passwordManagerHint) before reading text payloads.
  • Keep RAM flat: QListView.setUniformItemSizes(True) and fixed 64 px delegates paint only visible rows rather than full widget trees.
  • Instant key wake: A QLocalServer socket (ClipVaultLocalServer) wakes the running app on clipvault-teckscaler.toggle without Python startup lag.

1. Three-Tier Capture and Fixing GNOME Dock Flicker

Under core Wayland rules, wl_data_device shares copies only with the focused client. Tiling tools like Sway add wlr-data-control-unstable-v1, and ext-data-control-v1 builds the new shared protocol. Stock GNOME omits those hooks.

Running wl-paste every 500 ms on GNOME triggers an ugly side effect. Each call briefly opens a Wayland helper surface and makes Dash to Dock flash. We fixed that inside services/clipboard_service.py in three steps:

  • Pick XWayland first: _find_clipboard_command selects xclip, then xsel, and finally wl-paste. Off-thread xclip reads cross-window copies smoothly with zero dock flashes.
  • Strip launch env vars: Before running child processes, ClipboardWorker removes DESKTOP_STARTUP_ID and XDG_ACTIVATION_TOKEN so GTK never treats polls as app starts.
  • Coordinate three paths: A 1,000 ms focus timer (_check_focus_state) pauses polling while ClipVault holds focus. Qt signals (_on_qt_signal), a backup timer (_qt_timer), and a 500 ms echo guard (_ignore_next) handle in-app edits. When only wl-paste exists, ClipVault runs wl-paste -t text --watch and falls back to polling if watch exits.

2. Pre-Read MIME Password Checks and Size Limits

Password tools tag secret copies with MIME hints. KeePassXC’s Clipboard.cpp sets x-kde-passwordManagerHint with secret payload bytes. Reading text first puts secrets straight into Python RAM before any filter runs.

ClipVault checks MIME target names before touching clipboard text. Inside services/sensitive_detector.py, it tests eight common tags and allows false flags in _NEGATIVE_VALUES (0, false, no):

  • x-kde-passwordManagerHint and application/x-kde-passwordManagerHint
  • application/x-password-manager-hint and x-password-manager-hint
  • application/x-nspasteboard-concealed-type
  • application/x-copyq-secret
  • text/x-moz-secret and x-secret

Inside normalize_text, ClipVault also strips null bytes and skips giant blobs past 1,000,000 bytes so an accidental dump never stalls SQLite.

ClipVault search bar filtering clipboard history cards inside PyQt6 dark window
Instant substring search filters fixed 64 px clipboard cards without allocating per-row widgets.

3. Fixed-Height QListView and Fast Substring Search

Early tests that put custom widgets inside a box layout grew slow when history filled up. ClipVault switched to Qt Model-View-Delegate cards inside ui/clipboard_model.py and ui/clipboard_delegate.py:

  • Constant height rows: Calling QListView.setUniformItemSizes(True) with 64 px rows lets Qt scroll without measuring off-screen items.
  • Short card previews: Cards paint a clean preview capped at _MAX_PREVIEW_CHARS = 55 with time and pin states, while full text stays safe in the model.
  • Full key control: Pressing Down jumps from search to row zero. Enter copies and hides, Delete drops the item (calling remove_item() to clear active system text if it matched), and Escape closes focus.

4. UNIX Socket IPC and SQLite WAL Persistence

Booting Python and PyQt6 on every Super+V press feels slow. Inside main.py, ClipVault binds one resident instance on QLocalServer named ClipVaultLocalServer with UserAccessOption.

When you run clipvault-teckscaler.toggle, a small helper sends toggle\n to that socket and exits right away. In snap/snapcraft.yaml, that UNIX socket is why network-bind sits alongside x11, wayland, desktop, and opengl under strict core24 confinement.

For storage, services/database_service.py picks $SNAP_USER_DATA, XDG_DATA_HOME/clipvault, or the local root. It opens SQLite in Write-Ahead Logging mode, locks access down, and trims past 100 unpinned items:

PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;

5. Current Trade-Offs and Test Coverage

Shipping a fast v1.0.0 meant making clear scope cuts:

  • Text only: Images and binary clips are skipped by design. Users who copy screenshots should read ClipVault vs. CopyQ or compare GPaste and Copyous.
  • File permissions over disk encryption: Today SQLite relies on 0600 permissions and Snap AppArmor isolation rather than passphrase encryption at rest.
  • Tested modules: The local test suite checks database WAL pruning, MIME secret filters, negative hints, themes, and virtual list models across 40 passing unittest checks.

Try the package on the ClipVault Snapcraft store page or see the ClipVault setup guide.

Frequently Asked Questions

Why does ClipVault check xclip before wl-paste on Ubuntu Wayland?

Calling wl-paste repeatedly on GNOME Shell can trigger popup surfaces that make Dash to Dock flash. Off-thread XWayland xclip with cleared startup tokens stops that flicker while still capturing cross-app copies.

Why does ClipVault’s Snap list the network-bind plug?

Strict Snap rules block local UNIX socket servers without network-bind. ClipVault uses that permission strictly for its local ClipVaultLocalServer socket and never opens internet connections.

How does ClipVault keep PyQt6 scrolling smooth?

QListView.setUniformItemSizes(True) locks rows at 64 px and trims card text at 55 chars, so Qt paints only visible cards on screen.

Where does ClipVault store its SQLite vault?

Inside Snap installs, it writes to $SNAP_USER_DATA using SQLite WAL mode and strict 0600 file permissions.

Leave a Reply

Your email address will not be published. Required fields are marked *