From f03b16dfb7a1870efe78bb7d92165109210f983e Mon Sep 17 00:00:00 2001 From: DebuggingMax Date: Thu, 26 Feb 2026 11:13:37 +0000 Subject: [PATCH 1/6] feat: Add deeplinks for recording controls + Raycast extension Adds new deeplink actions for recording control: - pause_recording - Pause the current recording - resume_recording - Resume a paused recording - toggle_pause_recording - Toggle pause/resume state - restart_recording - Restart the current recording - set_microphone - Switch microphone input - set_camera - Switch camera input Also includes a complete Raycast extension with commands for: - Start Recording - Stop Recording - Pause Recording - Resume Recording - Toggle Pause - Restart Recording - Open Settings Closes #1540 --- .../desktop/src-tauri/src/deeplink_actions.rs | 32 ++++ extensions/raycast/DEEPLINKS.md | 176 ++++++++++++++++++ extensions/raycast/README.md | 50 +++++ extensions/raycast/assets/cap-icon.png | Bin 0 -> 7543 bytes extensions/raycast/package.json | 80 ++++++++ extensions/raycast/src/open-settings.ts | 5 + extensions/raycast/src/pause-recording.ts | 5 + extensions/raycast/src/restart-recording.ts | 5 + extensions/raycast/src/resume-recording.ts | 5 + extensions/raycast/src/start-recording.ts | 36 ++++ extensions/raycast/src/stop-recording.ts | 5 + extensions/raycast/src/toggle-pause.ts | 5 + extensions/raycast/src/utils/deeplink.ts | 58 ++++++ extensions/raycast/tsconfig.json | 16 ++ 14 files changed, 478 insertions(+) create mode 100644 extensions/raycast/DEEPLINKS.md create mode 100644 extensions/raycast/README.md create mode 100644 extensions/raycast/assets/cap-icon.png create mode 100644 extensions/raycast/package.json create mode 100644 extensions/raycast/src/open-settings.ts create mode 100644 extensions/raycast/src/pause-recording.ts create mode 100644 extensions/raycast/src/restart-recording.ts create mode 100644 extensions/raycast/src/resume-recording.ts create mode 100644 extensions/raycast/src/start-recording.ts create mode 100644 extensions/raycast/src/stop-recording.ts create mode 100644 extensions/raycast/src/toggle-pause.ts create mode 100644 extensions/raycast/src/utils/deeplink.ts create mode 100644 extensions/raycast/tsconfig.json diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index fce75b4a84..2e36afa157 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -26,6 +26,16 @@ pub enum DeepLinkAction { mode: RecordingMode, }, StopRecording, + PauseRecording, + ResumeRecording, + TogglePauseRecording, + RestartRecording, + SetMicrophone { + label: Option, + }, + SetCamera { + id: Option, + }, OpenEditor { project_path: PathBuf, }, @@ -146,6 +156,28 @@ 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::TogglePauseRecording => { + crate::recording::toggle_pause_recording(app.clone(), app.state()).await + } + DeepLinkAction::RestartRecording => { + crate::recording::restart_recording(app.clone(), app.state()) + .await + .map(|_| ()) + } + DeepLinkAction::SetMicrophone { label } => { + let state = app.state::>(); + crate::set_mic_input(state, label).await + } + DeepLinkAction::SetCamera { id } => { + let state = app.state::>(); + crate::set_camera_input(app.clone(), state, id, None).await + } DeepLinkAction::OpenEditor { project_path } => { crate::open_project_from_path(Path::new(&project_path), app.clone()) } diff --git a/extensions/raycast/DEEPLINKS.md b/extensions/raycast/DEEPLINKS.md new file mode 100644 index 0000000000..36710e70d8 --- /dev/null +++ b/extensions/raycast/DEEPLINKS.md @@ -0,0 +1,176 @@ +# Cap Deeplinks + +Cap supports deeplinks for controlling recordings and other app functionality. This enables integration with tools like Raycast, Alfred, Shortcuts, and custom scripts. + +## URL Scheme + +Cap uses the `cap-desktop://` URL scheme on macOS and Windows. + +## Action Format + +Actions are sent as JSON in the `value` query parameter: + +``` +cap-desktop://action?value= +``` + +## Available Actions + +### Recording Controls + +#### Stop Recording +```json +{"stop_recording":{}} +``` + +#### Pause Recording +```json +{"pause_recording":{}} +``` + +#### Resume Recording +```json +{"resume_recording":{}} +``` + +#### Toggle Pause/Resume +```json +{"toggle_pause_recording":{}} +``` + +#### Restart Recording +Stops and immediately restarts with the same settings: +```json +{"restart_recording":{}} +``` + +#### Start Recording +```json +{ + "start_recording": { + "capture_mode": {"screen": "Main Display"}, + "camera": null, + "mic_label": null, + "capture_system_audio": false, + "mode": "instant" + } +} +``` + +**capture_mode options:** +- `{"screen": "Display Name"}` - Record a specific display +- `{"window": "Window Name"}` - Record a specific window + +**mode options:** +- `"instant"` - Quick recording with immediate upload +- `"studio"` - Full editing capabilities + +### Input Controls + +#### Set Microphone +```json +{"set_microphone": {"label": "MacBook Pro Microphone"}} +``` + +Set to `null` to disable: +```json +{"set_microphone": {"label": null}} +``` + +#### Set Camera +```json +{"set_camera": {"id": "camera-device-id"}} +``` + +Set to `null` to disable: +```json +{"set_camera": {"id": null}} +``` + +### App Controls + +#### Open Settings +```json +{"open_settings": {"page": null}} +``` + +Open a specific settings page: +```json +{"open_settings": {"page": "recordings"}} +``` + +#### Open Editor +```json +{"open_editor": {"project_path": "/path/to/project.cap"}} +``` + +## Examples + +### Shell Script (macOS) + +```bash +#!/bin/bash + +# Stop recording +open "cap-desktop://action?value=%7B%22stop_recording%22%3A%7B%7D%7D" + +# Toggle pause +open "cap-desktop://action?value=%7B%22toggle_pause_recording%22%3A%7B%7D%7D" +``` + +### AppleScript + +```applescript +tell application "System Events" + open location "cap-desktop://action?value=%7B%22stop_recording%22%3A%7B%7D%7D" +end tell +``` + +### JavaScript/Node.js + +```javascript +const { exec } = require('child_process'); + +function capAction(action) { + const json = JSON.stringify(action); + const encoded = encodeURIComponent(json); + const url = `cap-desktop://action?value=${encoded}`; + exec(`open "${url}"`); +} + +// Stop recording +capAction({ stop_recording: {} }); + +// Toggle pause +capAction({ toggle_pause_recording: {} }); +``` + +### Python + +```python +import subprocess +import json +import urllib.parse + +def cap_action(action): + json_str = json.dumps(action) + encoded = urllib.parse.quote(json_str) + url = f"cap-desktop://action?value={encoded}" + subprocess.run(["open", url]) + +# Stop recording +cap_action({"stop_recording": {}}) + +# Toggle pause +cap_action({"toggle_pause_recording": {}}) +``` + +## Raycast Extension + +A full Raycast extension is included in `extensions/raycast/`. See its README for installation instructions. + +## Troubleshooting + +1. **Cap must be running** - Deeplinks only work when Cap is open +2. **URL encoding** - Make sure the JSON is properly URL-encoded +3. **Permissions** - Some actions require an active recording session diff --git a/extensions/raycast/README.md b/extensions/raycast/README.md new file mode 100644 index 0000000000..16f3721ad9 --- /dev/null +++ b/extensions/raycast/README.md @@ -0,0 +1,50 @@ +# Cap Raycast Extension + +Control [Cap](https://cap.so) screen recording from Raycast. + +## Features + +- **Start Recording** - Start a new screen recording +- **Stop Recording** - Stop the current recording +- **Pause Recording** - Pause the current recording +- **Resume Recording** - Resume a paused recording +- **Toggle Pause** - Toggle pause/resume on current recording +- **Restart Recording** - Restart the current recording +- **Open Settings** - Open Cap settings + +## Requirements + +- [Cap](https://cap.so) must be installed and running +- macOS only (Cap deeplinks use the `cap-desktop://` scheme) + +## How It Works + +This extension uses Cap's deeplink API to control recordings. Each command sends a URL like: + +``` +cap-desktop://action?value={"stop_recording":{}} +``` + +## Installation + +1. Clone this repository +2. Run `npm install` in the `extensions/raycast` directory +3. Run `npm run dev` to start development +4. Or `npm run build` to build for production + +## Available Deeplinks + +| Action | Deeplink | +|--------|----------| +| Stop Recording | `cap-desktop://action?value={"stop_recording":{}}` | +| Pause Recording | `cap-desktop://action?value={"pause_recording":{}}` | +| Resume Recording | `cap-desktop://action?value={"resume_recording":{}}` | +| Toggle Pause | `cap-desktop://action?value={"toggle_pause_recording":{}}` | +| Restart Recording | `cap-desktop://action?value={"restart_recording":{}}` | +| Set Microphone | `cap-desktop://action?value={"set_microphone":{"label":"Microphone Name"}}` | +| Set Camera | `cap-desktop://action?value={"set_camera":{"id":"camera-id"}}` | +| Open Settings | `cap-desktop://action?value={"open_settings":{"page":null}}` | + +## License + +MIT diff --git a/extensions/raycast/assets/cap-icon.png b/extensions/raycast/assets/cap-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..72dd4dcd0795ebde571a500c9a9a4aaef00403cf GIT binary patch literal 7543 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 } + | { pause_recording: Record } + | { resume_recording: Record } + | { toggle_pause_recording: Record } + | { restart_recording: Record } + | { set_microphone: { label: string | null } } + | { set_camera: { id: string | null } } + | { open_settings: { page: string | null } }; + +export async function executeDeepLink(action: DeepLinkAction, successMessage: string): Promise { + const jsonValue = JSON.stringify(action); + const encodedValue = encodeURIComponent(jsonValue); + const deeplink = `${DEEPLINK_SCHEME}?value=${encodedValue}`; + + try { + await open(deeplink); + await showHUD(successMessage); + } catch { + await showHUD("Failed to communicate with Cap. Is it running?"); + } +} + +export async function stopRecording(): Promise { + await executeDeepLink({ stop_recording: {} }, "⏹ Recording stopped"); +} + +export async function pauseRecording(): Promise { + await executeDeepLink({ pause_recording: {} }, "⏸ Recording paused"); +} + +export async function resumeRecording(): Promise { + await executeDeepLink({ resume_recording: {} }, "▶️ Recording resumed"); +} + +export async function togglePauseRecording(): Promise { + await executeDeepLink({ toggle_pause_recording: {} }, "⏯ Toggled pause"); +} + +export async function restartRecording(): Promise { + await executeDeepLink({ restart_recording: {} }, "🔄 Recording restarted"); +} + +export async function setMicrophone(label: string | null): Promise { + await executeDeepLink({ set_microphone: { label } }, label ? `🎤 Switched to ${label}` : "🎤 Microphone disabled"); +} + +export async function setCamera(id: string | null): Promise { + await executeDeepLink({ set_camera: { id } }, id ? `📷 Camera switched` : "📷 Camera disabled"); +} + +export async function openSettings(page?: string): Promise { + await executeDeepLink({ open_settings: { page: page ?? null } }, "⚙️ Opening settings"); +} diff --git a/extensions/raycast/tsconfig.json b/extensions/raycast/tsconfig.json new file mode 100644 index 0000000000..fde8b72149 --- /dev/null +++ b/extensions/raycast/tsconfig.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "strict": true, + "lib": ["ES2023"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2023", + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*"] +} From 9124b9bb16ab261b0e0ea836a42c3a249c081455 Mon Sep 17 00:00:00 2001 From: DebuggingMax Date: Sat, 28 Feb 2026 13:15:26 +0000 Subject: [PATCH 2/6] fix(deeplinks): make set_mic_input and set_camera_input pub for deeplink_actions Address review comment: these functions need pub visibility to be callable from deeplink_actions.rs --- apps/desktop/src-tauri/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 7497f352dc..7e50b1d47d 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -470,7 +470,7 @@ impl App { #[tauri::command] #[specta::specta] #[instrument(skip(state))] -async fn set_mic_input(state: MutableState<'_, App>, label: Option) -> Result<(), String> { +pub async fn set_mic_input(state: MutableState<'_, App>, label: Option) -> Result<(), String> { let desired_label = label; let (mic_feed, studio_handle, previous_label) = { @@ -573,7 +573,7 @@ fn get_system_diagnostics() -> cap_recording::diagnostics::SystemDiagnostics { #[specta::specta] #[instrument(skip(app_handle, state))] #[allow(unused_mut)] -async fn set_camera_input( +pub async fn set_camera_input( app_handle: AppHandle, state: MutableState<'_, App>, id: Option, From 91a72afaa03223b5e12e6bc34689282cf7f9e687 Mon Sep 17 00:00:00 2001 From: DebuggingMax Date: Sat, 28 Feb 2026 13:15:30 +0000 Subject: [PATCH 3/6] fix(raycast): serialize unit variants as strings, not objects Address review comment: serde serializes unit enum variants as JSON strings like "stop_recording", not objects like {"stop_recording": {}}. Added start_recording to DeepLinkAction type. --- extensions/raycast/src/utils/deeplink.ts | 40 +++++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/extensions/raycast/src/utils/deeplink.ts b/extensions/raycast/src/utils/deeplink.ts index 414dc8574d..d3c59df791 100644 --- a/extensions/raycast/src/utils/deeplink.ts +++ b/extensions/raycast/src/utils/deeplink.ts @@ -2,15 +2,31 @@ import { open, showHUD } from "@raycast/api"; const DEEPLINK_SCHEME = "cap-desktop://action"; -type DeepLinkAction = - | { stop_recording: Record } - | { pause_recording: Record } - | { resume_recording: Record } - | { toggle_pause_recording: Record } - | { restart_recording: Record } +// Unit variants serialize as strings in serde +type UnitAction = + | "stop_recording" + | "pause_recording" + | "resume_recording" + | "toggle_pause_recording" + | "restart_recording"; + +// Struct variants serialize as objects +type StructAction = + | { + start_recording: { + capture_mode: { screen: string } | { window: string }; + camera: string | null; + mic_label: string | null; + capture_system_audio: boolean; + mode: "instant" | "studio"; + }; + } | { set_microphone: { label: string | null } } | { set_camera: { id: string | null } } - | { open_settings: { page: string | null } }; + | { open_settings: { page: string | null } } + | { open_editor: { project_path: string } }; + +type DeepLinkAction = UnitAction | StructAction; export async function executeDeepLink(action: DeepLinkAction, successMessage: string): Promise { const jsonValue = JSON.stringify(action); @@ -26,23 +42,23 @@ export async function executeDeepLink(action: DeepLinkAction, successMessage: st } export async function stopRecording(): Promise { - await executeDeepLink({ stop_recording: {} }, "⏹ Recording stopped"); + await executeDeepLink("stop_recording", "⏹ Recording stopped"); } export async function pauseRecording(): Promise { - await executeDeepLink({ pause_recording: {} }, "⏸ Recording paused"); + await executeDeepLink("pause_recording", "⏸ Recording paused"); } export async function resumeRecording(): Promise { - await executeDeepLink({ resume_recording: {} }, "▶️ Recording resumed"); + await executeDeepLink("resume_recording", "▶️ Recording resumed"); } export async function togglePauseRecording(): Promise { - await executeDeepLink({ toggle_pause_recording: {} }, "⏯ Toggled pause"); + await executeDeepLink("toggle_pause_recording", "⏯ Toggled pause"); } export async function restartRecording(): Promise { - await executeDeepLink({ restart_recording: {} }, "🔄 Recording restarted"); + await executeDeepLink("restart_recording", "🔄 Recording restarted"); } export async function setMicrophone(label: string | null): Promise { From 3514617ef11618537af3eaace4a0e372f9b08c2a Mon Sep 17 00:00:00 2001 From: DebuggingMax Date: Sat, 28 Feb 2026 13:15:35 +0000 Subject: [PATCH 4/6] fix(raycast): make display name configurable via preferences Address review comment: don't hardcode "Main Display". Added preferences for: - Display name (configurable, defaults to Main Display) - Recording mode (instant/studio) - Capture system audio (checkbox) --- extensions/raycast/package.json | 31 +++++++++++++++++++++++ extensions/raycast/src/start-recording.ts | 22 +++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/extensions/raycast/package.json b/extensions/raycast/package.json index e8bf1e91da..434b16889e 100644 --- a/extensions/raycast/package.json +++ b/extensions/raycast/package.json @@ -7,6 +7,37 @@ "author": "cap", "categories": ["Productivity", "Media"], "license": "MIT", + "preferences": [ + { + "name": "displayName", + "title": "Display Name", + "description": "Name of the display to record. Leave empty to record the primary/main display.", + "type": "textfield", + "required": false, + "placeholder": "Main Display" + }, + { + "name": "recordingMode", + "title": "Recording Mode", + "description": "Default recording mode", + "type": "dropdown", + "required": false, + "default": "instant", + "data": [ + { "title": "Instant", "value": "instant" }, + { "title": "Studio", "value": "studio" } + ] + }, + { + "name": "captureSystemAudio", + "title": "Capture System Audio", + "description": "Whether to capture system audio by default", + "type": "checkbox", + "required": false, + "default": false, + "label": "Capture system audio" + } + ], "commands": [ { "name": "start-recording", diff --git a/extensions/raycast/src/start-recording.ts b/extensions/raycast/src/start-recording.ts index 279cd26101..6d4f7c2790 100644 --- a/extensions/raycast/src/start-recording.ts +++ b/extensions/raycast/src/start-recording.ts @@ -1,7 +1,13 @@ -import { showHUD, open } from "@raycast/api"; +import { showHUD, open, getPreferenceValues } from "@raycast/api"; const DEEPLINK_SCHEME = "cap-desktop://action"; +interface Preferences { + displayName?: string; + recordingMode?: "instant" | "studio"; + captureSystemAudio?: boolean; +} + interface StartRecordingAction { start_recording: { capture_mode: { screen: string } | { window: string }; @@ -13,13 +19,21 @@ interface StartRecordingAction { } export default async function Command() { + const preferences = getPreferenceValues(); + + // Use configured display name or fall back to empty string + // Empty string will let Cap use the primary/default display + const displayName = preferences.displayName?.trim() || "Main Display"; + const recordingMode = preferences.recordingMode || "instant"; + const captureSystemAudio = preferences.captureSystemAudio || false; + const action: StartRecordingAction = { start_recording: { - capture_mode: { screen: "Main Display" }, + capture_mode: { screen: displayName }, camera: null, mic_label: null, - capture_system_audio: false, - mode: "instant", + capture_system_audio: captureSystemAudio, + mode: recordingMode, }, }; From e7ba09a38860728cce079e1ff4da78f3426b0f05 Mon Sep 17 00:00:00 2001 From: DebuggingMax Date: Sat, 28 Feb 2026 13:15:40 +0000 Subject: [PATCH 5/6] docs(raycast): fix platform support, URL encoding, and JSON format Address review comments: - Changed "macOS only" to "macOS and Windows supported" - Fixed examples to show URL-encoded JSON - Fixed unit variant format: strings like "stop_recording", not objects - Added troubleshooting note about unit vs struct variants --- extensions/raycast/DEEPLINKS.md | 51 +++++++++++++++++++++------------ extensions/raycast/README.md | 48 +++++++++++++++++++++++-------- 2 files changed, 69 insertions(+), 30 deletions(-) diff --git a/extensions/raycast/DEEPLINKS.md b/extensions/raycast/DEEPLINKS.md index 36710e70d8..e24c5e6f38 100644 --- a/extensions/raycast/DEEPLINKS.md +++ b/extensions/raycast/DEEPLINKS.md @@ -14,37 +14,42 @@ Actions are sent as JSON in the `value` query parameter: cap-desktop://action?value= ``` +**Important:** The JSON value MUST be URL-encoded! + ## Available Actions ### Recording Controls +Unit variants are serialized as JSON strings (not objects): + #### Stop Recording ```json -{"stop_recording":{}} +"stop_recording" ``` #### Pause Recording ```json -{"pause_recording":{}} +"pause_recording" ``` #### Resume Recording ```json -{"resume_recording":{}} +"resume_recording" ``` #### Toggle Pause/Resume ```json -{"toggle_pause_recording":{}} +"toggle_pause_recording" ``` #### Restart Recording Stops and immediately restarts with the same settings: ```json -{"restart_recording":{}} +"restart_recording" ``` #### Start Recording +Struct variant (serialized as object): ```json { "start_recording": { @@ -111,18 +116,21 @@ Open a specific settings page: ```bash #!/bin/bash -# Stop recording -open "cap-desktop://action?value=%7B%22stop_recording%22%3A%7B%7D%7D" +# Stop recording (note: unit variant is a string, not object) +open "cap-desktop://action?value=%22stop_recording%22" # Toggle pause -open "cap-desktop://action?value=%7B%22toggle_pause_recording%22%3A%7B%7D%7D" +open "cap-desktop://action?value=%22toggle_pause_recording%22" + +# Set microphone (struct variant) +open "cap-desktop://action?value=%7B%22set_microphone%22%3A%7B%22label%22%3A%22MacBook%20Pro%20Microphone%22%7D%7D" ``` ### AppleScript ```applescript tell application "System Events" - open location "cap-desktop://action?value=%7B%22stop_recording%22%3A%7B%7D%7D" + open location "cap-desktop://action?value=%22stop_recording%22" end tell ``` @@ -138,11 +146,14 @@ function capAction(action) { exec(`open "${url}"`); } -// Stop recording -capAction({ stop_recording: {} }); +// Stop recording (unit variant = string) +capAction("stop_recording"); -// Toggle pause -capAction({ toggle_pause_recording: {} }); +// Toggle pause (unit variant = string) +capAction("toggle_pause_recording"); + +// Set microphone (struct variant = object) +capAction({ set_microphone: { label: "MacBook Pro Microphone" } }); ``` ### Python @@ -158,11 +169,14 @@ def cap_action(action): url = f"cap-desktop://action?value={encoded}" subprocess.run(["open", url]) -# Stop recording -cap_action({"stop_recording": {}}) +# Stop recording (unit variant = string) +cap_action("stop_recording") -# Toggle pause -cap_action({"toggle_pause_recording": {}}) +# Toggle pause (unit variant = string) +cap_action("toggle_pause_recording") + +# Set microphone (struct variant = dict) +cap_action({"set_microphone": {"label": "MacBook Pro Microphone"}}) ``` ## Raycast Extension @@ -173,4 +187,5 @@ A full Raycast extension is included in `extensions/raycast/`. See its README fo 1. **Cap must be running** - Deeplinks only work when Cap is open 2. **URL encoding** - Make sure the JSON is properly URL-encoded -3. **Permissions** - Some actions require an active recording session +3. **Unit vs Struct variants** - Unit actions (stop, pause, etc.) are JSON strings like `"stop_recording"`, not objects like `{"stop_recording": {}}` +4. **Permissions** - Some actions require an active recording session diff --git a/extensions/raycast/README.md b/extensions/raycast/README.md index 16f3721ad9..0022d4635b 100644 --- a/extensions/raycast/README.md +++ b/extensions/raycast/README.md @@ -15,16 +15,18 @@ Control [Cap](https://cap.so) screen recording from Raycast. ## Requirements - [Cap](https://cap.so) must be installed and running -- macOS only (Cap deeplinks use the `cap-desktop://` scheme) +- macOS and Windows supported (Cap deeplinks use the `cap-desktop://` scheme) ## How It Works This extension uses Cap's deeplink API to control recordings. Each command sends a URL like: ``` -cap-desktop://action?value={"stop_recording":{}} +cap-desktop://action?value=%22stop_recording%22 ``` +Note: Unit actions (stop, pause, resume, etc.) are sent as JSON strings, while actions with parameters are sent as JSON objects. + ## Installation 1. Clone this repository @@ -32,18 +34,40 @@ cap-desktop://action?value={"stop_recording":{}} 3. Run `npm run dev` to start development 4. Or `npm run build` to build for production +## Configuration + +The extension supports the following preferences (configurable in Raycast): + +- **Display Name** - Name of the display to record (leave empty for primary display) +- **Recording Mode** - Choose between "instant" or "studio" mode +- **Capture System Audio** - Whether to capture system audio by default + ## Available Deeplinks -| Action | Deeplink | -|--------|----------| -| Stop Recording | `cap-desktop://action?value={"stop_recording":{}}` | -| Pause Recording | `cap-desktop://action?value={"pause_recording":{}}` | -| Resume Recording | `cap-desktop://action?value={"resume_recording":{}}` | -| Toggle Pause | `cap-desktop://action?value={"toggle_pause_recording":{}}` | -| Restart Recording | `cap-desktop://action?value={"restart_recording":{}}` | -| Set Microphone | `cap-desktop://action?value={"set_microphone":{"label":"Microphone Name"}}` | -| Set Camera | `cap-desktop://action?value={"set_camera":{"id":"camera-id"}}` | -| Open Settings | `cap-desktop://action?value={"open_settings":{"page":null}}` | +| Action | Deeplink Value (URL-encoded) | +|--------|------------------------------| +| Stop Recording | `%22stop_recording%22` | +| Pause Recording | `%22pause_recording%22` | +| Resume Recording | `%22resume_recording%22` | +| Toggle Pause | `%22toggle_pause_recording%22` | +| Restart Recording | `%22restart_recording%22` | +| Set Microphone | `%7B%22set_microphone%22%3A%7B%22label%22%3A%22Microphone%20Name%22%7D%7D` | +| Set Camera | `%7B%22set_camera%22%3A%7B%22id%22%3A%22camera-id%22%7D%7D` | +| Open Settings | `%7B%22open_settings%22%3A%7B%22page%22%3Anull%7D%7D` | + +### Raw JSON Values (before URL encoding) + +Unit actions (no parameters): +- `"stop_recording"` +- `"pause_recording"` +- `"resume_recording"` +- `"toggle_pause_recording"` +- `"restart_recording"` + +Struct actions (with parameters): +- `{"set_microphone":{"label":"Microphone Name"}}` +- `{"set_camera":{"id":"camera-id"}}` +- `{"open_settings":{"page":null}}` ## License From 6a1225a580b81ea05a3b3d3428afca7d0538a64b Mon Sep 17 00:00:00 2001 From: DebuggingMax Date: Sat, 28 Feb 2026 19:35:01 +0000 Subject: [PATCH 6/6] Address PR review feedback (round 2) - Change set_mic_input and set_camera_input to pub(crate) in lib.rs - Remove explanatory type comments in deeplink.ts (types are self-explanatory) - Fix display preference description to accurately reflect default behavior --- apps/desktop/src-tauri/src/lib.rs | 4 ++-- extensions/raycast/package.json | 2 +- extensions/raycast/src/utils/deeplink.ts | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 7e50b1d47d..b7b0e9ed27 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -470,7 +470,7 @@ impl App { #[tauri::command] #[specta::specta] #[instrument(skip(state))] -pub async fn set_mic_input(state: MutableState<'_, App>, label: Option) -> Result<(), String> { +pub(crate) async fn set_mic_input(state: MutableState<'_, App>, label: Option) -> Result<(), String> { let desired_label = label; let (mic_feed, studio_handle, previous_label) = { @@ -573,7 +573,7 @@ fn get_system_diagnostics() -> cap_recording::diagnostics::SystemDiagnostics { #[specta::specta] #[instrument(skip(app_handle, state))] #[allow(unused_mut)] -pub async fn set_camera_input( +pub(crate) async fn set_camera_input( app_handle: AppHandle, state: MutableState<'_, App>, id: Option, diff --git a/extensions/raycast/package.json b/extensions/raycast/package.json index 434b16889e..a423ff4c63 100644 --- a/extensions/raycast/package.json +++ b/extensions/raycast/package.json @@ -11,7 +11,7 @@ { "name": "displayName", "title": "Display Name", - "description": "Name of the display to record. Leave empty to record the primary/main display.", + "description": "Name of the display to record (defaults to 'Main Display')", "type": "textfield", "required": false, "placeholder": "Main Display" diff --git a/extensions/raycast/src/utils/deeplink.ts b/extensions/raycast/src/utils/deeplink.ts index d3c59df791..96a32b727d 100644 --- a/extensions/raycast/src/utils/deeplink.ts +++ b/extensions/raycast/src/utils/deeplink.ts @@ -2,7 +2,6 @@ import { open, showHUD } from "@raycast/api"; const DEEPLINK_SCHEME = "cap-desktop://action"; -// Unit variants serialize as strings in serde type UnitAction = | "stop_recording" | "pause_recording" @@ -10,7 +9,6 @@ type UnitAction = | "toggle_pause_recording" | "restart_recording"; -// Struct variants serialize as objects type StructAction = | { start_recording: {