From 16012c282b4c33ae9b134c0ec5a20befda5fd7f7 Mon Sep 17 00:00:00 2001 From: pon024587 Date: Sat, 16 May 2026 08:15:30 +0900 Subject: [PATCH 1/2] feat: add deeplink support for recording controls and raycast extension --- .../desktop/src-tauri/src/deeplink_actions.rs | 53 ++++++++++++ apps/raycast-extension/package.json | 81 +++++++++++++++++++ apps/raycast-extension/src/pause-recording.ts | 5 ++ .../raycast-extension/src/resume-recording.ts | 5 ++ apps/raycast-extension/src/start-recording.ts | 5 ++ apps/raycast-extension/src/stop-recording.ts | 5 ++ apps/raycast-extension/src/switch-camera.ts | 9 +++ apps/raycast-extension/src/switch-mic.ts | 9 +++ apps/raycast-extension/src/utils.ts | 7 ++ 9 files changed, 179 insertions(+) create mode 100644 apps/raycast-extension/package.json create mode 100644 apps/raycast-extension/src/pause-recording.ts create mode 100644 apps/raycast-extension/src/resume-recording.ts create mode 100644 apps/raycast-extension/src/start-recording.ts create mode 100644 apps/raycast-extension/src/stop-recording.ts create mode 100644 apps/raycast-extension/src/switch-camera.ts create mode 100644 apps/raycast-extension/src/switch-mic.ts create mode 100644 apps/raycast-extension/src/utils.ts diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index a1170284877..71078fd6eee 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -18,6 +18,7 @@ pub enum CaptureMode { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum DeepLinkAction { + StartDefaultRecording, StartRecording { capture_mode: CaptureMode, camera: Option, @@ -26,6 +27,14 @@ pub enum DeepLinkAction { mode: RecordingMode, }, StopRecording, + PauseRecording, + ResumeRecording, + SwitchMic { + mic_label: String, + }, + SwitchCamera { + camera_id: DeviceOrModelID, + }, OpenEditor { project_path: PathBuf, }, @@ -108,6 +117,26 @@ impl TryFrom<&Url> for DeepLinkAction { impl DeepLinkAction { pub async fn execute(self, app: &AppHandle) -> Result<(), String> { match self { + DeepLinkAction::StartDefaultRecording => { + let settings = crate::recording_settings::RecordingSettingsStore::get(app)? + .unwrap_or_default(); + + let target = settings.target.ok_or("No capture target selected")?; + let mode = settings.mode.unwrap_or(RecordingMode::Studio); + + crate::recording::start_recording( + app.clone(), + app.state(), + crate::recording::StartRecordingInputs { + capture_target: target, + capture_system_audio: Some(settings.system_audio), + mode, + organization_id: settings.organization_id, + }, + ) + .await + .map(|_| ()) + } DeepLinkAction::StartRecording { capture_mode, camera, @@ -147,6 +176,30 @@ impl DeepLinkAction { DeepLinkAction::StopRecording => { crate::recording::stop_recording(app.clone(), app.state()).await } + DeepLinkAction::PauseRecording => { + let state = app.state::>(); + let mut app_state = state.write().await; + if let Some(recording) = app_state.current_recording_mut() { + recording.pause().await.map_err(|e| e.to_string())?; + } + Ok(()) + } + DeepLinkAction::ResumeRecording => { + let state = app.state::>(); + let mut app_state = state.write().await; + if let Some(recording) = app_state.current_recording_mut() { + recording.resume().await.map_err(|e| e.to_string())?; + } + Ok(()) + } + DeepLinkAction::SwitchMic { mic_label } => { + let state = app.state::>(); + crate::set_mic_input(state.clone(), Some(mic_label)).await + } + DeepLinkAction::SwitchCamera { camera_id } => { + let state = app.state::>(); + crate::set_camera_input(app.clone(), state.clone(), Some(camera_id), None).await + } DeepLinkAction::OpenEditor { project_path } => { crate::open_project_from_path(Path::new(&project_path), app.clone()) } diff --git a/apps/raycast-extension/package.json b/apps/raycast-extension/package.json new file mode 100644 index 00000000000..0ca1f0104b7 --- /dev/null +++ b/apps/raycast-extension/package.json @@ -0,0 +1,81 @@ +{ + "name": "cap-raycast-extension", + "title": "Cap", + "description": "Control Cap recording and settings via Raycast.", + "icon": "command-icon.png", + "author": "Cap Software", + "categories": [ + "Productivity" + ], + "license": "MIT", + "commands": [ + { + "name": "start-recording", + "title": "Start Recording", + "description": "Start a new recording in Cap.", + "mode": "no-view" + }, + { + "name": "stop-recording", + "title": "Stop Recording", + "description": "Stop the current recording in Cap.", + "mode": "no-view" + }, + { + "name": "pause-recording", + "title": "Pause Recording", + "description": "Pause the current recording in Cap.", + "mode": "no-view" + }, + { + "name": "resume-recording", + "title": "Resume Recording", + "description": "Resume the current recording in Cap.", + "mode": "no-view" + }, + { + "name": "switch-mic", + "title": "Switch Microphone", + "description": "Switch to a specific microphone by name.", + "mode": "no-view", + "arguments": [ + { + "name": "mic_label", + "placeholder": "Microphone Name", + "type": "text", + "required": true + } + ] + }, + { + "name": "switch-camera", + "title": "Switch Camera", + "description": "Switch to a specific camera by ID.", + "mode": "no-view", + "arguments": [ + { + "name": "camera_id", + "placeholder": "Camera ID", + "type": "text", + "required": true + } + ] + } + ], + "dependencies": { + "@raycast/api": "^1.72.1" + }, + "devDependencies": { + "@raycast/utils": "^1.14.2", + "@types/node": "20.8.10", + "@types/react": "18.2.27", + "typescript": "^5.2.2" + }, + "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/apps/raycast-extension/src/pause-recording.ts b/apps/raycast-extension/src/pause-recording.ts new file mode 100644 index 00000000000..e44ea6292bd --- /dev/null +++ b/apps/raycast-extension/src/pause-recording.ts @@ -0,0 +1,5 @@ +import { executeCapAction } from "./utils"; + +export default async function Command() { + await executeCapAction("pause_recording"); +} diff --git a/apps/raycast-extension/src/resume-recording.ts b/apps/raycast-extension/src/resume-recording.ts new file mode 100644 index 00000000000..312ea755ca2 --- /dev/null +++ b/apps/raycast-extension/src/resume-recording.ts @@ -0,0 +1,5 @@ +import { executeCapAction } from "./utils"; + +export default async function Command() { + await executeCapAction("resume_recording"); +} diff --git a/apps/raycast-extension/src/start-recording.ts b/apps/raycast-extension/src/start-recording.ts new file mode 100644 index 00000000000..1e5998bc469 --- /dev/null +++ b/apps/raycast-extension/src/start-recording.ts @@ -0,0 +1,5 @@ +import { executeCapAction } from "./utils"; + +export default async function Command() { + await executeCapAction("start_default_recording"); +} diff --git a/apps/raycast-extension/src/stop-recording.ts b/apps/raycast-extension/src/stop-recording.ts new file mode 100644 index 00000000000..c335b76ca23 --- /dev/null +++ b/apps/raycast-extension/src/stop-recording.ts @@ -0,0 +1,5 @@ +import { executeCapAction } from "./utils"; + +export default async function Command() { + await executeCapAction("stop_recording"); +} diff --git a/apps/raycast-extension/src/switch-camera.ts b/apps/raycast-extension/src/switch-camera.ts new file mode 100644 index 00000000000..8c4f10a4ba9 --- /dev/null +++ b/apps/raycast-extension/src/switch-camera.ts @@ -0,0 +1,9 @@ +import { executeCapAction } from "./utils"; + +export default async function Command(props: { arguments: { camera_id: string } }) { + await executeCapAction({ + switch_camera: { + camera_id: { DeviceID: props.arguments.camera_id }, + }, + }); +} diff --git a/apps/raycast-extension/src/switch-mic.ts b/apps/raycast-extension/src/switch-mic.ts new file mode 100644 index 00000000000..df699bea298 --- /dev/null +++ b/apps/raycast-extension/src/switch-mic.ts @@ -0,0 +1,9 @@ +import { executeCapAction } from "./utils"; + +export default async function Command(props: { arguments: { mic_label: string } }) { + await executeCapAction({ + switch_mic: { + mic_label: props.arguments.mic_label, + }, + }); +} diff --git a/apps/raycast-extension/src/utils.ts b/apps/raycast-extension/src/utils.ts new file mode 100644 index 00000000000..f4989dd7e7a --- /dev/null +++ b/apps/raycast-extension/src/utils.ts @@ -0,0 +1,7 @@ +import { open } from "@raycast/api"; + +export async function executeCapAction(action: any) { + const json = JSON.stringify(action); + const url = `cap://action?value=${encodeURIComponent(json)}`; + await open(url); +} From 18c1091a85c31873a21532c41a75101329bb710e Mon Sep 17 00:00:00 2001 From: pon024587 Date: Sat, 16 May 2026 08:34:29 +0900 Subject: [PATCH 2/2] fix: address PR review comments (security, type mismatch, missing events, and ID types) --- apps/desktop/src-tauri/src/deeplink_actions.rs | 8 ++++++-- apps/raycast-extension/package.json | 10 ++++++++-- apps/raycast-extension/src/switch-camera.ts | 7 +++++-- apps/raycast-extension/src/utils.ts | 7 ++++++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 71078fd6eee..2f5a93b306a 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -6,7 +6,9 @@ use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager, Url}; use tracing::trace; -use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow}; +use crate::{ + App, ArcLock, RecordingEvent, recording::StartRecordingInputs, windows::ShowCapWindow, +}; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -129,7 +131,7 @@ impl DeepLinkAction { app.state(), crate::recording::StartRecordingInputs { capture_target: target, - capture_system_audio: Some(settings.system_audio), + capture_system_audio: settings.system_audio, mode, organization_id: settings.organization_id, }, @@ -181,6 +183,7 @@ impl DeepLinkAction { let mut app_state = state.write().await; if let Some(recording) = app_state.current_recording_mut() { recording.pause().await.map_err(|e| e.to_string())?; + RecordingEvent::Paused.emit(app).ok(); } Ok(()) } @@ -189,6 +192,7 @@ impl DeepLinkAction { let mut app_state = state.write().await; if let Some(recording) = app_state.current_recording_mut() { recording.resume().await.map_err(|e| e.to_string())?; + RecordingEvent::Resumed.emit(app).ok(); } Ok(()) } diff --git a/apps/raycast-extension/package.json b/apps/raycast-extension/package.json index 0ca1f0104b7..45429c606ef 100644 --- a/apps/raycast-extension/package.json +++ b/apps/raycast-extension/package.json @@ -55,9 +55,15 @@ "arguments": [ { "name": "camera_id", - "placeholder": "Camera ID", + "placeholder": "Camera ID (e.g. from continuity camera)", "type": "text", "required": true + }, + { + "name": "type", + "placeholder": "ID Type (device or model)", + "type": "text", + "required": false } ] } @@ -76,6 +82,6 @@ "dev": "ray develop", "fix-lint": "ray lint --fix", "lint": "ray lint", - "publish": "npx @raycast/api@latest publish" + "publish": "ray publish" } } diff --git a/apps/raycast-extension/src/switch-camera.ts b/apps/raycast-extension/src/switch-camera.ts index 8c4f10a4ba9..1673bbee75c 100644 --- a/apps/raycast-extension/src/switch-camera.ts +++ b/apps/raycast-extension/src/switch-camera.ts @@ -1,9 +1,12 @@ import { executeCapAction } from "./utils"; -export default async function Command(props: { arguments: { camera_id: string } }) { +export default async function Command(props: { arguments: { camera_id: string; type?: string } }) { + const isModel = props.arguments.type?.toLowerCase() === "model"; await executeCapAction({ switch_camera: { - camera_id: { DeviceID: props.arguments.camera_id }, + camera_id: isModel + ? { ModelID: props.arguments.camera_id } + : { DeviceID: props.arguments.camera_id }, }, }); } diff --git a/apps/raycast-extension/src/utils.ts b/apps/raycast-extension/src/utils.ts index f4989dd7e7a..b87685ed77c 100644 --- a/apps/raycast-extension/src/utils.ts +++ b/apps/raycast-extension/src/utils.ts @@ -1,6 +1,11 @@ import { open } from "@raycast/api"; -export async function executeCapAction(action: any) { +type CapAction = + | string + | { switch_mic: { mic_label: string } } + | { switch_camera: { camera_id: { DeviceID: string } | { ModelID: string } } }; + +export async function executeCapAction(action: CapAction) { const json = JSON.stringify(action); const url = `cap://action?value=${encodeURIComponent(json)}`; await open(url);