Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fefb099
feat: extend deeplinks support + add Raycast extension\n\nExtends the…
tedy69 Mar 4, 2026
5b02801
fix: address review feedback for deeplinks + Raycast extension
tedy69 Mar 4, 2026
46e42a5
feat: enhance error handling in Raycast commands for better user feed…
tedy69 Mar 4, 2026
9f4f555
feat: simplify display selection for screen capture in deeplink actions
tedy69 Mar 4, 2026
03996ed
fix: update README
tedy69 Mar 4, 2026
f05b959
improve: toast labels and review feedback for Raycast extension
tedy69 Mar 4, 2026
f5ade32
improve: rework deeplinks + Raycast extension quality
tedy69 Mar 4, 2026
dffef12
fix: destructure RecordingSettingsStore to avoid partial move
tedy69 Mar 4, 2026
8c8538a
fix: address all PR review feedback
tedy69 Mar 4, 2026
25ee746
fix: use Display::primary() for default capture target
tedy69 Mar 4, 2026
50bd1ac
fix: address review feedback for deeplinks and Raycast extension
tedy69 Mar 4, 2026
eb713c1
fix: use Display::primary() for default target with empty-list guard
tedy69 Mar 5, 2026
cc849a4
fix: update capture_mode field to be optional and add permission chec…
tedy69 Mar 5, 2026
a517229
Update extensions/raycast/src/lib/deeplink.ts
tedy69 Mar 5, 2026
5dcca4a
Update apps/desktop/src-tauri/src/deeplink_actions.rs
tedy69 Mar 5, 2026
84b584c
fix: remove redundant screen recording permission check in TakeScreen…
tedy69 Mar 5, 2026
220f8a0
Update extensions/raycast/src/lib/deeplink.ts
tedy69 Mar 5, 2026
097ce4b
Update extensions/raycast/src/pause-recording.ts
tedy69 Mar 5, 2026
724f818
fix: add microphone permission checks in ListMicrophones and SetMicro…
tedy69 Mar 5, 2026
a0893ff
Merge branch 'feat/deeplinks-raycast-extension' of https://github.com…
tedy69 Mar 5, 2026
0547982
fix: update required fields in DEEPLINKS.md and standardize quotes in…
tedy69 Mar 5, 2026
8e704e6
fix: add permission guards to recording actions and improve Raycast UX
tedy69 Mar 6, 2026
4cdd3d6
fix: only gate SetCamera/SetMicrophone permission on activation, not …
tedy69 Mar 6, 2026
bc08204
fix: include organization ID in StartRecordingInputs for better context
tedy69 Mar 6, 2026
b0bcc1b
fix: improve error message for failed deep link action in Raycast ext…
tedy69 Mar 6, 2026
3165b74
fix: update required fields for camera and microphone in DEEPLINKS.md
tedy69 Mar 6, 2026
68b2560
fix: handle invalid file path in OpenEditor deep link action
tedy69 Mar 6, 2026
312152d
fix: add clipboard output formats for cameras, microphones, displays,…
tedy69 Mar 6, 2026
d377b34
fix: update deeplink documentation for set_camera id field and improv…
tedy69 Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 112 additions & 13 deletions apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -19,13 +20,30 @@ pub enum CaptureMode {
#[serde(rename_all = "snake_case")]
pub enum DeepLinkAction {
StartRecording {
capture_mode: CaptureMode,
capture_mode: Option<CaptureMode>,
camera: Option<DeviceOrModelID>,
mic_label: Option<String>,
capture_system_audio: bool,
mode: RecordingMode,
},
StopRecording,
PauseRecording,
ResumeRecording,
TogglePauseRecording,
RestartRecording,
TakeScreenshot {
capture_mode: Option<CaptureMode>,
},
ListCameras,
SetCamera {
id: Option<DeviceOrModelID>,
},
ListMicrophones,
SetMicrophone {
label: Option<String>,
},
ListDisplays,
ListWindows,
OpenEditor {
project_path: PathBuf,
},
Expand All @@ -49,7 +67,6 @@ pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) {
ActionParseFromUrlError::Invalid => {
eprintln!("Invalid deep link format \"{}\"", &url)
}
// Likely login action, not handled here.
ActionParseFromUrlError::NotAction => {}
})
.ok()
Expand Down Expand Up @@ -105,6 +122,21 @@ impl TryFrom<&Url> for DeepLinkAction {
}

