From 4eead891485135aaa4782521179a0acf667130b8 Mon Sep 17 00:00:00 2001 From: Ojaswee Upadhyay Date: Fri, 10 Apr 2026 21:20:43 +0530 Subject: [PATCH] feat: implement deep links for Pause/Resume/Mic/Camera and Raycast extension --- .../desktop/src-tauri/src/deeplink_actions.rs | 20 +++++ apps/raycast/package.json | 79 +++++++++++++++++++ apps/raycast/src/pause.ts | 5 ++ apps/raycast/src/resume.ts | 5 ++ apps/raycast/src/start.ts | 11 +++ apps/raycast/src/stop.ts | 5 ++ apps/raycast/src/switch-camera.ts | 12 +++ apps/raycast/src/switch-mic.ts | 8 ++ apps/raycast/src/utils.ts | 19 +++++ apps/raycast/tsconfig.json | 14 ++++ 10 files changed, 178 insertions(+) create mode 100644 apps/raycast/package.json create mode 100644 apps/raycast/src/pause.ts create mode 100644 apps/raycast/src/resume.ts create mode 100644 apps/raycast/src/start.ts create mode 100644 apps/raycast/src/stop.ts create mode 100644 apps/raycast/src/switch-camera.ts create mode 100644 apps/raycast/src/switch-mic.ts create mode 100644 apps/raycast/src/utils.ts create mode 100644 apps/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..7ac4e4fa9e 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -26,6 +26,14 @@ pub enum DeepLinkAction { mode: RecordingMode, }, StopRecording, + PauseRecording, + ResumeRecording, + SwitchMicrophone { + mic_label: Option, + }, + SwitchCamera { + camera: Option, + }, OpenEditor { project_path: PathBuf, }, @@ -147,6 +155,18 @@ 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::SwitchMicrophone { mic_label } => { + crate::set_mic_input(app.state(), mic_label).await + } + DeepLinkAction::SwitchCamera { camera } => { + crate::set_camera_input(app.clone(), app.state(), camera, None).await + } DeepLinkAction::OpenEditor { project_path } => { crate::open_project_from_path(Path::new(&project_path), app.clone()) } diff --git a/apps/raycast/package.json b/apps/raycast/package.json new file mode 100644 index 0000000000..24b8f8c279 --- /dev/null +++ b/apps/raycast/package.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://www.raycast.com/downloads/extension.schema.json", + "name": "cap-controls", + "title": "Cap Controls", + "description": "Control the Cap screen recording app directly from Raycast", + "icon": "command-icon.png", + "author": "Ojas2095", + "license": "MIT", + "commands": [ + { + "name": "start", + "title": "Start Recording", + "description": "Start a Cap recording", + "mode": "no-view" + }, + { + "name": "stop", + "title": "Stop Recording", + "description": "Stops the current Cap recording", + "mode": "no-view" + }, + { + "name": "pause", + "title": "Pause Recording", + "description": "Pauses the current Cap recording", + "mode": "no-view" + }, + { + "name": "resume", + "title": "Resume Recording", + "description": "Resumes the paused Cap recording", + "mode": "no-view" + }, + { + "name": "switch-mic", + "title": "Switch Microphone", + "description": "Switch Cap's recording microphone", + "mode": "no-view", + "arguments": [ + { + "name": "micLabel", + "type": "text", + "placeholder": "Microphone Name (or 'none')", + "required": true + } + ] + }, + { + "name": "switch-camera", + "title": "Switch Camera", + "description": "Switch Cap's recording camera", + "mode": "no-view", + "arguments": [ + { + "name": "cameraId", + "type": "text", + "placeholder": "Camera ID (or 'none')", + "required": true + } + ] + } + ], + "dependencies": { + "@raycast/api": "^1.65.0" + }, + "devDependencies": { + "@raycast/eslint-config": "^1.0.6", + "@types/node": "20.8.10", + "@types/react": "18.2.27", + "eslint": "^8.51.0", + "prettier": "^3.0.3", + "typescript": "^5.2.2" + }, + "scripts": { + "build": "ray build -e dist", + "dev": "ray develop", + "publish": "npx @raycast/api@latest publish" + } +} diff --git a/apps/raycast/src/pause.ts b/apps/raycast/src/pause.ts new file mode 100644 index 0000000000..56397c5baa --- /dev/null +++ b/apps/raycast/src/pause.ts @@ -0,0 +1,5 @@ +import { sendCapCommand } from "./utils"; + +export default async function Command() { + await sendCapCommand("pause_recording"); +} diff --git a/apps/raycast/src/resume.ts b/apps/raycast/src/resume.ts new file mode 100644 index 0000000000..0fd6f5e62a --- /dev/null +++ b/apps/raycast/src/resume.ts @@ -0,0 +1,5 @@ +import { sendCapCommand } from "./utils"; + +export default async function Command() { + await sendCapCommand("resume_recording"); +} diff --git a/apps/raycast/src/start.ts b/apps/raycast/src/start.ts new file mode 100644 index 0000000000..c375a75db3 --- /dev/null +++ b/apps/raycast/src/start.ts @@ -0,0 +1,11 @@ +import { sendCapCommand } from "./utils"; + +export default async function Command() { + await sendCapCommand("start_recording", { + capture_mode: { screen: "Built-in Retina Display" }, // Fallback default or need to handle better + camera: null, + mic_label: null, + capture_system_audio: true, + mode: "Screen" + }); +} diff --git a/apps/raycast/src/stop.ts b/apps/raycast/src/stop.ts new file mode 100644 index 0000000000..782f3e1ccc --- /dev/null +++ b/apps/raycast/src/stop.ts @@ -0,0 +1,5 @@ +import { sendCapCommand } from "./utils"; + +export default async function Command() { + await sendCapCommand("stop_recording"); +} diff --git a/apps/raycast/src/switch-camera.ts b/apps/raycast/src/switch-camera.ts new file mode 100644 index 0000000000..76a9eb7b3c --- /dev/null +++ b/apps/raycast/src/switch-camera.ts @@ -0,0 +1,12 @@ +import { LaunchProps } from "@raycast/api"; +import { sendCapCommand } from "./utils"; + +export default async function Command(props: LaunchProps<{ arguments: { cameraId: string } }>) { + // DeviceOrModelID has a few formats depending on Cap's implementation + // Passing it blindly string is difficult because Rust expects the exact Enum layout. + // We will assume "camera" in JSON is null to disable, or an object if specific. + // But for simple use, setting to an exact string from args. + await sendCapCommand("switch_camera", { + camera: props.arguments.cameraId === "none" ? null : props.arguments.cameraId + }); +} diff --git a/apps/raycast/src/switch-mic.ts b/apps/raycast/src/switch-mic.ts new file mode 100644 index 0000000000..d47117f553 --- /dev/null +++ b/apps/raycast/src/switch-mic.ts @@ -0,0 +1,8 @@ +import { LaunchProps } from "@raycast/api"; +import { sendCapCommand } from "./utils"; + +export default async function Command(props: LaunchProps<{ arguments: { micLabel: string } }>) { + await sendCapCommand("switch_microphone", { + mic_label: props.arguments.micLabel === "none" ? null : props.arguments.micLabel + }); +} diff --git a/apps/raycast/src/utils.ts b/apps/raycast/src/utils.ts new file mode 100644 index 0000000000..9667c9900d --- /dev/null +++ b/apps/raycast/src/utils.ts @@ -0,0 +1,19 @@ +import { open, showHUD } from "@raycast/api"; + +export async function sendCapCommand(action: string, objectValue?: Record) { + // If no object value is passed, it represents a simple unit enum variant. + // Rust expects: "stop_recording" + // If an object is passed, Rust expects: {"switch_microphone": {"mic_label": "mic"}} + const valuePayload = objectValue + ? JSON.stringify({ [action]: objectValue }) + : `"${action}"`; + + const url = `cap://action?value=${encodeURIComponent(valuePayload)}`; + try { + await open(url); + await showHUD(`Cap: Action Executed`); + } catch (error) { + await showHUD("Failed to communicate with Cap. Is it installed?"); + console.error(error); + } +} diff --git a/apps/raycast/tsconfig.json b/apps/raycast/tsconfig.json new file mode 100644 index 0000000000..440256b574 --- /dev/null +++ b/apps/raycast/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "ES2021", + "jsx": "react", + "lib": ["ES2021", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"] +}