Closes the macOS gap left by 9269ce6 ("Phase 4: Linux + Windows
only") and rebuilds the suppression-list UX around what macOS
actually exposes vs hides to a non-Cocoa LSUIElement child.
Concealed-pasteboard auto-suppression (`input-capture`)
- Wire `objc2` + `objc2-app-kit` (NSWorkspace / NSPasteboard /
NSImage / NSBitmapImageRep / NSRunningApplication) and
`objc2-foundation` (NSString / NSData / NSDictionary / NSURL).
- `clipboard.rs::is_concealed_clipboard` checks the general
pasteboard's `types` array for `org.nspasteboard.ConcealedType`
— the nspasteboard.org convention password managers use to opt
out of clipboard-manager capture. Honored before the user list
so 1Password etc. just work without a manual entry.
- `frontmost_app::macos::frontmost_app` now resolves via
`NSWorkspace.frontmostApplication.bundleIdentifier`, replacing
the Phase-4 stub. Doc updates in CLIPBOARD_PLAN.md mark the
macOS TODOs done.
Per-OS data model (`lan-mouse-ipc`, `src/config.rs`, `src/service.rs`)
- `ClipboardSuppression { macos, windows, linux_wayland, linux_x11:
Vec<String> }` replaces the flat `Vec<AppIdent>`. Each host
reads/writes only its own slot via `host()` / `host_mut()`; the
other slots round-trip untouched, so a config synced across
machines (dotfiles, Syncthing) keeps each machine's list intact.
- `HostKind::current()` picks `MacBundle` / `WindowsExe` /
`LinuxWayland` / `LinuxX11` (Wayland-vs-X11 decided at runtime
via `WAYLAND_DISPLAY`). `make_ident(value)` wraps a host string
in the matching `AppIdent` variant for the runtime suppression
check.
- `FrontendRequest::AddSuppressedApp(String)` /
`RemoveSuppressedApp(String)` and
`FrontendEvent::SuppressedAppsUpdated(Vec<String>)` now carry
plain identifier strings; the kind is implicit from the host
OS. Service rebuilds the runtime `HashSet<AppIdent>` shared
with `ClipboardMonitor` whenever the host slot changes.
Running-app picker with icons (`lan-mouse-gtk` ↔ `input-capture`)
- New `RunningApp { display_name, identifier, icon_png:
Option<Vec<u8>> }` IPC type. `FrontendEvent::RunningApps(Vec<
RunningApp>)` carries the picker payload.
- `frontmost_app::macos::list_running_apps` shells out to
`osascript` → System Events for `every process where background
only is false`. Three direct AppKit APIs (NSWorkspace
.runningApplications, NSRunningApplication
.runningApplicationWithProcessIdentifier, CGWindowListCopyWindow
Info) all silently scope to the caller's loginwindow / Aqua
session and return only ~3 entries from a non-Cocoa GTK process
— System Events is itself fully session-attached so it returns
the real list. Apple Events permission is already declared via
`NSAppleEventsUsageDescription` (we use it for input emulation).
- Icons via `NSWorkspace.iconForFile:` (path-based, session-
independent), encoded to PNG by picking the closest-but-no-
smaller-than-64 px rep. Per-bundle-id icon cache amortizes the
5-second auto-refresh.
- New `frontmost_app::lookup_app_metadata(identifier)` resolves a
bundle ID to display-name + icon via Launch Services
(`URLForApplicationWithBundleIdentifier`) so the suppressed-
apps list renders 1Password's name + icon even when 1Password
isn't currently running.
- `lan-mouse-gtk` gains a direct `input-capture` dep (default
features off) and bumps `gtk4` to `v4_6` for `Texture::from_
bytes`. Picker enumeration runs in the GUI process — the daemon
child can't see other apps (same Aqua-session restriction).
GTK rewrite (`clipboard_privacy_window`, `window`)
- `AdwComboRow` with a custom `SignalListItemFactory` renders
Image + Label per row at a fixed 320 px min-width so the
popover doesn't shrink horizontally as the user types into
search. `RunningAppObject` GObject carries display_name +
identifier + decoded `gdk::Texture`.
- Already-suppressed apps are filtered out of the picker so the
user can't add a duplicate; selection is preserved across
refreshes if the picked app is still present.
- The suppressed-apps list (above the picker) renders the same
`Image + display_name` treatment instead of raw bundle IDs;
metadata is mirrored from the running-apps cache and lazily
filled via `lookup_app_metadata` for not-currently-running
entries. Trash button uses `error` style (red) to match the
authorize-key UI in `key_row.ui`.
- `Window::open_clipboard_privacy_window` calls
`frontmost_app::list_running_apps()` directly on first open,
then via a 5-second `glib::timeout_add_local` while the modal
is visible. Refresh is skipped while the picker's popover is
open so a search-in-progress isn't disrupted.
- Removing a suppressed entry now re-applies the picker filter
against the cached running-apps snapshot immediately, so the
removed app reappears as a candidate without waiting for the
next 5 s tick.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>