Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
frontend:
name: Frontend type-check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install
- run: bun run build

rust:
name: Rust build / test
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# Tauri's Linux stack links against system GTK/WebKit; the repo
# additionally needs gtk-layer-shell (palette overlay) and D-Bus
# headers (keyring/Secret Service).
- name: Install Linux system libraries
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \
librsvg2-dev libxdo-dev libssl-dev libgtk-layer-shell-dev \
libdbus-1-dev
- run: bun install
- name: Build Tauri backend
run: cargo build --manifest-path src-tauri/Cargo.toml
- name: Run Rust tests
run: cargo test --manifest-path src-tauri/Cargo.toml
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ src-tauri/target/
# Tauri generated
src-tauri/gen/

# Binary (built artifact, not source)
bin/commandeer.exe
# Binary build artifacts (not source)
bin/

# Env / local config
.env
Expand Down
27 changes: 19 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project

Commandeer is a Raycast-style command palette built with Tauri 2 (React/TypeScript frontend, Rust backend). It is **cross-platform: Windows and Linux (Wayland/COSMIC)** — originally Windows-only, later ported. It is **still in active development** with new features being added regularly; keep this file updated as the app evolves.
Commandeer is a Raycast-style command palette built with Tauri 2 (React/TypeScript frontend, Rust backend). It is **cross-platform: Windows, Linux (Wayland/COSMIC), and macOS** — originally Windows-only, then ported to Linux, then to macOS. It is **still in active development** with new features being added regularly; keep this file updated as the app evolves.

There is no test suite or linter configured. `npm run build` runs `tsc` and is the type-check.

