From fefb09966334275ef1117a2cfea1f654644f9ef7 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Wed, 4 Mar 2026 17:25:55 +0700 Subject: [PATCH 01/28] feat: extend deeplinks support + add Raycast extension\n\nExtends the existing deeplink infrastructure with new actions:\n- PauseRecording, ResumeRecording, TogglePauseRecording\n- RestartRecording\n- TakeScreenshot (with optional capture mode)\n- ListCameras, SetCamera\n- ListMicrophones, SetMicrophone\n- ListDisplays, ListWindows\n\nAdds a Raycast extension (apps/raycast) with commands:\n- Start Instant Recording / Start Studio Recording\n- Stop / Pause / Resume / Toggle Pause / Restart Recording\n- Take Screenshot\n- Open Settings\n\nAll commands communicate with Cap via the cap-desktop:// deeplink scheme.\n\nCloses #1540" --- .../desktop/src-tauri/src/deeplink_actions.rs | 108 +++++++++++++++--- apps/raycast/README.md | 68 +++++++++++ apps/raycast/assets/cap-icon.png | Bin 0 -> 34649 bytes apps/raycast/package.json | 86 ++++++++++++++ apps/raycast/src/open-settings.ts | 14 +++ apps/raycast/src/pause-recording.ts | 5 + apps/raycast/src/restart-recording.ts | 5 + apps/raycast/src/resume-recording.ts | 5 + apps/raycast/src/start-instant-recording.ts | 18 +++ apps/raycast/src/start-studio-recording.ts | 18 +++ apps/raycast/src/stop-recording.ts | 5 + apps/raycast/src/take-screenshot.ts | 14 +++ apps/raycast/src/toggle-pause-recording.ts | 5 + apps/raycast/src/utils.ts | 24 ++++ apps/raycast/tsconfig.json | 16 +++ 15 files changed, 378 insertions(+), 13 deletions(-) create mode 100644 apps/raycast/README.md create mode 100644 apps/raycast/assets/cap-icon.png create mode 100644 apps/raycast/package.json create mode 100644 apps/raycast/src/open-settings.ts create mode 100644 apps/raycast/src/pause-recording.ts create mode 100644 apps/raycast/src/restart-recording.ts create mode 100644 apps/raycast/src/resume-recording.ts create mode 100644 apps/raycast/src/start-instant-recording.ts create mode 100644 apps/raycast/src/start-studio-recording.ts create mode 100644 apps/raycast/src/stop-recording.ts create mode 100644 apps/raycast/src/take-screenshot.ts create mode 100644 apps/raycast/src/toggle-pause-recording.ts create mode 100644 apps/raycast/src/utils.ts create mode 100644 apps/raycast/tsconfig.json diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index fce75b4a84..d7f2152b3c 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -26,6 +26,23 @@ pub enum DeepLinkAction { mode: RecordingMode, }, StopRecording, + PauseRecording, + ResumeRecording, + TogglePauseRecording, + RestartRecording, + TakeScreenshot { + capture_mode: Option, + }, + ListCameras, + SetCamera { + id: Option, + }, + ListMicrophones, + SetMicrophone { + label: Option, + }, + ListDisplays, + ListWindows, OpenEditor { project_path: PathBuf, }, @@ -49,7 +66,6 @@ pub fn handle(app_handle: &AppHandle, urls: Vec) { ActionParseFromUrlError::Invalid => { eprintln!("Invalid deep link format \"{}\"", &url) } - // Likely login action, not handled here. ActionParseFromUrlError::NotAction => {} }) .ok() @@ -105,6 +121,21 @@ impl TryFrom<&Url> for DeepLinkAction { } impl DeepLinkAction { + fn resolve_capture_target(capture_mode: &CaptureMode) -> Result { + match capture_mode { + CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays() + .into_iter() + .find(|(s, _)| s.name == *name) + .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) + .ok_or(format!("No screen with name \"{}\"", name)), + CaptureMode::Window(name) => cap_recording::screen_capture::list_windows() + .into_iter() + .find(|(w, _)| w.name == *name) + .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) + .ok_or(format!("No window with name \"{}\"", name)), + } + } + pub async fn execute(self, app: &AppHandle) -> Result<(), String> { match self { DeepLinkAction::StartRecording { @@ -119,18 +150,7 @@ impl DeepLinkAction { crate::set_camera_input(app.clone(), state.clone(), camera, None).await?; crate::set_mic_input(state.clone(), mic_label).await?; - let capture_target: ScreenCaptureTarget = match capture_mode { - CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays() - .into_iter() - .find(|(s, _)| s.name == name) - .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) - .ok_or(format!("No screen with name \"{}\"", &name))?, - CaptureMode::Window(name) => cap_recording::screen_capture::list_windows() - .into_iter() - .find(|(w, _)| w.name == name) - .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) - .ok_or(format!("No window with name \"{}\"", &name))?, - }; + let capture_target = Self::resolve_capture_target(&capture_mode)?; let inputs = StartRecordingInputs { mode, @@ -146,6 +166,68 @@ 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::TogglePauseRecording => { + crate::recording::toggle_pause_recording(app.clone(), app.state()).await + } + DeepLinkAction::RestartRecording => { + crate::recording::restart_recording(app.clone(), app.state()) + .await + .map(|_| ()) + } + DeepLinkAction::TakeScreenshot { capture_mode } => { + let target = match capture_mode { + Some(mode) => Self::resolve_capture_target(&mode)?, + None => { + let displays = cap_recording::screen_capture::list_displays(); + let (display, _) = + displays.into_iter().next().ok_or("No displays available")?; + ScreenCaptureTarget::Display { id: display.id } + } + }; + + crate::recording::take_screenshot(app.clone(), target) + .await + .map(|_| ()) + } + DeepLinkAction::ListCameras => { + let cameras = crate::recording::list_cameras(); + let json = serde_json::to_string(&cameras).map_err(|e| e.to_string())?; + tracing::info!("Available cameras: {}", json); + Ok(()) + } + DeepLinkAction::SetCamera { id } => { + let state = app.state::>(); + crate::set_camera_input(app.clone(), state, id, None).await + } + DeepLinkAction::ListMicrophones => { + let mics = cap_recording::feeds::microphone::MicrophoneFeed::list(); + let labels: Vec = mics.keys().cloned().collect(); + let json = serde_json::to_string(&labels).map_err(|e| e.to_string())?; + tracing::info!("Available microphones: {}", json); + Ok(()) + } + DeepLinkAction::SetMicrophone { label } => { + let state = app.state::>(); + crate::set_mic_input(state, label).await + } + DeepLinkAction::ListDisplays => { + let displays = crate::recording::list_capture_displays().await; + let json = serde_json::to_string(&displays).map_err(|e| e.to_string())?; + tracing::info!("Available displays: {}", json); + Ok(()) + } + DeepLinkAction::ListWindows => { + let windows = crate::recording::list_capture_windows().await; + let json = serde_json::to_string(&windows).map_err(|e| e.to_string())?; + tracing::info!("Available windows: {}", json); + Ok(()) + } DeepLinkAction::OpenEditor { project_path } => { crate::open_project_from_path(Path::new(&project_path), app.clone()) } diff --git a/apps/raycast/README.md b/apps/raycast/README.md new file mode 100644 index 0000000000..0e4d00905d --- /dev/null +++ b/apps/raycast/README.md @@ -0,0 +1,68 @@ +# Cap Raycast Extension + +Control [Cap](https://cap.so) screen recorder directly from Raycast. + +## Commands + +| Command | Description | +| ----------------------- | --------------------------------- | +| Start Instant Recording | Start an instant screen recording | +| Start Studio Recording | Start a studio screen recording | +| Stop Recording | Stop the current recording | +| Pause Recording | Pause the current recording | +| Resume Recording | Resume a paused recording | +| Toggle Pause Recording | Toggle pause/resume | +| Restart Recording | Restart the current recording | +| Take Screenshot | Take a screenshot | +| Open Settings | Open Cap settings | + +## How It Works + +The extension communicates with the Cap desktop app through deeplinks using the `cap-desktop://` URL scheme. All commands dispatch actions via deeplink URLs that Cap handles natively. + +### Deeplink Format + +Unit actions (no parameters): + +``` +cap-desktop://action?value="stop_recording" +``` + +Actions with parameters: + +``` +cap-desktop://action?value={"start_recording":{"capture_mode":{"screen":"Built-in Retina Display"},"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"studio"}} +``` + +### Available Deeplink Actions + +| Action | Type | Parameters | +| ------------------------ | ------------- | --------------------------------------------------------------------- | +| `start_recording` | Parameterized | `capture_mode`, `camera`, `mic_label`, `capture_system_audio`, `mode` | +| `stop_recording` | Unit | — | +| `pause_recording` | Unit | — | +| `resume_recording` | Unit | — | +| `toggle_pause_recording` | Unit | — | +| `restart_recording` | Unit | — | +| `take_screenshot` | Parameterized | `capture_mode` (optional) | +| `list_cameras` | Unit | — | +| `set_camera` | Parameterized | `id` | +| `list_microphones` | Unit | — | +| `set_microphone` | Parameterized | `label` | +| `list_displays` | Unit | — | +| `list_windows` | Unit | — | +| `open_editor` | Parameterized | `project_path` | +| `open_settings` | Parameterized | `page` (optional) | + +## Prerequisites + +- [Cap](https://cap.so) desktop app installed and running +- [Raycast](https://raycast.com) installed + +## Development + +```bash +cd apps/raycast +npm install +npm run dev +``` diff --git a/apps/raycast/assets/cap-icon.png b/apps/raycast/assets/cap-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..718226caf20b6437f5b37318ee191ef9358e6c09 GIT binary patch literal 34649 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 { + const valueJson = typeof action === 'string' ? JSON.stringify(action) : JSON.stringify(action); + const url = `${DEEPLINK_SCHEME}?value=${encodeURIComponent(valueJson)}`; + + try { + await open(url); + } catch { + await showToast({ + style: Toast.Style.Failure, + title: 'Failed to communicate with Cap', + message: 'Make sure Cap is running.', + }); + } +} + +export async function fireSimpleAction(action: string, label: string) { + await showToast({ style: Toast.Style.Animated, title: `${label}...` }); + await dispatchAction(action); + await showToast({ style: Toast.Style.Success, title: label }); +} diff --git a/apps/raycast/tsconfig.json b/apps/raycast/tsconfig.json new file mode 100644 index 0000000000..756769e94d --- /dev/null +++ b/apps/raycast/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "lib": ["ES2023"], + "module": "Node16", + "target": "ES2022", + "moduleResolution": "Node16", + "strict": true, + "declaration": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "exclude": ["node_modules"] +} From 5b028015375e77493f5ce5d8abf87c89bb98ebf3 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Wed, 4 Mar 2026 17:37:46 +0700 Subject: [PATCH 02/28] fix: address review feedback for deeplinks + Raycast extension --- .../desktop/src-tauri/src/deeplink_actions.rs | 33 ++++++++++++++----- apps/raycast/src/start-instant-recording.ts | 4 +-- apps/raycast/src/start-studio-recording.ts | 2 +- apps/raycast/src/utils.ts | 15 ++++----- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index d7f2152b3c..19ed0532fe 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -4,6 +4,7 @@ use cap_recording::{ use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager, Url}; +use tauri_plugin_clipboard_manager::ClipboardExt; use tracing::trace; use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow}; @@ -19,7 +20,7 @@ pub enum CaptureMode { #[serde(rename_all = "snake_case")] pub enum DeepLinkAction { StartRecording { - capture_mode: CaptureMode, + capture_mode: Option, camera: Option, mic_label: Option, capture_system_audio: bool, @@ -125,12 +126,12 @@ impl DeepLinkAction { match capture_mode { CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays() .into_iter() - .find(|(s, _)| s.name == *name) + .find(|(s, _)| s.name == name) .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) .ok_or(format!("No screen with name \"{}\"", name)), CaptureMode::Window(name) => cap_recording::screen_capture::list_windows() .into_iter() - .find(|(w, _)| w.name == *name) + .find(|(w, _)| w.name == name) .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) .ok_or(format!("No window with name \"{}\"", name)), } @@ -150,7 +151,15 @@ impl DeepLinkAction { crate::set_camera_input(app.clone(), state.clone(), camera, None).await?; crate::set_mic_input(state.clone(), mic_label).await?; - let capture_target = Self::resolve_capture_target(&capture_mode)?; + let capture_target = match capture_mode { + Some(mode) => Self::resolve_capture_target(&mode)?, + None => { + let displays = cap_recording::screen_capture::list_displays(); + let (display, _) = + displays.into_iter().next().ok_or("No displays available")?; + ScreenCaptureTarget::Display { id: display.id } + } + }; let inputs = StartRecordingInputs { mode, @@ -198,7 +207,9 @@ impl DeepLinkAction { DeepLinkAction::ListCameras => { let cameras = crate::recording::list_cameras(); let json = serde_json::to_string(&cameras).map_err(|e| e.to_string())?; - tracing::info!("Available cameras: {}", json); + app.clipboard() + .write_text(&json) + .map_err(|e| e.to_string())?; Ok(()) } DeepLinkAction::SetCamera { id } => { @@ -209,7 +220,9 @@ impl DeepLinkAction { let mics = cap_recording::feeds::microphone::MicrophoneFeed::list(); let labels: Vec = mics.keys().cloned().collect(); let json = serde_json::to_string(&labels).map_err(|e| e.to_string())?; - tracing::info!("Available microphones: {}", json); + app.clipboard() + .write_text(&json) + .map_err(|e| e.to_string())?; Ok(()) } DeepLinkAction::SetMicrophone { label } => { @@ -219,13 +232,17 @@ impl DeepLinkAction { DeepLinkAction::ListDisplays => { let displays = crate::recording::list_capture_displays().await; let json = serde_json::to_string(&displays).map_err(|e| e.to_string())?; - tracing::info!("Available displays: {}", json); + app.clipboard() + .write_text(&json) + .map_err(|e| e.to_string())?; Ok(()) } DeepLinkAction::ListWindows => { let windows = crate::recording::list_capture_windows().await; let json = serde_json::to_string(&windows).map_err(|e| e.to_string())?; - tracing::info!("Available windows: {}", json); + app.clipboard() + .write_text(&json) + .map_err(|e| e.to_string())?; Ok(()) } DeepLinkAction::OpenEditor { project_path } => { diff --git a/apps/raycast/src/start-instant-recording.ts b/apps/raycast/src/start-instant-recording.ts index df1493c6ed..f40e324ded 100644 --- a/apps/raycast/src/start-instant-recording.ts +++ b/apps/raycast/src/start-instant-recording.ts @@ -1,12 +1,12 @@ import { dispatchAction } from './utils'; -import { showToast, Toast, getPreferenceValues } from '@raycast/api'; +import { showToast, Toast } from '@raycast/api'; export default async function startInstantRecording() { await showToast({ style: Toast.Style.Animated, title: 'Starting instant recording...' }); await dispatchAction({ start_recording: { - capture_mode: { screen: 'Built-in Retina Display' }, + capture_mode: null, camera: null, mic_label: null, capture_system_audio: false, diff --git a/apps/raycast/src/start-studio-recording.ts b/apps/raycast/src/start-studio-recording.ts index a11e6d2953..530ff68c66 100644 --- a/apps/raycast/src/start-studio-recording.ts +++ b/apps/raycast/src/start-studio-recording.ts @@ -6,7 +6,7 @@ export default async function startStudioRecording() { await dispatchAction({ start_recording: { - capture_mode: { screen: 'Built-in Retina Display' }, + capture_mode: null, camera: null, mic_label: null, capture_system_audio: false, diff --git a/apps/raycast/src/utils.ts b/apps/raycast/src/utils.ts index 7c7c0d3158..482e2822c0 100644 --- a/apps/raycast/src/utils.ts +++ b/apps/raycast/src/utils.ts @@ -3,11 +3,16 @@ import { open, showToast, Toast } from '@raycast/api'; const DEEPLINK_SCHEME = 'cap-desktop://action'; export async function dispatchAction(action: string | Record) { - const valueJson = typeof action === 'string' ? JSON.stringify(action) : JSON.stringify(action); + const valueJson = JSON.stringify(action); const url = `${DEEPLINK_SCHEME}?value=${encodeURIComponent(valueJson)}`; + await open(url); +} +export async function fireSimpleAction(action: string, label: string) { + await showToast({ style: Toast.Style.Animated, title: `${label}...` }); try { - await open(url); + await dispatchAction(action); + await showToast({ style: Toast.Style.Success, title: label }); } catch { await showToast({ style: Toast.Style.Failure, @@ -16,9 +21,3 @@ export async function dispatchAction(action: string | Record) { }); } } - -export async function fireSimpleAction(action: string, label: string) { - await showToast({ style: Toast.Style.Animated, title: `${label}...` }); - await dispatchAction(action); - await showToast({ style: Toast.Style.Success, title: label }); -} From 46e42a5a848a53a23d1ddf73b5d16991cf1b9363 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Wed, 4 Mar 2026 17:43:12 +0700 Subject: [PATCH 03/28] feat: enhance error handling in Raycast commands for better user feedback --- .../desktop/src-tauri/src/deeplink_actions.rs | 3 +- apps/raycast/src/open-settings.ts | 21 +++++++++----- apps/raycast/src/start-instant-recording.ts | 29 ++++++++++++------- apps/raycast/src/start-studio-recording.ts | 29 ++++++++++++------- apps/raycast/src/take-screenshot.ts | 21 +++++++++----- 5 files changed, 66 insertions(+), 37 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 19ed0532fe..cf3eeae1c1 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -218,7 +218,8 @@ impl DeepLinkAction { } DeepLinkAction::ListMicrophones => { let mics = cap_recording::feeds::microphone::MicrophoneFeed::list(); - let labels: Vec = mics.keys().cloned().collect(); + let mut labels: Vec = mics.keys().cloned().collect(); + labels.sort(); let json = serde_json::to_string(&labels).map_err(|e| e.to_string())?; app.clipboard() .write_text(&json) diff --git a/apps/raycast/src/open-settings.ts b/apps/raycast/src/open-settings.ts index 1540d560a5..b4889fc8b5 100644 --- a/apps/raycast/src/open-settings.ts +++ b/apps/raycast/src/open-settings.ts @@ -4,11 +4,18 @@ import { showToast, Toast } from '@raycast/api'; export default async function openSettings() { await showToast({ style: Toast.Style.Animated, title: 'Opening Cap settings...' }); - await dispatchAction({ - open_settings: { - page: null, - }, - }); - - await showToast({ style: Toast.Style.Success, title: 'Settings opened' }); + try { + await dispatchAction({ + open_settings: { + page: null, + }, + }); + await showToast({ style: Toast.Style.Success, title: 'Settings opened' }); + } catch { + await showToast({ + style: Toast.Style.Failure, + title: 'Failed to communicate with Cap', + message: 'Make sure Cap is running.', + }); + } } diff --git a/apps/raycast/src/start-instant-recording.ts b/apps/raycast/src/start-instant-recording.ts index f40e324ded..09a8e98a84 100644 --- a/apps/raycast/src/start-instant-recording.ts +++ b/apps/raycast/src/start-instant-recording.ts @@ -4,15 +4,22 @@ import { showToast, Toast } from '@raycast/api'; export default async function startInstantRecording() { await showToast({ style: Toast.Style.Animated, title: 'Starting instant recording...' }); - await dispatchAction({ - start_recording: { - capture_mode: null, - camera: null, - mic_label: null, - capture_system_audio: false, - mode: 'instant', - }, - }); - - await showToast({ style: Toast.Style.Success, title: 'Instant recording started' }); + try { + await dispatchAction({ + start_recording: { + capture_mode: null, + camera: null, + mic_label: null, + capture_system_audio: false, + mode: 'instant', + }, + }); + await showToast({ style: Toast.Style.Success, title: 'Instant recording started' }); + } catch { + await showToast({ + style: Toast.Style.Failure, + title: 'Failed to communicate with Cap', + message: 'Make sure Cap is running.', + }); + } } diff --git a/apps/raycast/src/start-studio-recording.ts b/apps/raycast/src/start-studio-recording.ts index 530ff68c66..a4ec6161a3 100644 --- a/apps/raycast/src/start-studio-recording.ts +++ b/apps/raycast/src/start-studio-recording.ts @@ -4,15 +4,22 @@ import { showToast, Toast } from '@raycast/api'; export default async function startStudioRecording() { await showToast({ style: Toast.Style.Animated, title: 'Starting studio recording...' }); - await dispatchAction({ - start_recording: { - capture_mode: null, - camera: null, - mic_label: null, - capture_system_audio: false, - mode: 'studio', - }, - }); - - await showToast({ style: Toast.Style.Success, title: 'Studio recording started' }); + try { + await dispatchAction({ + start_recording: { + capture_mode: null, + camera: null, + mic_label: null, + capture_system_audio: false, + mode: 'studio', + }, + }); + await showToast({ style: Toast.Style.Success, title: 'Studio recording started' }); + } catch { + await showToast({ + style: Toast.Style.Failure, + title: 'Failed to communicate with Cap', + message: 'Make sure Cap is running.', + }); + } } diff --git a/apps/raycast/src/take-screenshot.ts b/apps/raycast/src/take-screenshot.ts index d5ad15e780..c7881d62d3 100644 --- a/apps/raycast/src/take-screenshot.ts +++ b/apps/raycast/src/take-screenshot.ts @@ -4,11 +4,18 @@ import { showToast, Toast } from '@raycast/api'; export default async function takeScreenshot() { await showToast({ style: Toast.Style.Animated, title: 'Taking screenshot...' }); - await dispatchAction({ - take_screenshot: { - capture_mode: null, - }, - }); - - await showToast({ style: Toast.Style.Success, title: 'Screenshot taken' }); + try { + await dispatchAction({ + take_screenshot: { + capture_mode: null, + }, + }); + await showToast({ style: Toast.Style.Success, title: 'Screenshot taken' }); + } catch { + await showToast({ + style: Toast.Style.Failure, + title: 'Failed to communicate with Cap', + message: 'Make sure Cap is running.', + }); + } } From 9f4f5554f87c7e7dc1876ad4329c3bfb53eb952e Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Wed, 4 Mar 2026 17:47:33 +0700 Subject: [PATCH 04/28] feat: simplify display selection for screen capture in deeplink actions --- .../desktop/src-tauri/src/deeplink_actions.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index cf3eeae1c1..ff16c75280 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -1,6 +1,7 @@ use cap_recording::{ RecordingMode, feeds::camera::DeviceOrModelID, sources::screen_capture::ScreenCaptureTarget, }; +use scap_targets::Display; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager, Url}; @@ -153,12 +154,9 @@ impl DeepLinkAction { let capture_target = match capture_mode { Some(mode) => Self::resolve_capture_target(&mode)?, - None => { - let displays = cap_recording::screen_capture::list_displays(); - let (display, _) = - displays.into_iter().next().ok_or("No displays available")?; - ScreenCaptureTarget::Display { id: display.id } - } + None => ScreenCaptureTarget::Display { + id: Display::primary().id(), + }, }; let inputs = StartRecordingInputs { @@ -192,12 +190,9 @@ impl DeepLinkAction { DeepLinkAction::TakeScreenshot { capture_mode } => { let target = match capture_mode { Some(mode) => Self::resolve_capture_target(&mode)?, - None => { - let displays = cap_recording::screen_capture::list_displays(); - let (display, _) = - displays.into_iter().next().ok_or("No displays available")?; - ScreenCaptureTarget::Display { id: display.id } - } + None => ScreenCaptureTarget::Display { + id: Display::primary().id(), + }, }; crate::recording::take_screenshot(app.clone(), target) From 03996ed1ee60ee0e05f2f28fe7b3d13b8a2a82c8 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Wed, 4 Mar 2026 17:48:27 +0700 Subject: [PATCH 05/28] fix: update README --- apps/raycast/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/raycast/README.md b/apps/raycast/README.md index 0e4d00905d..8ab9295dea 100644 --- a/apps/raycast/README.md +++ b/apps/raycast/README.md @@ -31,7 +31,7 @@ cap-desktop://action?value="stop_recording" Actions with parameters: ``` -cap-desktop://action?value={"start_recording":{"capture_mode":{"screen":"Built-in Retina Display"},"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"studio"}} +cap-desktop://action?value={"start_recording":{"capture_mode":null,"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"studio"}} ``` ### Available Deeplink Actions @@ -63,6 +63,6 @@ cap-desktop://action?value={"start_recording":{"capture_mode":{"screen":"Built-i ```bash cd apps/raycast -npm install -npm run dev +pnpm install +pnpm dev ``` From f05b9598485da366eddbe72a56d23f112d408011 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Wed, 4 Mar 2026 18:07:27 +0700 Subject: [PATCH 06/28] improve: toast labels and review feedback for Raycast extension --- apps/raycast/package.json | 2 +- apps/raycast/src/pause-recording.ts | 2 +- apps/raycast/src/restart-recording.ts | 2 +- apps/raycast/src/resume-recording.ts | 2 +- apps/raycast/src/stop-recording.ts | 2 +- apps/raycast/src/toggle-pause-recording.ts | 6 +++++- apps/raycast/src/utils.ts | 10 +++++++--- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/raycast/package.json b/apps/raycast/package.json index 234d51c8ba..0e5bbb4d40 100644 --- a/apps/raycast/package.json +++ b/apps/raycast/package.json @@ -4,7 +4,7 @@ "title": "Cap", "description": "Control Cap screen recorder — start, stop, pause, resume recordings, take screenshots, and manage devices.", "icon": "cap-icon.png", - "author": "cap", + "author": "capsoftware", "categories": [ "Productivity", "Developer Tools" diff --git a/apps/raycast/src/pause-recording.ts b/apps/raycast/src/pause-recording.ts index c7cb37c3dd..3f6e8676c9 100644 --- a/apps/raycast/src/pause-recording.ts +++ b/apps/raycast/src/pause-recording.ts @@ -1,5 +1,5 @@ import { fireSimpleAction } from './utils'; export default async function pauseRecording() { - await fireSimpleAction('pause_recording', 'Recording paused'); + await fireSimpleAction('pause_recording', 'Pausing recording…', 'Recording paused'); } diff --git a/apps/raycast/src/restart-recording.ts b/apps/raycast/src/restart-recording.ts index 0ff36b598f..bd8ed11a09 100644 --- a/apps/raycast/src/restart-recording.ts +++ b/apps/raycast/src/restart-recording.ts @@ -1,5 +1,5 @@ import { fireSimpleAction } from './utils'; export default async function restartRecording() { - await fireSimpleAction('restart_recording', 'Recording restarted'); + await fireSimpleAction('restart_recording', 'Restarting recording…', 'Recording restarted'); } diff --git a/apps/raycast/src/resume-recording.ts b/apps/raycast/src/resume-recording.ts index 7e08996357..dfb67440a6 100644 --- a/apps/raycast/src/resume-recording.ts +++ b/apps/raycast/src/resume-recording.ts @@ -1,5 +1,5 @@ import { fireSimpleAction } from './utils'; export default async function resumeRecording() { - await fireSimpleAction('resume_recording', 'Recording resumed'); + await fireSimpleAction('resume_recording', 'Resuming recording…', 'Recording resumed'); } diff --git a/apps/raycast/src/stop-recording.ts b/apps/raycast/src/stop-recording.ts index dd4edb7580..2b4beb16cc 100644 --- a/apps/raycast/src/stop-recording.ts +++ b/apps/raycast/src/stop-recording.ts @@ -1,5 +1,5 @@ import { fireSimpleAction } from './utils'; export default async function stopRecording() { - await fireSimpleAction('stop_recording', 'Recording stopped'); + await fireSimpleAction('stop_recording', 'Stopping recording…', 'Recording stopped'); } diff --git a/apps/raycast/src/toggle-pause-recording.ts b/apps/raycast/src/toggle-pause-recording.ts index 5d0609b23c..a9427f28b4 100644 --- a/apps/raycast/src/toggle-pause-recording.ts +++ b/apps/raycast/src/toggle-pause-recording.ts @@ -1,5 +1,9 @@ import { fireSimpleAction } from './utils'; export default async function togglePauseRecording() { - await fireSimpleAction('toggle_pause_recording', 'Toggle pause'); + await fireSimpleAction( + 'toggle_pause_recording', + 'Toggling recording pause…', + 'Recording pause toggled', + ); } diff --git a/apps/raycast/src/utils.ts b/apps/raycast/src/utils.ts index 482e2822c0..84aba2cbbb 100644 --- a/apps/raycast/src/utils.ts +++ b/apps/raycast/src/utils.ts @@ -8,11 +8,15 @@ export async function dispatchAction(action: string | Record) { await open(url); } -export async function fireSimpleAction(action: string, label: string) { - await showToast({ style: Toast.Style.Animated, title: `${label}...` }); +export async function fireSimpleAction( + action: string, + inProgressLabel: string, + successLabel: string, +) { + await showToast({ style: Toast.Style.Animated, title: inProgressLabel }); try { await dispatchAction(action); - await showToast({ style: Toast.Style.Success, title: label }); + await showToast({ style: Toast.Style.Success, title: successLabel }); } catch { await showToast({ style: Toast.Style.Failure, From f5ade3271bef8d1365b00ab17c06ef3e269c0726 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Wed, 4 Mar 2026 21:00:28 +0700 Subject: [PATCH 07/28] improve: rework deeplinks + Raycast extension quality - Add start_current_recording deeplink action (uses saved app settings) - Fix URL parsing: use host_str() instead of domain() for reliability - Add comprehensive unit tests for deeplink parsing (15 tests) - Add DEEPLINKS.md documentation for all available actions - Move Raycast extension from apps/raycast to extensions/raycast - Switch UX from Toast to HUD + closeMainWindow (cleaner for no-view) - Add start-current-recording Raycast command - Add extensions/* to pnpm-workspace.yaml - Use shared runDeepLinkAction utility in lib/deeplink.ts --- apps/desktop/src-tauri/DEEPLINKS.md | 145 ++++++++++ .../desktop/src-tauri/src/deeplink_actions.rs | 261 +++++++++++++++++- apps/raycast/README.md | 68 ----- apps/raycast/src/open-settings.ts | 21 -- apps/raycast/src/pause-recording.ts | 5 - apps/raycast/src/restart-recording.ts | 5 - apps/raycast/src/resume-recording.ts | 5 - apps/raycast/src/start-instant-recording.ts | 25 -- apps/raycast/src/start-studio-recording.ts | 25 -- apps/raycast/src/stop-recording.ts | 5 - apps/raycast/src/take-screenshot.ts | 21 -- apps/raycast/src/toggle-pause-recording.ts | 9 - apps/raycast/src/utils.ts | 27 -- extensions/raycast/README.md | 37 +++ .../raycast/assets/cap-icon.png | Bin {apps => extensions}/raycast/package.json | 11 +- extensions/raycast/src/lib/deeplink.ts | 19 ++ extensions/raycast/src/open-settings.ts | 8 + extensions/raycast/src/pause-recording.ts | 5 + extensions/raycast/src/restart-recording.ts | 5 + extensions/raycast/src/resume-recording.ts | 5 + .../raycast/src/start-current-recording.ts | 8 + .../raycast/src/start-instant-recording.ts | 16 ++ .../raycast/src/start-studio-recording.ts | 16 ++ extensions/raycast/src/stop-recording.ts | 5 + extensions/raycast/src/take-screenshot.ts | 8 + .../raycast/src/toggle-pause-recording.ts | 8 + {apps => extensions}/raycast/tsconfig.json | 0 pnpm-workspace.yaml | 1 + 29 files changed, 549 insertions(+), 225 deletions(-) create mode 100644 apps/desktop/src-tauri/DEEPLINKS.md delete mode 100644 apps/raycast/README.md delete mode 100644 apps/raycast/src/open-settings.ts delete mode 100644 apps/raycast/src/pause-recording.ts delete mode 100644 apps/raycast/src/restart-recording.ts delete mode 100644 apps/raycast/src/resume-recording.ts delete mode 100644 apps/raycast/src/start-instant-recording.ts delete mode 100644 apps/raycast/src/start-studio-recording.ts delete mode 100644 apps/raycast/src/stop-recording.ts delete mode 100644 apps/raycast/src/take-screenshot.ts delete mode 100644 apps/raycast/src/toggle-pause-recording.ts delete mode 100644 apps/raycast/src/utils.ts create mode 100644 extensions/raycast/README.md rename {apps => extensions}/raycast/assets/cap-icon.png (100%) rename {apps => extensions}/raycast/package.json (89%) create mode 100644 extensions/raycast/src/lib/deeplink.ts create mode 100644 extensions/raycast/src/open-settings.ts create mode 100644 extensions/raycast/src/pause-recording.ts create mode 100644 extensions/raycast/src/restart-recording.ts create mode 100644 extensions/raycast/src/resume-recording.ts create mode 100644 extensions/raycast/src/start-current-recording.ts create mode 100644 extensions/raycast/src/start-instant-recording.ts create mode 100644 extensions/raycast/src/start-studio-recording.ts create mode 100644 extensions/raycast/src/stop-recording.ts create mode 100644 extensions/raycast/src/take-screenshot.ts create mode 100644 extensions/raycast/src/toggle-pause-recording.ts rename {apps => extensions}/raycast/tsconfig.json (100%) diff --git a/apps/desktop/src-tauri/DEEPLINKS.md b/apps/desktop/src-tauri/DEEPLINKS.md new file mode 100644 index 0000000000..79c5d3722c --- /dev/null +++ b/apps/desktop/src-tauri/DEEPLINKS.md @@ -0,0 +1,145 @@ +# Cap Desktop Deeplinks + +Cap desktop registers the `cap-desktop://` URL scheme for external automation and integrations (e.g. Raycast, Alfred, shell scripts). + +## URL Format + +All actions use the `action` host with a JSON-encoded `value` query parameter: + +``` +cap-desktop://action?value= +``` + +### Unit actions (no parameters) + +The JSON value is a quoted string: + +``` +cap-desktop://action?value="stop_recording" +``` + +### Parameterized actions + +The JSON value is an object keyed by the action name: + +``` +cap-desktop://action?value={"start_recording":{"capture_mode":null,"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"studio"}} +``` + +## Available Actions + +### Recording Controls + +| Action | Type | Description | +|---|---|---| +| `start_recording` | Parameterized | Start a new recording with explicit settings | +| `start_current_recording` | Parameterized | Start a recording using saved settings from the app | +| `stop_recording` | Unit | Stop the current recording | +| `pause_recording` | Unit | Pause the current recording | +| `resume_recording` | Unit | Resume a paused recording | +| `toggle_pause_recording` | Unit | Toggle pause/resume on the current recording | +| `restart_recording` | Unit | Restart the current recording | + +### Screenshots + +| Action | Type | Description | +|---|---|---| +| `take_screenshot` | Parameterized | Capture a screenshot | + +### Device Management + +| Action | Type | Description | +|---|---|---| +| `list_cameras` | Unit | Copy available cameras as JSON to clipboard | +| `set_camera` | Parameterized | Set the active camera | +| `list_microphones` | Unit | Copy available microphones as JSON to clipboard | +| `set_microphone` | Parameterized | Set the active microphone | +| `list_displays` | Unit | Copy available displays as JSON to clipboard | +| `list_windows` | Unit | Copy available windows as JSON to clipboard | + +### Other + +| Action | Type | Description | +|---|---|---| +| `open_editor` | Parameterized | Open a project in the editor | +| `open_settings` | Parameterized | Open the settings window | + +## Action Parameters + +### `start_recording` + +| Field | Type | Required | Description | +|---|---|---|---| +| `capture_mode` | `null` \| `{"screen":""}` \| `{"window":""}` | Yes | Target to capture. `null` uses the primary display. | +| `camera` | `null` \| device ID object | Yes | Camera device. `null` disables the camera. | +| `mic_label` | `null` \| `string` | Yes | Microphone label. `null` disables the microphone. | +| `capture_system_audio` | `boolean` | Yes | Whether to capture system audio. | +| `mode` | `"studio"` \| `"instant"` | Yes | Recording mode. | + +### `start_current_recording` + +| Field | Type | Required | Description | +|---|---|---|---| +| `mode` | `null` \| `"studio"` \| `"instant"` | Yes | Override the saved recording mode. `null` uses the saved mode (defaults to studio). | + +### `take_screenshot` + +| Field | Type | Required | Description | +|---|---|---|---| +| `capture_mode` | `null` \| `{"screen":""}` \| `{"window":""}` | Yes | Target to capture. `null` uses the primary display. | + +### `set_camera` + +| Field | Type | Required | Description | +|---|---|---|---| +| `id` | `null` \| device ID object | Yes | Camera to activate. `null` disables the camera. | + +### `set_microphone` + +| Field | Type | Required | Description | +|---|---|---|---| +| `label` | `null` \| `string` | Yes | Microphone label. `null` disables the microphone. | + +### `open_editor` + +| Field | Type | Required | Description | +|---|---|---|---| +| `project_path` | `string` | Yes | Absolute path to the project directory. | + +### `open_settings` + +| Field | Type | Required | Description | +|---|---|---|---| +| `page` | `null` \| `string` | Yes | Settings page to open. `null` opens the default page. | + +## Examples + +Start a studio recording on the primary display: + +```bash +open "cap-desktop://action?value=$(python3 -c "import urllib.parse, json; print(urllib.parse.quote(json.dumps({'start_recording':{'capture_mode':None,'camera':None,'mic_label':None,'capture_system_audio':False,'mode':'studio'}})))")" +``` + +Start a recording using saved app settings: + +```bash +open "cap-desktop://action?value=$(python3 -c "import urllib.parse, json; print(urllib.parse.quote(json.dumps({'start_current_recording':{'mode':None}})))")" +``` + +Stop a recording: + +```bash +open "cap-desktop://action?value=%22stop_recording%22" +``` + +Take a screenshot: + +```bash +open "cap-desktop://action?value=$(python3 -c "import urllib.parse, json; print(urllib.parse.quote(json.dumps({'take_screenshot':{'capture_mode':None}})))")" +``` + +List available microphones (copies JSON to clipboard): + +```bash +open "cap-desktop://action?value=%22list_microphones%22" +``` diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index ff16c75280..1a12733e9a 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -8,7 +8,10 @@ use tauri::{AppHandle, Manager, Url}; use tauri_plugin_clipboard_manager::ClipboardExt; use tracing::trace; -use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow}; +use crate::{ + App, ArcLock, recording::StartRecordingInputs, recording_settings::RecordingSettingsStore, + windows::ShowCapWindow, +}; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -27,6 +30,9 @@ pub enum DeepLinkAction { capture_system_audio: bool, mode: RecordingMode, }, + StartCurrentRecording { + mode: Option, + }, StopRecording, PauseRecording, ResumeRecording, @@ -105,10 +111,11 @@ impl TryFrom<&Url> for DeepLinkAction { }); } - match url.domain() { - Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction), - _ => Err(ActionParseFromUrlError::Invalid), - }?; + match url.host_str() { + Some("action") => {} + Some(_) => return Err(ActionParseFromUrlError::NotAction), + None => return Err(ActionParseFromUrlError::Invalid), + } let params = url .query_pairs() @@ -170,6 +177,33 @@ impl DeepLinkAction { .await .map(|_| ()) } + DeepLinkAction::StartCurrentRecording { mode } => { + let settings = RecordingSettingsStore::get(app) + .ok() + .flatten() + .unwrap_or_default(); + + let state = app.state::>(); + + crate::set_mic_input(state.clone(), settings.mic_name).await?; + crate::set_camera_input(app.clone(), state.clone(), settings.camera_id, None) + .await?; + + let inputs = StartRecordingInputs { + mode: mode.or(settings.mode).unwrap_or(RecordingMode::Studio), + capture_target: settings.target.unwrap_or_else(|| { + ScreenCaptureTarget::Display { + id: Display::primary().id(), + } + }), + capture_system_audio: settings.system_audio, + organization_id: settings.organization_id, + }; + + crate::recording::start_recording(app.clone(), state, inputs) + .await + .map(|_| ()) + } DeepLinkAction::StopRecording => { crate::recording::stop_recording(app.clone(), app.state()).await } @@ -250,3 +284,220 @@ impl DeepLinkAction { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn action_url(value: &str) -> String { + let mut url = Url::parse("cap-desktop://action").unwrap(); + url.query_pairs_mut().append_pair("value", value); + url.to_string() + } + + fn parse(url_str: &str) -> Result { + let url = Url::parse(url_str).unwrap(); + DeepLinkAction::try_from(&url) + } + + #[test] + fn parse_unit_variants() { + let cases = [ + ("stop_recording", "StopRecording"), + ("pause_recording", "PauseRecording"), + ("resume_recording", "ResumeRecording"), + ("toggle_pause_recording", "TogglePauseRecording"), + ("restart_recording", "RestartRecording"), + ("list_cameras", "ListCameras"), + ("list_microphones", "ListMicrophones"), + ("list_displays", "ListDisplays"), + ("list_windows", "ListWindows"), + ]; + + for (action_str, label) in cases { + let url = action_url(&format!("\"{}\"", action_str)); + let result = parse(&url); + assert!( + result.is_ok(), + "Failed to parse {label}: {:?}", + result.err() + ); + } + } + + #[test] + fn parse_start_recording_studio() { + let json = serde_json::json!({ + "start_recording": { + "capture_mode": null, + "camera": null, + "mic_label": null, + "capture_system_audio": false, + "mode": "studio" + } + }); + let url = action_url(&json.to_string()); + let action = parse(&url).unwrap(); + assert!(matches!( + action, + DeepLinkAction::StartRecording { + mode: RecordingMode::Studio, + .. + } + )); + } + + #[test] + fn parse_start_recording_instant() { + let json = serde_json::json!({ + "start_recording": { + "capture_mode": null, + "camera": null, + "mic_label": null, + "capture_system_audio": true, + "mode": "instant" + } + }); + let url = action_url(&json.to_string()); + let action = parse(&url).unwrap(); + assert!(matches!( + action, + DeepLinkAction::StartRecording { + mode: RecordingMode::Instant, + capture_system_audio: true, + .. + } + )); + } + + #[test] + fn parse_start_current_recording() { + let json = serde_json::json!({ "start_current_recording": { "mode": null } }); + let url = action_url(&json.to_string()); + let action = parse(&url).unwrap(); + assert!(matches!( + action, + DeepLinkAction::StartCurrentRecording { mode: None } + )); + } + + #[test] + fn parse_start_current_recording_with_mode() { + let json = serde_json::json!({ "start_current_recording": { "mode": "instant" } }); + let url = action_url(&json.to_string()); + let action = parse(&url).unwrap(); + assert!(matches!( + action, + DeepLinkAction::StartCurrentRecording { + mode: Some(RecordingMode::Instant) + } + )); + } + + #[test] + fn parse_take_screenshot() { + let json = serde_json::json!({ "take_screenshot": { "capture_mode": null } }); + let url = action_url(&json.to_string()); + let action = parse(&url).unwrap(); + assert!(matches!( + action, + DeepLinkAction::TakeScreenshot { capture_mode: None } + )); + } + + #[test] + fn parse_set_camera() { + let json = serde_json::json!({ "set_camera": { "id": null } }); + let url = action_url(&json.to_string()); + let action = parse(&url).unwrap(); + assert!(matches!(action, DeepLinkAction::SetCamera { id: None })); + } + + #[test] + fn parse_set_microphone() { + let json = serde_json::json!({ "set_microphone": { "label": "Built-in Microphone" } }); + let url = action_url(&json.to_string()); + let action = parse(&url).unwrap(); + assert!(matches!( + action, + DeepLinkAction::SetMicrophone { label: Some(_) } + )); + } + + #[test] + fn parse_open_editor() { + let json = serde_json::json!({ "open_editor": { "project_path": "/tmp/test-project" } }); + let url = action_url(&json.to_string()); + let action = parse(&url).unwrap(); + assert!(matches!(action, DeepLinkAction::OpenEditor { .. })); + } + + #[test] + fn parse_open_settings() { + let json = serde_json::json!({ "open_settings": { "page": "general" } }); + let url = action_url(&json.to_string()); + let action = parse(&url).unwrap(); + assert!(matches!( + action, + DeepLinkAction::OpenSettings { page: Some(_) } + )); + } + + #[test] + fn parse_invalid_domain_returns_not_action() { + let url = "cap-desktop://something-else?value=%22stop_recording%22"; + let result = parse(url); + assert!(matches!(result, Err(ActionParseFromUrlError::NotAction))); + } + + #[test] + fn parse_missing_value_param_returns_invalid() { + let url = "cap-desktop://action?other=123"; + let result = parse(url); + assert!(matches!(result, Err(ActionParseFromUrlError::Invalid))); + } + + #[test] + fn parse_malformed_json_returns_parse_failed() { + let url = "cap-desktop://action?value=not-valid-json"; + let result = parse(url); + assert!(matches!( + result, + Err(ActionParseFromUrlError::ParseFailed(_)) + )); + } + + #[test] + fn parse_capture_mode_screen() { + let json = serde_json::json!({ + "take_screenshot": { + "capture_mode": { "screen": "Main Display" } + } + }); + let url = action_url(&json.to_string()); + let action = parse(&url).unwrap(); + assert!(matches!( + action, + DeepLinkAction::TakeScreenshot { + capture_mode: Some(CaptureMode::Screen(_)) + } + )); + } + + #[test] + fn parse_capture_mode_window() { + let json = serde_json::json!({ + "take_screenshot": { + "capture_mode": { "window": "Safari" } + } + }); + let url = action_url(&json.to_string()); + let action = parse(&url).unwrap(); + assert!(matches!( + action, + DeepLinkAction::TakeScreenshot { + capture_mode: Some(CaptureMode::Window(_)) + } + )); + } +} diff --git a/apps/raycast/README.md b/apps/raycast/README.md deleted file mode 100644 index 8ab9295dea..0000000000 --- a/apps/raycast/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Cap Raycast Extension - -Control [Cap](https://cap.so) screen recorder directly from Raycast. - -## Commands - -| Command | Description | -| ----------------------- | --------------------------------- | -| Start Instant Recording | Start an instant screen recording | -| Start Studio Recording | Start a studio screen recording | -| Stop Recording | Stop the current recording | -| Pause Recording | Pause the current recording | -| Resume Recording | Resume a paused recording | -| Toggle Pause Recording | Toggle pause/resume | -| Restart Recording | Restart the current recording | -| Take Screenshot | Take a screenshot | -| Open Settings | Open Cap settings | - -## How It Works - -The extension communicates with the Cap desktop app through deeplinks using the `cap-desktop://` URL scheme. All commands dispatch actions via deeplink URLs that Cap handles natively. - -### Deeplink Format - -Unit actions (no parameters): - -``` -cap-desktop://action?value="stop_recording" -``` - -Actions with parameters: - -``` -cap-desktop://action?value={"start_recording":{"capture_mode":null,"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"studio"}} -``` - -### Available Deeplink Actions - -| Action | Type | Parameters | -| ------------------------ | ------------- | --------------------------------------------------------------------- | -| `start_recording` | Parameterized | `capture_mode`, `camera`, `mic_label`, `capture_system_audio`, `mode` | -| `stop_recording` | Unit | — | -| `pause_recording` | Unit | — | -| `resume_recording` | Unit | — | -| `toggle_pause_recording` | Unit | — | -| `restart_recording` | Unit | — | -| `take_screenshot` | Parameterized | `capture_mode` (optional) | -| `list_cameras` | Unit | — | -| `set_camera` | Parameterized | `id` | -| `list_microphones` | Unit | — | -| `set_microphone` | Parameterized | `label` | -| `list_displays` | Unit | — | -| `list_windows` | Unit | — | -| `open_editor` | Parameterized | `project_path` | -| `open_settings` | Parameterized | `page` (optional) | - -## Prerequisites - -- [Cap](https://cap.so) desktop app installed and running -- [Raycast](https://raycast.com) installed - -## Development - -```bash -cd apps/raycast -pnpm install -pnpm dev -``` diff --git a/apps/raycast/src/open-settings.ts b/apps/raycast/src/open-settings.ts deleted file mode 100644 index b4889fc8b5..0000000000 --- a/apps/raycast/src/open-settings.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { dispatchAction } from './utils'; -import { showToast, Toast } from '@raycast/api'; - -export default async function openSettings() { - await showToast({ style: Toast.Style.Animated, title: 'Opening Cap settings...' }); - - try { - await dispatchAction({ - open_settings: { - page: null, - }, - }); - await showToast({ style: Toast.Style.Success, title: 'Settings opened' }); - } catch { - await showToast({ - style: Toast.Style.Failure, - title: 'Failed to communicate with Cap', - message: 'Make sure Cap is running.', - }); - } -} diff --git a/apps/raycast/src/pause-recording.ts b/apps/raycast/src/pause-recording.ts deleted file mode 100644 index 3f6e8676c9..0000000000 --- a/apps/raycast/src/pause-recording.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { fireSimpleAction } from './utils'; - -export default async function pauseRecording() { - await fireSimpleAction('pause_recording', 'Pausing recording…', 'Recording paused'); -} diff --git a/apps/raycast/src/restart-recording.ts b/apps/raycast/src/restart-recording.ts deleted file mode 100644 index bd8ed11a09..0000000000 --- a/apps/raycast/src/restart-recording.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { fireSimpleAction } from './utils'; - -export default async function restartRecording() { - await fireSimpleAction('restart_recording', 'Restarting recording…', 'Recording restarted'); -} diff --git a/apps/raycast/src/resume-recording.ts b/apps/raycast/src/resume-recording.ts deleted file mode 100644 index dfb67440a6..0000000000 --- a/apps/raycast/src/resume-recording.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { fireSimpleAction } from './utils'; - -export default async function resumeRecording() { - await fireSimpleAction('resume_recording', 'Resuming recording…', 'Recording resumed'); -} diff --git a/apps/raycast/src/start-instant-recording.ts b/apps/raycast/src/start-instant-recording.ts deleted file mode 100644 index 09a8e98a84..0000000000 --- a/apps/raycast/src/start-instant-recording.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { dispatchAction } from './utils'; -import { showToast, Toast } from '@raycast/api'; - -export default async function startInstantRecording() { - await showToast({ style: Toast.Style.Animated, title: 'Starting instant recording...' }); - - try { - await dispatchAction({ - start_recording: { - capture_mode: null, - camera: null, - mic_label: null, - capture_system_audio: false, - mode: 'instant', - }, - }); - await showToast({ style: Toast.Style.Success, title: 'Instant recording started' }); - } catch { - await showToast({ - style: Toast.Style.Failure, - title: 'Failed to communicate with Cap', - message: 'Make sure Cap is running.', - }); - } -} diff --git a/apps/raycast/src/start-studio-recording.ts b/apps/raycast/src/start-studio-recording.ts deleted file mode 100644 index a4ec6161a3..0000000000 --- a/apps/raycast/src/start-studio-recording.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { dispatchAction } from './utils'; -import { showToast, Toast } from '@raycast/api'; - -export default async function startStudioRecording() { - await showToast({ style: Toast.Style.Animated, title: 'Starting studio recording...' }); - - try { - await dispatchAction({ - start_recording: { - capture_mode: null, - camera: null, - mic_label: null, - capture_system_audio: false, - mode: 'studio', - }, - }); - await showToast({ style: Toast.Style.Success, title: 'Studio recording started' }); - } catch { - await showToast({ - style: Toast.Style.Failure, - title: 'Failed to communicate with Cap', - message: 'Make sure Cap is running.', - }); - } -} diff --git a/apps/raycast/src/stop-recording.ts b/apps/raycast/src/stop-recording.ts deleted file mode 100644 index 2b4beb16cc..0000000000 --- a/apps/raycast/src/stop-recording.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { fireSimpleAction } from './utils'; - -export default async function stopRecording() { - await fireSimpleAction('stop_recording', 'Stopping recording…', 'Recording stopped'); -} diff --git a/apps/raycast/src/take-screenshot.ts b/apps/raycast/src/take-screenshot.ts deleted file mode 100644 index c7881d62d3..0000000000 --- a/apps/raycast/src/take-screenshot.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { dispatchAction } from './utils'; -import { showToast, Toast } from '@raycast/api'; - -export default async function takeScreenshot() { - await showToast({ style: Toast.Style.Animated, title: 'Taking screenshot...' }); - - try { - await dispatchAction({ - take_screenshot: { - capture_mode: null, - }, - }); - await showToast({ style: Toast.Style.Success, title: 'Screenshot taken' }); - } catch { - await showToast({ - style: Toast.Style.Failure, - title: 'Failed to communicate with Cap', - message: 'Make sure Cap is running.', - }); - } -} diff --git a/apps/raycast/src/toggle-pause-recording.ts b/apps/raycast/src/toggle-pause-recording.ts deleted file mode 100644 index a9427f28b4..0000000000 --- a/apps/raycast/src/toggle-pause-recording.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { fireSimpleAction } from './utils'; - -export default async function togglePauseRecording() { - await fireSimpleAction( - 'toggle_pause_recording', - 'Toggling recording pause…', - 'Recording pause toggled', - ); -} diff --git a/apps/raycast/src/utils.ts b/apps/raycast/src/utils.ts deleted file mode 100644 index 84aba2cbbb..0000000000 --- a/apps/raycast/src/utils.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { open, showToast, Toast } from '@raycast/api'; - -const DEEPLINK_SCHEME = 'cap-desktop://action'; - -export async function dispatchAction(action: string | Record) { - const valueJson = JSON.stringify(action); - const url = `${DEEPLINK_SCHEME}?value=${encodeURIComponent(valueJson)}`; - await open(url); -} - -export async function fireSimpleAction( - action: string, - inProgressLabel: string, - successLabel: string, -) { - await showToast({ style: Toast.Style.Animated, title: inProgressLabel }); - try { - await dispatchAction(action); - await showToast({ style: Toast.Style.Success, title: successLabel }); - } catch { - await showToast({ - style: Toast.Style.Failure, - title: 'Failed to communicate with Cap', - message: 'Make sure Cap is running.', - }); - } -} diff --git a/extensions/raycast/README.md b/extensions/raycast/README.md new file mode 100644 index 0000000000..29a53b4b21 --- /dev/null +++ b/extensions/raycast/README.md @@ -0,0 +1,37 @@ +# Cap Raycast Extension + +Control [Cap](https://cap.so) screen recorder directly from Raycast. + +## Commands + +| Command | Description | +| ------------------------------ | --------------------------------------------- | +| Start Instant Recording | Start an instant screen recording | +| Start Studio Recording | Start a studio screen recording | +| Start Recording (Saved Settings) | Start a recording using your saved Cap settings | +| Stop Recording | Stop the current recording | +| Pause Recording | Pause the current recording | +| Resume Recording | Resume a paused recording | +| Toggle Pause Recording | Toggle pause/resume | +| Restart Recording | Restart the current recording | +| Take Screenshot | Take a screenshot | +| Open Settings | Open Cap settings | + +## How It Works + +The extension communicates with the Cap desktop app through deeplinks using the `cap-desktop://` URL scheme. All commands dispatch actions via deeplink URLs that Cap handles natively. + +See [DEEPLINKS.md](../../apps/desktop/src-tauri/DEEPLINKS.md) for full deeplink documentation. + +## Prerequisites + +- [Cap](https://cap.so) desktop app installed and running +- [Raycast](https://raycast.com) installed + +## Development + +```bash +cd extensions/raycast +pnpm install +pnpm dev +``` diff --git a/apps/raycast/assets/cap-icon.png b/extensions/raycast/assets/cap-icon.png similarity index 100% rename from apps/raycast/assets/cap-icon.png rename to extensions/raycast/assets/cap-icon.png diff --git a/apps/raycast/package.json b/extensions/raycast/package.json similarity index 89% rename from apps/raycast/package.json rename to extensions/raycast/package.json index 0e5bbb4d40..0c2fd27b45 100644 --- a/apps/raycast/package.json +++ b/extensions/raycast/package.json @@ -5,10 +5,7 @@ "description": "Control Cap screen recorder — start, stop, pause, resume recordings, take screenshots, and manage devices.", "icon": "cap-icon.png", "author": "capsoftware", - "categories": [ - "Productivity", - "Developer Tools" - ], + "categories": ["Productivity", "Developer Tools"], "license": "MIT", "commands": [ { @@ -23,6 +20,12 @@ "description": "Start a studio screen recording with Cap", "mode": "no-view" }, + { + "name": "start-current-recording", + "title": "Start Recording (Saved Settings)", + "description": "Start a recording using your saved Cap settings", + "mode": "no-view" + }, { "name": "stop-recording", "title": "Stop Recording", diff --git a/extensions/raycast/src/lib/deeplink.ts b/extensions/raycast/src/lib/deeplink.ts new file mode 100644 index 0000000000..5ed00a8b90 --- /dev/null +++ b/extensions/raycast/src/lib/deeplink.ts @@ -0,0 +1,19 @@ +import { closeMainWindow, open, showHUD } from "@raycast/api"; + +type DeepLinkAction = string | Record; + +export async function runDeepLinkAction( + action: DeepLinkAction, + successMessage: string, +) { + const value = JSON.stringify(action); + const deeplink = `cap-desktop://action?value=${encodeURIComponent(value)}`; + await closeMainWindow(); + + try { + await open(deeplink); + await showHUD(successMessage); + } catch { + await showHUD("Failed to open Cap — make sure it is installed and running"); + } +} diff --git a/extensions/raycast/src/open-settings.ts b/extensions/raycast/src/open-settings.ts new file mode 100644 index 0000000000..5bbafff8ff --- /dev/null +++ b/extensions/raycast/src/open-settings.ts @@ -0,0 +1,8 @@ +import { runDeepLinkAction } from "./lib/deeplink"; + +export default async function openSettings() { + await runDeepLinkAction( + { open_settings: { page: null } }, + "Settings opened", + ); +} diff --git a/extensions/raycast/src/pause-recording.ts b/extensions/raycast/src/pause-recording.ts new file mode 100644 index 0000000000..a224fe4346 --- /dev/null +++ b/extensions/raycast/src/pause-recording.ts @@ -0,0 +1,5 @@ +import { runDeepLinkAction } from "./lib/deeplink"; + +export default async function pauseRecording() { + await runDeepLinkAction("pause_recording", "Recording paused"); +} diff --git a/extensions/raycast/src/restart-recording.ts b/extensions/raycast/src/restart-recording.ts new file mode 100644 index 0000000000..93fe43bb60 --- /dev/null +++ b/extensions/raycast/src/restart-recording.ts @@ -0,0 +1,5 @@ +import { runDeepLinkAction } from "./lib/deeplink"; + +export default async function restartRecording() { + await runDeepLinkAction("restart_recording", "Recording restarted"); +} diff --git a/extensions/raycast/src/resume-recording.ts b/extensions/raycast/src/resume-recording.ts new file mode 100644 index 0000000000..be93c9ee45 --- /dev/null +++ b/extensions/raycast/src/resume-recording.ts @@ -0,0 +1,5 @@ +import { runDeepLinkAction } from "./lib/deeplink"; + +export default async function resumeRecording() { + await runDeepLinkAction("resume_recording", "Recording resumed"); +} diff --git a/extensions/raycast/src/start-current-recording.ts b/extensions/raycast/src/start-current-recording.ts new file mode 100644 index 0000000000..23b5e03439 --- /dev/null +++ b/extensions/raycast/src/start-current-recording.ts @@ -0,0 +1,8 @@ +import { runDeepLinkAction } from "./lib/deeplink"; + +export default async function startCurrentRecording() { + await runDeepLinkAction( + { start_current_recording: { mode: null } }, + "Recording started with saved settings", + ); +} diff --git a/extensions/raycast/src/start-instant-recording.ts b/extensions/raycast/src/start-instant-recording.ts new file mode 100644 index 0000000000..8e4052a74c --- /dev/null +++ b/extensions/raycast/src/start-instant-recording.ts @@ -0,0 +1,16 @@ +import { runDeepLinkAction } from "./lib/deeplink"; + +export default async function startInstantRecording() { + await runDeepLinkAction( + { + start_recording: { + capture_mode: null, + camera: null, + mic_label: null, + capture_system_audio: false, + mode: "instant", + }, + }, + "Instant recording started", + ); +} diff --git a/extensions/raycast/src/start-studio-recording.ts b/extensions/raycast/src/start-studio-recording.ts new file mode 100644 index 0000000000..7e5e32ea2b --- /dev/null +++ b/extensions/raycast/src/start-studio-recording.ts @@ -0,0 +1,16 @@ +import { runDeepLinkAction } from "./lib/deeplink"; + +export default async function startStudioRecording() { + await runDeepLinkAction( + { + start_recording: { + capture_mode: null, + camera: null, + mic_label: null, + capture_system_audio: false, + mode: "studio", + }, + }, + "Studio recording started", + ); +} diff --git a/extensions/raycast/src/stop-recording.ts b/extensions/raycast/src/stop-recording.ts new file mode 100644 index 0000000000..989569a659 --- /dev/null +++ b/extensions/raycast/src/stop-recording.ts @@ -0,0 +1,5 @@ +import { runDeepLinkAction } from "./lib/deeplink"; + +export default async function stopRecording() { + await runDeepLinkAction("stop_recording", "Recording stopped"); +} diff --git a/extensions/raycast/src/take-screenshot.ts b/extensions/raycast/src/take-screenshot.ts new file mode 100644 index 0000000000..2ec2cb299a --- /dev/null +++ b/extensions/raycast/src/take-screenshot.ts @@ -0,0 +1,8 @@ +import { runDeepLinkAction } from "./lib/deeplink"; + +export default async function takeScreenshot() { + await runDeepLinkAction( + { take_screenshot: { capture_mode: null } }, + "Screenshot taken", + ); +} diff --git a/extensions/raycast/src/toggle-pause-recording.ts b/extensions/raycast/src/toggle-pause-recording.ts new file mode 100644 index 0000000000..bb306ffffa --- /dev/null +++ b/extensions/raycast/src/toggle-pause-recording.ts @@ -0,0 +1,8 @@ +import { runDeepLinkAction } from "./lib/deeplink"; + +export default async function togglePauseRecording() { + await runDeepLinkAction( + "toggle_pause_recording", + "Recording pause toggled", + ); +} diff --git a/apps/raycast/tsconfig.json b/extensions/raycast/tsconfig.json similarity index 100% rename from apps/raycast/tsconfig.json rename to extensions/raycast/tsconfig.json diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 614993f77e..76d6bff551 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - "apps/*" + - "extensions/*" - "packages/*" - "crates/tauri-plugin-*" - "infra" From dffef128017b99b60027368ce2a5cec2b2d6d988 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Wed, 4 Mar 2026 21:02:09 +0700 Subject: [PATCH 08/28] fix: destructure RecordingSettingsStore to avoid partial move --- .../desktop/src-tauri/src/deeplink_actions.rs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 1a12733e9a..17453a930d 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -183,21 +183,27 @@ impl DeepLinkAction { .flatten() .unwrap_or_default(); + let RecordingSettingsStore { + target, + mic_name, + camera_id, + mode: saved_mode, + system_audio, + organization_id, + } = settings; + let state = app.state::>(); - crate::set_mic_input(state.clone(), settings.mic_name).await?; - crate::set_camera_input(app.clone(), state.clone(), settings.camera_id, None) - .await?; + crate::set_mic_input(state.clone(), mic_name).await?; + crate::set_camera_input(app.clone(), state.clone(), camera_id, None).await?; let inputs = StartRecordingInputs { - mode: mode.or(settings.mode).unwrap_or(RecordingMode::Studio), - capture_target: settings.target.unwrap_or_else(|| { - ScreenCaptureTarget::Display { - id: Display::primary().id(), - } + mode: mode.or(saved_mode).unwrap_or(RecordingMode::Studio), + capture_target: target.unwrap_or_else(|| ScreenCaptureTarget::Display { + id: Display::primary().id(), }), - capture_system_audio: settings.system_audio, - organization_id: settings.organization_id, + capture_system_audio: system_audio, + organization_id, }; crate::recording::start_recording(app.clone(), state, inputs) From 8c8538a5cb63ea8c74db040810e843cf99f1a042 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Wed, 4 Mar 2026 21:11:19 +0700 Subject: [PATCH 09/28] fix: address all PR review feedback - Case-insensitive display/window name matching (eq_ignore_ascii_case) - Camera permission check before listing cameras - Display::list() fallback instead of Display::primary() for headless safety - Log settings read errors with inspect_err before fallback - Move closeMainWindow() inside try/catch block - Fix DEEPLINKS.md: URL-encode unit example, fix nested shell quoting - Use start_current_recording for instant/studio commands (respects saved settings) - Soften HUD messages to 'dispatched' (deeplinks are fire-and-forget) - Update command descriptions to clarify they use saved settings --- apps/desktop/src-tauri/DEEPLINKS.md | 8 ++--- .../desktop/src-tauri/src/deeplink_actions.rs | 35 ++++++++++++------- extensions/raycast/package.json | 4 +-- extensions/raycast/src/lib/deeplink.ts | 3 +- extensions/raycast/src/pause-recording.ts | 2 +- extensions/raycast/src/restart-recording.ts | 2 +- extensions/raycast/src/resume-recording.ts | 2 +- .../raycast/src/start-current-recording.ts | 2 +- .../raycast/src/start-instant-recording.ts | 12 ++----- .../raycast/src/start-studio-recording.ts | 12 ++----- extensions/raycast/src/stop-recording.ts | 2 +- extensions/raycast/src/take-screenshot.ts | 2 +- .../raycast/src/toggle-pause-recording.ts | 2 +- 13 files changed, 40 insertions(+), 48 deletions(-) diff --git a/apps/desktop/src-tauri/DEEPLINKS.md b/apps/desktop/src-tauri/DEEPLINKS.md index 79c5d3722c..88f55f4264 100644 --- a/apps/desktop/src-tauri/DEEPLINKS.md +++ b/apps/desktop/src-tauri/DEEPLINKS.md @@ -15,7 +15,7 @@ cap-desktop://action?value= The JSON value is a quoted string: ``` -cap-desktop://action?value="stop_recording" +cap-desktop://action?value=%22stop_recording%22 ``` ### Parameterized actions @@ -117,13 +117,13 @@ cap-desktop://action?value={"start_recording":{"capture_mode":null,"camera":null Start a studio recording on the primary display: ```bash -open "cap-desktop://action?value=$(python3 -c "import urllib.parse, json; print(urllib.parse.quote(json.dumps({'start_recording':{'capture_mode':None,'camera':None,'mic_label':None,'capture_system_audio':False,'mode':'studio'}})))")" +open "cap-desktop://action?value=$(python3 -c 'import urllib.parse, json; print(urllib.parse.quote(json.dumps({"start_recording":{"capture_mode":None,"camera":None,"mic_label":None,"capture_system_audio":False,"mode":"studio"}})))')" ``` Start a recording using saved app settings: ```bash -open "cap-desktop://action?value=$(python3 -c "import urllib.parse, json; print(urllib.parse.quote(json.dumps({'start_current_recording':{'mode':None}})))")" +open "cap-desktop://action?value=$(python3 -c 'import urllib.parse, json; print(urllib.parse.quote(json.dumps({"start_current_recording":{"mode":None}})))')" ``` Stop a recording: @@ -135,7 +135,7 @@ open "cap-desktop://action?value=%22stop_recording%22" Take a screenshot: ```bash -open "cap-desktop://action?value=$(python3 -c "import urllib.parse, json; print(urllib.parse.quote(json.dumps({'take_screenshot':{'capture_mode':None}})))")" +open "cap-desktop://action?value=$(python3 -c 'import urllib.parse, json; print(urllib.parse.quote(json.dumps({"take_screenshot":{"capture_mode":None}})))')" ``` List available microphones (copies JSON to clipboard): diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 17453a930d..1be4422323 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -9,8 +9,8 @@ use tauri_plugin_clipboard_manager::ClipboardExt; use tracing::trace; use crate::{ - App, ArcLock, recording::StartRecordingInputs, recording_settings::RecordingSettingsStore, - windows::ShowCapWindow, + App, ArcLock, permissions, recording::StartRecordingInputs, + recording_settings::RecordingSettingsStore, windows::ShowCapWindow, }; #[derive(Debug, Serialize, Deserialize)] @@ -134,17 +134,25 @@ impl DeepLinkAction { match capture_mode { CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays() .into_iter() - .find(|(s, _)| s.name == name) + .find(|(s, _)| s.name.eq_ignore_ascii_case(name)) .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) .ok_or(format!("No screen with name \"{}\"", name)), CaptureMode::Window(name) => cap_recording::screen_capture::list_windows() .into_iter() - .find(|(w, _)| w.name == name) + .find(|(w, _)| w.name.eq_ignore_ascii_case(name)) .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) .ok_or(format!("No window with name \"{}\"", name)), } } + fn default_display_target() -> Result { + let display = Display::list() + .into_iter() + .next() + .ok_or("No displays found")?; + Ok(ScreenCaptureTarget::Display { id: display.id() }) + } + pub async fn execute(self, app: &AppHandle) -> Result<(), String> { match self { DeepLinkAction::StartRecording { @@ -161,9 +169,7 @@ impl DeepLinkAction { let capture_target = match capture_mode { Some(mode) => Self::resolve_capture_target(&mode)?, - None => ScreenCaptureTarget::Display { - id: Display::primary().id(), - }, + None => Self::default_display_target()?, }; let inputs = StartRecordingInputs { @@ -179,6 +185,7 @@ impl DeepLinkAction { } DeepLinkAction::StartCurrentRecording { mode } => { let settings = RecordingSettingsStore::get(app) + .inspect_err(|e| eprintln!("Failed to read recording settings: {e}")) .ok() .flatten() .unwrap_or_default(); @@ -199,9 +206,10 @@ impl DeepLinkAction { let inputs = StartRecordingInputs { mode: mode.or(saved_mode).unwrap_or(RecordingMode::Studio), - capture_target: target.unwrap_or_else(|| ScreenCaptureTarget::Display { - id: Display::primary().id(), - }), + capture_target: match target { + Some(t) => t, + None => Self::default_display_target()?, + }, capture_system_audio: system_audio, organization_id, }; @@ -230,9 +238,7 @@ impl DeepLinkAction { DeepLinkAction::TakeScreenshot { capture_mode } => { let target = match capture_mode { Some(mode) => Self::resolve_capture_target(&mode)?, - None => ScreenCaptureTarget::Display { - id: Display::primary().id(), - }, + None => Self::default_display_target()?, }; crate::recording::take_screenshot(app.clone(), target) @@ -240,6 +246,9 @@ impl DeepLinkAction { .map(|_| ()) } DeepLinkAction::ListCameras => { + if !permissions::do_permissions_check(false).camera.permitted() { + return Err("Camera permission not granted".to_string()); + } let cameras = crate::recording::list_cameras(); let json = serde_json::to_string(&cameras).map_err(|e| e.to_string())?; app.clipboard() diff --git a/extensions/raycast/package.json b/extensions/raycast/package.json index 0c2fd27b45..2e490d7f34 100644 --- a/extensions/raycast/package.json +++ b/extensions/raycast/package.json @@ -11,13 +11,13 @@ { "name": "start-instant-recording", "title": "Start Instant Recording", - "description": "Start an instant screen recording with Cap", + "description": "Start an instant screen recording using your saved Cap settings", "mode": "no-view" }, { "name": "start-studio-recording", "title": "Start Studio Recording", - "description": "Start a studio screen recording with Cap", + "description": "Start a studio screen recording using your saved Cap settings", "mode": "no-view" }, { diff --git a/extensions/raycast/src/lib/deeplink.ts b/extensions/raycast/src/lib/deeplink.ts index 5ed00a8b90..714d14e159 100644 --- a/extensions/raycast/src/lib/deeplink.ts +++ b/extensions/raycast/src/lib/deeplink.ts @@ -8,9 +8,8 @@ export async function runDeepLinkAction( ) { const value = JSON.stringify(action); const deeplink = `cap-desktop://action?value=${encodeURIComponent(value)}`; - await closeMainWindow(); - try { + await closeMainWindow(); await open(deeplink); await showHUD(successMessage); } catch { diff --git a/extensions/raycast/src/pause-recording.ts b/extensions/raycast/src/pause-recording.ts index a224fe4346..052685f7ec 100644 --- a/extensions/raycast/src/pause-recording.ts +++ b/extensions/raycast/src/pause-recording.ts @@ -1,5 +1,5 @@ import { runDeepLinkAction } from "./lib/deeplink"; export default async function pauseRecording() { - await runDeepLinkAction("pause_recording", "Recording paused"); + await runDeepLinkAction("pause_recording", "Pause recording dispatched"); } diff --git a/extensions/raycast/src/restart-recording.ts b/extensions/raycast/src/restart-recording.ts index 93fe43bb60..77bf59a54d 100644 --- a/extensions/raycast/src/restart-recording.ts +++ b/extensions/raycast/src/restart-recording.ts @@ -1,5 +1,5 @@ import { runDeepLinkAction } from "./lib/deeplink"; export default async function restartRecording() { - await runDeepLinkAction("restart_recording", "Recording restarted"); + await runDeepLinkAction("restart_recording", "Restart recording dispatched"); } diff --git a/extensions/raycast/src/resume-recording.ts b/extensions/raycast/src/resume-recording.ts index be93c9ee45..5554d51f31 100644 --- a/extensions/raycast/src/resume-recording.ts +++ b/extensions/raycast/src/resume-recording.ts @@ -1,5 +1,5 @@ import { runDeepLinkAction } from "./lib/deeplink"; export default async function resumeRecording() { - await runDeepLinkAction("resume_recording", "Recording resumed"); + await runDeepLinkAction("resume_recording", "Resume recording dispatched"); } diff --git a/extensions/raycast/src/start-current-recording.ts b/extensions/raycast/src/start-current-recording.ts index 23b5e03439..e501d972fd 100644 --- a/extensions/raycast/src/start-current-recording.ts +++ b/extensions/raycast/src/start-current-recording.ts @@ -3,6 +3,6 @@ import { runDeepLinkAction } from "./lib/deeplink"; export default async function startCurrentRecording() { await runDeepLinkAction( { start_current_recording: { mode: null } }, - "Recording started with saved settings", + "Recording dispatched with saved settings", ); } diff --git a/extensions/raycast/src/start-instant-recording.ts b/extensions/raycast/src/start-instant-recording.ts index 8e4052a74c..3203c0dd81 100644 --- a/extensions/raycast/src/start-instant-recording.ts +++ b/extensions/raycast/src/start-instant-recording.ts @@ -2,15 +2,7 @@ import { runDeepLinkAction } from "./lib/deeplink"; export default async function startInstantRecording() { await runDeepLinkAction( - { - start_recording: { - capture_mode: null, - camera: null, - mic_label: null, - capture_system_audio: false, - mode: "instant", - }, - }, - "Instant recording started", + { start_current_recording: { mode: "instant" } }, + "Instant recording dispatched", ); } diff --git a/extensions/raycast/src/start-studio-recording.ts b/extensions/raycast/src/start-studio-recording.ts index 7e5e32ea2b..a685368f1e 100644 --- a/extensions/raycast/src/start-studio-recording.ts +++ b/extensions/raycast/src/start-studio-recording.ts @@ -2,15 +2,7 @@ import { runDeepLinkAction } from "./lib/deeplink"; export default async function startStudioRecording() { await runDeepLinkAction( - { - start_recording: { - capture_mode: null, - camera: null, - mic_label: null, - capture_system_audio: false, - mode: "studio", - }, - }, - "Studio recording started", + { start_current_recording: { mode: "studio" } }, + "Studio recording dispatched", ); } diff --git a/extensions/raycast/src/stop-recording.ts b/extensions/raycast/src/stop-recording.ts index 989569a659..a3ae9b5be2 100644 --- a/extensions/raycast/src/stop-recording.ts +++ b/extensions/raycast/src/stop-recording.ts @@ -1,5 +1,5 @@ import { runDeepLinkAction } from "./lib/deeplink"; export default async function stopRecording() { - await runDeepLinkAction("stop_recording", "Recording stopped"); + await runDeepLinkAction("stop_recording", "Stop recording dispatched"); } diff --git a/extensions/raycast/src/take-screenshot.ts b/extensions/raycast/src/take-screenshot.ts index 2ec2cb299a..2c9967f313 100644 --- a/extensions/raycast/src/take-screenshot.ts +++ b/extensions/raycast/src/take-screenshot.ts @@ -3,6 +3,6 @@ import { runDeepLinkAction } from "./lib/deeplink"; export default async function takeScreenshot() { await runDeepLinkAction( { take_screenshot: { capture_mode: null } }, - "Screenshot taken", + "Screenshot dispatched", ); } diff --git a/extensions/raycast/src/toggle-pause-recording.ts b/extensions/raycast/src/toggle-pause-recording.ts index bb306ffffa..b350e0a904 100644 --- a/extensions/raycast/src/toggle-pause-recording.ts +++ b/extensions/raycast/src/toggle-pause-recording.ts @@ -3,6 +3,6 @@ import { runDeepLinkAction } from "./lib/deeplink"; export default async function togglePauseRecording() { await runDeepLinkAction( "toggle_pause_recording", - "Recording pause toggled", + "Toggle pause dispatched", ); } From 25ee74646496e9f5adb5e5f637e8f6d90160f4cc Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Wed, 4 Mar 2026 21:16:40 +0700 Subject: [PATCH 10/28] fix: use Display::primary() for default capture target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns implementation with docs — capture_mode: null defaults to the primary display, so use the explicit Display::primary() API. --- apps/desktop/src-tauri/src/deeplink_actions.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 1be4422323..b6f9c42b96 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -146,11 +146,9 @@ impl DeepLinkAction { } fn default_display_target() -> Result { - let display = Display::list() - .into_iter() - .next() - .ok_or("No displays found")?; - Ok(ScreenCaptureTarget::Display { id: display.id() }) + Ok(ScreenCaptureTarget::Display { + id: Display::primary().id(), + }) } pub async fn execute(self, app: &AppHandle) -> Result<(), String> { From 50bd1acdb5448163d976b901f8ca51985723c79a Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Wed, 4 Mar 2026 21:45:44 +0700 Subject: [PATCH 11/28] fix: address review feedback for deeplinks and Raycast extension --- apps/desktop/src-tauri/DEEPLINKS.md | 96 +++++++++---------- .../desktop/src-tauri/src/deeplink_actions.rs | 21 +++- extensions/raycast/package.json | 8 +- extensions/raycast/src/open-settings.ts | 7 +- 4 files changed, 72 insertions(+), 60 deletions(-) diff --git a/apps/desktop/src-tauri/DEEPLINKS.md b/apps/desktop/src-tauri/DEEPLINKS.md index 88f55f4264..e9aa477b1c 100644 --- a/apps/desktop/src-tauri/DEEPLINKS.md +++ b/apps/desktop/src-tauri/DEEPLINKS.md @@ -20,7 +20,7 @@ cap-desktop://action?value=%22stop_recording%22 ### Parameterized actions -The JSON value is an object keyed by the action name: +The JSON value is an object keyed by the action name (shown unencoded for readability; URL-encode the JSON in actual deeplinks): ``` cap-desktop://action?value={"start_recording":{"capture_mode":null,"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"studio"}} @@ -30,87 +30,87 @@ cap-desktop://action?value={"start_recording":{"capture_mode":null,"camera":null ### Recording Controls -| Action | Type | Description | -|---|---|---| -| `start_recording` | Parameterized | Start a new recording with explicit settings | +| Action | Type | Description | +| ------------------------- | ------------- | --------------------------------------------------- | +| `start_recording` | Parameterized | Start a new recording with explicit settings | | `start_current_recording` | Parameterized | Start a recording using saved settings from the app | -| `stop_recording` | Unit | Stop the current recording | -| `pause_recording` | Unit | Pause the current recording | -| `resume_recording` | Unit | Resume a paused recording | -| `toggle_pause_recording` | Unit | Toggle pause/resume on the current recording | -| `restart_recording` | Unit | Restart the current recording | +| `stop_recording` | Unit | Stop the current recording | +| `pause_recording` | Unit | Pause the current recording | +| `resume_recording` | Unit | Resume a paused recording | +| `toggle_pause_recording` | Unit | Toggle pause/resume on the current recording | +| `restart_recording` | Unit | Restart the current recording | ### Screenshots -| Action | Type | Description | -|---|---|---| +| Action | Type | Description | +| ----------------- | ------------- | -------------------- | | `take_screenshot` | Parameterized | Capture a screenshot | ### Device Management -| Action | Type | Description | -|---|---|---| -| `list_cameras` | Unit | Copy available cameras as JSON to clipboard | -| `set_camera` | Parameterized | Set the active camera | -| `list_microphones` | Unit | Copy available microphones as JSON to clipboard | -| `set_microphone` | Parameterized | Set the active microphone | -| `list_displays` | Unit | Copy available displays as JSON to clipboard | -| `list_windows` | Unit | Copy available windows as JSON to clipboard | +| Action | Type | Description | +| ------------------ | ------------- | ----------------------------------------------- | +| `list_cameras` | Unit | Copy available cameras as JSON to clipboard | +| `set_camera` | Parameterized | Set the active camera | +| `list_microphones` | Unit | Copy available microphones as JSON to clipboard | +| `set_microphone` | Parameterized | Set the active microphone | +| `list_displays` | Unit | Copy available displays as JSON to clipboard | +| `list_windows` | Unit | Copy available windows as JSON to clipboard | ### Other -| Action | Type | Description | -|---|---|---| -| `open_editor` | Parameterized | Open a project in the editor | -| `open_settings` | Parameterized | Open the settings window | +| Action | Type | Description | +| --------------- | ------------- | ---------------------------- | +| `open_editor` | Parameterized | Open a project in the editor | +| `open_settings` | Parameterized | Open the settings window | ## Action Parameters ### `start_recording` -| Field | Type | Required | Description | -|---|---|---|---| -| `capture_mode` | `null` \| `{"screen":""}` \| `{"window":""}` | Yes | Target to capture. `null` uses the primary display. | -| `camera` | `null` \| device ID object | Yes | Camera device. `null` disables the camera. | -| `mic_label` | `null` \| `string` | Yes | Microphone label. `null` disables the microphone. | -| `capture_system_audio` | `boolean` | Yes | Whether to capture system audio. | -| `mode` | `"studio"` \| `"instant"` | Yes | Recording mode. | +| Field | Type | Required | Description | +| ---------------------- | -------------------------------------------------------- | -------- | ---------------------------------------------------------------------- | +| `capture_mode` | `null` \| `{"screen":""}` \| `{"window":""}` | No | Target to capture. Defaults to primary display when omitted or `null`. | +| `camera` | `null` \| device ID object | No | Camera device. Defaults to no camera when omitted or `null`. | +| `mic_label` | `null` \| `string` | No | Microphone label. Defaults to no microphone when omitted or `null`. | +| `capture_system_audio` | `boolean` | Yes | Whether to capture system audio. | +| `mode` | `"studio"` \| `"instant"` | Yes | Recording mode. | ### `start_current_recording` -| Field | Type | Required | Description | -|---|---|---|---| -| `mode` | `null` \| `"studio"` \| `"instant"` | Yes | Override the saved recording mode. `null` uses the saved mode (defaults to studio). | +| Field | Type | Required | Description | +| ------ | ----------------------------------- | -------- | ----------------------------------------------------------------------------------- | +| `mode` | `null` \| `"studio"` \| `"instant"` | Yes | Override the saved recording mode. `null` uses the saved mode (defaults to studio). | ### `take_screenshot` -| Field | Type | Required | Description | -|---|---|---|---| -| `capture_mode` | `null` \| `{"screen":""}` \| `{"window":""}` | Yes | Target to capture. `null` uses the primary display. | +| Field | Type | Required | Description | +| -------------- | -------------------------------------------------------- | -------- | --------------------------------------------------- | +| `capture_mode` | `null` \| `{"screen":""}` \| `{"window":""}` | Yes | Target to capture. `null` uses the primary display. | ### `set_camera` -| Field | Type | Required | Description | -|---|---|---|---| -| `id` | `null` \| device ID object | Yes | Camera to activate. `null` disables the camera. | +| Field | Type | Required | Description | +| ----- | -------------------------- | -------- | ----------------------------------------------- | +| `id` | `null` \| device ID object | Yes | Camera to activate. `null` disables the camera. | ### `set_microphone` -| Field | Type | Required | Description | -|---|---|---|---| -| `label` | `null` \| `string` | Yes | Microphone label. `null` disables the microphone. | +| Field | Type | Required | Description | +| ------- | ------------------ | -------- | ------------------------------------------------- | +| `label` | `null` \| `string` | Yes | Microphone label. `null` disables the microphone. | ### `open_editor` -| Field | Type | Required | Description | -|---|---|---|---| -| `project_path` | `string` | Yes | Absolute path to the project directory. | +| Field | Type | Required | Description | +| -------------- | -------- | -------- | --------------------------------------- | +| `project_path` | `string` | Yes | Absolute path to the project directory. | ### `open_settings` -| Field | Type | Required | Description | -|---|---|---|---| -| `page` | `null` \| `string` | Yes | Settings page to open. `null` opens the default page. | +| Field | Type | Required | Description | +| ------ | ------------------ | -------- | ----------------------------------------------------- | +| `page` | `null` \| `string` | Yes | Settings page to open. `null` opens the default page. | ## Examples diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index b6f9c42b96..7fd934dd3a 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -1,7 +1,6 @@ use cap_recording::{ RecordingMode, feeds::camera::DeviceOrModelID, sources::screen_capture::ScreenCaptureTarget, }; -use scap_targets::Display; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager, Url}; @@ -146,9 +145,11 @@ impl DeepLinkAction { } fn default_display_target() -> Result { - Ok(ScreenCaptureTarget::Display { - id: Display::primary().id(), - }) + cap_recording::screen_capture::list_displays() + .into_iter() + .next() + .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) + .ok_or_else(|| "No displays found".to_string()) } pub async fn execute(self, app: &AppHandle) -> Result<(), String> { @@ -273,6 +274,12 @@ impl DeepLinkAction { crate::set_mic_input(state, label).await } DeepLinkAction::ListDisplays => { + if !permissions::do_permissions_check(false) + .screen_recording + .permitted() + { + return Err("Screen recording permission not granted".to_string()); + } let displays = crate::recording::list_capture_displays().await; let json = serde_json::to_string(&displays).map_err(|e| e.to_string())?; app.clipboard() @@ -281,6 +288,12 @@ impl DeepLinkAction { Ok(()) } DeepLinkAction::ListWindows => { + if !permissions::do_permissions_check(false) + .screen_recording + .permitted() + { + return Err("Screen recording permission not granted".to_string()); + } let windows = crate::recording::list_capture_windows().await; let json = serde_json::to_string(&windows).map_err(|e| e.to_string())?; app.clipboard() diff --git a/extensions/raycast/package.json b/extensions/raycast/package.json index 2e490d7f34..65f0e94d82 100644 --- a/extensions/raycast/package.json +++ b/extensions/raycast/package.json @@ -5,7 +5,10 @@ "description": "Control Cap screen recorder — start, stop, pause, resume recordings, take screenshots, and manage devices.", "icon": "cap-icon.png", "author": "capsoftware", - "categories": ["Productivity", "Developer Tools"], + "categories": [ + "Productivity", + "Developer Tools" + ], "license": "MIT", "commands": [ { @@ -70,8 +73,7 @@ } ], "dependencies": { - "@raycast/api": "^1.93.3", - "@raycast/utils": "^1.19.1" + "@raycast/api": "^1.93.3" }, "devDependencies": { "@raycast/eslint-config": "^1.0.11", diff --git a/extensions/raycast/src/open-settings.ts b/extensions/raycast/src/open-settings.ts index 5bbafff8ff..833bc935d6 100644 --- a/extensions/raycast/src/open-settings.ts +++ b/extensions/raycast/src/open-settings.ts @@ -1,8 +1,5 @@ -import { runDeepLinkAction } from "./lib/deeplink"; +import { runDeepLinkAction } from './lib/deeplink'; export default async function openSettings() { - await runDeepLinkAction( - { open_settings: { page: null } }, - "Settings opened", - ); + await runDeepLinkAction({ open_settings: { page: null } }, 'Open settings dispatched'); } From eb713c163b1e70635a21bf648a52cceddf7962cf Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Thu, 5 Mar 2026 11:16:10 +0700 Subject: [PATCH 12/28] fix: use Display::primary() for default target with empty-list guard --- apps/desktop/src-tauri/src/deeplink_actions.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 7fd934dd3a..8a0e9c8251 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -1,6 +1,7 @@ use cap_recording::{ RecordingMode, feeds::camera::DeviceOrModelID, sources::screen_capture::ScreenCaptureTarget, }; +use scap_targets::Display; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager, Url}; @@ -145,11 +146,12 @@ impl DeepLinkAction { } fn default_display_target() -> Result { - cap_recording::screen_capture::list_displays() - .into_iter() - .next() - .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) - .ok_or_else(|| "No displays found".to_string()) + if cap_recording::screen_capture::list_displays().is_empty() { + return Err("No displays found".to_string()); + } + Ok(ScreenCaptureTarget::Display { + id: Display::primary().id(), + }) } pub async fn execute(self, app: &AppHandle) -> Result<(), String> { From cc849a4f949a388910ef6b1582b7d967a3058049 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Thu, 5 Mar 2026 11:18:03 +0700 Subject: [PATCH 13/28] fix: update capture_mode field to be optional and add permission check for screen recording --- apps/desktop/src-tauri/DEEPLINKS.md | 2 +- apps/desktop/src-tauri/src/deeplink_actions.rs | 6 ++++++ extensions/raycast/src/lib/deeplink.ts | 11 ++++------- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src-tauri/DEEPLINKS.md b/apps/desktop/src-tauri/DEEPLINKS.md index e9aa477b1c..00152d3e97 100644 --- a/apps/desktop/src-tauri/DEEPLINKS.md +++ b/apps/desktop/src-tauri/DEEPLINKS.md @@ -86,7 +86,7 @@ cap-desktop://action?value={"start_recording":{"capture_mode":null,"camera":null | Field | Type | Required | Description | | -------------- | -------------------------------------------------------- | -------- | --------------------------------------------------- | -| `capture_mode` | `null` \| `{"screen":""}` \| `{"window":""}` | Yes | Target to capture. `null` uses the primary display. | +| `capture_mode` | `null` \| `{"screen":""}` \| `{"window":""}` | No | Target to capture. `null` uses the primary display. | ### `set_camera` diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 8a0e9c8251..5eab9e38b7 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -237,6 +237,12 @@ impl DeepLinkAction { .map(|_| ()) } DeepLinkAction::TakeScreenshot { capture_mode } => { + if !permissions::do_permissions_check(false) + .screen_recording + .permitted() + { + return Err("Screen recording permission not granted".to_string()); + } let target = match capture_mode { Some(mode) => Self::resolve_capture_target(&mode)?, None => Self::default_display_target()?, diff --git a/extensions/raycast/src/lib/deeplink.ts b/extensions/raycast/src/lib/deeplink.ts index 714d14e159..ead020261a 100644 --- a/extensions/raycast/src/lib/deeplink.ts +++ b/extensions/raycast/src/lib/deeplink.ts @@ -1,18 +1,15 @@ -import { closeMainWindow, open, showHUD } from "@raycast/api"; +import { closeMainWindow, open, showHUD } from '@raycast/api'; type DeepLinkAction = string | Record; -export async function runDeepLinkAction( - action: DeepLinkAction, - successMessage: string, -) { +export async function runDeepLinkAction(action: DeepLinkAction, successMessage: string) { const value = JSON.stringify(action); const deeplink = `cap-desktop://action?value=${encodeURIComponent(value)}`; + await closeMainWindow(); try { - await closeMainWindow(); await open(deeplink); await showHUD(successMessage); } catch { - await showHUD("Failed to open Cap — make sure it is installed and running"); + await showHUD('Failed to open Cap — make sure it is installed and running'); } } From a517229b44a074c7c0a6b7bfdb219753b8202272 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Thu, 5 Mar 2026 11:21:18 +0700 Subject: [PATCH 14/28] Update extensions/raycast/src/lib/deeplink.ts Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com> --- extensions/raycast/src/lib/deeplink.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/raycast/src/lib/deeplink.ts b/extensions/raycast/src/lib/deeplink.ts index ead020261a..becb09bc7c 100644 --- a/extensions/raycast/src/lib/deeplink.ts +++ b/extensions/raycast/src/lib/deeplink.ts @@ -5,7 +5,7 @@ type DeepLinkAction = string | Record; export async function runDeepLinkAction(action: DeepLinkAction, successMessage: string) { const value = JSON.stringify(action); const deeplink = `cap-desktop://action?value=${encodeURIComponent(value)}`; - await closeMainWindow(); + await closeMainWindow().catch(() => {}); try { await open(deeplink); await showHUD(successMessage); From 5dcca4aaa767c11e40a2956f39863f042122028c Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Thu, 5 Mar 2026 11:21:55 +0700 Subject: [PATCH 15/28] Update apps/desktop/src-tauri/src/deeplink_actions.rs Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com> --- apps/desktop/src-tauri/src/deeplink_actions.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 5eab9e38b7..7cfaa198e7 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -237,6 +237,12 @@ impl DeepLinkAction { .map(|_| ()) } DeepLinkAction::TakeScreenshot { capture_mode } => { + if !permissions::do_permissions_check(false) + .screen_recording + .permitted() + { + return Err("Screen recording permission not granted".to_string()); + } if !permissions::do_permissions_check(false) .screen_recording .permitted() From 84b584cd93ad73c8c819753851410883800005a7 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Thu, 5 Mar 2026 11:24:28 +0700 Subject: [PATCH 16/28] fix: remove redundant screen recording permission check in TakeScreenshot action --- apps/desktop/src-tauri/src/deeplink_actions.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 7cfaa198e7..5eab9e38b7 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -237,12 +237,6 @@ impl DeepLinkAction { .map(|_| ()) } DeepLinkAction::TakeScreenshot { capture_mode } => { - if !permissions::do_permissions_check(false) - .screen_recording - .permitted() - { - return Err("Screen recording permission not granted".to_string()); - } if !permissions::do_permissions_check(false) .screen_recording .permitted() From 220f8a028322c69d5fb4c82dcc8f48ca19924e91 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Thu, 5 Mar 2026 11:24:59 +0700 Subject: [PATCH 17/28] Update extensions/raycast/src/lib/deeplink.ts Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com> --- extensions/raycast/src/lib/deeplink.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/raycast/src/lib/deeplink.ts b/extensions/raycast/src/lib/deeplink.ts index becb09bc7c..008629b565 100644 --- a/extensions/raycast/src/lib/deeplink.ts +++ b/extensions/raycast/src/lib/deeplink.ts @@ -5,7 +5,7 @@ type DeepLinkAction = string | Record; export async function runDeepLinkAction(action: DeepLinkAction, successMessage: string) { const value = JSON.stringify(action); const deeplink = `cap-desktop://action?value=${encodeURIComponent(value)}`; - await closeMainWindow().catch(() => {}); + await closeMainWindow().catch(() => undefined); try { await open(deeplink); await showHUD(successMessage); From 097ce4bc26be53d94da94d8da633a9d84b733b46 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Thu, 5 Mar 2026 11:27:55 +0700 Subject: [PATCH 18/28] Update extensions/raycast/src/pause-recording.ts Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com> --- extensions/raycast/src/pause-recording.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/raycast/src/pause-recording.ts b/extensions/raycast/src/pause-recording.ts index 052685f7ec..733ff05c03 100644 --- a/extensions/raycast/src/pause-recording.ts +++ b/extensions/raycast/src/pause-recording.ts @@ -1,5 +1,6 @@ -import { runDeepLinkAction } from "./lib/deeplink"; +import { runDeepLinkAction } from './lib/deeplink'; export default async function pauseRecording() { - await runDeepLinkAction("pause_recording", "Pause recording dispatched"); + await runDeepLinkAction('pause_recording', 'Pause recording dispatched'); +} } From 724f818429e5ecfa6ac95209e40692d329e78d42 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Thu, 5 Mar 2026 11:32:19 +0700 Subject: [PATCH 19/28] fix: add microphone permission checks in ListMicrophones and SetMicrophone actions --- apps/desktop/src-tauri/src/deeplink_actions.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 5eab9e38b7..0f1e650ef5 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -268,6 +268,12 @@ impl DeepLinkAction { crate::set_camera_input(app.clone(), state, id, None).await } DeepLinkAction::ListMicrophones => { + if !permissions::do_permissions_check(false) + .microphone + .permitted() + { + return Err("Microphone permission not granted".to_string()); + } let mics = cap_recording::feeds::microphone::MicrophoneFeed::list(); let mut labels: Vec = mics.keys().cloned().collect(); labels.sort(); @@ -278,6 +284,12 @@ impl DeepLinkAction { Ok(()) } DeepLinkAction::SetMicrophone { label } => { + if !permissions::do_permissions_check(false) + .microphone + .permitted() + { + return Err("Microphone permission not granted".to_string()); + } let state = app.state::>(); crate::set_mic_input(state, label).await } From 054798275251cef9bfbb508fab50036d34b1d55b Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Thu, 5 Mar 2026 11:39:10 +0700 Subject: [PATCH 20/28] fix: update required fields in DEEPLINKS.md and standardize quotes in pause-recording.ts --- apps/desktop/src-tauri/DEEPLINKS.md | 4 ++-- extensions/raycast/src/pause-recording.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src-tauri/DEEPLINKS.md b/apps/desktop/src-tauri/DEEPLINKS.md index 00152d3e97..4585a63b75 100644 --- a/apps/desktop/src-tauri/DEEPLINKS.md +++ b/apps/desktop/src-tauri/DEEPLINKS.md @@ -80,7 +80,7 @@ cap-desktop://action?value={"start_recording":{"capture_mode":null,"camera":null | Field | Type | Required | Description | | ------ | ----------------------------------- | -------- | ----------------------------------------------------------------------------------- | -| `mode` | `null` \| `"studio"` \| `"instant"` | Yes | Override the saved recording mode. `null` uses the saved mode (defaults to studio). | +| `mode` | `null` \| `"studio"` \| `"instant"` | No | Override the saved recording mode. `null` uses the saved mode (defaults to studio). | ### `take_screenshot` @@ -110,7 +110,7 @@ cap-desktop://action?value={"start_recording":{"capture_mode":null,"camera":null | Field | Type | Required | Description | | ------ | ------------------ | -------- | ----------------------------------------------------- | -| `page` | `null` \| `string` | Yes | Settings page to open. `null` opens the default page. | +| `page` | `null` \| `string` | No | Settings page to open. `null` opens the default page. | ## Examples diff --git a/extensions/raycast/src/pause-recording.ts b/extensions/raycast/src/pause-recording.ts index 733ff05c03..052685f7ec 100644 --- a/extensions/raycast/src/pause-recording.ts +++ b/extensions/raycast/src/pause-recording.ts @@ -1,6 +1,5 @@ -import { runDeepLinkAction } from './lib/deeplink'; +import { runDeepLinkAction } from "./lib/deeplink"; export default async function pauseRecording() { - await runDeepLinkAction('pause_recording', 'Pause recording dispatched'); -} + await runDeepLinkAction("pause_recording", "Pause recording dispatched"); } From 8e704e652dbd969fe1abb86af616c33e05e93b3c Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Fri, 6 Mar 2026 14:48:32 +0700 Subject: [PATCH 21/28] fix: add permission guards to recording actions and improve Raycast UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add screen recording, camera, and microphone permission checks to StartRecording and StartCurrentRecording, consistent with TakeScreenshot, SetCamera, and SetMicrophone - Add camera permission guard to SetCamera for consistency with ListCameras and SetMicrophone - Fix error message in deeplink.ts: macOS auto-launches installed apps so "running" is not required — only "installed" - Clarify command descriptions in package.json to distinguish instant/studio (mode override, saved device settings) from start-current (all saved settings) - Fix quote style inconsistency in open-settings.ts - Improve toggle-pause HUD message wording --- .../desktop/src-tauri/src/deeplink_actions.rs | 25 +++++++++++++++++++ apps/desktop/src-tauri/src/recording.rs | 1 - extensions/raycast/package.json | 6 ++--- extensions/raycast/src/lib/deeplink.ts | 2 +- extensions/raycast/src/open-settings.ts | 4 +-- .../raycast/src/toggle-pause-recording.ts | 2 +- 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 0f1e650ef5..67468ba89a 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -163,6 +163,17 @@ impl DeepLinkAction { capture_system_audio, mode, } => { + let perms = permissions::do_permissions_check(false); + if !perms.screen_recording.permitted() { + return Err("Screen recording permission not granted".to_string()); + } + if camera.is_some() && !perms.camera.permitted() { + return Err("Camera permission not granted".to_string()); + } + if mic_label.is_some() && !perms.microphone.permitted() { + return Err("Microphone permission not granted".to_string()); + } + let state = app.state::>(); crate::set_camera_input(app.clone(), state.clone(), camera, None).await?; @@ -200,6 +211,17 @@ impl DeepLinkAction { organization_id, } = settings; + let perms = permissions::do_permissions_check(false); + if !perms.screen_recording.permitted() { + return Err("Screen recording permission not granted".to_string()); + } + if camera_id.is_some() && !perms.camera.permitted() { + return Err("Camera permission not granted".to_string()); + } + if mic_name.is_some() && !perms.microphone.permitted() { + return Err("Microphone permission not granted".to_string()); + } + let state = app.state::>(); crate::set_mic_input(state.clone(), mic_name).await?; @@ -264,6 +286,9 @@ impl DeepLinkAction { Ok(()) } DeepLinkAction::SetCamera { id } => { + if !permissions::do_permissions_check(false).camera.permitted() { + return Err("Camera permission not granted".to_string()); + } let state = app.state::>(); crate::set_camera_input(app.clone(), state, id, None).await } diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 4723d0e2be..281a0529c2 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -950,7 +950,6 @@ pub async fn start_recording( e })?; - let progressive_upload = InstantMultipartUpload::spawn( app_handle.clone(), recording_dir.join("content/output.mp4"), diff --git a/extensions/raycast/package.json b/extensions/raycast/package.json index 65f0e94d82..fc298b3cbd 100644 --- a/extensions/raycast/package.json +++ b/extensions/raycast/package.json @@ -14,19 +14,19 @@ { "name": "start-instant-recording", "title": "Start Instant Recording", - "description": "Start an instant screen recording using your saved Cap settings", + "description": "Start an instant recording using your saved Cap device and display settings", "mode": "no-view" }, { "name": "start-studio-recording", "title": "Start Studio Recording", - "description": "Start a studio screen recording using your saved Cap settings", + "description": "Start a studio recording using your saved Cap device and display settings", "mode": "no-view" }, { "name": "start-current-recording", "title": "Start Recording (Saved Settings)", - "description": "Start a recording using your saved Cap settings", + "description": "Start a recording using all of your saved Cap settings, including mode", "mode": "no-view" }, { diff --git a/extensions/raycast/src/lib/deeplink.ts b/extensions/raycast/src/lib/deeplink.ts index 008629b565..39e3ed8ddb 100644 --- a/extensions/raycast/src/lib/deeplink.ts +++ b/extensions/raycast/src/lib/deeplink.ts @@ -10,6 +10,6 @@ export async function runDeepLinkAction(action: DeepLinkAction, successMessage: await open(deeplink); await showHUD(successMessage); } catch { - await showHUD('Failed to open Cap — make sure it is installed and running'); + await showHUD("Failed to open Cap — make sure it is installed"); } } diff --git a/extensions/raycast/src/open-settings.ts b/extensions/raycast/src/open-settings.ts index 833bc935d6..cc63705d01 100644 --- a/extensions/raycast/src/open-settings.ts +++ b/extensions/raycast/src/open-settings.ts @@ -1,5 +1,5 @@ -import { runDeepLinkAction } from './lib/deeplink'; +import { runDeepLinkAction } from "./lib/deeplink"; export default async function openSettings() { - await runDeepLinkAction({ open_settings: { page: null } }, 'Open settings dispatched'); + await runDeepLinkAction({ open_settings: { page: null } }, "Open settings dispatched"); } diff --git a/extensions/raycast/src/toggle-pause-recording.ts b/extensions/raycast/src/toggle-pause-recording.ts index b350e0a904..4b38fa1009 100644 --- a/extensions/raycast/src/toggle-pause-recording.ts +++ b/extensions/raycast/src/toggle-pause-recording.ts @@ -3,6 +3,6 @@ import { runDeepLinkAction } from "./lib/deeplink"; export default async function togglePauseRecording() { await runDeepLinkAction( "toggle_pause_recording", - "Toggle pause dispatched", + "Pause toggle dispatched", ); } From 4cdd3d65ff5edb87cfe279d9204bdd210464b27a Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Fri, 6 Mar 2026 14:59:46 +0700 Subject: [PATCH 22/28] fix: only gate SetCamera/SetMicrophone permission on activation, not disable SetCamera { id: None } and SetMicrophone { label: None } disable the device and require no hardware access. Blocking them behind a permission check prevents users from turning off a device overlay if permission was revoked after a recording session. Mirror the conditional pattern already used in StartRecording. --- apps/desktop/src-tauri/src/deeplink_actions.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 67468ba89a..f9af1935ee 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -286,7 +286,7 @@ impl DeepLinkAction { Ok(()) } DeepLinkAction::SetCamera { id } => { - if !permissions::do_permissions_check(false).camera.permitted() { + if id.is_some() && !permissions::do_permissions_check(false).camera.permitted() { return Err("Camera permission not granted".to_string()); } let state = app.state::>(); @@ -309,9 +309,10 @@ impl DeepLinkAction { Ok(()) } DeepLinkAction::SetMicrophone { label } => { - if !permissions::do_permissions_check(false) - .microphone - .permitted() + if label.is_some() + && !permissions::do_permissions_check(false) + .microphone + .permitted() { return Err("Microphone permission not granted".to_string()); } From bc082041f5aabe187f006c6bdf8dc4d7b1a57af1 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Fri, 6 Mar 2026 15:32:55 +0700 Subject: [PATCH 23/28] fix: include organization ID in StartRecordingInputs for better context --- apps/desktop/src-tauri/src/deeplink_actions.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index f9af1935ee..ad201c0702 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -184,11 +184,16 @@ impl DeepLinkAction { None => Self::default_display_target()?, }; + let organization_id = RecordingSettingsStore::get(app) + .ok() + .flatten() + .and_then(|s| s.organization_id); + let inputs = StartRecordingInputs { mode, capture_target, capture_system_audio, - organization_id: None, + organization_id, }; crate::recording::start_recording(app.clone(), state, inputs) From b0bcc1bacd7b117df736561288f4b5de86196c89 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Fri, 6 Mar 2026 17:38:47 +0700 Subject: [PATCH 24/28] fix: improve error message for failed deep link action in Raycast extension --- apps/desktop/src-tauri/src/deeplink_actions.rs | 1 + extensions/raycast/src/lib/deeplink.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index ad201c0702..f7f64d3e62 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -185,6 +185,7 @@ impl DeepLinkAction { }; let organization_id = RecordingSettingsStore::get(app) + .inspect_err(|e| eprintln!("Failed to read recording settings: {e}")) .ok() .flatten() .and_then(|s| s.organization_id); diff --git a/extensions/raycast/src/lib/deeplink.ts b/extensions/raycast/src/lib/deeplink.ts index 39e3ed8ddb..21fd7e8018 100644 --- a/extensions/raycast/src/lib/deeplink.ts +++ b/extensions/raycast/src/lib/deeplink.ts @@ -10,6 +10,6 @@ export async function runDeepLinkAction(action: DeepLinkAction, successMessage: await open(deeplink); await showHUD(successMessage); } catch { - await showHUD("Failed to open Cap — make sure it is installed"); + await showHUD("Failed to open Cap — make sure it is installed and running"); } } From 3165b74261a1a126fb7f890a570d6a5f658c1ca9 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Fri, 6 Mar 2026 17:49:17 +0700 Subject: [PATCH 25/28] fix: update required fields for camera and microphone in DEEPLINKS.md --- apps/desktop/src-tauri/DEEPLINKS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/DEEPLINKS.md b/apps/desktop/src-tauri/DEEPLINKS.md index 4585a63b75..d561b69856 100644 --- a/apps/desktop/src-tauri/DEEPLINKS.md +++ b/apps/desktop/src-tauri/DEEPLINKS.md @@ -92,13 +92,13 @@ cap-desktop://action?value={"start_recording":{"capture_mode":null,"camera":null | Field | Type | Required | Description | | ----- | -------------------------- | -------- | ----------------------------------------------- | -| `id` | `null` \| device ID object | Yes | Camera to activate. `null` disables the camera. | +| `id` | `null` \| device ID object | No | Camera to activate. `null` disables the camera. | ### `set_microphone` | Field | Type | Required | Description | | ------- | ------------------ | -------- | ------------------------------------------------- | -| `label` | `null` \| `string` | Yes | Microphone label. `null` disables the microphone. | +| `label` | `null` \| `string` | No | Microphone label. `null` disables the microphone. | ### `open_editor` From 68b2560abd3a930a9da3379968a344dd919d0ba2 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Fri, 6 Mar 2026 17:52:31 +0700 Subject: [PATCH 26/28] fix: handle invalid file path in OpenEditor deep link action --- apps/desktop/src-tauri/src/deeplink_actions.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index f7f64d3e62..4fc5d77b13 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -107,7 +107,9 @@ impl TryFrom<&Url> for DeepLinkAction { #[cfg(target_os = "macos")] if url.scheme() == "file" { return Ok(Self::OpenEditor { - project_path: url.to_file_path().unwrap(), + project_path: url + .to_file_path() + .map_err(|_| ActionParseFromUrlError::Invalid)?, }); } From 312152d830bfb55e0cf52f40920d47afbc227dda Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Fri, 6 Mar 2026 20:23:21 +0700 Subject: [PATCH 27/28] fix: add clipboard output formats for cameras, microphones, displays, and windows in DEEPLINKS.md --- apps/desktop/src-tauri/DEEPLINKS.md | 36 +++++++++++++++++++ .../desktop/src-tauri/src/deeplink_actions.rs | 12 +++---- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src-tauri/DEEPLINKS.md b/apps/desktop/src-tauri/DEEPLINKS.md index d561b69856..a5ace1604b 100644 --- a/apps/desktop/src-tauri/DEEPLINKS.md +++ b/apps/desktop/src-tauri/DEEPLINKS.md @@ -100,6 +100,42 @@ cap-desktop://action?value={"start_recording":{"capture_mode":null,"camera":null | ------- | ------------------ | -------- | ------------------------------------------------- | | `label` | `null` \| `string` | No | Microphone label. `null` disables the microphone. | +### `list_cameras` clipboard output + +JSON array of camera objects: + +```json +[{ "device_id": "...", "model_id": "...", "display_name": "..." }] +``` + +Pass `device_id` or `model_id` as the `id` field of `set_camera`. + +### `list_microphones` clipboard output + +JSON array of label strings (sorted): + +```json +["Built-in Microphone", "External Headset"] +``` + +Pass a label string directly as the `label` field of `set_microphone`. + +### `list_displays` clipboard output + +JSON array of display objects: + +```json +[{ "id": 1, "name": "Built-in Retina Display", "refresh_rate": 60 }] +``` + +### `list_windows` clipboard output + +JSON array of window objects: + +```json +[{ "id": 1, "owner_name": "Safari", "name": "Page Title", "bounds": { ... }, "refresh_rate": 60, "bundle_identifier": "com.apple.Safari" }] +``` + ### `open_editor` | Field | Type | Required | Description | diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 4fc5d77b13..c3e779b735 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -1,7 +1,6 @@ use cap_recording::{ RecordingMode, feeds::camera::DeviceOrModelID, sources::screen_capture::ScreenCaptureTarget, }; -use scap_targets::Display; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager, Url}; @@ -148,12 +147,11 @@ impl DeepLinkAction { } fn default_display_target() -> Result { - if cap_recording::screen_capture::list_displays().is_empty() { - return Err("No displays found".to_string()); - } - Ok(ScreenCaptureTarget::Display { - id: Display::primary().id(), - }) + cap_recording::screen_capture::list_displays() + .into_iter() + .next() + .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) + .ok_or_else(|| "No displays found".to_string()) } pub async fn execute(self, app: &AppHandle) -> Result<(), String> { From d377b3438b62d38223de4dd8886a1585335a8686 Mon Sep 17 00:00:00 2001 From: Tedy Fazrin Date: Fri, 6 Mar 2026 20:37:01 +0700 Subject: [PATCH 28/28] fix: update deeplink documentation for set_camera id field and improve error handling in screen/window actions --- apps/desktop/src-tauri/DEEPLINKS.md | 2 +- apps/desktop/src-tauri/src/deeplink_actions.rs | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src-tauri/DEEPLINKS.md b/apps/desktop/src-tauri/DEEPLINKS.md index a5ace1604b..9a13e5aa89 100644 --- a/apps/desktop/src-tauri/DEEPLINKS.md +++ b/apps/desktop/src-tauri/DEEPLINKS.md @@ -108,7 +108,7 @@ JSON array of camera objects: [{ "device_id": "...", "model_id": "...", "display_name": "..." }] ``` -Pass `device_id` or `model_id` as the `id` field of `set_camera`. +Pass `{ "DeviceID": "" }` or `{ "ModelID": "" }` as the `id` field of `set_camera`. ### `list_microphones` clipboard output diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index c3e779b735..64962ac1ff 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -1,6 +1,7 @@ use cap_recording::{ RecordingMode, feeds::camera::DeviceOrModelID, sources::screen_capture::ScreenCaptureTarget, }; +use scap_targets::Display; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager, Url}; @@ -137,20 +138,23 @@ impl DeepLinkAction { .into_iter() .find(|(s, _)| s.name.eq_ignore_ascii_case(name)) .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) - .ok_or(format!("No screen with name \"{}\"", name)), + .ok_or_else(|| format!("No screen with name \"{}\"", name)), CaptureMode::Window(name) => cap_recording::screen_capture::list_windows() .into_iter() .find(|(w, _)| w.name.eq_ignore_ascii_case(name)) .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) - .ok_or(format!("No window with name \"{}\"", name)), + .ok_or_else(|| format!("No window with name \"{}\"", name)), } } fn default_display_target() -> Result { - cap_recording::screen_capture::list_displays() - .into_iter() - .next() - .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) + let primary_id = Display::primary().id(); + let displays = cap_recording::screen_capture::list_displays(); + displays + .iter() + .find(|(s, _)| s.id == primary_id) + .or_else(|| displays.first()) + .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id.clone() }) .ok_or_else(|| "No displays found".to_string()) }