Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
023e569
feat: Add deeplinks support for pause/resume and device switching + R…
Jan 31, 2026
249865d
Update apps/raycast-extension/src/switch-camera.tsx
gyanu2507 Jan 31, 2026
a4ea0f5
Update apps/raycast-extension/src/switch-camera.tsx
gyanu2507 Jan 31, 2026
0a5236c
Update apps/raycast-extension/src/switch-microphone.tsx
gyanu2507 Jan 31, 2026
cdd7224
Update apps/raycast-extension/src/switch-microphone.tsx
gyanu2507 Jan 31, 2026
efbbfd3
Update apps/raycast-extension/README.md
gyanu2507 Jan 31, 2026
8395281
Update apps/raycast-extension/src/switch-camera.tsx
gyanu2507 Jan 31, 2026
8c87dcb
Update apps/raycast-extension/src/stop-recording.ts
gyanu2507 Jan 31, 2026
a4f08f4
Update apps/raycast-extension/src/switch-microphone.tsx
gyanu2507 Jan 31, 2026
2929326
Update apps/raycast-extension/src/pause-recording.ts
gyanu2507 Jan 31, 2026
362a67c
Update apps/raycast-extension/src/switch-camera.tsx
gyanu2507 Jan 31, 2026
9c47ebb
Update apps/raycast-extension/src/switch-microphone.tsx
gyanu2507 Jan 31, 2026
e7b65cf
Update apps/raycast-extension/src/switch-camera.tsx
gyanu2507 Jan 31, 2026
738cfe8
Update apps/raycast-extension/README.md
gyanu2507 Jan 31, 2026
9e444c0
fix: Add missing icon.png for Raycast extension
Jan 31, 2026
a01f1b3
Update apps/raycast-extension/src/resume-recording.ts
gyanu2507 Jan 31, 2026
830daad
Update apps/raycast-extension/src/switch-camera.tsx
gyanu2507 Jan 31, 2026
7d5bbd5
Update apps/raycast-extension/src/switch-microphone.tsx
gyanu2507 Jan 31, 2026
7617be6
Update apps/raycast-extension/src/switch-camera.tsx
gyanu2507 Jan 31, 2026
a57e6c7
Update apps/raycast-extension/src/switch-camera.tsx
gyanu2507 Jan 31, 2026
0675dea
Update apps/raycast-extension/README.md
gyanu2507 Jan 31, 2026
cda0e4e
Update apps/raycast-extension/src/switch-microphone.tsx
gyanu2507 Jan 31, 2026
7a246a2
Update apps/raycast-extension/src/resume-recording.ts
gyanu2507 Jan 31, 2026
63d1d38
Update apps/raycast-extension/src/start-recording.ts
gyanu2507 Jan 31, 2026
9a74b03
Update apps/raycast-extension/src/switch-microphone.tsx
gyanu2507 Jan 31, 2026
82e3e5c
security: Add opt-in setting for deeplink actions
Jan 31, 2026
1e31d7a
Update apps/desktop/src-tauri/src/deeplink_actions.rs
gyanu2507 Jan 31, 2026
040108c
chore: Remove local PR automation artifacts
Jan 31, 2026
5a29956
fix: Add missing execute function signature in deeplink_actions
Jan 31, 2026
76a278c
Update apps/raycast-extension/src/resume-recording.ts
gyanu2507 Jan 31, 2026
5230711
Update apps/raycast-extension/src/switch-camera.tsx
gyanu2507 Jan 31, 2026
d2beeaf
Update apps/raycast-extension/src/switch-microphone.tsx
gyanu2507 Jan 31, 2026
d9b4436
Update apps/raycast-extension/src/resume-recording.ts
gyanu2507 Jan 31, 2026
436896c
Update apps/raycast-extension/src/start-recording.ts
gyanu2507 Jan 31, 2026
a590e42
Update apps/raycast-extension/src/pause-recording.ts
gyanu2507 Jan 31, 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
45 changes: 44 additions & 1 deletion apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
use tauri::{AppHandle, Manager, Url};
use tracing::trace;

