feat: Extend deeplinks support + Raycast Extension#1634
feat: Extend deeplinks support + Raycast Extension#1634769066112-ops wants to merge 2 commits intoCapSoftware:mainfrom
Conversation
Extends the existing deeplink infrastructure with new actions: - PauseRecording, ResumeRecording, TogglePause - TakeScreenshot (with capture mode) - SetCamera, SetMicrophone Adds a Raycast extension (apps/raycast) with commands: - Start Instant Recording - Start Studio Recording - Stop Recording - Toggle Pause Recording - Take Screenshot - Open Settings All commands communicate with Cap via the cap-desktop:// deeplink scheme. Closes CapSoftware#1540
| .into_iter() | ||
| .find(|(s, _)| s.name == name) | ||
| .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) | ||
| .ok_or(format!("No screen with name \"{}\"", &name))?, |
There was a problem hiding this comment.
Minor perf thing: ok_or(format!(...)) eagerly builds the error string even when a match is found. ok_or_else keeps it lazy.
| .ok_or(format!("No screen with name \"{}\"", &name))?, | |
| .ok_or_else(|| format!("No screen with name \"{}\"", name))?, |
| .into_iter() | ||
| .find(|(w, _)| w.name == name) | ||
| .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) | ||
| .ok_or(format!("No window with name \"{}\"", &name))?, |
There was a problem hiding this comment.
Same here—prefer the lazy ok_or_else so we only format on the error path.
| .ok_or(format!("No window with name \"{}\"", &name))?, | |
| .ok_or_else(|| format!("No window with name \"{}\"", name))?, |
apps/raycast/tsconfig.json
Outdated
| @@ -0,0 +1,19 @@ | |||
| { | |||
| "$schema": "https://www.raycast.com/schemas/extension.json", | |||
There was a problem hiding this comment.
$schema looks like it’s pointing at the Raycast extension schema, but this is a tsconfig. Tweaking it helps editor validation/autocomplete.
| "$schema": "https://www.raycast.com/schemas/extension.json", | |
| "$schema": "https://json.schemastore.org/tsconfig", |
apps/raycast/src/utils.ts
Outdated
| const DEEPLINK_SCHEME = "cap-desktop"; | ||
|
|
||
| /** | ||
| * Build a Cap deeplink URL for the given action. | ||
| * | ||
| * Format: cap-desktop://action?value=<json-encoded action> | ||
| */ | ||
| export function buildDeepLink(action: Record<string, unknown>): string { | ||
| const json = JSON.stringify(action); | ||
| return `${DEEPLINK_SCHEME}://action?value=${encodeURIComponent(json)}`; | ||
| } | ||
|
|
||
| /** | ||
| * Open a Cap deeplink and show appropriate toast feedback. | ||
| */ |
There was a problem hiding this comment.
Repo guidelines disallow code comments; can we drop the JSDoc blocks here?
| const DEEPLINK_SCHEME = "cap-desktop"; | |
| /** | |
| * Build a Cap deeplink URL for the given action. | |
| * | |
| * Format: cap-desktop://action?value=<json-encoded action> | |
| */ | |
| export function buildDeepLink(action: Record<string, unknown>): string { | |
| const json = JSON.stringify(action); | |
| return `${DEEPLINK_SCHEME}://action?value=${encodeURIComponent(json)}`; | |
| } | |
| /** | |
| * Open a Cap deeplink and show appropriate toast feedback. | |
| */ | |
| const DEEPLINK_SCHEME = "cap-desktop"; | |
| export function buildDeepLink(action: Record<string, unknown>): string { | |
| const json = JSON.stringify(action); | |
| return `${DEEPLINK_SCHEME}://action?value=${encodeURIComponent(json)}`; | |
| } | |
apps/raycast/src/utils.ts
Outdated
| /** | ||
| * Build a Cap deeplink URL for the given action. | ||
| * | ||
| * Format: cap-desktop://action?value=<json-encoded action> | ||
| */ |
There was a problem hiding this comment.
violates the NO COMMENTS rule from repository guidelines - remove JSDoc comments
| /** | |
| * Build a Cap deeplink URL for the given action. | |
| * | |
| * Format: cap-desktop://action?value=<json-encoded action> | |
| */ | |
| export function buildDeepLink(action: Record<string, unknown>): string { | |
| const json = JSON.stringify(action); | |
| return `${DEEPLINK_SCHEME}://action?value=${encodeURIComponent(json)}`; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/utils.ts
Line: 5-9
Comment:
violates the NO COMMENTS rule from repository guidelines - remove JSDoc comments
```suggestion
export function buildDeepLink(action: Record<string, unknown>): string {
const json = JSON.stringify(action);
return `${DEEPLINK_SCHEME}://action?value=${encodeURIComponent(json)}`;
}
```
How can I resolve this? If you propose a fix, please make it concise.
apps/raycast/src/utils.ts
Outdated
| /** | ||
| * Open a Cap deeplink and show appropriate toast feedback. | ||
| */ |
There was a problem hiding this comment.
violates the NO COMMENTS rule from repository guidelines - remove JSDoc comments
| /** | |
| * Open a Cap deeplink and show appropriate toast feedback. | |
| */ | |
| export async function executeDeepLink( | |
| action: Record<string, unknown>, | |
| successMessage: string, | |
| ): Promise<void> { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/utils.ts
Line: 15-17
Comment:
violates the NO COMMENTS rule from repository guidelines - remove JSDoc comments
```suggestion
export async function executeDeepLink(
action: Record<string, unknown>,
successMessage: string,
): Promise<void> {
```
How can I resolve this? If you propose a fix, please make it concise.| let capture_target: ScreenCaptureTarget = match capture_mode { | ||
| CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays() | ||
| .into_iter() | ||
| .find(|(s, _)| s.name == name) | ||
| .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) | ||
| .ok_or(format!("No screen with name \"{}\"", &name))?, | ||
| CaptureMode::Window(name) => cap_recording::screen_capture::list_windows() | ||
| .into_iter() | ||
| .find(|(w, _)| w.name == name) | ||
| .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) | ||
| .ok_or(format!("No window with name \"{}\"", &name))?, | ||
| }; |
There was a problem hiding this comment.
duplicates the CaptureMode to ScreenCaptureTarget conversion logic from lines 134-145 - consider extracting to helper function
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 171-182
Comment:
duplicates the `CaptureMode` to `ScreenCaptureTarget` conversion logic from lines 134-145 - consider extracting to helper function
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.| await executeDeepLink( | ||
| { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, |
There was a problem hiding this comment.
hardcoded "Main Display" may not work on all systems - consider using dynamic display detection or making it configurable
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/start-instant-recording.ts
Line: 7
Comment:
hardcoded `"Main Display"` may not work on all systems - consider using dynamic display detection or making it configurable
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.| await executeDeepLink( | ||
| { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, |
There was a problem hiding this comment.
hardcoded "Main Display" may not work on all systems - consider using dynamic display detection or making it configurable
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/start-studio-recording.ts
Line: 7
Comment:
hardcoded `"Main Display"` may not work on all systems - consider using dynamic display detection or making it configurable
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.
apps/raycast/src/take-screenshot.ts
Outdated
| await executeDeepLink( | ||
| { | ||
| take_screenshot: { | ||
| capture_mode: { screen: "Main Display" }, |
There was a problem hiding this comment.
hardcoded "Main Display" may not work on all systems - consider using dynamic display detection or making it configurable
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/take-screenshot.ts
Line: 7
Comment:
hardcoded `"Main Display"` may not work on all systems - consider using dynamic display detection or making it configurable
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.|
|
||
| export function getDisplayName(): string { | ||
| const { displayName } = getPreferenceValues<Preferences>(); | ||
| return displayName || "Main Display"; |
There was a problem hiding this comment.
package.json says leaving the preference empty should use the main display, but getDisplayName() defaults to the literal "Main Display". If you want empty to mean “default display”, I’d make the preference optional and return a trimmed string (possibly empty) here.
| return displayName || "Main Display"; | |
| interface Preferences { | |
| displayName?: string; | |
| } | |
| export function getDisplayName(): string { | |
| const { displayName } = getPreferenceValues<Preferences>(); | |
| return displayName?.trim() ?? ""; | |
| } |
| .into_iter() | ||
| .find(|(s, _)| s.name == name) | ||
| .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) | ||
| .ok_or_else(|| format!("No screen with name \"{}\"", &name)), |
There was a problem hiding this comment.
If clients send an empty screen name (Raycast pref “leave empty to use main display”), we currently error. Might be worth treating empty (and maybe "Main Display" for compatibility) as “pick a default display” so deeplinks work out of the box.
| .ok_or_else(|| format!("No screen with name \"{}\"", &name)), | |
| CaptureMode::Screen(name) => { | |
| let match_name = !name.is_empty() && name != "Main Display"; | |
| let mut displays = cap_recording::screen_capture::list_displays().into_iter(); | |
| let display = if match_name { | |
| displays.find(|(s, _)| s.name == name) | |
| } else { | |
| displays.next() | |
| }; | |
| display | |
| .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) | |
| .ok_or_else(|| { | |
| if match_name { | |
| format!("No screen with name \"{}\"", &name) | |
| } else { | |
| "No displays found".to_string() | |
| } | |
| }) | |
| }, |
Summary
Closes #1540
This PR adds extended deeplink support to the Cap desktop app and introduces a Raycast extension that leverages these deeplinks.
Deeplink Additions
The existing deeplink infrastructure (
deeplink_actions.rs) already supportedStartRecording,StopRecording,OpenEditor, andOpenSettings. This PR extends it with:All new actions follow the existing pattern and use the same
cap-desktop://action?value=<json>URL format.Raycast Extension
A new Raycast extension is added at
apps/raycast/with the following commands:Each command uses the
cap-desktop://deeplink scheme to communicate with the running Cap desktop app.Testing
ray developinapps/raycast/, then use Raycast to trigger each command while Cap is running.Implementation Notes
pause_recording,resume_recording,toggle_pause_recording,take_screenshot,set_camera_input,set_mic_input), ensuring consistent behavior with the UI controls.Greptile Summary
This PR extends Cap's deeplink infrastructure with 6 new actions and adds a Raycast extension for remote control. The implementation follows existing patterns by delegating to established recording functions.
Key Changes:
PauseRecording,ResumeRecording,TogglePause,TakeScreenshot,SetCamera, andSetMicrophonedeeplink actionsIssues Found:
utils.tsviolate the NO COMMENTS repository ruledeeplink_actions.rs"Main Display"in Raycast commands may fail on systems with different display namesConfidence Score: 4/5
apps/raycast/src/utils.ts(comments removal) and consider refactoring the duplicate capture mode logic inapps/desktop/src-tauri/src/deeplink_actions.rsImportant Files Changed
Sequence Diagram
sequenceDiagram participant User participant Raycast participant Deeplink as cap-desktop:// participant DeeplinkActions as deeplink_actions.rs participant Recording as recording.rs participant CapApp as Cap Desktop App User->>Raycast: Trigger command Raycast->>Raycast: buildDeepLink(action) Raycast->>Deeplink: open(cap-desktop://action?value=...) Deeplink->>DeeplinkActions: handle(urls) DeeplinkActions->>DeeplinkActions: parse URL to DeepLinkAction alt StartRecording DeeplinkActions->>Recording: start_recording() else StopRecording DeeplinkActions->>Recording: stop_recording() else PauseRecording DeeplinkActions->>Recording: pause_recording() else ResumeRecording DeeplinkActions->>Recording: resume_recording() else TogglePause DeeplinkActions->>Recording: toggle_pause_recording() else TakeScreenshot DeeplinkActions->>Recording: take_screenshot() else SetCamera/SetMicrophone DeeplinkActions->>CapApp: set_camera_input() / set_mic_input() end Recording-->>CapApp: Update state CapApp-->>User: Visual feedbackLast reviewed commit: 7851676
(2/5) Greptile learns from your feedback when you react with thumbs up/down!
Context used:
dashboard- CLAUDE.md (source)