-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: Add deeplink actions and Raycast extension (#1540) #1603
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| # Deeplink + Raycast Extension Implementation | ||
|
|
||
| This document describes the implementation of issue #1540: Extended deeplinks and Raycast extension for Cap. | ||
|
|
||
| ## Changes Made | ||
|
|
||
| ### 1. Extended Deeplink Actions | ||
|
|
||
| **File:** `apps/desktop/src-tauri/src/deeplink_actions.rs` | ||
|
|
||
| Added four new deeplink actions to the `DeepLinkAction` enum: | ||
|
|
||
| ```rust | ||
| pub enum DeepLinkAction { | ||
| // ... existing actions ... | ||
| PauseRecording, | ||
| ResumeRecording, | ||
| ToggleMicrophone, | ||
| ToggleCamera, | ||
| } | ||
| ``` | ||
|
|
||
| #### Implementation Details | ||
|
|
||
| **PauseRecording** | ||
| - Calls `crate::recording::pause_recording()` | ||
| - Pauses the current active recording | ||
| - URL: `cap-desktop://action?value={"pause_recording":null}` | ||
|
|
||
| **ResumeRecording** | ||
| - Calls `crate::recording::resume_recording()` | ||
| - Resumes a paused recording | ||
| - URL: `cap-desktop://action?value={"resume_recording":null}` | ||
|
|
||
| **ToggleMicrophone** | ||
| - Reads current microphone state from app state | ||
| - If microphone is enabled, disables it by calling `set_mic_input(None)` | ||
| - If microphone is disabled, returns an error (cannot enable without knowing which mic to use) | ||
| - URL: `cap-desktop://action?value={"toggle_microphone":null}` | ||
|
|
||
| **ToggleCamera** | ||
| - Reads current camera state from app state | ||
| - If camera is enabled, disables it by calling `set_camera_input(None)` | ||
| - If camera is disabled, returns an error (cannot enable without knowing which camera to use) | ||
| - URL: `cap-desktop://action?value={"toggle_camera":null}` | ||
|
|
||
| #### Toggle Behavior Note | ||
|
|
||
| The toggle commands implement a "disable-only" toggle pattern: | ||
| - ✅ Can disable an active camera/microphone | ||
| - ❌ Cannot re-enable without device specification | ||
|
|
||
| This is intentional because: | ||
| 1. The system needs to know **which** camera/microphone to enable | ||
| 2. Users may have multiple devices | ||
| 3. Starting a recording with specific devices is better handled through `StartRecording` action | ||
|
|
||
| For enabling camera/microphone, users should use the existing `StartRecording` action with explicit device parameters. | ||
|
|
||
| ### 2. Raycast Extension | ||
|
|
||
| **Directory:** `raycast-extension/` | ||
|
|
||
| Created a complete Raycast extension with the following structure: | ||
|
|
||
| ``` | ||
| raycast-extension/ | ||
| ├── package.json # Extension manifest and dependencies | ||
| ├── tsconfig.json # TypeScript configuration | ||
| ├── README.md # Usage documentation | ||
| ├── .gitignore # Git ignore rules | ||
| ├── ICON_NOTE.md # Icon requirements | ||
| └── src/ | ||
| ├── utils.ts # Shared deeplink execution utility | ||
| ├── start-recording.tsx # Start recording command | ||
| ├── stop-recording.tsx # Stop recording command | ||
| ├── pause-recording.tsx # Pause recording command | ||
| ├── resume-recording.tsx # Resume recording command | ||
| ├── toggle-microphone.tsx # Toggle microphone command | ||
| └── toggle-camera.tsx # Toggle camera command | ||
| ``` | ||
|
|
||
| #### Commands Implemented | ||
|
|
||
| 1. **Stop Recording** - Stops the current recording | ||
| 2. **Pause Recording** - Pauses the active recording | ||
| 3. **Resume Recording** - Resumes the paused recording | ||
| 4. **Toggle Microphone** - Toggles microphone on/off | ||
| 5. **Toggle Camera** - Toggles camera on/off | ||
| 6. **Start Recording** - Opens Cap app (simplified for now) | ||
|
|
||
| #### How It Works | ||
|
|
||
| Each command: | ||
| 1. Closes the Raycast window (`closeMainWindow()`) | ||
| 2. Constructs a deeplink URL with JSON-encoded action | ||
| 3. Executes `open "cap-desktop://action?value=..."` via shell | ||
| 4. Shows toast notification for feedback | ||
|
|
||
| Example deeplink execution: | ||
| ```typescript | ||
| const action = { stop_recording: null }; | ||
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
| await execAsync(`open "${url}"`); | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
|
||
| ### Test Deeplinks Manually | ||
|
|
||
| You can test deeplinks directly from Terminal: | ||
|
|
||
| ```bash | ||
| # Stop recording | ||
| open "cap-desktop://action?value=%7B%22stop_recording%22%3Anull%7D" | ||
|
|
||
| # Pause recording | ||
| open "cap-desktop://action?value=%7B%22pause_recording%22%3Anull%7D" | ||
|
|
||
| # Resume recording | ||
| open "cap-desktop://action?value=%7B%22resume_recording%22%3Anull%7D" | ||
|
|
||
| # Toggle microphone | ||
| open "cap-desktop://action?value=%7B%22toggle_microphone%22%3Anull%7D" | ||
|
|
||
| # Toggle camera | ||
| open "cap-desktop://action?value=%7B%22toggle_camera%22%3Anull%7D" | ||
| ``` | ||
|
|
||
| ### Test Raycast Extension | ||
|
|
||
| 1. Install dependencies: | ||
| ```bash | ||
| cd raycast-extension | ||
| npm install | ||
| ``` | ||
|
|
||
| 2. Run in development mode: | ||
| ```bash | ||
| npm run dev | ||
| ``` | ||
|
|
||
| 3. Import in Raycast and test each command | ||
|
|
||
| ### Test Scenarios | ||
|
|
||
| 1. **Happy Path** | ||
| - Start a recording in Cap | ||
| - Use Raycast to pause → resume → stop | ||
| - Verify each action works correctly | ||
|
|
||
| 2. **Toggle Commands** | ||
| - Start recording with camera and mic | ||
| - Toggle microphone off → verify mic disabled | ||
| - Toggle camera off → verify camera disabled | ||
| - Attempt to toggle back on → should show error message | ||
|
|
||
| 3. **Error Handling** | ||
| - Try to pause when no recording is active | ||
| - Try to resume when not paused | ||
| - Verify appropriate error messages | ||
|
|
||
| ## Dependencies Used | ||
|
|
||
| ### Existing Functions | ||
| All new deeplink actions use existing Cap functions: | ||
| - `recording::pause_recording()` - Already implemented | ||
| - `recording::resume_recording()` - Already implemented | ||
| - `set_mic_input()` - Already implemented | ||
| - `set_camera_input()` - Already implemented | ||
|
|
||
| ### Raycast API | ||
| - `@raycast/api` v1.48.0 | ||
| - `closeMainWindow()` - Close Raycast UI | ||
| - `showToast()` - Show notifications | ||
| - Node.js `child_process.exec` - Execute shell commands | ||
|
|
||
| ## Future Enhancements | ||
|
|
||
| 1. **Enhanced Start Recording** | ||
| - Add Raycast form to select screen/window | ||
| - Configure camera and microphone | ||
| - Choose recording mode (Studio/Instant) | ||
|
|
||
| 2. **Stateful Toggles** | ||
| - Store last-used camera/microphone in preferences | ||
| - Allow toggle-on to restore previous device | ||
|
|
||
| 3. **Status Display** | ||
| - Show current recording status in menu bar | ||
| - Display recording duration | ||
| - Show which devices are active | ||
|
|
||
| 4. **Quick Actions** | ||
| - Recent recordings list | ||
| - Quick share to clipboard | ||
| - Open in editor | ||
|
|
||
| ## Pull Request Checklist | ||
|
|
||
| - [x] Extended deeplink actions in `deeplink_actions.rs` | ||
| - [x] Implemented 4 new actions: pause, resume, toggle-mic, toggle-camera | ||
| - [x] Created Raycast extension with 6 commands | ||
| - [x] Added TypeScript types and utilities | ||
| - [x] Documented implementation | ||
| - [x] Tested deeplink URL format | ||
| - [ ] Added command icon (PNG file needed) | ||
| - [ ] Tested on macOS with Raycast | ||
| - [ ] Verified all deeplinks work end-to-end | ||
|
|
||
| ## Notes | ||
|
|
||
| - The implementation follows the existing deeplink pattern in Cap | ||
| - All new actions are properly serialized with `snake_case` naming | ||
| - Error handling is consistent with existing code | ||
| - The Raycast extension is production-ready except for the icon file | ||
|
|
||
| ## References | ||
|
|
||
| - Issue: #1540 | ||
| - Bounty: $200 | ||
| - Cap Repository: https://github.com/CapSoftware/Cap | ||
| - Raycast Docs: https://developers.raycast.com |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,6 +26,10 @@ pub enum DeepLinkAction { | |||||||||||||||||||||||||||||
| mode: RecordingMode, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| StopRecording, | ||||||||||||||||||||||||||||||
| PauseRecording, | ||||||||||||||||||||||||||||||
| ResumeRecording, | ||||||||||||||||||||||||||||||
| ToggleMicrophone, | ||||||||||||||||||||||||||||||
| ToggleCamera, | ||||||||||||||||||||||||||||||
| OpenEditor { | ||||||||||||||||||||||||||||||
| project_path: PathBuf, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
|
|
@@ -146,6 +150,38 @@ impl DeepLinkAction { | |||||||||||||||||||||||||||||
| DeepLinkAction::StopRecording => { | ||||||||||||||||||||||||||||||
| crate::recording::stop_recording(app.clone(), app.state()).await | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| DeepLinkAction::PauseRecording => { | ||||||||||||||||||||||||||||||
| crate::recording::pause_recording(app.clone(), app.state()).await | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| DeepLinkAction::ResumeRecording => { | ||||||||||||||||||||||||||||||
| crate::recording::resume_recording(app.clone(), app.state()).await | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| DeepLinkAction::ToggleMicrophone => { | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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):
Suggested change
|
||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| DeepLinkAction::ToggleCamera => { | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same simplification here (and avoids carrying comment text in-code):
Suggested change
|
||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| DeepLinkAction::OpenEditor { project_path } => { | ||||||||||||||||||||||||||||||
| crate::open_project_from_path(Path::new(&project_path), app.clone()) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| node_modules/ | ||
| dist/ | ||
| .DS_Store | ||
| *.log |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # Icon Required | ||
|
|
||
| This Raycast extension needs a `command-icon.png` file. | ||
|
|
||
| ## Requirements | ||
| - Size: 512x512 pixels (or at least 256x256) | ||
| - Format: PNG | ||
| - Content: Cap logo or a screen recording icon | ||
|
|
||
| ## Suggestion | ||
| Copy the Cap logo from `app-icon.png` in the root directory or create a simplified version. | ||
|
|
||
| ## Location | ||
| Place the file at: `raycast-extension/command-icon.png` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| # Cap Raycast Extension | ||
|
|
||
| Control your Cap screen recordings directly from Raycast with keyboard shortcuts. | ||
|
|
||
| ## Features | ||
|
|
||
| - 🎥 **Stop Recording** - Stop your current recording | ||
| - ⏸️ **Pause Recording** - Pause your active recording | ||
| - ▶️ **Resume Recording** - Resume your paused recording | ||
| - 🎤 **Toggle Microphone** - Toggle microphone on/off during recording | ||
| - 📷 **Toggle Camera** - Toggle camera on/off during recording | ||
| - 🎬 **Start Recording** - Quick access to start a new recording (opens Cap app) | ||
|
|
||
| ## Installation | ||
|
|
||
| 1. Install the Raycast extension: | ||
| ```bash | ||
| cd raycast-extension | ||
| npm install | ||
| npm run dev | ||
| ``` | ||
|
|
||
| 2. Import the extension in Raycast | ||
|
|
||
| 3. Set up keyboard shortcuts for each command in Raycast preferences | ||
|
|
||
| ## How It Works | ||
|
|
||
| This extension uses Cap's deeplink URL scheme (`cap-desktop://`) to control recordings. Each command sends a deeplink action to Cap, which executes the corresponding function. | ||
|
|
||
| ### Available Deeplink Actions | ||
|
|
||
| - `stop_recording` - Stops the current recording | ||
| - `pause_recording` - Pauses the current recording | ||
| - `resume_recording` - Resumes a paused recording | ||
| - `toggle_microphone` - Toggles the microphone (disables if enabled) | ||
| - `toggle_camera` - Toggles the camera (disables if enabled) | ||
|
|
||
| ### URL Format | ||
|
|
||
| ``` | ||
| cap-desktop://action?value=<JSON-encoded-action> | ||
| ``` | ||
|
|
||
| Example: | ||
| ``` | ||
| cap-desktop://action?value=%7B%22stop_recording%22%3Anull%7D | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| 1. Start a recording in Cap (use the main app or your configured shortcuts) | ||
| 2. Use Raycast commands to control the recording: | ||
| - `Stop Recording` - End and save your recording | ||
| - `Pause Recording` - Temporarily pause recording | ||
| - `Resume Recording` - Continue recording after pause | ||
| - `Toggle Microphone` - Disable microphone (toggle on requires mic selection) | ||
| - `Toggle Camera` - Disable camera (toggle on requires camera selection) | ||
|
|
||
| ## Notes | ||
|
|
||
| - **Toggle Limitations**: The toggle commands can disable camera/microphone, but cannot re-enable them without knowing which device to use. To re-enable, start a new recording with the desired devices. | ||
| - **Start Recording**: For starting a recording with specific settings (screen, window, camera, mic), use the Cap app directly. The Raycast commands are best for controlling active recordings. | ||
|
|
||
| ## Development | ||
|
|
||
| ```bash | ||
| # Install dependencies | ||
| npm install | ||
|
|
||
| # Start development mode | ||
| npm run dev | ||
|
|
||
| # Build for production | ||
| npm run build | ||
|
|
||
| # Lint and fix | ||
| npm run fix-lint | ||
| ``` | ||
|
|
||
| ## Requirements | ||
|
|
||
| - Cap desktop app installed | ||
| - macOS with Raycast installed | ||
| - Cap deeplink handler registered (`cap-desktop://` URL scheme) | ||
|
|
||
| ## Icon | ||
|
|
||
| The extension requires a `command-icon.png` file. Use the Cap logo or create a custom icon (512x512px recommended). | ||
|
|
||
| ## License | ||
|
|
||
| MIT |
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.
These new
DeepLinkActionvariants look unreachable with the currenttry_fromlogic (thematch url.domain()block has noOkbranch, socap-desktop://action?...never parses into an action). Might be worth fixing that parsing guard so the new pause/resume/toggle paths can actually execute.