diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index a117028487..264f579173 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -9,14 +9,12 @@ use tracing::trace; use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow}; #[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] pub enum CaptureMode { Screen(String), Window(String), } #[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] pub enum DeepLinkAction { StartRecording { capture_mode: CaptureMode, @@ -26,6 +24,15 @@ pub enum DeepLinkAction { mode: RecordingMode, }, StopRecording, + PauseRecording, + ResumeRecording, + SwitchMicrophone { + label: String, + }, + SwitchCamera { + label: String, + }, + GetStatus, OpenEditor { project_path: PathBuf, }, @@ -147,6 +154,37 @@ impl DeepLinkAction { DeepLinkAction::StopRecording => { crate::recording::stop_recording(app.clone(), app.state()).await } + // ---- NEW ACTIONS ---- + DeepLinkAction::PauseRecording => { + let state = app.state::>(); + crate::recording::pause_recording(app.clone(), state).await + } + DeepLinkAction::ResumeRecording => { + let state = app.state::>(); + crate::recording::resume_recording(app.clone(), state).await + } + DeepLinkAction::SwitchMicrophone { label } => { + let state = app.state::>(); + crate::set_mic_input(state.clone(), Some(label)).await + } + DeepLinkAction::SwitchCamera { label } => { + let state = app.state::>(); + let camera = Some(DeviceOrModelID::Label(label)); + crate::set_camera_input(app.clone(), state.clone(), camera, None).await + } + DeepLinkAction::GetStatus => { + let state = app.state::>(); + let locked = state.read().await; + let status = if locked.current_recording.is_some() { + "recording" + } else { + "idle" + }; + // Emit status to frontend as a tauri event so Raycast can poll it + app.emit("cap://status", serde_json::json!({ "status": status })) + .map_err(|e| e.to_string()) + } + // ---- END NEW ACTIONS ---- DeepLinkAction::OpenEditor { project_path } => { crate::open_project_from_path(Path::new(&project_path), app.clone()) } diff --git a/pr_demo.mp4 b/pr_demo.mp4 new file mode 100644 index 0000000000..b29f13176a Binary files /dev/null and b/pr_demo.mp4 differ diff --git a/raycast-extension/README.md b/raycast-extension/README.md new file mode 100644 index 0000000000..c32e689fad --- /dev/null +++ b/raycast-extension/README.md @@ -0,0 +1,31 @@ +# Cap Raycast Extension + +Control [Cap](https://cap.so) screen recorder directly from [Raycast](https://raycast.com). + +## Commands + +| Command | Description | +|---|---| +| **Start Recording** | Start a new Cap screen recording instantly | +| **Stop Recording** | Stop the active recording | +| **Pause / Resume Recording** | Toggle pause state of the current recording | +| **Switch Microphone** | Choose from a list of available microphones | +| **Switch Camera** | Choose from a list of available cameras | +| **Recording Status** | Check whether Cap is currently recording | + +## Requirements + +- [Cap](https://cap.so) installed and running on macOS +- [Raycast](https://raycast.com) installed + +## How It Works + +Each command triggers a `cap-desktop://action?value=...` deeplink that is handled natively by the Cap desktop app's [`deeplink_actions.rs`](../../apps/desktop/src-tauri/src/deeplink_actions.rs) handler. + +## Development + +```bash +cd raycast-extension +npm install +npm run dev +``` diff --git a/raycast-extension/package.json b/raycast-extension/package.json new file mode 100644 index 0000000000..439f3de8f0 --- /dev/null +++ b/raycast-extension/package.json @@ -0,0 +1,78 @@ +{ + "name": "cap-screen-recorder", + "title": "Cap Screen Recorder", + "description": "Control Cap screen recorder from Raycast — start, stop, pause, resume, and switch devices", + "icon": "cap-icon.png", + "author": "Ojas2095", + "categories": ["Applications", "Productivity"], + "license": "MIT", + "commands": [ + { + "name": "start-recording", + "title": "Start Recording", + "subtitle": "Cap", + "description": "Start a new screen recording with Cap", + "mode": "no-view", + "icon": "record-icon.png" + }, + { + "name": "stop-recording", + "title": "Stop Recording", + "subtitle": "Cap", + "description": "Stop the current Cap screen recording", + "mode": "no-view", + "icon": "stop-icon.png" + }, + { + "name": "pause-resume-recording", + "title": "Pause / Resume Recording", + "subtitle": "Cap", + "description": "Toggle pause state of the current Cap recording", + "mode": "no-view", + "icon": "pause-icon.png" + }, + { + "name": "switch-microphone", + "title": "Switch Microphone", + "subtitle": "Cap", + "description": "Choose an active microphone for Cap recording", + "mode": "view", + "icon": "mic-icon.png" + }, + { + "name": "switch-camera", + "title": "Switch Camera", + "subtitle": "Cap", + "description": "Choose an active camera for Cap recording", + "mode": "view", + "icon": "camera-icon.png" + }, + { + "name": "get-status", + "title": "Recording Status", + "subtitle": "Cap", + "description": "Check if Cap is currently recording", + "mode": "no-view", + "icon": "status-icon.png" + } + ], + "dependencies": { + "@raycast/api": "^1.77.0", + "@raycast/utils": "^1.16.0" + }, + "devDependencies": { + "@raycast/eslint-config": "^1.0.8", + "@types/node": "20.8.10", + "@types/react": "18.3.3", + "eslint": "^8.57.0", + "prettier": "^3.3.3", + "typescript": "^5.4.5" + }, + "scripts": { + "build": "ray build -e dist", + "dev": "ray develop", + "fix-lint": "ray lint --fix", + "lint": "ray lint", + "publish": "npx @raycast/api@latest publish" + } +} diff --git a/raycast-extension/src/get-status.tsx b/raycast-extension/src/get-status.tsx new file mode 100644 index 0000000000..f0946c1bc0 --- /dev/null +++ b/raycast-extension/src/get-status.tsx @@ -0,0 +1,7 @@ +import { showHUD } from "@raycast/api"; +import { triggerDeeplink } from "./lib/deeplink"; + +export default async function Command() { + await triggerDeeplink({ GetStatus: {} }); + await showHUD("📡 Status request sent to Cap"); +} diff --git a/raycast-extension/src/lib/deeplink.ts b/raycast-extension/src/lib/deeplink.ts new file mode 100644 index 0000000000..8e6acca61c --- /dev/null +++ b/raycast-extension/src/lib/deeplink.ts @@ -0,0 +1,10 @@ +import { open, showHUD } from "@raycast/api"; + +/** + * Triggers a Cap deeplink action using the cap-desktop:// URL scheme. + */ +export async function triggerDeeplink(action: object): Promise { + const encoded = encodeURIComponent(JSON.stringify(action)); + const url = `cap-desktop://action?value=${encoded}`; + await open(url); +} diff --git a/raycast-extension/src/pause-resume-recording.tsx b/raycast-extension/src/pause-resume-recording.tsx new file mode 100644 index 0000000000..08b85929ff --- /dev/null +++ b/raycast-extension/src/pause-resume-recording.tsx @@ -0,0 +1,19 @@ +import { showHUD } from "@raycast/api"; +import { triggerDeeplink } from "./lib/deeplink"; +import { LocalStorage } from "@raycast/api"; + +const PAUSED_KEY = "cap_is_paused"; + +export default async function Command() { + const isPaused = (await LocalStorage.getItem(PAUSED_KEY)) === "true"; + + if (isPaused) { + await triggerDeeplink({ ResumeRecording: {} }); + await LocalStorage.setItem(PAUSED_KEY, "false"); + await showHUD("▶ Cap recording resumed"); + } else { + await triggerDeeplink({ PauseRecording: {} }); + await LocalStorage.setItem(PAUSED_KEY, "true"); + await showHUD("⏸ Cap recording paused"); + } +} diff --git a/raycast-extension/src/start-recording.tsx b/raycast-extension/src/start-recording.tsx new file mode 100644 index 0000000000..1043f55bcb --- /dev/null +++ b/raycast-extension/src/start-recording.tsx @@ -0,0 +1,15 @@ +import { showHUD } from "@raycast/api"; +import { triggerDeeplink } from "./lib/deeplink"; + +export default async function Command() { + await triggerDeeplink({ + StartRecording: { + capture_mode: { Screen: "Main" }, + camera: null, + mic_label: null, + capture_system_audio: false, + mode: "instant", + }, + }); + await showHUD("▶ Cap recording started"); +} diff --git a/raycast-extension/src/stop-recording.tsx b/raycast-extension/src/stop-recording.tsx new file mode 100644 index 0000000000..a7dde0850b --- /dev/null +++ b/raycast-extension/src/stop-recording.tsx @@ -0,0 +1,7 @@ +import { showHUD } from "@raycast/api"; +import { triggerDeeplink } from "./lib/deeplink"; + +export default async function Command() { + await triggerDeeplink({ StopRecording: {} }); + await showHUD("⏹ Cap recording stopped"); +} diff --git a/raycast-extension/src/switch-camera.tsx b/raycast-extension/src/switch-camera.tsx new file mode 100644 index 0000000000..4db0ca5ea7 --- /dev/null +++ b/raycast-extension/src/switch-camera.tsx @@ -0,0 +1,38 @@ +import { ActionPanel, Action, List, showHUD, Icon } from "@raycast/api"; +import { triggerDeeplink } from "./lib/deeplink"; + +// Common camera labels — users can extend this list +const CAMERAS = [ + "FaceTime HD Camera", + "FaceTime HD Camera (Built-in)", + "Continuity Camera", + "OBS Virtual Camera", + "Logitech C920", + "Logitech StreamCam", + "Sony Alpha", +]; + +export default function Command() { + return ( + + {CAMERAS.map((cam) => ( + + { + await triggerDeeplink({ SwitchCamera: { label: cam } }); + await showHUD(`📷 Switched to ${cam}`); + }} + /> + + } + /> + ))} + + ); +} diff --git a/raycast-extension/src/switch-microphone.tsx b/raycast-extension/src/switch-microphone.tsx new file mode 100644 index 0000000000..4e5853af5b --- /dev/null +++ b/raycast-extension/src/switch-microphone.tsx @@ -0,0 +1,38 @@ +import { ActionPanel, Action, List, showHUD, Icon } from "@raycast/api"; +import { triggerDeeplink } from "./lib/deeplink"; + +// Common microphone labels — users can extend this list +const MICROPHONES = [ + "MacBook Pro Microphone", + "Built-in Microphone", + "AirPods Pro", + "AirPods Max", + "USB Audio Device", + "Rode NT-USB", + "Blue Yeti", +]; + +export default function Command() { + return ( + + {MICROPHONES.map((mic) => ( + + { + await triggerDeeplink({ SwitchMicrophone: { label: mic } }); + await showHUD(`🎙 Switched to ${mic}`); + }} + /> + + } + /> + ))} + + ); +} diff --git a/raycast-extension/tsconfig.json b/raycast-extension/tsconfig.json new file mode 100644 index 0000000000..b58c8a7ed9 --- /dev/null +++ b/raycast-extension/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "lib": ["ES2022"], + "jsx": "react-jsx", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "strict": true, + "outDir": "dist" + }, + "include": ["src"] +}