feat: Add deeplink actions and Raycast extension (#1540)#1603
feat: Add deeplink actions and Raycast extension (#1540)#1603MrLawrenceKwan wants to merge 3 commits intoCapSoftware:mainfrom
Conversation
Extended deeplink actions in Cap desktop app:
- Added PauseRecording action to pause active recording
- Added ResumeRecording action to resume paused recording
- Added ToggleMicrophone action to toggle microphone on/off
- Added ToggleCamera action to toggle camera on/off
Built Raycast extension with 6 commands:
- Stop Recording
- Pause Recording
- Resume Recording
- Toggle Microphone
- Toggle Camera
- Start Recording (simplified)
All actions use existing Cap functions and follow the established
deeplink pattern (cap-desktop://action?value={JSON}).
Implementation details in DEEPLINK_IMPLEMENTATION.md
| DeepLinkAction::ResumeRecording => { | ||
| crate::recording::resume_recording(app.clone(), app.state()).await | ||
| } | ||
| DeepLinkAction::ToggleMicrophone => { |
There was a problem hiding this comment.
The new toggle blocks include a bunch of inline comments + whitespace-only lines, and the logic can be simplified a bit (same behavior, fewer clones/locals):
| DeepLinkAction::ToggleMicrophone => { | |
| DeepLinkAction::ToggleMicrophone => { | |
| let state = app.state::<ArcLock<App>>(); | |
| let has_mic = { state.read().await.selected_mic_label.is_some() }; | |
| if !has_mic { | |
| return Err( | |
| "Cannot toggle microphone on without specifying which microphone to use. Please use StartRecording with mic_label instead." | |
| .to_string(), | |
| ); | |
| } | |
| crate::set_mic_input(state, None).await | |
| } |
|
|
||
| crate::set_mic_input(state, new_mic).await | ||
| } | ||
| DeepLinkAction::ToggleCamera => { |
There was a problem hiding this comment.
Same simplification here (and avoids carrying comment text in-code):
| DeepLinkAction::ToggleCamera => { | |
| DeepLinkAction::ToggleCamera => { | |
| let state = app.state::<ArcLock<App>>(); | |
| let has_camera = { state.read().await.selected_camera_id.is_some() }; | |
| if !has_camera { | |
| return Err( | |
| "Cannot toggle camera on without specifying which camera to use. Please use StartRecording with camera instead." | |
| .to_string(), | |
| ); | |
| } | |
| crate::set_camera_input(app.clone(), state, None, None).await | |
| } |
raycast-extension/src/utils.ts
Outdated
| @@ -0,0 +1,45 @@ | |||
| import { exec } from "child_process"; | |||
There was a problem hiding this comment.
Worth using execFile here instead of exec to avoid going through a shell, and this also drops the JSDoc block (repo-wide no-comments rule):
| import { exec } from "child_process"; | |
| import { execFile } from "child_process"; | |
| import { promisify } from "util"; | |
| import { showToast, Toast } from "@raycast/api"; | |
| const execFileAsync = promisify(execFile); | |
| export type DeepLinkAction = Record<string, unknown>; | |
| export async function executeDeepLink( | |
| action: DeepLinkAction, | |
| successMessage: string, | |
| errorMessage: string | |
| ): Promise<void> { | |
| try { | |
| const encodedAction = encodeURIComponent(JSON.stringify(action)); | |
| const deeplinkUrl = `cap-desktop://action?value=${encodedAction}`; | |
| await showToast({ | |
| style: Toast.Style.Animated, | |
| title: "Executing...", | |
| }); | |
| await execFileAsync("open", [deeplinkUrl]); | |
| await showToast({ | |
| style: Toast.Style.Success, | |
| title: successMessage, | |
| }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: errorMessage, | |
| message: error instanceof Error ? error.message : String(error), | |
| }); | |
| } | |
| } |
| @@ -0,0 +1,21 @@ | |||
| import { closeMainWindow, showToast, Toast } from "@raycast/api"; | |||
There was a problem hiding this comment.
executeDeepLink is currently unused, and the command doesn’t actually open Cap (plus it shows a failure toast). If the intent is “open Cap so the user can start recording”, this keeps it simple and avoids the in-code // notes:
| import { closeMainWindow, showToast, Toast } from "@raycast/api"; | |
| import { closeMainWindow, showToast, Toast } from "@raycast/api"; | |
| import { execFile } from "child_process"; | |
| import { promisify } from "util"; | |
| const execFileAsync = promisify(execFile); | |
| export default async function Command() { | |
| await closeMainWindow(); | |
| await showToast({ | |
| style: Toast.Style.Animated, | |
| title: "Opening Cap...", | |
| }); | |
| try { | |
| await execFileAsync("open", ["cap-desktop://"]); | |
| await showToast({ | |
| style: Toast.Style.Success, | |
| title: "Cap opened", | |
| message: "Start a recording in Cap, then use the other commands to control it.", | |
| }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to open Cap", | |
| message: error instanceof Error ? error.message : String(error), | |
| }); | |
| } | |
| } |
| // Toggle: if mic is set, disable it; if disabled, can't enable without knowing which mic to use | ||
| let new_mic = if current_mic.is_some() { | ||
| None | ||
| } else { | ||
| // When toggling on, we need to know which mic to use | ||
| // For now, we'll return an error. Users should provide the mic name |
There was a problem hiding this comment.
Remove comments - code must be self-explanatory per CLAUDE.md and AGENTS.md. The variable names and error messages already clarify the logic.
| // Toggle: if mic is set, disable it; if disabled, can't enable without knowing which mic to use | |
| let new_mic = if current_mic.is_some() { | |
| None | |
| } else { | |
| // When toggling on, we need to know which mic to use | |
| // For now, we'll return an error. Users should provide the mic name | |
| let new_mic = if current_mic.is_some() { | |
| None | |
| } else { | |
| return Err("Cannot toggle microphone on without specifying which microphone to use. Please use StartRecording with mic_label instead.".to_string()); | |
| }; |
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: 166:171
Comment:
Remove comments - code must be self-explanatory per CLAUDE.md and AGENTS.md. The variable names and error messages already clarify the logic.
```suggestion
let new_mic = if current_mic.is_some() {
None
} else {
return Err("Cannot toggle microphone on without specifying which microphone to use. Please use StartRecording with mic_label instead.".to_string());
};
```
<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.| // Toggle: if camera is set, disable it; if disabled, can't enable without knowing which camera to use | ||
| let new_camera = if current_camera.is_some() { | ||
| None | ||
| } else { | ||
| // When toggling on, we need to know which camera to use | ||
| // For now, we'll return an error. Users should provide the camera ID |
There was a problem hiding this comment.
Remove comments - code must be self-explanatory per CLAUDE.md and AGENTS.md. The variable names and error messages already clarify the logic.
| // Toggle: if camera is set, disable it; if disabled, can't enable without knowing which camera to use | |
| let new_camera = if current_camera.is_some() { | |
| None | |
| } else { | |
| // When toggling on, we need to know which camera to use | |
| // For now, we'll return an error. Users should provide the camera ID | |
| let new_camera = if current_camera.is_some() { | |
| None | |
| } else { | |
| return Err("Cannot toggle camera on without specifying which camera to use. Please use StartRecording with camera instead.".to_string()); | |
| }; |
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: 184:189
Comment:
Remove comments - code must be self-explanatory per CLAUDE.md and AGENTS.md. The variable names and error messages already clarify the logic.
```suggestion
let new_camera = if current_camera.is_some() {
None
} else {
return Err("Cannot toggle camera on without specifying which camera to use. Please use StartRecording with camera instead.".to_string());
};
```
<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.
raycast-extension/src/utils.ts
Outdated
| /** | ||
| * Execute a Cap deeplink action | ||
| * @param action The action object to serialize and pass to Cap | ||
| * @param successMessage Message to show on success | ||
| * @param errorMessage Message to show on error | ||
| */ |
There was a problem hiding this comment.
Remove JSDoc comments per CLAUDE.md and AGENTS.md. TypeScript types already document the function signature.
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: raycast-extension/src/utils.ts
Line: 11:16
Comment:
Remove JSDoc comments per CLAUDE.md and AGENTS.md. TypeScript types already document the function signature.
<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.| // Note: This is a simplified version that would need the user to configure | ||
| // their default recording settings in Cap itself. A more advanced version | ||
| // could present a form to select screen/window and recording mode. |
There was a problem hiding this comment.
Remove comments per CLAUDE.md and AGENTS.md. The toast message already explains what's happening.
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: raycast-extension/src/start-recording.tsx
Line: 13:15
Comment:
Remove comments per CLAUDE.md and AGENTS.md. The toast message already explains what's happening.
<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.
raycast-extension/src/utils.ts
Outdated
| title: "Executing...", | ||
| }); | ||
|
|
||
| await execAsync(`open "${deeplinkUrl}"`); |
There was a problem hiding this comment.
Potential command injection via double quotes. If encodedAction contains ", it could escape the quotes. While encodeURIComponent should handle this, consider using execFile from child_process with arguments array for safer command execution:
import { execFile } from "child_process";
const execFileAsync = promisify(execFile);
await execFileAsync("open", [deeplinkUrl]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: raycast-extension/src/utils.ts
Line: 32:32
Comment:
Potential command injection via double quotes. If `encodedAction` contains `"`, it could escape the quotes. While `encodeURIComponent` should handle this, consider using `execFile` from `child_process` with arguments array for safer command execution:
```
import { execFile } from "child_process";
const execFileAsync = promisify(execFile);
await execFileAsync("open", [deeplinkUrl]);
```
How can I resolve this? If you propose a fix, please make it concise.…ments, use execFile
| DeepLinkAction::StopRecording => { | ||
| crate::recording::stop_recording(app.clone(), app.state()).await | ||
| } | ||
| DeepLinkAction::PauseRecording => { |
There was a problem hiding this comment.
These new DeepLinkAction variants look unreachable with the current try_from logic (the match url.domain() block has no Ok branch, so cap-desktop://action?... never parses into an action). Might be worth fixing that parsing guard so the new pause/resume/toggle paths can actually execute.
|
Closing as requested by author. We will re-record a proper demo and re-open/resubmit after confirmation. |
Overview
This PR implements the bounty from issue #1540 by extending Cap's deeplink actions and creating a Raycast extension for quick keyboard control.
Changes
1. Extended Deeplink Actions
File:
apps/desktop/src-tauri/src/deeplink_actions.rsAdded 4 new deeplink actions:
PauseRecording- Pauses the current active recordingResumeRecording- Resumes a paused recordingToggleMicrophone- Toggles microphone on/off (disable only)ToggleCamera- Toggles camera on/off (disable only)All actions use existing Cap functions (
pause_recording,resume_recording,set_mic_input,set_camera_input) and follow the established deeplink pattern.2. Raycast Extension
Directory:
raycast-extension/Complete TypeScript/React extension with 6 commands:
Each command executes a deeplink via
open cap-desktop://action?value={JSON}and provides toast notifications for user feedback.How It Works
Deeplinks use the URL format:
Example:
open "cap-desktop://action?value=%7B%22pause_recording%22%3Anull%7D"The Raycast extension provides a user-friendly interface to trigger these deeplinks with keyboard shortcuts.
Toggle Behavior
The toggle commands implement a "disable-only" pattern:
This is intentional because the system needs to know which specific device to enable. To enable camera/microphone, users should use
StartRecordingwith explicit device parameters.Testing
Manual Testing
Test Scenarios
Files Changed
apps/desktop/src-tauri/src/deeplink_actions.rs(+52 lines)raycast-extension/(complete extension)DEEPLINK_IMPLEMENTATION.md(detailed documentation)Documentation
See
DEEPLINK_IMPLEMENTATION.mdfor complete implementation details, testing guide, and future enhancement ideas.Checklist
Related
Closes #1540
Demo
The implementation enables quick keyboard shortcuts for recording control:
Ready for review! Happy to make any adjustments or add additional features.
Greptile Summary
Extends Cap's deeplink API with 4 new recording control actions (pause, resume, toggle microphone, toggle camera) and adds a complete Raycast extension for keyboard-based control.
Key Changes:
PauseRecording,ResumeRecording,ToggleMicrophone, andToggleCameravariants to theDeepLinkActionenum in Rustpause_recording,resume_recording,set_mic_input,set_camera_input)cap-desktop://action?value=<JSON>deeplinksStyle Issues:
utils.tsshould be removedSecurity Concern:
utils.tsuses string interpolation withexec()which could be vulnerable to command injection, thoughencodeURIComponentprovides mitigation. UsingexecFilewith arguments array would be safer.Confidence Score: 4/5
execFile. All changes are additive with no breaking modifications.apps/desktop/src-tauri/src/deeplink_actions.rs(remove comments) andraycast-extension/src/utils.ts(remove comments, consider safer command execution)Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant Raycast participant Shell participant CapDesktop participant RecordingSystem User->>Raycast: Trigger command (e.g., Pause Recording) Raycast->>Raycast: closeMainWindow() Raycast->>Raycast: Construct deeplink JSON Raycast->>Raycast: encodeURIComponent(JSON) Raycast->>Shell: execAsync("open cap-desktop://action?value=...") Shell->>CapDesktop: Open deeplink URL CapDesktop->>CapDesktop: Parse URL query parameter CapDesktop->>CapDesktop: Deserialize JSON to DeepLinkAction CapDesktop->>CapDesktop: Execute action (match enum variant) alt PauseRecording CapDesktop->>RecordingSystem: pause_recording() RecordingSystem-->>CapDesktop: Result<(), String> else ResumeRecording CapDesktop->>RecordingSystem: resume_recording() RecordingSystem-->>CapDesktop: Result<(), String> else ToggleMicrophone CapDesktop->>CapDesktop: Read app_state.selected_mic_label alt Mic enabled CapDesktop->>RecordingSystem: set_mic_input(None) RecordingSystem-->>CapDesktop: Result<(), String> else Mic disabled CapDesktop-->>Shell: Error: Cannot enable without device spec end else ToggleCamera CapDesktop->>CapDesktop: Read app_state.selected_camera_id alt Camera enabled CapDesktop->>RecordingSystem: set_camera_input(None) RecordingSystem-->>CapDesktop: Result<(), String> else Camera disabled CapDesktop-->>Shell: Error: Cannot enable without device spec end end CapDesktop-->>Shell: Success/Error Shell-->>Raycast: Command result Raycast->>User: Show toast notificationLast reviewed commit: 4847dcc
(2/5) Greptile learns from your feedback when you react with thumbs up/down!
Context used:
dashboard- CLAUDE.md (source)