use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow};
use crate::{App, ArcLock, general_settings::GeneralSettingsStore, recording::StartRecordingInputs, windows::ShowCapWindow};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
Expand All @@ -26,6 +26,14 @@ pub enum DeepLinkAction {
mode: RecordingMode,
},
StopRecording,
PauseRecording,
ResumeRecording,
SwitchMicrophone {
mic_label: Option<String>,
},
SwitchCamera {
camera_id: Option<DeviceOrModelID>,
},
OpenEditor {
project_path: PathBuf,
},
Expand Down Expand Up @@ -106,6 +114,29 @@ impl TryFrom<&Url> for DeepLinkAction {

impl DeepLinkAction {
pub async fn execute(self, app: &AppHandle) -> Result<(), String> {
// Check if deeplink actions are enabled for sensitive operations
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repo-wide nit: we try to keep the codebase comment-free. This line can probably go since requires_permission already explains intent.

let requires_permission = matches!(
Comment on lines +117 to +118
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repo policy is no inline code comments; can we drop this one?

Suggested change
// Check if deeplink actions are enabled for sensitive operations
let requires_permission = matches!(
let requires_permission = matches!(

&self,
DeepLinkAction::StartRecording { .. }
| DeepLinkAction::StopRecording
| DeepLinkAction::PauseRecording
| DeepLinkAction::ResumeRecording
| DeepLinkAction::SwitchMicrophone { .. }
| DeepLinkAction::SwitchCamera { .. }
);

if requires_permission {
let settings = GeneralSettingsStore::get(app)
.map_err(|e| format!("Failed to read settings: {e}"))?
.unwrap_or_default();

if !settings.enable_deeplink_actions {
return Err(
"Deeplink actions are disabled. Enable 'Allow deeplink actions' in Settings to use this feature.".to_string()
);
}
}

match self {
DeepLinkAction::StartRecording {
capture_mode,
Expand Down Expand Up @@ -146,6 +177,18 @@ 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::SwitchMicrophone { mic_label } => {
crate::set_mic_input(app.state(), mic_label).await
}
DeepLinkAction::SwitchCamera { camera_id } => {
crate::set_camera_input(app.clone(), app.state(), camera_id, None).await
}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src-tauri/src/general_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ pub struct GeneralSettingsStore {
pub main_window_position: Option<WindowPosition>,
#[serde(default)]
pub camera_window_position: Option<WindowPosition>,
#[serde(default)]
pub enable_deeplink_actions: bool,
}

fn default_enable_native_camera_preview() -> bool {
Expand Down Expand Up @@ -207,6 +209,7 @@ impl Default for GeneralSettingsStore {
editor_preview_quality: EditorPreviewQuality::Half,
main_window_position: None,
camera_window_position: None,
enable_deeplink_actions: false,
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/routes/(window-chrome)/settings/general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,12 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) {
handleChange("enableNotifications", value);
}}
/>
<ToggleSettingItem
label="Allow deeplink actions"
description="Enable deeplink actions for recording control (start, stop, pause, resume, switch devices). When enabled, any app or website can trigger these actions via cap-desktop:// URLs. Only enable if you need automation features like Raycast extensions."
value={!!settings.enableDeeplinkActions}
onChange={(value) => handleChange("enableDeeplinkActions", value)}
/>
</SettingGroup>
)}

Expand Down
4 changes: 4 additions & 0 deletions apps/raycast-extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
dist/
.DS_Store
*.log
63 changes: 63 additions & 0 deletions apps/raycast-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Cap Raycast Extension

A Raycast extension to control the Cap recording app via deeplinks.

## Features

- **Start Recording**: Start a new recording session
- **Stop Recording**: Stop the current recording
- **Pause Recording**: Pause the current recording
- **Resume Recording**: Resume a paused recording
- **Switch Microphone**: Switch to a different microphone input
- **Switch Camera**: Switch to a different camera input

## Installation

1. Open Raycast
2. Go to Extensions → Create Extension
3. Select "Import Extension"
4. Point to this directory

Or use the Raycast CLI:

```bash
cd apps/raycast-extension
pnpm install
ray dev
Comment on lines +23 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fenced code block here is missing the closing backticks.

Suggested change
```bash
cd apps/raycast-extension
pnpm install
ray dev
```bash
cd apps/raycast-extension
pnpm install
ray dev

```

## Usage

All commands are available through Raycast's command palette. Simply search for "Cap" and select the desired action.

**Important Security Note**: Deeplink actions for recording control (start, stop, pause, resume, switch devices) require opt-in permission in Cap settings. Go to Settings → General and enable "Allow deeplink actions" to use these features. This prevents unauthorized apps or websites from controlling your recordings via URL schemes.

## Deeplink Format

The extension uses the `cap-desktop://` URL scheme to communicate with the Cap app. The format is:

```
cap-desktop://action?value={JSON_ACTION}
```

Where `JSON_ACTION` is a JSON-encoded action matching the `DeepLinkAction` enum in the Cap desktop app.

## Development

```bash
# Install dependencies
pnpm install

# Develop in Raycast
pnpm dev

# Build for production
pnpm build

# Lint
pnpm lint
```

## License

MIT
Binary file added apps/raycast-extension/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 69 additions & 0 deletions apps/raycast-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "cap",
"title": "Cap",
"description": "Control Cap recording app via Raycast",
"icon": "icon.png",
"author": "CapSoftware",
"categories": [
"Productivity",
"Media"
],
"license": "MIT",
"commands": [
{
"name": "start-recording",
"title": "Start Recording",
"description": "Start a new recording in Cap",
"mode": "no-view"
},
{
"name": "stop-recording",
"title": "Stop Recording",
"description": "Stop the current recording",
"mode": "no-view"
},
{
"name": "pause-recording",
"title": "Pause Recording",
"description": "Pause the current recording",
"mode": "no-view"
},
{
"name": "resume-recording",
"title": "Resume Recording",
"description": "Resume a paused recording",
"mode": "no-view"
},
{
"name": "switch-microphone",
"title": "Switch Microphone",
"description": "Switch to a different microphone",
"mode": "view"
},
{
"name": "switch-camera",
"title": "Switch Camera",
"description": "Switch to a different camera",
"mode": "view"
}
],
"dependencies": {
"@raycast/api": "^1.69.0"
},
"devDependencies": {
"@raycast/eslint-config": "^1.0.11",
"@types/node": "^20.11.5",
"@types/react": "^18.2.48",
"eslint": "^8.56.0",
"prettier": "^3.2.4",
"typescript": "^5.3.3"
},
"scripts": {
"build": "ray build -e dist",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"publish": "npx @raycast/api@latest publish"
}
}
19 changes: 19 additions & 0 deletions apps/raycast-extension/src/pause-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { open, showToast, Toast } from "@raycast/api";

export default async function Command() {
const action = { pause_recording: null };

const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;

try {
await open(url);
await showToast({ style: Toast.Style.Success, title: "Recording paused" });
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to pause recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}

35 changes: 35 additions & 0 deletions apps/raycast-extension/src/resume-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { open, showToast, Toast } from "@raycast/api";

export default async function Command() {
const action = { resume_recording: null };

const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;

try {
await open(url);
await showToast({ style: Toast.Style.Success, title: "Recording resumed" });
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to resume recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}
import { open, showToast, Toast } from "@raycast/api";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file currently has a duplicated import and multiple export default command definitions. I’d trim it down to a single command export (keeping the toast/try-catch version) so it compiles and behaves consistently.


export default async function Command() {
const action = { resume_recording: null };

const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;

try {
await open(url);
await showToast({ style: Toast.Style.Success, title: "Recording resumed" });
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to resume recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
Comment on lines +1 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a copy/paste duplication slipped in here (second import + second Command()). Suggest trimming it back to a single implementation.

Suggested change
import { open, showToast, Toast } from "@raycast/api";
export default async function Command() {
const action = { resume_recording: null };
const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
try {
await open(url);
await showToast({ style: Toast.Style.Success, title: "Recording resumed" });
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to resume recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}
import { open, showToast, Toast } from "@raycast/api";
export default async function Command() {
const action = { resume_recording: null };
const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
try {
await open(url);
await showToast({ style: Toast.Style.Success, title: "Recording resumed" });
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to resume recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
import { open, showToast, Toast } from "@raycast/api";
export default async function Command() {
const action = { resume_recording: null };
const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
try {
await open(url);
await showToast({ style: Toast.Style.Success, title: "Resume command sent" });
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to resume recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}

66 changes: 66 additions & 0 deletions apps/raycast-extension/src/start-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { open, showToast, Toast } from "@raycast/api";

export default async function Command() {
const action = {
start_recording: {
capture_mode: { screen: "Main Display" },
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoding "Main Display" seems brittle (the app looks up displays by name and will error if it doesn't match). Might be worth picking a safer default or making this configurable in the command.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: hardcoding "Main Display" will fail on many machines because Cap matches by exact display name. Might be worth switching this command to mode: "view" and letting the user pick/enter the screen name (or at least documenting that it may need editing).

camera: null,
mic_label: null,
capture_system_audio: false,
mode: "studio",
},
};

const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;

try {
await open(url);
await showToast({ style: Toast.Style.Success, title: "Started recording" });
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to start recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}

import { open, showToast, Toast } from "@raycast/api";

export default async function Command() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this file accidentally includes two export default async function Command() blocks. TypeScript will error on duplicate default exports; I'd drop the second one (starting at line 28) and keep the try/catch + toast version.

const action = {
start_recording: {
capture_mode: { screen: "Main Display" },
camera: null,
mic_label: null,
capture_system_audio: false,
mode: "studio",
},
};

const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;

try {
await open(url);
await showToast({ style: Toast.Style.Success, title: "Started recording" });
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to start recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}
const action = {
start_recording: {
capture_mode: { screen: "Main Display" },
camera: null,
mic_label: null,
capture_system_audio: false,
mode: "studio",
},
};

const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
await open(url);
}
Comment on lines +1 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file currently has duplicated blocks + stray statements at the end, so it won't typecheck. Also, since we can't confirm Cap executed the action, the success toast is a bit optimistic.

Suggested change
import { open, showToast, Toast } from "@raycast/api";
export default async function Command() {
const action = {
start_recording: {
capture_mode: { screen: "Main Display" },
camera: null,
mic_label: null,
capture_system_audio: false,
mode: "studio",
},
};
const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
try {
await open(url);
await showToast({ style: Toast.Style.Success, title: "Started recording" });
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to start recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}
import { open, showToast, Toast } from "@raycast/api";
export default async function Command() {
const action = {
start_recording: {
capture_mode: { screen: "Main Display" },
camera: null,
mic_label: null,
capture_system_audio: false,
mode: "studio",
},
};
const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
try {
await open(url);
await showToast({ style: Toast.Style.Success, title: "Started recording" });
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to start recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}
const action = {
start_recording: {
capture_mode: { screen: "Main Display" },
camera: null,
mic_label: null,
capture_system_audio: false,
mode: "studio",
},
};
const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
await open(url);
}
import { open, showToast, Toast } from "@raycast/api";
export default async function Command() {
const action = {
start_recording: {
capture_mode: { screen: "Main Display" },
camera: null,
mic_label: null,
capture_system_audio: false,
mode: "studio",
},
};
const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
try {
await open(url);
await showToast({ style: Toast.Style.Success, title: "Start command sent" });
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to start recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}

Minor: hardcoding "Main Display" is pretty brittle if display names differ; a small view command to let users pick/enter the name might be safer.

18 changes: 18 additions & 0 deletions apps/raycast-extension/src/stop-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { open, showToast, Toast } from "@raycast/api";

export default async function Command() {
try {
const action = { stop_recording: null };

const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
await open(url);

await showToast({ style: Toast.Style.Success, title: "Stopped recording" });
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to stop recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}
Loading