diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index a117028487..2f5a93b306 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")] @@ -18,6 +20,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 +29,14 @@ pub enum DeepLinkAction { mode: RecordingMode, }, StopRecording, + PauseRecording, + ResumeRecording, + SwitchMic { + mic_label: String, + }, + SwitchCamera { + camera_id: DeviceOrModelID, + }, OpenEditor { project_path: PathBuf, }, @@ -108,6 +119,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: settings.system_audio, + mode, + organization_id: settings.organization_id, + }, + ) + .await + .map(|_| ()) + } DeepLinkAction::StartRecording { capture_mode, camera, @@ -147,6 +178,32 @@ 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())?; + RecordingEvent::Paused.emit(app).ok(); + } + 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())?; + RecordingEvent::Resumed.emit(app).ok(); + } + 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 0000000000..45429c606e --- /dev/null +++ b/apps/raycast-extension/package.json @@ -0,0 +1,87 @@ +{ + "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 (e.g. from continuity camera)", + "type": "text", + "required": true + }, + { + "name": "type", + "placeholder": "ID Type (device or model)", + "type": "text", + "required": false + } + ] + } + ], + "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": "ray publish" + } +} diff --git a/apps/raycast-extension/src/pause-recording.ts b/apps/raycast-extension/src/pause-recording.ts new file mode 100644 index 0000000000..e44ea6292b --- /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 0000000000..312ea755ca --- /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 0000000000..1e5998bc46 --- /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 0000000000..c335b76ca2 --- /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 0000000000..1673bbee75 --- /dev/null +++ b/apps/raycast-extension/src/switch-camera.ts @@ -0,0 +1,12 @@ +import { executeCapAction } from "./utils"; + +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: isModel + ? { ModelID: props.arguments.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 0000000000..df699bea29 --- /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 0000000000..b87685ed77 --- /dev/null +++ b/apps/raycast-extension/src/utils.ts @@ -0,0 +1,12 @@ +import { open } from "@raycast/api"; + +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); +}