Expand All @@ -18,7 +18,10 @@ npm run tauri build -- --no-bundle # release build (on Linux: source ~/.cargo/
# NEVER `cargo build --release` directly: without the tauri
# CLI the binary is dev-mode and loads localhost:5173
npm run build # tsc + vite build (frontend only; use as the type-check)
npm run release # Windows-only: build + copy commandeer.exe to bin/
npm run release # cross-platform release build + copy artifact to bin/
# Windows: commandeer.exe
# Linux: commandeer binary
# macOS: commandeer.app bundle
```

Linux dev/test notes:
Expand All @@ -28,6 +31,14 @@ Linux dev/test notes:
- Screenshots: `cosmic-screenshot --interactive=false --notify=false --save-dir DIR`.
- Icons in `src-tauri/icons/*.png` must be RGBA — RGB fails the `generate_context!` macro on Linux.

macOS dev/test notes:
- The app is an Accessory (no Dock icon / Cmd-Tab entry). Use the tray icon or the global hotkey to surface it.
- Default toggle hotkey is `Cmd+Shift+Space` (Spotlight owns `Cmd+Space`, input-source switching owns `Ctrl+Space`).
- Screenshot capture and paste-to-previous require permission grants: **Screen Recording** for screenshots, **Accessibility** for paste. Until granted the commands fail with instructions rather than silently no-oping.
- Shutdown/Restart/Logout/Empty Trash trigger a one-time **Automation** prompt on first use (System Events / Finder). `@search` over the focused Finder folder uses the same Finder Automation channel (and only queries Finder when the palette opened over it; otherwise it falls back to the home folder like Linux).
- Clipboard history is encrypted at rest on all three platforms: DPAPI on Windows; ChaCha20-Poly1305 on Linux (key in the Secret Service, 0600 key-file fallback) and macOS (0600 key file next to the db). **Do not move the macOS key to the Keychain** while the app ships ad-hoc-signed: Keychain ACLs bind to the code signature, so every rebuild re-prompts — and the prompt fires during setup and blocks launch (verified on-device).
- `npm run release` produces a signed/unsigned `bin/commandeer.app` bundle; right-click → Open the first time if unsigned.

## Shipping changes

After **every** completed task, bug fix, or feature — once the work is done and verified — ship it: **(1) commit and push, (2) rebuild the release binary, (3) restart the running process** on the new binary. The running app should always reflect committed code. Use the `ship-change` skill (`.claude/skills/ship-change/SKILL.md`), which encodes the exact per-OS steps (Windows/macOS/Linux):
Expand All @@ -46,7 +57,7 @@ Two always-running Tauri windows that hide/show rather than launching per use: t

### Screenshot tool

Lightshot-style region capture: trigger → Rust freezes the screen to `<app-cache>/frame.png` (on Linux a four-CLI fallback chain — `cosmic-screenshot` → `gnome-screenshot` → `spectacle` → `grim`, first one present wins; on Windows a GDI BitBlt of the full virtual screen — all monitors — with the overlay spanning the same bounds) → the `screenshot` window (same JS bundle; `main.tsx` branches on window label to `ScreenshotOverlay.tsx`) shows the frame under a dim veil → drag a region → Rust crops, saves to `~/Pictures/Screenshots`, and copies PNG to the clipboard (`wl-copy` on Linux, arboard on Windows). Esc cancels. Backend in `commands/screenshot.rs`. Triggers: `commandeer://screenshot` deep link (bound to PrtScn via a second managed COSMIC shortcut line on Linux), a configurable global shortcut on Windows (`screenshot_hotkey` config, **default `Insert`**, editable via Settings → Screenshot Hotkey), and a Tools → Take Screenshot palette command. **Do not default the Windows shortcut to PrintScreen**: `RegisterHotKey(VK_SNAPSHOT)` returns success but never fires `WM_HOTKEY` because PrtScn emits no `WM_KEYDOWN` — so it silently does nothing. Any ordinary key (Insert, Fn keys, letters+modifiers) works. The frame is encoded as fast/unfiltered PNG (transient file, reloaded once then deleted) — ~50 ms capture on a 2560×1440 release build; the unoptimized dev build is ~15× slower, so judge screenshot latency only from a release build. On Windows the overlay appears via a cloak-then-reveal handshake: at capture time the window is positioned/sized and shown **DWM-cloaked** (composited but not displayed — WebView2 only renders while visible), the frame `<img>` loads and rasterizes off-screen, and `reveal_screenshot_overlay` uncloaks only when the webview reports the image was actually **presented**, via an **Element Timing** observer (`elementtiming="shot-frame"`). onLoad/double-rAF are NOT sufficient signals — they race the GPU raster of the multi-monitor-sized texture and flashed black. Fallbacks: a 500 ms frontend timer post-onload and a 1500 ms Rust-side force-show+uncloak; all commands are idempotent. On Linux the overlay is a 4-edge-anchored, exclusive-keyboard layer-shell surface; the frontend's `show_screenshot_overlay` call on img onload is the show path there and the cloak machinery is Windows-only. Linux has its own stale-frame defense (there is no DWM cloak, and GTK3 toplevel opacity is a no-op on Wayland): WebKitGTK replays its **last composite** as the first frame when a hidden window is re-mapped, so the overlay window is **transparent on Linux** (`tauri.linux.conf.json` — platform configs replace the whole `app.windows` array, keep it in sync with `tauri.conf.json`) and the frontend always paints a cleared, fully transparent state (no frame, no veil) and waits a double-rAF (`afterClearPaint`) **before** any hide — finish, Esc-cancel, and the re-trigger path (Rust emits `screenshot-clear`; the webview clears then calls `hide_screenshot_overlay`; Rust force-hides after its pre-capture delay as fallback). The replayed composite is then invisible, the live desktop underneath is pixel-identical to the frozen frame, and the overlay's appearance reads as a single smooth veil dim. On Windows, both windows set `additionalBrowserArgs` with `CalculateNativeWinOcclusion` disabled (WebView2 browser args are process-wide — keep the two windows' args identical): without it, Chromium suspends rendering of hidden windows, the new frame never paints before `show()`, and the window flashes its stale surface (the previous capture) for a frame.
Lightshot-style region capture: trigger → Rust freezes the screen to `<app-cache>/frame.png` (on Linux a four-CLI fallback chain — `cosmic-screenshot` → `gnome-screenshot` → `spectacle` → `grim`, first one present wins; on Windows a GDI BitBlt of the full virtual screen — all monitors — with the overlay spanning the same bounds; on macOS `screencapture -R` of the cursor monitor) → the `screenshot` window (same JS bundle; `main.tsx` branches on window label to `ScreenshotOverlay.tsx`) shows the frame under a dim veil → drag a region → Rust crops, saves to `~/Pictures/Screenshots`, and copies PNG to the clipboard (`wl-copy` on Linux, arboard on Windows/macOS). Esc cancels. Backend in `commands/screenshot.rs`. Triggers: `commandeer://screenshot` deep link (bound to PrtScn via a second managed COSMIC shortcut line on Linux), a configurable global shortcut on Windows/macOS (`screenshot_hotkey` config, **default `Insert` on Windows**, editable via Settings → Screenshot Hotkey), and a Tools → Take Screenshot palette command. **Do not default the Windows shortcut to PrintScreen**: `RegisterHotKey(VK_SNAPSHOT)` returns success but never fires `WM_HOTKEY` because PrtScn emits no `WM_KEYDOWN` — so it silently does nothing. Any ordinary key (Insert, Fn keys, letters+modifiers) works. macOS has no default screenshot hotkey because Mac keyboards lack PrintScreen and the common system shortcuts are `Cmd+Shift+3/4/5`. The frame is encoded as fast/unfiltered PNG (transient file, reloaded once then deleted) — ~50 ms capture on a 2560×1440 release build; the unoptimized dev build is ~15× slower, so judge screenshot latency only from a release build. On Windows the overlay appears via a cloak-then-reveal handshake: at capture time the window is positioned/sized and shown **DWM-cloaked** (composited but not displayed — WebView2 only renders while visible), the frame `<img>` loads and rasterizes off-screen, and `reveal_screenshot_overlay` uncloaks only when the webview reports the image was actually **presented**, via an **Element Timing** observer (`elementtiming="shot-frame"`). onLoad/double-rAF are NOT sufficient signals — they race the GPU raster of the multi-monitor-sized texture and flashed black. Fallbacks: a 500 ms frontend timer post-onload and a 1500 ms Rust-side force-show+uncloak; all commands are idempotent. On Linux the overlay is a 4-edge-anchored, exclusive-keyboard layer-shell surface; the frontend's `show_screenshot_overlay` call on img onload is the show path there (and on macOS) and the cloak machinery is Windows-only. Linux has its own stale-frame defense (there is no DWM cloak, and GTK3 toplevel opacity is a no-op on Wayland): WebKitGTK replays its **last composite** as the first frame when a hidden window is re-mapped, so the overlay window is **transparent on Linux** (`tauri.linux.conf.json` — platform configs replace the whole `app.windows` array, keep it in sync with `tauri.conf.json`) and the frontend always paints a cleared, fully transparent state (no frame, no veil) and waits a double-rAF (`afterClearPaint`) **before** any hide — finish, Esc-cancel, and the re-trigger path (Rust emits `screenshot-clear`; the webview clears then calls `hide_screenshot_overlay`; Rust force-hides after its pre-capture delay as fallback). The replayed composite is then invisible, the live desktop underneath is pixel-identical to the frozen frame, and the overlay's appearance reads as a single smooth veil dim. On Windows, both windows set `additionalBrowserArgs` with `CalculateNativeWinOcclusion` disabled (WebView2 browser args are process-wide — keep the two windows' args identical): without it, Chromium suspends rendering of hidden windows, the new frame never paints before `show()`, and the window flashes its stale surface (the previous capture) for a frame.

### Frontend (`src/`)

Expand All @@ -58,18 +69,18 @@ Everything hangs off three types in `src/types.ts`:

`App.tsx` builds the command list (grouping `folderName`-tagged commands under virtual folders) and hands it to `components/Palette.tsx` (~1500 lines), which owns the step stack, query state, fuzzy ranking (fzf + frecency in `src/lib/`), keyboard handling, and the Ctrl+K action panel. `src/lib/tauri.ts` is the single wrapper around all Rust `invoke` calls. `src/lib/appEvents.ts` is a mutable bridge so settings commands can flip App-level state without prop drilling.

User-facing "commands" also come from a scripts directory on disk (configurable `scripts_dir`; `.ps1`/`.lnk` on Windows, `.sh`/`.desktop`/`.AppImage`/executables on Linux), scanned by the Rust side.
User-facing "commands" also come from a scripts directory on disk (configurable `scripts_dir`; `.ps1`/`.lnk` on Windows, `.sh`/`.desktop`/`.AppImage`/executables on Linux, `.sh`/`.command`/executables on macOS), scanned by the Rust side.

### Backend (`src-tauri/src/`)

`lib.rs` holds setup (window creation, global shortcut, tray, single-instance, deep links) plus window show/hide/positioning. One module per feature in `commands/` (audio, clipboard, file_index, launcher, process, stats, …), all registered in the `invoke_handler` in `lib.rs`. The file index is self-hosted: SQLite + FTS5 (trigram) fed by a `notify` filesystem watcher.

### Platform split

All OS-specific code is behind `#[cfg(target_os = "windows")]` / `#[cfg(not(windows))]` in Rust and an `IS_LINUX` (user-agent) check in the frontend. The two platforms differ most in:
All OS-specific code is behind `#[cfg(target_os = "windows")]` / `#[cfg(target_os = "linux")]` / `#[cfg(target_os = "macos")]` in Rust and `IS_LINUX` / `IS_MAC` (user-agent) checks in the frontend. Never use a bare `#[cfg(not(windows))]` branch for Linux/macOS-specific code — gate each OS explicitly (or `unix` only when the code is genuinely identical, like `PermissionsExt`). The three platforms differ most in:

- **Window sizing/positioning.** Windows: frontend `setSize` + min/max + cursor-monitor positioning. Linux/Wayland: cosmic-comp ignores client resizes/moves of mapped toplevels, so the palette is rendered as a **wlr-layer-shell overlay** (gtk-layer-shell, set up in `lib.rs`), anchored to the top edge with a fixed margin; the frontend measures content height and calls the `resize_palette` Rust command, which changes the GTK size request to resize in place without flicker. `html,body,#root` are content-height on purpose so this measurement works.
- **Launching & icons.** Windows uses PowerShell/shell32 (`.lnk` icon extraction); Linux parses `.desktop` files and launches via direct exec / `sh` / `gio launch` / `xdg-open`.
- **Global hotkey.** See Linux notes above; `set_game_mode` in `lib.rs` rewrites the COSMIC custom-shortcut config on Linux.
- **Window sizing/positioning.** Windows: frontend `setSize` + min/max + cursor-monitor positioning. Linux/Wayland: cosmic-comp ignores client resizes/moves of mapped toplevels, so the palette is rendered as a **wlr-layer-shell overlay** (gtk-layer-shell, set up in `lib.rs`), anchored to the top edge with a fixed margin; the frontend measures content height and calls the `resize_palette` Rust command, which changes the GTK size request to resize in place without flicker. `html,body,#root` are content-height on purpose so this measurement works. macOS: a normal always-on-top transparent window positioned via Tauri monitor APIs; vibrancy and rounded corners are applied with `window-vibrancy`.
- **Launching & icons.** Windows uses PowerShell/shell32 (`.lnk` icon extraction); Linux parses `.desktop` files and launches via direct exec / `sh` / `gio launch` / `xdg-open`; macOS scans `.app` bundles and launches via `open`. File-search icons use the same shell APIs (`SHGetFileInfoW` on Windows, `NSWorkspace.iconForFile:` on macOS).
- **Global hotkey.** See Linux/macOS notes above; `set_game_mode` in `lib.rs` rewrites the COSMIC custom-shortcut config on Linux and switches the registered base hotkey everywhere.

Config is JSON read/written by the Rust side (`commands/config.rs`; `scripts_dir` defaults per-platform). Lightweight UI prefs (game mode, widget visibility, script cache) live in webview `localStorage`.
Loading
Loading