impl DeepLinkAction {
fn resolve_capture_target(capture_mode: &CaptureMode) -> Result<ScreenCaptureTarget, String> {
match capture_mode {
CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays()
.into_iter()
.find(|(s, _)| s.name == name)
Comment thread
tedy69 marked this conversation as resolved.
Outdated
.map(|(s, _)| ScreenCaptureTarget::Display { id: s.id })
.ok_or(format!("No screen with name \"{}\"", name)),
Comment thread
tedy69 marked this conversation as resolved.
Outdated
CaptureMode::Window(name) => cap_recording::screen_capture::list_windows()
.into_iter()
.find(|(w, _)| w.name == name)
Comment thread
tedy69 marked this conversation as resolved.
Outdated
.map(|(w, _)| ScreenCaptureTarget::Window { id: w.id })
.ok_or(format!("No window with name \"{}\"", name)),
Comment thread
tedy69 marked this conversation as resolved.
Outdated
Comment thread
tedy69 marked this conversation as resolved.
Outdated
}
}

pub async fn execute(self, app: &AppHandle) -> Result<(), String> {
match self {
DeepLinkAction::StartRecording {
Expand All @@ -119,17 +151,14 @@ 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 = 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")?;
Comment thread
tedy69 marked this conversation as resolved.
Outdated
ScreenCaptureTarget::Display { id: display.id }
}
};

let inputs = StartRecordingInputs {
Expand All @@ -146,6 +175,76 @@ 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 } => {
Comment thread
tedy69 marked this conversation as resolved.
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)
Comment thread
tedy69 marked this conversation as resolved.
.await
.map(|_| ())
}
DeepLinkAction::ListCameras => {
let cameras = crate::recording::list_cameras();
Comment thread
tedy69 marked this conversation as resolved.
let json = serde_json::to_string(&cameras).map_err(|e| e.to_string())?;
app.clipboard()
.write_text(&json)
.map_err(|e| e.to_string())?;
Ok(())
}
Comment thread
tedy69 marked this conversation as resolved.
DeepLinkAction::SetCamera { id } => {
let state = app.state::<ArcLock<App>>();
Comment thread
tedy69 marked this conversation as resolved.
crate::set_camera_input(app.clone(), state, id, None).await
}
Comment thread
tedy69 marked this conversation as resolved.
DeepLinkAction::ListMicrophones => {
Comment thread
tedy69 marked this conversation as resolved.
let mics = cap_recording::feeds::microphone::MicrophoneFeed::list();
let labels: Vec<String> = mics.keys().cloned().collect();
Comment thread
tedy69 marked this conversation as resolved.
Outdated
let json = serde_json::to_string(&labels).map_err(|e| e.to_string())?;
app.clipboard()
.write_text(&json)
.map_err(|e| e.to_string())?;
Ok(())
}
DeepLinkAction::SetMicrophone { label } => {
let state = app.state::<ArcLock<App>>();
crate::set_mic_input(state, label).await
}
Comment thread
tedy69 marked this conversation as resolved.
DeepLinkAction::ListDisplays => {
let displays = crate::recording::list_capture_displays().await;
Comment thread
tedy69 marked this conversation as resolved.
let json = serde_json::to_string(&displays).map_err(|e| e.to_string())?;
app.clipboard()
.write_text(&json)
.map_err(|e| e.to_string())?;
Ok(())
}
DeepLinkAction::ListWindows => {
let windows = crate::recording::list_capture_windows().await;
Comment thread
tedy69 marked this conversation as resolved.
let json = serde_json::to_string(&windows).map_err(|e| e.to_string())?;
app.clipboard()
.write_text(&json)
.map_err(|e| e.to_string())?;
Ok(())
Comment thread
tedy69 marked this conversation as resolved.
}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
Expand Down
68 changes: 68 additions & 0 deletions apps/raycast/README.md
Original file line number Diff line number Diff line change
@@ -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"}}
Comment thread
tedy69 marked this conversation as resolved.
Outdated
```

### 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
Comment thread
tedy69 marked this conversation as resolved.
Outdated
```
Binary file added apps/raycast/assets/cap-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 86 additions & 0 deletions apps/raycast/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "cap",
"title": "Cap",
"description": "Control Cap screen recorder — start, stop, pause, resume recordings, take screenshots, and manage devices.",
"icon": "cap-icon.png",
"author": "cap",
Comment thread
tedy69 marked this conversation as resolved.
Outdated
"categories": [
"Productivity",
"Developer Tools"
],
"license": "MIT",
"commands": [
{
"name": "start-instant-recording",
"title": "Start Instant Recording",
"description": "Start an instant screen recording with Cap",
"mode": "no-view"
},
{
"name": "start-studio-recording",
"title": "Start Studio Recording",
"description": "Start a studio screen recording with Cap",
"mode": "no-view"
},
{
"name": "stop-recording",
"title": "Stop Recording",
"description": "Stop the current Cap recording",
"mode": "no-view"
},
{
"name": "pause-recording",
"title": "Pause Recording",
"description": "Pause the current Cap recording",
"mode": "no-view"
},
{
"name": "resume-recording",
"title": "Resume Recording",
"description": "Resume a paused Cap recording",
"mode": "no-view"
},
{
"name": "toggle-pause-recording",
"title": "Toggle Pause Recording",
"description": "Toggle pause/resume on the current Cap recording",
"mode": "no-view"
},
{
"name": "restart-recording",
"title": "Restart Recording",
"description": "Restart the current Cap recording",
"mode": "no-view"
},
{
"name": "take-screenshot",
"title": "Take Screenshot",
"description": "Take a screenshot with Cap",
"mode": "no-view"
},
{
"name": "open-settings",
"title": "Open Settings",
"description": "Open Cap settings",
"mode": "no-view"
}
],
"dependencies": {
"@raycast/api": "^1.93.3",
"@raycast/utils": "^1.19.1"
},
"devDependencies": {
"@raycast/eslint-config": "^1.0.11",
"@types/node": "22.13.14",
"@types/react": "19.0.10",
"eslint": "^9.22.0",
"typescript": "^5.8.2"
},
"scripts": {
"build": "ray build",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint"
}
}
14 changes: 14 additions & 0 deletions apps/raycast/src/open-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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...' });

