diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index fce75b4a84..950933f176 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -26,6 +26,15 @@ pub enum DeepLinkAction { mode: RecordingMode, }, StopRecording, + PauseRecording, + ResumeRecording, + TogglePause, + SetCamera { + camera: Option, + }, + SetMicrophone { + mic_label: Option, + }, OpenEditor { project_path: PathBuf, }, @@ -146,6 +155,23 @@ 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::TogglePause => { + crate::recording::toggle_pause_recording(app.clone(), app.state()).await + } + DeepLinkAction::SetCamera { camera } => { + let state = app.state::>(); + crate::set_camera_input(app.clone(), state, camera, None).await + } + DeepLinkAction::SetMicrophone { mic_label } => { + let state = app.state::>(); + crate::set_mic_input(state, mic_label).await + } DeepLinkAction::OpenEditor { project_path } => { crate::open_project_from_path(Path::new(&project_path), app.clone()) } diff --git a/apps/raycast-extension/README.md b/apps/raycast-extension/README.md new file mode 100644 index 0000000000..276a9ec568 --- /dev/null +++ b/apps/raycast-extension/README.md @@ -0,0 +1,55 @@ +# Cap Raycast Extension + +Control [Cap](https://cap.so) screen recordings directly from [Raycast](https://raycast.com). + +## Commands + +| Command | Description | +|---------|-------------| +| **Start Recording** | Start a new screen recording with default settings | +| **Stop Recording** | Stop the current recording | +| **Toggle Pause** | Pause or resume the current recording | +| **Open Settings** | Open Cap settings window | +| **Recent Recordings** | Browse and open recent recordings | + +## How It Works + +This extension communicates with Cap using deeplinks (`cap-desktop://action?value=...`). Cap must be running for commands to work. + +## Deeplinks + +Cap supports the following deeplink actions: + +- `start_recording` — Start recording with capture mode, camera, mic, and mode options +- `stop_recording` — Stop the current recording +- `pause_recording` — Pause the current recording +- `resume_recording` — Resume a paused recording +- `toggle_pause` — Toggle pause/resume +- `set_camera` — Switch the active camera +- `set_microphone` — Switch the active microphone +- `open_editor` — Open a recording in the editor +- `open_settings` — Open the settings window + +### URL Format + +``` +cap-desktop://action?value= +``` + +Unit actions (no parameters): +``` +cap-desktop://action?value=%22stop_recording%22 +``` + +Actions with parameters (URL-encode the JSON): +``` +cap-desktop://action?value=%7B%22start_recording%22%3A%7B%22capture_mode%22%3A%7B%22screen%22%3A%22Main%20Display%22%7D%2C%22camera%22%3Anull%2C%22mic_label%22%3Anull%2C%22capture_system_audio%22%3Afalse%2C%22mode%22%3A%22studio%22%7D%7D +``` + +## Development + +```bash +cd apps/raycast-extension +pnpm install +pnpm dev +``` diff --git a/apps/raycast-extension/assets/extension-icon.png b/apps/raycast-extension/assets/extension-icon.png new file mode 100644 index 0000000000..b1ac1ef7d8 Binary files /dev/null and b/apps/raycast-extension/assets/extension-icon.png differ diff --git a/apps/raycast-extension/package.json b/apps/raycast-extension/package.json new file mode 100644 index 0000000000..f7e7102b3f --- /dev/null +++ b/apps/raycast-extension/package.json @@ -0,0 +1,61 @@ +{ + "name": "cap", + "title": "Cap", + "description": "Control Cap screen recordings from Raycast", + "icon": "extension-icon.png", + "author": "capsoftware", + "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 Cap recording", + "mode": "no-view" + }, + { + "name": "toggle-pause", + "title": "Toggle Pause", + "subtitle": "Cap", + "description": "Pause or resume the current Cap recording", + "mode": "no-view" + }, + { + "name": "open-settings", + "title": "Open Settings", + "subtitle": "Cap", + "description": "Open Cap settings", + "mode": "no-view" + }, + { + "name": "list-recent-recordings", + "title": "Recent Recordings", + "subtitle": "Cap", + "description": "Browse recent Cap recordings", + "mode": "view" + } + ], + "dependencies": { + "@raycast/api": "^1.93.2", + "@raycast/utils": "^1.19.1" + }, + "devDependencies": { + "@raycast/eslint-config": "^1.0.11", + "typescript": "^5.8.3", + "@types/node": "^22.13.4", + "eslint": "^8.57.0" + }, + "scripts": { + "build": "ray build", + "dev": "ray develop", + "fix-lint": "ray lint --fix", + "lint": "ray lint" + } +} diff --git a/apps/raycast-extension/src/deeplink.ts b/apps/raycast-extension/src/deeplink.ts new file mode 100644 index 0000000000..1800a72950 --- /dev/null +++ b/apps/raycast-extension/src/deeplink.ts @@ -0,0 +1,46 @@ +import { open, showToast, Toast } from "@raycast/api"; + +const SCHEME = "cap-desktop"; + +type DeepLinkAction = + | "stop_recording" + | "pause_recording" + | "resume_recording" + | "toggle_pause" + | { + start_recording: { + capture_mode: { screen: string } | { window: string }; + camera: { DeviceID: string } | { ModelID: string } | null; + mic_label: string | null; + capture_system_audio: boolean; + mode: "studio" | "instant" | "screenshot"; + }; + } + | { set_camera: { camera: { DeviceID: string } | { ModelID: string } | null } } + | { set_microphone: { mic_label: string | null } } + | { open_editor: { project_path: string } } + | { open_settings: { page: string | null } }; + +function buildDeeplink(action: DeepLinkAction): string { + const json = JSON.stringify(action); + return `${SCHEME}://action?value=${encodeURIComponent(json)}`; +} + +export async function openDeeplink( + action: DeepLinkAction, + successMessage?: string, +): Promise { + const url = buildDeeplink(action); + try { + await open(url); + if (successMessage) { + await showToast({ style: Toast.Style.Success, title: successMessage }); + } + } catch (error) { + await showToast({ + style: Toast.Style.Failure, + title: "Failed to communicate with Cap", + message: error instanceof Error ? error.message : String(error), + }); + } +} diff --git a/apps/raycast-extension/src/list-recent-recordings.tsx b/apps/raycast-extension/src/list-recent-recordings.tsx new file mode 100644 index 0000000000..351c74a243 --- /dev/null +++ b/apps/raycast-extension/src/list-recent-recordings.tsx @@ -0,0 +1,78 @@ +import { Action, ActionPanel, List } from "@raycast/api"; +import { usePromise } from "@raycast/utils"; +import { readdirSync, statSync } from "fs"; +import { join } from "path"; +import { homedir } from "os"; +import { openDeeplink } from "./deeplink"; + +function getRecordingsDir(): string { + return join( + homedir(), + "Library", + "Application Support", + "so.cap.desktop", + "recordings", + ); +} + + +interface Recording { + name: string; + path: string; + modifiedAt: Date; +} + +function listRecordings(): Recording[] { + const dir = getRecordingsDir(); + try { + return readdirSync(dir) + .filter((f) => f.endsWith(".cap")) + .map((f) => { + const fullPath = join(dir, f); + const stat = statSync(fullPath); + return { + name: f.replace(/\.cap$/, ""), + path: fullPath, + modifiedAt: stat.mtime, + }; + }) + .sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime()); + } catch { + return []; + } +} + +export default function RecentRecordings() { + const { data: recordings, isLoading } = usePromise(async () => listRecordings()); + + return ( + + {recordings?.length === 0 ? ( + + ) : ( + recordings?.map((rec) => ( + + + openDeeplink({ open_editor: { project_path: rec.path } }) + } + /> + + + } + /> + )) + )} + + ); +} diff --git a/apps/raycast-extension/src/open-settings.ts b/apps/raycast-extension/src/open-settings.ts new file mode 100644 index 0000000000..d7fd61f69d --- /dev/null +++ b/apps/raycast-extension/src/open-settings.ts @@ -0,0 +1,7 @@ +import { closeMainWindow } from "@raycast/api"; +import { openDeeplink } from "./deeplink"; + +export default async function openSettings() { + await openDeeplink({ open_settings: { page: null } }, "Opening settings"); + await closeMainWindow(); +} diff --git a/apps/raycast-extension/src/start-recording.ts b/apps/raycast-extension/src/start-recording.ts new file mode 100644 index 0000000000..60bfe8fd29 --- /dev/null +++ b/apps/raycast-extension/src/start-recording.ts @@ -0,0 +1,18 @@ +import { closeMainWindow } from "@raycast/api"; +import { openDeeplink } from "./deeplink"; + +export default async function startRecording() { + await openDeeplink( + { + start_recording: { + capture_mode: { screen: "Main Display" }, + camera: null, + mic_label: null, + capture_system_audio: false, + mode: "studio", + }, + }, + "Recording started", + ); + await closeMainWindow(); +} diff --git a/apps/raycast-extension/src/stop-recording.ts b/apps/raycast-extension/src/stop-recording.ts new file mode 100644 index 0000000000..4548f36ea0 --- /dev/null +++ b/apps/raycast-extension/src/stop-recording.ts @@ -0,0 +1,7 @@ +import { closeMainWindow } from "@raycast/api"; +import { openDeeplink } from "./deeplink"; + +export default async function stopRecording() { + await openDeeplink("stop_recording", "Recording stopped"); + await closeMainWindow(); +} diff --git a/apps/raycast-extension/src/toggle-pause.ts b/apps/raycast-extension/src/toggle-pause.ts new file mode 100644 index 0000000000..6f9854840d --- /dev/null +++ b/apps/raycast-extension/src/toggle-pause.ts @@ -0,0 +1,7 @@ +import { closeMainWindow } from "@raycast/api"; +import { openDeeplink } from "./deeplink"; + +export default async function togglePause() { + await openDeeplink("toggle_pause", "Toggled pause"); + await closeMainWindow(); +} diff --git a/apps/raycast-extension/tsconfig.json b/apps/raycast-extension/tsconfig.json new file mode 100644 index 0000000000..0bc6b27ee3 --- /dev/null +++ b/apps/raycast-extension/tsconfig.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://www.raycast.com/schemas/extension.json", + "compilerOptions": { + "strict": true, + "jsx": "react-jsx", + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"] +}