diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index fce75b4a84..33831cd922 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, + SwitchCamera { + device_id: String, + }, + SwitchMicrophone { + mic_label: String, + }, OpenEditor { project_path: PathBuf, }, @@ -146,6 +154,31 @@ impl DeepLinkAction { DeepLinkAction::StopRecording => { crate::recording::stop_recording(app.clone(), app.state()).await } + DeepLinkAction::PauseRecording => { + let state = app.state::>(); + crate::recording::pause_recording(state) + .await + .map_err(|e| e.to_string()) + } + DeepLinkAction::ResumeRecording => { + let state = app.state::>(); + crate::recording::resume_recording(state) + .await + .map_err(|e| e.to_string()) + } + DeepLinkAction::SwitchCamera { device_id } => { + let state = app.state::>(); + let camera = DeviceOrModelID::ModelID(device_id.clone()); + crate::set_camera_input(app.clone(), state.clone(), Some(camera), None) + .await + .map(|_| ()) + } + DeepLinkAction::SwitchMicrophone { mic_label } => { + let state = app.state::>(); + crate::set_mic_input(state, Some(mic_label)) + .await + .map(|_| ()) + } DeepLinkAction::OpenEditor { project_path } => { crate::open_project_from_path(Path::new(&project_path), app.clone()) } diff --git a/packages/raycast-extension/package.json b/packages/raycast-extension/package.json new file mode 100644 index 0000000000..993166f20f --- /dev/null +++ b/packages/raycast-extension/package.json @@ -0,0 +1,13 @@ +{ + "name": "cap-recording-control", + "title": "Cap Recording Control", + "description": "Control Cap screen recording via deeplinks (pause, resume, start, stop, switch camera/microphone)", + "author": "Cap Software", + "version": "1.0.0", + "keywords": [ + "cap", + "screen-recording", + "recording-control", + "deeplink" + ] +} diff --git a/packages/raycast-extension/src/commands/pause-recording.tsx b/packages/raycast-extension/src/commands/pause-recording.tsx new file mode 100644 index 0000000000..be265d0f61 --- /dev/null +++ b/packages/raycast-extension/src/commands/pause-recording.tsx @@ -0,0 +1,13 @@ +import { open, showToast, Toast } from "@raycast/api"; +import { generateDeeplink } from "../utils/deeplink"; + +export default async function Command() { + const deeplink = generateDeeplink("pause_recording"); + + try { + await open(deeplink); + await showToast({ style: Toast.Style.Success, title: "Opened Cap" }); + } catch (error) { + await showToast({ style: Toast.Style.Failure, title: "Error", message: String(error) }); + } +} diff --git a/packages/raycast-extension/src/commands/resume-recording.tsx b/packages/raycast-extension/src/commands/resume-recording.tsx new file mode 100644 index 0000000000..9b4c04c07b --- /dev/null +++ b/packages/raycast-extension/src/commands/resume-recording.tsx @@ -0,0 +1,13 @@ +import { open, showToast, Toast } from "@raycast/api"; +import { generateDeeplink } from "../utils/deeplink"; + +export default async function Command() { + const deeplink = generateDeeplink("resume_recording"); + + try { + await open(deeplink); + await showToast({ style: Toast.Style.Success, title: "Opened Cap" }); + } catch (error) { + await showToast({ style: Toast.Style.Failure, title: "Error", message: String(error) }); + } +} diff --git a/packages/raycast-extension/src/commands/start-recording.tsx b/packages/raycast-extension/src/commands/start-recording.tsx new file mode 100644 index 0000000000..0043a4090d --- /dev/null +++ b/packages/raycast-extension/src/commands/start-recording.tsx @@ -0,0 +1,19 @@ +import { open, showToast, Toast } from "@raycast/api"; +import { generateDeeplink } from "../utils/deeplink"; + +export default async function Command() { + const deeplink = generateDeeplink("start_recording", { + capture_mode: { screen: "default" }, + camera: null, + mic_label: null, + capture_system_audio: false, + mode: "normal" + }); + + try { + await open(deeplink); + await showToast({ style: Toast.Style.Success, title: "Opened Cap" }); + } catch (error) { + await showToast({ style: Toast.Style.Failure, title: "Error", message: String(error) }); + } +} diff --git a/packages/raycast-extension/src/commands/stop-recording.tsx b/packages/raycast-extension/src/commands/stop-recording.tsx new file mode 100644 index 0000000000..3d94727b2e --- /dev/null +++ b/packages/raycast-extension/src/commands/stop-recording.tsx @@ -0,0 +1,13 @@ +import { open, showToast, Toast } from "@raycast/api"; +import { generateDeeplink } from "../utils/deeplink"; + +export default async function Command() { + const deeplink = generateDeeplink("stop_recording"); + + try { + await open(deeplink); + await showToast({ style: Toast.Style.Success, title: "Opened Cap" }); + } catch (error) { + await showToast({ style: Toast.Style.Failure, title: "Error", message: String(error) }); + } +} diff --git a/packages/raycast-extension/src/commands/switch-camera.tsx b/packages/raycast-extension/src/commands/switch-camera.tsx new file mode 100644 index 0000000000..9432d19d54 --- /dev/null +++ b/packages/raycast-extension/src/commands/switch-camera.tsx @@ -0,0 +1,17 @@ +import { open, showToast, Toast } from "@raycast/api"; +import { generateDeeplink } from "../utils/deeplink"; + +export default async function Command() { + // TODO: Replace "default" with actual device picker when available + // For now, this will use the default/primary camera device + const deeplink = generateDeeplink("switch_camera", { + device_id: "default" + }); + + try { + await open(deeplink); + await showToast({ style: Toast.Style.Success, title: "Opened Cap" }); + } catch (error) { + await showToast({ style: Toast.Style.Failure, title: "Error", message: String(error) }); + } +} diff --git a/packages/raycast-extension/src/commands/switch-microphone.tsx b/packages/raycast-extension/src/commands/switch-microphone.tsx new file mode 100644 index 0000000000..392799331d --- /dev/null +++ b/packages/raycast-extension/src/commands/switch-microphone.tsx @@ -0,0 +1,17 @@ +import { open, showToast, Toast } from "@raycast/api"; +import { generateDeeplink } from "../utils/deeplink"; + +export default async function Command() { + // TODO: Replace "default" with actual mic picker when available + // For now, this will use the default/system microphone + const deeplink = generateDeeplink("switch_microphone", { + mic_label: "default" + }); + + try { + await open(deeplink); + await showToast({ style: Toast.Style.Success, title: "Opened Cap" }); + } catch (error) { + await showToast({ style: Toast.Style.Failure, title: "Error", message: String(error) }); + } +} diff --git a/packages/raycast-extension/src/utils/deeplink.ts b/packages/raycast-extension/src/utils/deeplink.ts new file mode 100644 index 0000000000..60201a40f8 --- /dev/null +++ b/packages/raycast-extension/src/utils/deeplink.ts @@ -0,0 +1,38 @@ +/** + * Generate a deeplink for Cap recording control actions + * + * Supported actions: + * - pause_recording: {} (no params) + * - resume_recording: {} (no params) + * - stop_recording: {} (no params) + * - start_recording: { capture_mode, camera?, mic_label?, capture_system_audio, mode } + * - switch_camera: { device_id } + * - switch_microphone: { mic_label } + */ +export const generateDeeplink = (action: string, params?: Record): string => { + const validActions = [ + "pause_recording", + "resume_recording", + "stop_recording", + "start_recording", + "switch_camera", + "switch_microphone" + ]; + + if (!validActions.includes(action)) { + throw new Error(`Invalid action: ${action}. Must be one of: ${validActions.join(", ")}`); + } + + const url = new URL(`cap://action`); + + const actionObj: any = {}; + + if (params) { + actionObj[action] = params; + } else { + actionObj[action] = {}; + } + + url.searchParams.append("value", JSON.stringify(actionObj)); + return url.toString(); +};