await dispatchAction({
open_settings: {
page: null,
},
});

await showToast({ style: Toast.Style.Success, title: 'Settings opened' });
}
5 changes: 5 additions & 0 deletions apps/raycast/src/pause-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { fireSimpleAction } from './utils';

export default async function pauseRecording() {
await fireSimpleAction('pause_recording', 'Recording paused');
}
5 changes: 5 additions & 0 deletions apps/raycast/src/restart-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { fireSimpleAction } from './utils';

export default async function restartRecording() {
await fireSimpleAction('restart_recording', 'Recording restarted');
}
5 changes: 5 additions & 0 deletions apps/raycast/src/resume-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { fireSimpleAction } from './utils';

export default async function resumeRecording() {
await fireSimpleAction('resume_recording', 'Recording resumed');
}
18 changes: 18 additions & 0 deletions apps/raycast/src/start-instant-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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...' });

await dispatchAction({
start_recording: {
capture_mode: null,
camera: null,
mic_label: null,
capture_system_audio: false,
mode: 'instant',
},
});
Comment thread
tedy69 marked this conversation as resolved.
Outdated

await showToast({ style: Toast.Style.Success, title: 'Instant recording started' });
Comment thread
tedy69 marked this conversation as resolved.
Outdated
}
18 changes: 18 additions & 0 deletions apps/raycast/src/start-studio-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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...' });

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' });
}
Loading