From 62deb007dc1a6808f2f15e7a8716c1469e722d47 Mon Sep 17 00:00:00 2001 From: AliaksandrNazaruk Date: Thu, 26 Mar 2026 14:11:12 +0100 Subject: [PATCH 1/2] feat: extend deeplinks for recording control + add Raycast extension Add new deeplink actions for full recording control: - PauseRecording - ResumeRecording - TogglePauseRecording - RestartRecording - TakeScreenshot (with capture mode) Build Raycast extension (extensions/raycast/) with commands for: - Start/Stop/Pause/Resume/Toggle pause recording - Restart recording - Take screenshot - Open settings All commands use the cap-desktop:// deeplink protocol. Closes #1540 --- .../desktop/src-tauri/src/deeplink_actions.rs | 39 ++++++++ extensions/raycast/README.md | 44 ++++++++++ extensions/raycast/package.json | 88 +++++++++++++++++++ extensions/raycast/src/open-settings.ts | 8 ++ 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 | 16 ++++ extensions/raycast/src/stop-recording.ts | 5 ++ extensions/raycast/src/take-screenshot.ts | 12 +++ extensions/raycast/src/toggle-pause.ts | 5 ++ extensions/raycast/src/utils.ts | 30 +++++++ extensions/raycast/tsconfig.json | 17 ++++ 13 files changed, 279 insertions(+) create mode 100644 extensions/raycast/README.md 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/take-screenshot.ts create mode 100644 extensions/raycast/src/toggle-pause.ts create mode 100644 extensions/raycast/src/utils.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 a117028487..bf41c6689e 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -26,6 +26,13 @@ pub enum DeepLinkAction { mode: RecordingMode, }, StopRecording, + PauseRecording, + ResumeRecording, + TogglePauseRecording, + RestartRecording, + TakeScreenshot { + capture_mode: CaptureMode, + }, OpenEditor { project_path: PathBuf, }, @@ -147,6 +154,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::TogglePauseRecording => { + crate::recording::toggle_pause_recording(app.clone(), app.state()).await + } + DeepLinkAction::RestartRecording => { + crate::recording::restart_recording(app.clone(), app.state()) + .await + .map(|_| ()) + } + DeepLinkAction::TakeScreenshot { capture_mode } => { + 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))?, + }; + + crate::recording::take_screenshot(app.clone(), capture_target) + .await + .map(|_| ()) + } DeepLinkAction::OpenEditor { project_path } => { crate::open_project_from_path(Path::new(&project_path), app.clone()) } diff --git a/extensions/raycast/README.md b/extensions/raycast/README.md new file mode 100644 index 0000000000..43fe2916ff --- /dev/null +++ b/extensions/raycast/README.md @@ -0,0 +1,44 @@ +# Cap for Raycast + +Control [Cap](https://cap.so) screen recorder directly from Raycast. + +## Commands + +| Command | Description | +| --- | --- | +| 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 | +| Restart Recording | Restart the current recording | +| Take Screenshot | Take a screenshot | +| Open Settings | Open Cap settings | + +## How It Works + +This extension uses Cap's deeplink protocol (`cap-desktop://action?value=`) to communicate with the desktop app. Make sure Cap is running before using the commands. + +## Deeplink Protocol + +Cap supports the following deeplink actions: + +``` +cap-desktop://action?value="stop_recording" +cap-desktop://action?value="pause_recording" +cap-desktop://action?value="resume_recording" +cap-desktop://action?value="toggle_pause_recording" +cap-desktop://action?value="restart_recording" +cap-desktop://action?value={"start_recording":{"capture_mode":{"screen":"Main Display"},"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"studio"}} +cap-desktop://action?value={"take_screenshot":{"capture_mode":{"screen":"Main Display"}}} +cap-desktop://action?value={"open_editor":{"project_path":"/path/to/project"}} +cap-desktop://action?value={"open_settings":{"page":null}} +``` + +## Development + +```bash +cd extensions/raycast +npm install +npm run dev +``` diff --git a/extensions/raycast/package.json b/extensions/raycast/package.json new file mode 100644 index 0000000000..8aa8b2e210 --- /dev/null +++ b/extensions/raycast/package.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://www.raycast.com/schemas/extension.json", + "name": "cap", + "title": "Cap", + "description": "Control Cap screen recorder — start/stop recording, pause, resume, take screenshots, and more.", + "icon": "command-icon.png", + "author": "cap", + "categories": ["Productivity", "Applications"], + "license": "AGPL-3.0", + "commands": [ + { + "name": "start-recording", + "title": "Start Recording", + "subtitle": "Cap", + "description": "Start a new screen recording with Cap", + "mode": "no-view" + }, + { + "name": "stop-recording", + "title": "Stop Recording", + "subtitle": "Cap", + "description": "Stop the current recording", + "mode": "no-view" + }, + { + "name": "pause-recording", + "title": "Pause Recording", + "subtitle": "Cap", + "description": "Pause the current recording", + "mode": "no-view" + }, + { + "name": "resume-recording", + "title": "Resume Recording", + "subtitle": "Cap", + "description": "Resume a paused recording", + "mode": "no-view" + }, + { + "name": "toggle-pause", + "title": "Toggle Pause", + "subtitle": "Cap", + "description": "Toggle pause/resume on the current recording", + "mode": "no-view" + }, + { + "name": "restart-recording", + "title": "Restart Recording", + "subtitle": "Cap", + "description": "Restart the current recording", + "mode": "no-view" + }, + { + "name": "take-screenshot", + "title": "Take Screenshot", + "subtitle": "Cap", + "description": "Take a screenshot with Cap", + "mode": "no-view" + }, + { + "name": "open-settings", + "title": "Open Settings", + "subtitle": "Cap", + "description": "Open Cap settings", + "mode": "no-view" + } + ], + "dependencies": { + "@raycast/api": "^1.93.2", + "@raycast/utils": "^1.19.1" + }, + "devDependencies": { + "@raycast/eslint-config": "^1.0.11", + "@types/node": "22.13.14", + "@types/react": "19.0.12", + "eslint": "^9.23.0", + "prettier": "^3.5.3", + "typescript": "^5.8.2" + }, + "scripts": { + "build": "ray build", + "dev": "ray develop", + "fix-lint": "ray lint --fix", + "lint": "ray lint", + "prepublishOnly": "echo \"Error: no publish\" && exit 1", + "publish": "npx @raycast/api@latest publish" + } +} diff --git a/extensions/raycast/src/open-settings.ts b/extensions/raycast/src/open-settings.ts new file mode 100644 index 0000000000..8b2ebbbf67 --- /dev/null +++ b/extensions/raycast/src/open-settings.ts @@ -0,0 +1,8 @@ +import { triggerDeepLink } from "./utils"; + +export default async function Command() { + await triggerDeepLink( + { open_settings: { page: null } }, + "⚙️ Opening Cap settings…", + ); +} diff --git a/extensions/raycast/src/pause-recording.ts b/extensions/raycast/src/pause-recording.ts new file mode 100644 index 0000000000..e57843395a --- /dev/null +++ b/extensions/raycast/src/pause-recording.ts @@ -0,0 +1,5 @@ +import { triggerDeepLink } from "./utils"; + +export default async function Command() { + await triggerDeepLink("pause_recording", "⏸ Pausing Cap recording…"); +} diff --git a/extensions/raycast/src/restart-recording.ts b/extensions/raycast/src/restart-recording.ts new file mode 100644 index 0000000000..395bf2fbc2 --- /dev/null +++ b/extensions/raycast/src/restart-recording.ts @@ -0,0 +1,5 @@ +import { triggerDeepLink } from "./utils"; + +export default async function Command() { + await triggerDeepLink("restart_recording", "🔄 Restarting Cap recording…"); +} diff --git a/extensions/raycast/src/resume-recording.ts b/extensions/raycast/src/resume-recording.ts new file mode 100644 index 0000000000..1aa0badbd6 --- /dev/null +++ b/extensions/raycast/src/resume-recording.ts @@ -0,0 +1,5 @@ +import { triggerDeepLink } from "./utils"; + +export default async function Command() { + await triggerDeepLink("resume_recording", "▶️ Resuming Cap recording…"); +} diff --git a/extensions/raycast/src/start-recording.ts b/extensions/raycast/src/start-recording.ts new file mode 100644 index 0000000000..a31f646bec --- /dev/null +++ b/extensions/raycast/src/start-recording.ts @@ -0,0 +1,16 @@ +import { triggerDeepLink } from "./utils"; + +export default async function Command() { + await triggerDeepLink( + { + start_recording: { + capture_mode: { screen: "Main Display" }, + camera: null, + mic_label: null, + capture_system_audio: false, + mode: "studio", + }, + }, + "📹 Starting Cap recording…", + ); +} diff --git a/extensions/raycast/src/stop-recording.ts b/extensions/raycast/src/stop-recording.ts new file mode 100644 index 0000000000..af904b854c --- /dev/null +++ b/extensions/raycast/src/stop-recording.ts @@ -0,0 +1,5 @@ +import { triggerDeepLink } from "./utils"; + +export default async function Command() { + await triggerDeepLink("stop_recording", "⏹ Stopping Cap recording…"); +} diff --git a/extensions/raycast/src/take-screenshot.ts b/extensions/raycast/src/take-screenshot.ts new file mode 100644 index 0000000000..fe55fc4e7e --- /dev/null +++ b/extensions/raycast/src/take-screenshot.ts @@ -0,0 +1,12 @@ +import { triggerDeepLink } from "./utils"; + +export default async function Command() { + await triggerDeepLink( + { + take_screenshot: { + capture_mode: { screen: "Main Display" }, + }, + }, + "📸 Taking screenshot with Cap…", + ); +} diff --git a/extensions/raycast/src/toggle-pause.ts b/extensions/raycast/src/toggle-pause.ts new file mode 100644 index 0000000000..73093db0ef --- /dev/null +++ b/extensions/raycast/src/toggle-pause.ts @@ -0,0 +1,5 @@ +import { triggerDeepLink } from "./utils"; + +export default async function Command() { + await triggerDeepLink("toggle_pause_recording", "⏯ Toggling Cap recording pause…"); +} diff --git a/extensions/raycast/src/utils.ts b/extensions/raycast/src/utils.ts new file mode 100644 index 0000000000..0b11cf858c --- /dev/null +++ b/extensions/raycast/src/utils.ts @@ -0,0 +1,30 @@ +import { open, showHUD } from "@raycast/api"; + +const SCHEME = "cap-desktop"; + +type DeepLinkAction = string | Record; + +/** + * Build a Cap deeplink URL. + * + * Actions are sent as: cap-desktop://action?value= + * + * Unit variants (e.g. StopRecording) serialize as a plain string: "stop_recording" + * Struct variants serialize as: {"start_recording": {...}} + */ +export function buildDeepLink(action: DeepLinkAction): string { + const json = JSON.stringify(action); + return `${SCHEME}://action?value=${encodeURIComponent(json)}`; +} + +/** + * Open a Cap deeplink and show a HUD message. + */ +export async function triggerDeepLink( + action: DeepLinkAction, + hudMessage: string, +): Promise { + const url = buildDeepLink(action); + await open(url); + await showHUD(hudMessage); +} diff --git a/extensions/raycast/tsconfig.json b/extensions/raycast/tsconfig.json new file mode 100644 index 0000000000..6401246f16 --- /dev/null +++ b/extensions/raycast/tsconfig.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node 22", + "compilerOptions": { + "lib": ["ES2023"], + "module": "Node16", + "target": "ES2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "Node16", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*"] +} From 0940d592b5cf8f05f488139e012c121648ee7723 Mon Sep 17 00:00:00 2001 From: AliaksandrNazaruk Date: Thu, 26 Mar 2026 15:58:05 +0100 Subject: [PATCH 2/2] fix: address PR review comments for Raycast extension - Add icon.png placeholder (was referencing missing command-icon.png) - Use default display instead of hardcoded 'Main Display' - Extract duplicated capture target resolution into CaptureMode::resolve() - Show error HUD when Cap is not running instead of false success --- .../desktop/src-tauri/src/deeplink_actions.rs | 53 +++++++++--------- extensions/raycast/icon.png | Bin 0 -> 2200 bytes extensions/raycast/package.json | 2 +- extensions/raycast/src/start-recording.ts | 2 +- extensions/raycast/src/take-screenshot.ts | 2 +- extensions/raycast/src/utils.ts | 9 ++- 6 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 extensions/raycast/icon.png diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index bf41c6689e..91823db65a 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -15,11 +15,34 @@ pub enum CaptureMode { Window(String), } +impl CaptureMode { + /// Resolve a CaptureMode into a ScreenCaptureTarget, or default to the primary display. + fn resolve(mode: Option) -> Result { + match mode { + Some(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)), + Some(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)), + None => cap_recording::screen_capture::list_displays() + .into_iter() + .next() + .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) + .ok_or_else(|| "No displays available".to_string()), + } + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum DeepLinkAction { StartRecording { - capture_mode: CaptureMode, + capture_mode: Option, camera: Option, mic_label: Option, capture_system_audio: bool, @@ -31,7 +54,7 @@ pub enum DeepLinkAction { TogglePauseRecording, RestartRecording, TakeScreenshot { - capture_mode: CaptureMode, + capture_mode: Option, }, OpenEditor { project_path: PathBuf, @@ -127,18 +150,7 @@ impl DeepLinkAction { crate::set_camera_input(app.clone(), state.clone(), camera, None).await?; crate::set_mic_input(state.clone(), mic_label).await?; - 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))?, - }; + let capture_target = CaptureMode::resolve(capture_mode)?; let inputs = StartRecordingInputs { mode, @@ -169,18 +181,7 @@ impl DeepLinkAction { .map(|_| ()) } DeepLinkAction::TakeScreenshot { capture_mode } => { - 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))?, - }; + let capture_target = CaptureMode::resolve(capture_mode)?; crate::recording::take_screenshot(app.clone(), capture_target) .await diff --git a/extensions/raycast/icon.png b/extensions/raycast/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b7750294896d0273a389734b619a5bb36f572ffb GIT binary patch literal 2200 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4YzZe)e;yqm)Ln`LHy{5>>puljz zz)7(0Nkj(YBjL}}<%{1kGi>R(&&Y6~8mOa*hk=1b5~#(9fk7aJfkEK}1B1f^Mur9j zW(EewQ3a#nFq$4l^TlZSFj`KIRtKZiBBXh6fil-cr literal 0 HcmV?d00001 diff --git a/extensions/raycast/package.json b/extensions/raycast/package.json index 8aa8b2e210..6b2ba69026 100644 --- a/extensions/raycast/package.json +++ b/extensions/raycast/package.json @@ -3,7 +3,7 @@ "name": "cap", "title": "Cap", "description": "Control Cap screen recorder — start/stop recording, pause, resume, take screenshots, and more.", - "icon": "command-icon.png", + "icon": "icon.png", "author": "cap", "categories": ["Productivity", "Applications"], "license": "AGPL-3.0", diff --git a/extensions/raycast/src/start-recording.ts b/extensions/raycast/src/start-recording.ts index a31f646bec..5ef7b16da7 100644 --- a/extensions/raycast/src/start-recording.ts +++ b/extensions/raycast/src/start-recording.ts @@ -4,7 +4,7 @@ export default async function Command() { await triggerDeepLink( { start_recording: { - capture_mode: { screen: "Main Display" }, + capture_mode: null, camera: null, mic_label: null, capture_system_audio: false, diff --git a/extensions/raycast/src/take-screenshot.ts b/extensions/raycast/src/take-screenshot.ts index fe55fc4e7e..ec36395dbe 100644 --- a/extensions/raycast/src/take-screenshot.ts +++ b/extensions/raycast/src/take-screenshot.ts @@ -4,7 +4,7 @@ export default async function Command() { await triggerDeepLink( { take_screenshot: { - capture_mode: { screen: "Main Display" }, + capture_mode: null, }, }, "📸 Taking screenshot with Cap…", diff --git a/extensions/raycast/src/utils.ts b/extensions/raycast/src/utils.ts index 0b11cf858c..b2b4244ac3 100644 --- a/extensions/raycast/src/utils.ts +++ b/extensions/raycast/src/utils.ts @@ -19,12 +19,17 @@ export function buildDeepLink(action: DeepLinkAction): string { /** * Open a Cap deeplink and show a HUD message. + * Shows an error HUD if Cap is not running or the deeplink fails. */ export async function triggerDeepLink( action: DeepLinkAction, hudMessage: string, ): Promise { const url = buildDeepLink(action); - await open(url); - await showHUD(hudMessage); + try { + await open(url); + await showHUD(hudMessage); + } catch { + await showHUD("❌ Failed — is Cap running?"); + } }