Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
20 changes: 20 additions & 0 deletions apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -146,6 +154,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
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
61 changes: 61 additions & 0 deletions apps/raycast-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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.

## 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
npm install

# Develop in Raycast
ray dev

# Build for production
npm run build

# Lint
npm run lint
```

## License

MIT
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"
}
}
25 changes: 25 additions & 0 deletions apps/raycast-extension/src/pause-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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",
});
}
}

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.

This file exports default twice (lines 3 and 20), which will fail TypeScript compilation. Dropping the second export default block should fix it.

Copy link

Choose a reason for hiding this comment

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

Same issue here: there are two export default async function Command() blocks in one file. Dropping the second export keeps this as a valid no-view command and avoids a duplicate-default-export error.

Copy link

Choose a reason for hiding this comment

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

This file has two export default commands now, which will fail to compile in Raycast. Suggest collapsing to a single implementation.

Suggested change
export default async function Command() {
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",
});
}
}

const action = { pause_recording: null };

const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
await open(url);
}
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 two export default functions; TypeScript will error. Removing the second copy should fix it.

Suggested change
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",
});
}
}
export default async function Command() {
const action = { pause_recording: null };
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 = { 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",
});
}
}

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

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

const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
await open(url);
}
16 changes: 16 additions & 0 deletions apps/raycast-extension/src/start-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { open } 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" },
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.

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 30 to 66
Copy link

Choose a reason for hiding this comment

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

There are two export default async function Command() blocks in this file; the second one overrides the intended behavior and will break TypeScript. I'd remove the duplicate block.

Suggested change
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))}`;
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",
});
}
}
91 changes: 91 additions & 0 deletions apps/raycast-extension/src/switch-camera.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { List, ActionPanel, Action, showToast, Toast, open } from "@raycast/api";

export default function Command() {
const isLoading = false;
import { useState, useEffect } from "react";

interface Camera {
id: string;
label: string;
}

export default function Command() {
const isLoading = false;


const handleSwitchCamera = async (cameraId: string | null) => {
try {
// Camera ID can be either a model string or a device ID object
// For simplicity, we'll use model string format
const action = {
switch_camera: {
camera_id: cameraId,
},
};

const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
await open(url);
await showToast({
style: Toast.Style.Success,
title: "Camera switched",
message: cameraId ? `Switched to ${cameraId}` : "Camera disabled",
});
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to switch camera",
message: error instanceof Error ? error.message : "Unknown error",
});
}
};

return (
<List
isLoading={isLoading}
searchBarPlaceholder="Search or enter camera ID..."
actions={
<ActionPanel>
<Action
title="Disable Camera"
onAction={() => handleSwitchCamera(null)}
/>
</ActionPanel>
}
>
<List.Item
title="Disable Camera"
subtitle="Turn off camera input"
actions={
<ActionPanel>
<Action
title="Disable Camera"
onAction={() => handleSwitchCamera(null)}
/>
</ActionPanel>
}
/>
<List.Section title="Quick Actions">
<List.Item
title="Enter Camera ID"
subtitle="Manually specify camera ID or model"
actions={
<ActionPanel>
<Action
title="Switch Camera"
onAction={async () => {
// In a real implementation, you'd show a form to enter the camera ID
// For now, this is a placeholder
await showToast({
style: Toast.Style.Failure,
title: "Not implemented",
message: "Please use the deeplink directly with the camera ID",
});
}}
/>
</ActionPanel>
}
/>
</List.Section>
</List>
);
}
Comment on lines +1 to +95
Copy link

Choose a reason for hiding this comment

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

This looks like an unfinished merge/edit (duplicate imports/exports + partial function body) and currently won't compile. Also, we try to keep the repo comment-free.

Suggested change
import { List, ActionPanel, Action, showToast, Toast, open } from "@raycast/api";
export default function Command() {
const isLoading = false;
const handleSwitchCamera = async (cameraId: string | null) => {
try {
// Camera ID can be either a model string or a device ID object
// For simplicity, we'll use model string format
const action = {
switch_camera: {
const action = {
switch_camera: {
camera_id: cameraId ? { DeviceID: cameraId } : null,
},
};
},
};
import { Action, ActionPanel, Form, List, Toast, open, showToast } from "@raycast/api";
import { useState } from "react";
async function switchCamera(cameraId: string | null) {
const action = {
switch_camera: {
camera_id: cameraId ? { DeviceID: cameraId } : null,
},
};
const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
try {
await open(url);
await showToast({
style: Toast.Style.Success,
title: "Camera switched",
message: cameraId ? `Switched to ${cameraId}` : "Camera disabled",
});
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to switch camera",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}
function CameraIdForm() {
const [cameraId, setCameraId] = useState("");
return (
<Form
actions={
<ActionPanel>
<Action.SubmitForm
title="Switch Camera"
onSubmit={() => switchCamera(cameraId.trim() || null)}
/>
</ActionPanel>
}
>
<Form.TextField
id="cameraId"
title="Camera Device ID"
value={cameraId}
onChange={setCameraId}
/>
</Form>
);
}
export default function Command() {
return (
<List searchBarPlaceholder="Camera...">
<List.Item
title="Disable Camera"
subtitle="Turn off camera input"
actions={
<ActionPanel>
<Action title="Disable Camera" onAction={() => switchCamera(null)} />
</ActionPanel>
}
/>
<List.Item
title="Switch Camera"
subtitle="Enter a camera device ID"
actions={
<ActionPanel>
<Action.Push title="Enter Camera ID" target={<CameraIdForm />} />
</ActionPanel>
}
/>
</List>
);
}
import { Action, ActionPanel, Form, List, Toast, open, showToast } from "@raycast/api";
import { useState } from "react";
function buildActionUrl(action: unknown) {
return `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
}
async function switchCamera(cameraId: string | null) {
const action = {
switch_camera: {
camera_id: cameraId ? { DeviceID: cameraId } : null,
},
};
try {
await open(buildActionUrl(action));
await showToast({
style: Toast.Style.Success,
title: "Camera switch command sent",
message: cameraId ?? "Disable camera",
});
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to switch camera",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}
function CameraIdForm() {
const [cameraId, setCameraId] = useState("");
return (
<Form
actions={
<ActionPanel>
<Action.SubmitForm title="Switch Camera" onSubmit={() => switchCamera(cameraId.trim() || null)} />
</ActionPanel>
}
>
<Form.TextField id="cameraId" title="Camera Device ID" value={cameraId} onChange={setCameraId} />
</Form>
);
}
export default function Command() {
return (
<List searchBarPlaceholder="Camera...">
<List.Item
title="Disable Camera"
subtitle="Turn off camera input"
actions={
<ActionPanel>
<Action title="Disable Camera" onAction={() => switchCamera(null)} />
</ActionPanel>
}
/>
<List.Item
title="Switch Camera"
subtitle="Enter a camera device ID"
actions={
<ActionPanel>
<Action.Push title="Enter Camera ID" target={<CameraIdForm />} />
</ActionPanel>
}
/>
</List>
);
}

Loading