Polish clipboard panel UX and interactions

This commit is contained in:
Akshay Kolli
2026-07-09 19:32:57 -04:00
parent 23cd8b64a9
commit 52f712eb73
18 changed files with 2109 additions and 3273 deletions

View File

@@ -1,64 +1,76 @@
# Architecture
ClipBored is a single-process AppKit utility built with Swift Package Manager.
ClipBored is a single-process AppKit utility built with Swift Package Manager. UI, capture, persistence, preview generation, and paste orchestration stay in-process. Capture persistence, card-thumbnail loading, archive and sync operations, image rotation, and OCR use bounded background queues; their UI state and completion feedback return to the main thread.
## Runtime Shape
- `ClipBoredApp` creates `NSApplication`, sets accessory activation, installs `AppDelegate`, and starts the run loop.
- `AppDelegate` wires shared services, status menu items, settings observers, and global shortcuts.
- `ClipboardMonitorService` polls `NSPasteboard.changeCount` on a utility queue with adaptive active/idle intervals.
- `ClipboardStore` keeps the in-memory item list and persists rows to SQLite on a serial queue.
- `ClipboardCacheService` stores bounded image previews under Application Support and keeps a small `NSCache`.
- `ClipboardCloudSyncService` resolves the app-private iCloud ubiquity container when sync is enabled and pushes or pulls the portable archive file.
- `ShortcutManager` registers Carbon hotkeys for app-wide commands.
- `ClipboardPanelController` owns the side panel lifecycle, Dock-aware frame planning, configured left/right placement, and target-app tracking.
- `ClipboardPanelViewModel` filters, sorts, selects, copies, pastes, pins, organizes, deletes, opens, and reveals items.
- `LinkPreviewWindowController` opens selected HTTP(S) links in an ephemeral WebKit preview window instead of handing them to another browser.
- `OnboardingWindowController` shows the first-run setup assistant for shortcut, retention, system entry points, launch-at-login, iCloud sync, and Accessibility permission choices.
- `SettingsWindowController` exposes native controls for capture, privacy, performance, shortcuts, and data management.
- `ClipBoredApp` creates `NSApplication`, installs `AppDelegate`, and starts the run loop.
- `AppDelegate` wires services, menu-bar commands, settings observers, and global shortcuts.
- `ClipboardMonitorService` watches `NSPasteboard.changeCount` on a utility queue with adaptive active/idle polling intervals selected by the Performance setting.
- `ClipboardStore` owns the in-memory item list and SQLite persistence on a serial queue.
- `ClipboardCacheService` stores bounded encrypted preview sidecars under Application Support and maintains a small in-memory cache.
- `ClipboardCloudSyncService` resolves the private iCloud ubiquity container and pushes or pulls portable archives only when sync is enabled.
- `ShortcutManager` registers only intentional system-wide Carbon hotkeys (open shelf and Stack capture). The Settings binding is handled by the shelf's local key monitor and is never registered globally.
- `ClipboardPanelController` owns shelf lifecycle, current-screen placement, left/right frame planning, target-app tracking, and show/hide/reflow animation.
- `ClipboardPanelViewModel` owns query parsing, indexed category unions, sorting, selection, copy/paste, pinning, Pinboards, Stack, deletion, opening, and asynchronous thumbnail request coalescing.
- `ClipboardPanelView` renders the toolbar, vertical category icon rail, and viewport-aware card list beside it.
- `LinkPreviewWindowController` opens user-selected HTTP(S) links in an ephemeral WebKit preview window.
- `OnboardingWindowController` handles first-run shortcut, retention, lifecycle, sync, and Accessibility choices.
- `SettingsWindowController` presents six resizable, vertically scrollable settings pages and routes common settings changes through targeted control refreshes.
## Data Flow
## Capture And Presentation Flow
1. The monitor notices a pasteboard change.
2. Source app metadata is checked against ignored apps.
3. Pasteboard content is normalized into a `ClipboardItem`.
4. Sensitive text is skipped when exclusion is enabled.
5. Copied images run local Vision OCR only when `Search in image labels` is enabled; image cards can also run the same local OCR on demand from their quick actions.
6. The store deduplicates, preserves pinned and collection-assigned items, enforces limits, and persists the mutation.
7. The panel view model receives store updates and recomputes the visible list.
2. Capture rules check paused state, ignored source apps, allowed content kinds, and optional sensitive-text exclusion.
3. Pasteboard content is normalized into a `ClipboardItem`; local Vision OCR runs when image-label search is enabled.
4. The store deduplicates, preserves pinned and Pinboard-assigned items, enforces retention/length limits, and persists the mutation.
5. The panel view model maintains category/Pinboard indexes, applies the text query and selected category union, and caches parsed search matches across counts and category changes.
6. The view lays out card slots beside the category rail in one vertical document and materializes cards near the visible viewport.
7. Cards render immediately with a fallback presentation. Preview thumbnails load on a bounded operation queue; identical in-flight requests share work, and a still-relevant card is replaced in place on the main thread when its image arrives.
## Persistence
## Shelf Interaction Model
History is stored in SQLite at:
The shelf uses a fixed vertical layout on the configured left or right edge of the active screen.
- Header row one contains the collapsed/expanded search control plus clear-history and settings actions.
- Header row two is a labeled, horizontally scrollable category rail. Built-in categories without clips are omitted unless currently selected; custom Pinboards remain available when empty.
- A normal chip click replaces the active category filter. Command-click adds or removes chips from a union. Hover changes chrome only and never changes filtering.
- An empty, unfocused search field collapses to an icon. Click, typing, or `Command + F` expands and focuses it; clicking elsewhere collapses it only when the query is empty. Repeated `Command + F` keeps focus in the same field.
- Cards scroll vertically and fill the usable shelf width. Hover expansion changes visual presentation only: it does not mutate keyboard selection, the selected range, or query/category state.
- Card commands are discoverable through context menus, VoiceOver descriptions, and keyboard shortcuts. Hover-only action controls are not part of the interaction contract.
- Category changes and card/search expansion use short AppKit/Core Animation transitions. Both panel-controller and panel-view durations resolve to zero when macOS Reduce Motion is enabled.
The panel has no user-facing resize lip, alternate density/layout mode, close button, new-text action, or persistent status bar. It is dismissed with `Esc` or the configured global shortcut.
## Settings UI
Settings uses a custom segmented selector backed by a borderless `NSTabView`:
- `General` - history, sorting, shelf side, launch, and menu-bar/Dock presence
- `Shortcuts` - a system-wide open-shelf binding and a pane-local open-settings binding
- `Capture` - pause state, content kinds, image-label search, likely-secret exclusion, ignored apps, and capture status
- `Privacy` - local-data behavior, screen-capture hiding, Accessibility permission, and paste status
- `Performance` - adaptive polling profile and thumbnail-cache cap
- `Data` - iCloud archive sync, local archive import/export, storage location, and destructive clears
Each tab is a top-aligned document inside its own vertical scroll view. The window has a practical minimum size, no horizontal scrollers, and commits focused text/shortcut drafts when it closes. Common narrow settings changes update their bound controls without rebuilding the whole window; expensive cloud status checks are cached across unrelated refreshes.
## Persistence And Privacy Boundaries
History is stored in:
```text
~/Library/Application Support/ClipBored/history.sqlite
```
Images are stored under:
Image previews are stored under `images/`; restorable audio, video, rich-text, and PDF payloads are stored under `attachments/`. Legacy JSON import remains for migration from early builds.
```text
~/Library/Application Support/ClipBored/images/
```
Portable `.clipboredarchive` files preserve item metadata, Pinboards, and decrypted bytes for app-managed sidecars so another Mac can re-cache them with its own storage paths and encryption key. External file references remain path-based. Optional iCloud sync uses the same unencrypted archive format inside the app-private ubiquity container and is disabled by default.
Restorable non-image payloads such as audio clips, rich text, and PDFs are stored under:
```text
~/Library/Application Support/ClipBored/attachments/
```
Legacy JSON import still exists for migration from early builds.
Portable `.clipboredarchive` files are JSON exports created by `ClipboardArchiveService`. They preserve item metadata and include decrypted bytes for app-managed image, URL-thumbnail, audio, video, rich-text, and PDF sidecars so a different Mac can re-cache them under its own storage directory and encryption key. External file references remain path-based and are not copied into the archive.
Optional iCloud Sync uses that same archive format at `Documents/ClipBored/ClipBored.clipboredarchive` inside the app-private ubiquity container. It is disabled by default, requires a signed build with iCloud entitlement access, pulls once when enabled at launch, and debounces pushes after local store changes. Shared Pinboard collaboration is not implemented.
Textual SQLite fields, including optional collection names and image OCR text, are encrypted and decrypted at the `ClipboardStore` boundary. App-managed image cache files, URL preview thumbnails, audio clips, rich text sidecars, and PDF attachments are encrypted and decrypted at the `ClipboardCacheService` boundary. The encryption key is stored in Keychain when available, with an owner-only app-local fallback key if Keychain access blocks or fails. Full history clears remove the local fallback key when present and reset cached key state after SQLite deletion succeeds. Runtime `ClipboardItem` values remain plaintext in memory so search, duplicate detection, copy, paste, organization, archive export/import, and cache cleanup operate normally. Opening or revealing encrypted media creates a temporary decrypted copy for macOS handoff; stale temporary previews are cleared on launch, cache/history clear, and quit. Link previews are user-triggered and use a non-persistent WebKit data store.
Textual SQLite fields are encrypted and decrypted at the `ClipboardStore` boundary. Managed cache and attachment files are encrypted and decrypted at the `ClipboardCacheService` boundary. The key lives in Keychain when available, with an owner-only local fallback if Keychain access fails. Runtime `ClipboardItem` values remain plaintext in memory for search and clipboard operations. Opening encrypted media may create a temporary decrypted file; stale previews are cleared on launch, cache/history clear, and quit. Link previews are user-triggered and use a non-persistent WebKit data store.
## Size And Power Constraints
The release build intentionally avoids SwiftUI, Combine, Swift Concurrency, third-party packages, bundled media, and app resources beyond `Info.plist`.
The release build intentionally avoids SwiftUI, Combine, Swift Concurrency, third-party packages, and bundled media. The shelf avoids continuous layout work, renders only cards near the viewport, coalesces preview requests, keeps card-thumbnail decoding off the main thread, and bounds both memory and disk caches. Clipboard monitoring uses change-count polling with selectable adaptive profiles rather than continuous file scans.
The side shelf is anchored to the current screen's visible frame and can be placed on the left or right edge. It uses compact horizontal rows in a vertical list; the active or hovered row expands in place while the panel stays clear of side Docks and reserves bottom Dock space for content padding.
The build script uses `-Osize`, whole-module optimization, disabled reflection metadata, linker dead stripping, symbol stripping, and hardened-runtime signing. The current public targets, enforced by `scripts/build-macos-app.sh`, are 2 MiB gates for both the executable and app bundle.
The build script uses `-Osize`, whole-module optimization, disabled reflection metadata, linker dead stripping, symbol stripping, and hardened-runtime signing. `scripts/build-macos-app.sh` enforces 2 MiB gates for both the executable and app bundle.