-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: Add deeplinks support and Raycast extension #1569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
This PR adds comprehensive deeplink support for Cap and a Raycast extension to control Cap from Raycast. ## Deeplinks Added (deeplink_actions.rs) New deeplink actions: - `pause_recording` - Pause current recording - `resume_recording` - Resume paused recording - `toggle_pause` - Toggle pause state - `set_microphone` - Switch microphone input - `set_camera` - Switch camera input - `take_screenshot` - Take a screenshot of primary display - `show_main_window` - Show main Cap window - `list_displays` - List available displays - `list_windows` - List available windows - `list_microphones` - List available microphones - `list_cameras` - List available cameras - `get_recording_status` - Get current recording status ## Raycast Extension (extensions/raycast-cap/) Commands included: - Start Recording - Start a new screen recording - Stop Recording - Stop current recording - Pause Recording - Pause current recording - Resume Recording - Resume paused recording - Toggle Pause - Toggle pause state - Take Screenshot - Capture screen - Open Cap - Open the Cap application - Open Settings - Open Cap settings - Recording Controls - Quick access list view Deeplink format: `cap-desktop://action?value=<JSON_ENCODED_ACTION>` Closes CapSoftware#1540
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
15 files reviewed, 5 comments
| /// Response types for deeplink queries | ||
| #[derive(Debug, Serialize, Deserialize)] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub struct RecordingStatusResponse { | ||
| pub is_recording: bool, | ||
| pub is_paused: bool, | ||
| pub mode: Option<RecordingMode>, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] RecordingStatusResponse/DisplayInfo/etc are never used and (more importantly) the deep link “query” actions (List*, GetRecordingStatus) don’t return anything to the caller. Right now they only trace! and Ok(()), which makes them effectively no-ops from the deeplink consumer’s perspective (Raycast can’t read stdout/stderr/tracing). This also means these new structs add dead code unless a return mechanism is introduced.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 18:25
Comment:
[P1] `RecordingStatusResponse`/`DisplayInfo`/etc are never used and (more importantly) the deep link “query” actions (`List*`, `GetRecordingStatus`) don’t return anything to the caller. Right now they only `trace!` and `Ok(())`, which makes them effectively no-ops from the deeplink consumer’s perspective (Raycast can’t read stdout/stderr/tracing). This also means these new structs add dead code unless a return mechanism is introduced.
How can I resolve this? If you propose a fix, please make it concise.| DeepLinkAction::ListDisplays => { | ||
| let displays: Vec<DisplayInfo> = cap_recording::screen_capture::list_displays() | ||
| .into_iter() | ||
| .map(|(d, _)| DisplayInfo { | ||
| name: d.name.clone(), | ||
| id: format!("{:?}", d.id), | ||
| }) | ||
| .collect(); | ||
| // Log for debugging; in practice this could be returned via a different mechanism | ||
| trace!("Available displays: {:?}", displays); | ||
| Ok(()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] ListDisplays formats d.id with format!("{:?}", d.id) instead of a stable identifier. If DisplayId’s Debug impl changes across versions/platforms, the value becomes non-portable for consumers (e.g., a Raycast command that wants to persist an ID). Consider exposing the underlying id type/string in a stable way.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 256:266
Comment:
[P2] `ListDisplays` formats `d.id` with `format!("{:?}", d.id)` instead of a stable identifier. If `DisplayId`’s `Debug` impl changes across versions/platforms, the value becomes non-portable for consumers (e.g., a Raycast command that wants to persist an ID). Consider exposing the underlying id type/string in a stable way.
How can I resolve this? If you propose a fix, please make it concise.| DeepLinkAction::TakeScreenshot => { | ||
| // Take a screenshot of the primary display | ||
| let displays = cap_recording::screen_capture::list_displays(); | ||
| if let Some((display, _)) = displays.into_iter().next() { | ||
| let target = ScreenCaptureTarget::Display { id: display.id }; | ||
| crate::recording::take_screenshot(app.clone(), target) | ||
| .await | ||
| .map(|_| ()) | ||
| } else { | ||
| Err("No display found for screenshot".to_string()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] TakeScreenshot picks list_displays().into_iter().next() as “primary display”. list_displays() ordering isn’t guaranteed, so this can capture a non-primary monitor depending on platform/ordering. If “primary” matters, it should be selected explicitly (or allow a display id/name parameter).
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 235:245
Comment:
[P2] `TakeScreenshot` picks `list_displays().into_iter().next()` as “primary display”. `list_displays()` ordering isn’t guaranteed, so this can capture a non-primary monitor depending on platform/ordering. If “primary” matters, it should be selected explicitly (or allow a display id/name parameter).
How can I resolve this? If you propose a fix, please make it concise.| export function buildDeeplinkUrl(action: DeepLinkAction): string { | ||
| const actionValue = | ||
| typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action); | ||
| const encodedValue = encodeURIComponent(actionValue); | ||
| return `${CAP_DEEPLINK_SCHEME}://action?value=${encodedValue}`; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P3] buildDeeplinkUrl has a redundant conditional: typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action). It always does the same thing, so the typeof check can be removed to reduce noise.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast-cap/src/utils/deeplink.ts
Line: 56:61
Comment:
[P3] `buildDeeplinkUrl` has a redundant conditional: `typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action)`. It always does the same thing, so the `typeof` check can be removed to reduce noise.
How can I resolve this? If you propose a fix, please make it concise.| import { ActionPanel, Action, List, Icon, Color, showToast, Toast } from "@raycast/api"; | ||
| import { executeCapAction, isCapInstalled, openCap } from "./utils/deeplink"; | ||
| import { useEffect, useState } from "react"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P3] Unused imports: showToast and Toast are imported but never used. This will trip linters (ray lint) and adds noise.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast-cap/src/recording-controls.tsx
Line: 1:3
Comment:
[P3] Unused imports: `showToast` and `Toast` are imported but never used. This will trip linters (`ray lint`) and adds noise.
How can I resolve this? If you propose a fix, please make it concise.| const [isInstalled, setIsInstalled] = useState<boolean | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| isCapInstalled().then(setIsInstalled); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider handling isCapInstalled() errors + avoiding state updates after unmount; also makes showToast/Toast imports meaningful.
| isCapInstalled().then(setIsInstalled); | |
| useEffect(() => { | |
| let active = true; | |
| void (async () => { | |
| try { | |
| const installed = await isCapInstalled(); | |
| if (active) setIsInstalled(installed); | |
| } catch (error) { | |
| if (!active) return; | |
| setIsInstalled(false); | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to detect Cap", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| })(); | |
| return () => { | |
| active = false; | |
| }; | |
| }, []); |
| await executeCapAction( | ||
| { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heads up: capture_mode.screen needs to exactly match Cap's display names (from cap_recording::screen_capture::list_displays()); hardcoding "Main Display" may not resolve on many systems. Might be worth supporting it as an alias in the deeplink handler or switching to ID-based selection.
| */ | ||
| export function buildDeeplinkUrl(action: DeepLinkAction): string { | ||
| const actionValue = | ||
| typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: this branch is redundant (both sides do JSON.stringify).
| typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action); | |
| const actionValue = JSON.stringify(action); |
| } | ||
|
|
||
| try { | ||
| await open("", CAP_BUNDLE_ID); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Launching via a deeplink is usually more reliable than open("", bundleId) (it also gets you into a known UI state).
| await open("", CAP_BUNDLE_ID); | |
| await open(buildDeeplinkUrl("show_main_window")); |
| Window(String), | ||
| } | ||
|
|
||
| /// Response types for deeplink queries |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Repo style seems to avoid code comments; these new ////// comments in the deeplink handler might be worth removing for consistency.
Deeplinks Support + Raycast Extension
Closes #1540
Summary
This PR adds comprehensive deeplink support for Cap and a complete Raycast extension to control Cap directly from Raycast.
Changes
1. Extended Deeplink Actions (
apps/desktop/src-tauri/src/deeplink_actions.rs)Added the following new deeplink actions:
pause_recordingresume_recordingtoggle_pauseset_microphoneset_cameratake_screenshotshow_main_windowlist_displayslist_windowslist_microphoneslist_camerasget_recording_status2. Raycast Extension (
extensions/raycast-cap/)A complete Raycast extension with the following commands:
Deeplink Format
Examples
Stop Recording:
Pause Recording:
Toggle Pause:
Start Recording:
Testing
Test Deeplinks (macOS Terminal)
Test Raycast Extension
cd extensions/raycast-cap npm install npm run devScreenshots
(Add screenshots of Raycast extension here)
Checklist
Greptile Overview
Greptile Summary
This PR adds new Cap deeplink actions on the desktop side (pause/resume/toggle, input switching, screenshots, window/display/device listing, and recording status) and introduces a new Raycast extension that triggers these actions by opening
cap-desktop://URLs.The Raycast extension is straightforward (each command calls a shared deeplink helper), and the desktop handler wires the new actions into existing recording/window APIs.
Main issue to address: the new “list/query” deeplink actions (
list_displays,list_windows,list_microphones,list_cameras,get_recording_status) only emittrace!logs and returnOk(()), so consumers like Raycast cannot actually receive the requested data. There are also smaller correctness/maintainability nits (e.g., choosing the first display as “primary”,Debug-formatted IDs, and a couple of unused/redundant TS bits).Confidence Score: 3/5
Important Files Changed
Sequence Diagram
sequenceDiagram participant Raycast as Raycast Extension participant OS as macOS URL Handler participant Cap as Cap Desktop (Tauri) participant DL as deeplink_actions::handle participant Rec as recording subsystem Raycast->>Raycast: buildDeeplinkUrl(action) Raycast->>OS: open(cap-desktop://action?value=...) OS->>Cap: deliver URL event Cap->>DL: handle(app_handle, urls) DL->>DL: DeepLinkAction::try_from(url) alt start_recording DL->>Rec: set_camera_input / set_mic_input DL->>Rec: start_recording(inputs) else stop/pause/resume/toggle DL->>Rec: stop_recording / pause_recording / resume_recording / toggle_pause_recording else take_screenshot DL->>Rec: list_displays() & take_screenshot(target) else list/query actions DL->>DL: list_* / get_recording_status DL-->>DL: trace!(...) (no return to Raycast) end DL-->>Cap: Result logged on error(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!
Context used:
dashboard- CLAUDE.md (source)dashboard- AGENTS.md (source)