feat: Extended deeplinks + Raycast extension - #1611
Conversation
…d Raycast extension - Add new deeplink actions: PauseRecording, ResumeRecording, TogglePauseRecording, TakeScreenshot, SetMicrophone, SetCamera - Add path-based deeplink routing (cap-desktop://record/stop, etc.) alongside existing legacy JSON format for backward compatibility - Add comprehensive unit tests for deeplink parsing - Add deeplink documentation (DEEPLINKS.md) - Create Raycast extension with commands for all recording controls, device switching, screenshots, and settings - Raycast extension dynamically enumerates displays, microphones, and cameras via system_profiler (no hardcoded device names) Fixes CapSoftware#1540
| let domain = url.domain(); | ||
|
|
||
| // Path-based deeplinks: cap-desktop://record/start, cap-desktop://device/microphone, etc. | ||
| if let Some(d) = domain { | ||
| if let Some(action) = Self::try_from_path(d, url)? { | ||
| return Ok(action); | ||
| } | ||
| } | ||
|
|
||
| // Legacy JSON-based deeplinks: cap-desktop://action?value={...} | ||
| match domain { | ||
| Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction), | ||
| _ => Err(ActionParseFromUrlError::Invalid), | ||
| }?; |
There was a problem hiding this comment.
Repo guideline here is no code comments; this reads fine without the two // ... lines.
| let domain = url.domain(); | |
| // Path-based deeplinks: cap-desktop://record/start, cap-desktop://device/microphone, etc. | |
| if let Some(d) = domain { | |
| if let Some(action) = Self::try_from_path(d, url)? { | |
| return Ok(action); | |
| } | |
| } | |
| // Legacy JSON-based deeplinks: cap-desktop://action?value={...} | |
| match domain { | |
| Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction), | |
| _ => Err(ActionParseFromUrlError::Invalid), | |
| }?; | |
| let domain = url.domain(); | |
| if let Some(d) = domain { | |
| if let Some(action) = Self::try_from_path(d, url)? { | |
| return Ok(action); | |
| } | |
| } | |
| match domain { | |
| Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction), | |
| _ => Err(ActionParseFromUrlError::Invalid), | |
| }?; |
| /// Parse path-based deeplink URLs. | ||
| /// | ||
| /// Supported routes: | ||
| /// - `cap-desktop://record/start?screen=<name>&mode=<studio|instant>` | ||
| /// - `cap-desktop://record/stop` | ||
| /// - `cap-desktop://record/pause` | ||
| /// - `cap-desktop://record/resume` | ||
| /// - `cap-desktop://record/toggle-pause` | ||
| /// - `cap-desktop://record/restart` | ||
| /// - `cap-desktop://screenshot?screen=<name>` | ||
| /// - `cap-desktop://device/microphone?label=<name>` (omit label to disable) | ||
| /// - `cap-desktop://device/camera?device_id=<id>` or `?model_id=<id>` or `?off=true` | ||
| /// - `cap-desktop://settings?page=<page>` | ||
| fn try_from_path(domain: &str, url: &Url) -> Result<Option<Self>, ActionParseFromUrlError> { |
There was a problem hiding this comment.
This adds a big /// block, but the repo guidelines forbid code comments. Since apps/desktop/src-tauri/DEEPLINKS.md exists now, I’d move the route list there and drop this.
| /// Parse path-based deeplink URLs. | |
| /// | |
| /// Supported routes: | |
| /// - `cap-desktop://record/start?screen=<name>&mode=<studio|instant>` | |
| /// - `cap-desktop://record/stop` | |
| /// - `cap-desktop://record/pause` | |
| /// - `cap-desktop://record/resume` | |
| /// - `cap-desktop://record/toggle-pause` | |
| /// - `cap-desktop://record/restart` | |
| /// - `cap-desktop://screenshot?screen=<name>` | |
| /// - `cap-desktop://device/microphone?label=<name>` (omit label to disable) | |
| /// - `cap-desktop://device/camera?device_id=<id>` or `?model_id=<id>` or `?off=true` | |
| /// - `cap-desktop://settings?page=<page>` | |
| fn try_from_path(domain: &str, url: &Url) -> Result<Option<Self>, ActionParseFromUrlError> { | |
| fn try_from_path(domain: &str, url: &Url) -> Result<Option<Self>, ActionParseFromUrlError> { |
| const CAP_SCHEME = "cap-desktop://"; | ||
|
|
||
| /** | ||
| * Open a path-based Cap deeplink. |
There was a problem hiding this comment.
Minor: this repo has a no-code-comments rule; apps/raycast/src/utils.ts introduces several /** */ and // comments that should probably be removed to match that.
| try { | ||
| await open(url.toString()); | ||
| } catch { | ||
| await showHUD("❌ Failed to connect to Cap. Is it running?"); | ||
| throw new Error("Could not open Cap deeplink"); | ||
| } |
There was a problem hiding this comment.
Catching without the error drops useful context; keeping the original error (and including the deeplink) makes failures easier to debug.
| try { | |
| await open(url.toString()); | |
| } catch { | |
| await showHUD("❌ Failed to connect to Cap. Is it running?"); | |
| throw new Error("Could not open Cap deeplink"); | |
| } | |
| try { | |
| await open(url.toString()); | |
| } catch (error) { | |
| await showHUD("Failed to connect to Cap. Is it running?"); | |
| throw new Error(`Could not open Cap deeplink: ${url.toString()}`, { cause: error }); | |
| } |
|
Closing this along with the deeplinks and Raycast bounty (#1540), which we are not pursuing. Since that issue was opened we shipped the Cap CLI, which covers this need more completely than deeplinks would have: cap record starts and stops recordings, cap targets lists screens, windows and devices, plus screenshot, export, upload and library commands, all with --json output, and cap mcp for driving Cap from an agent. A Raycast extension can be built directly on the CLI, which is a better foundation than a URL scheme. This is not a reflection on your work, and leaving it unreviewed for this long is our failure. Sorry for the wasted effort, and thank you for taking the time. If you want to contribute again, the open bug reports are the most useful place and we will review promptly. |
Summary
Adds comprehensive deeplink support for recording controls, device switching, and screenshots, plus a Raycast extension that uses them.
Fixes #1540
Deeplink Changes (
apps/desktop/src-tauri/src/deeplink_actions.rs)New actions added to
DeepLinkActionenum:PauseRecording— pause the active recordingResumeRecording— resume a paused recordingTogglePauseRecording— toggle pause stateTakeScreenshot— capture a screenshot of a specified display/windowSetMicrophone— switch or disable the active microphoneSetCamera— switch or disable the active camera (by device ID or model ID)New path-based routing alongside legacy JSON format:
cap-desktop://record/start?screen=<name>&mode=studiocap-desktop://record/stopcap-desktop://record/pause/resume/toggle-pausecap-desktop://screenshot?screen=<name>cap-desktop://device/microphone?label=<name>cap-desktop://device/camera?device_id=<id>/?off=truecap-desktop://settings?page=<page>Full backward compatibility with existing
cap-desktop://action?value={...}JSON deeplinks.Error messages include available device/display names for easier debugging when names don't match exactly.
Unit tests: 12 tests covering path-based parsing, legacy JSON, edge cases (missing params, unknown actions, camera disable).
Raycast Extension (
apps/raycast/)Key design decisions:
system_profilerUnique IDfield, not display namesexecuteDeepLinkhelper — consistent error handling across all commandsDocumentation
Added
apps/desktop/src-tauri/DEEPLINKS.mdwith full reference for all deeplink routes and examples.Testing
npx tsc --noEmitpasses for the Raycast extensionGreptile Summary
Extends Cap's deeplink system with path-based routing for recording controls, device switching, and screenshots, plus a new Raycast extension that leverages these deeplinks. The Rust implementation uses proper error handling with helpful debugging messages, maintains backward compatibility with legacy JSON deeplinks, and includes comprehensive unit tests. The Raycast extension dynamically enumerates devices via
system_profilerwithout hardcoded values, uses stable camera IDs for reliability, and provides a clean UX with consistent error handling across all commands.Key improvements:
cap-desktop://record/start) are cleaner than legacy JSON formatDEEPLINKS.mdConfidence Score: 5/5
Important Files Changed
Sequence Diagram
sequenceDiagram participant Raycast participant system_profiler participant Cap Desktop participant Recording Engine Note over Raycast,Cap Desktop: Command Execution Flow Raycast->>system_profiler: Get devices/displays system_profiler-->>Raycast: Device list (JSON) Raycast->>Raycast: Parse devices Note over Raycast: User selects device/display Raycast->>Cap Desktop: cap-desktop://record/start?screen=name&mode=studio Cap Desktop->>Cap Desktop: Parse deeplink URL Cap Desktop->>Cap Desktop: Resolve capture target Cap Desktop->>Recording Engine: Start recording with params Recording Engine-->>Cap Desktop: Recording started Cap Desktop-->>Raycast: Success (via OS) Raycast->>Raycast: Show HUD confirmation Note over Raycast,Cap Desktop: Device Switching Flow Raycast->>Cap Desktop: cap-desktop://device/microphone?label=name Cap Desktop->>Cap Desktop: Parse deeplink Cap Desktop->>Recording Engine: Set microphone input Recording Engine-->>Cap Desktop: Input changed Cap Desktop-->>Raycast: Success (via OS) Raycast->>Raycast: Show HUD confirmationLast reviewed commit: 765e6dc
(2/5) Greptile learns from your feedback when you react with thumbs up/down!