Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 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,15 @@ pub enum DeepLinkAction {
mode: RecordingMode,
},
StopRecording,
PauseRecording,
ResumeRecording,
TogglePauseRecording,
SetCamera {
camera: Option<DeviceOrModelID>,
},
SetMicrophone {
mic_label: Option<String>,
},
OpenEditor {
project_path: PathBuf,
},
Expand Down Expand Up @@ -147,6 +156,22 @@ 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::SetCamera { camera } => {
let state = app.state::<ArcLock<App>>();
crate::set_camera_input(app.clone(), state, camera, None).await
}
DeepLinkAction::SetMicrophone { mic_label } => {
crate::set_mic_input(app.state(), mic_label).await
}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
Expand Down
66 changes: 66 additions & 0 deletions apps/raycast/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Cap Raycast Extension

Control [Cap](https://cap.so) screen recordings directly from Raycast using deeplinks.

## Commands

| Command | Description | Mode |
|---------|-------------|------|
| **Start Recording** | Start a screen/window recording with configurable options | Form |
| **Stop Recording** | Stop the current recording | Instant |
Copy link

Choose a reason for hiding this comment

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

Minor docs mismatch: Set Camera / Set Microphone are mode: "view" in apps/raycast/package.json (argument entry), but the table says Instant.

Suggested change
| **Stop Recording** | Stop the current recording | Instant |
| **Set Camera** | Switch camera input (pass name as argument) | Form |
| **Set Microphone** | Switch microphone input (pass label as argument) | Form |

| **Pause Recording** | Pause the current recording | Instant |
| **Resume Recording** | Resume a paused recording | Instant |
| **Toggle Pause** | Toggle pause/resume on the current recording | Instant |
| **Set Camera** | Switch camera input (pass name as argument) | Instant |
| **Set Microphone** | Switch microphone input (pass label as argument) | Instant |
| **Open Settings** | Open Cap settings window | Instant |

## How It Works

This extension communicates with Cap desktop via the `cap-desktop://` URL scheme. Each command constructs a deeplink URL in the format:

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

Cap desktop must be running for the commands to work.

## Deeplink Reference

### Simple actions (no parameters)

```
cap-desktop://action?value="stop_recording"
cap-desktop://action?value="pause_recording"
cap-desktop://action?value="resume_recording"
cap-desktop://action?value="toggle_pause_recording"
```

### Start recording

```json
{
"start_recording": {
"capture_mode": { "screen": "Built-in Retina Display" },
"camera": { "ModelID": "FaceTime HD Camera" },
Copy link
Contributor

Choose a reason for hiding this comment

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

Misleading ModelID example in docs

The example shows "ModelID": "FaceTime HD Camera", but in the Rust codebase ModelID actually deserializes from a "vid:pid" format string (e.g., "0x1234:0x5678"). This applies to both the "Start recording" and "Switch camera" examples (lines 45 and 56). Using a friendly camera name here would crash the desktop app due to an unwrap() panic in the ModelID deserializer.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/README.md
Line: 45

Comment:
**Misleading `ModelID` example in docs**

The example shows `"ModelID": "FaceTime HD Camera"`, but in the Rust codebase `ModelID` actually deserializes from a `"vid:pid"` format string (e.g., `"0x1234:0x5678"`). This applies to both the "Start recording" and "Switch camera" examples (lines 45 and 56). Using a friendly camera name here would crash the desktop app due to an `unwrap()` panic in the `ModelID` deserializer.

How can I resolve this? If you propose a fix, please make it concise.

"mic_label": "MacBook Pro Microphone",
"capture_system_audio": true,
"mode": "studio"
}
}
```

### Switch camera/microphone

```json
{ "set_camera": { "camera": { "ModelID": "FaceTime HD Camera" } } }
{ "set_camera": { "camera": null } }
{ "set_microphone": { "mic_label": "External Microphone" } }
{ "set_microphone": { "mic_label": null } }
```

### Open settings

```json
{ "open_settings": { "page": "recordings" } }
```
Binary file added apps/raycast/assets/command-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 92 additions & 0 deletions apps/raycast/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "cap",
"title": "Cap",
"description": "Control Cap screen recordings via deeplinks",
"icon": "command-icon.png",
"author": "cap",
"categories": ["Productivity"],
"license": "AGPL-3.0",
"commands": [
{
"name": "start-recording",
"title": "Start Recording",
"description": "Start a Cap screen recording",
"mode": "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",
"title": "Toggle Pause",
"description": "Toggle pause/resume on the current Cap recording",
"mode": "no-view"
},
{
"name": "set-camera",
"title": "Set Camera",
"description": "Switch the camera input for Cap recordings",
"mode": "view",
"arguments": [
{
"name": "camera",
"placeholder": "Camera name or model",
"type": "text",
"required": false
}
]
},
{
"name": "set-microphone",
"title": "Set Microphone",
"description": "Switch the microphone input for Cap recordings",
"mode": "view",
"arguments": [
{
"name": "microphone",
"placeholder": "Microphone label",
"type": "text",
"required": false
}
]
},
{
"name": "open-settings",
"title": "Open Settings",
"description": "Open Cap settings",
"mode": "no-view"
}
],
"dependencies": {
"@raycast/api": "^1.87.0"
},
"devDependencies": {
"@raycast/eslint-config": "^1.0.11",
"@types/node": "22.13.4",
"@types/react": "19.0.8",
"eslint": "^9.18.0",
"typescript": "^5.7.3"
},
"scripts": {
"build": "ray build",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint"
}
}
7 changes: 7 additions & 0 deletions apps/raycast/src/open-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { open, showHUD } from "@raycast/api";
import { buildDeeplinkUrl } from "./utils";

export default async function Command() {
await open(buildDeeplinkUrl({ open_settings: { page: null } }));
await showHUD("Opening Cap settings");
}
7 changes: 7 additions & 0 deletions apps/raycast/src/pause-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { open, showHUD } from "@raycast/api";
import { buildDeeplinkUrl } from "./utils";

export default async function Command() {
await open(buildDeeplinkUrl("pause_recording"));
await showHUD("Pausing Cap recording");
}
7 changes: 7 additions & 0 deletions apps/raycast/src/resume-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { open, showHUD } from "@raycast/api";
import { buildDeeplinkUrl } from "./utils";

export default async function Command() {
await open(buildDeeplinkUrl("resume_recording"));
await showHUD("Resuming Cap recording");
}
19 changes: 19 additions & 0 deletions apps/raycast/src/set-camera.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { LaunchProps, open, showHUD } from "@raycast/api";
import { buildDeeplinkUrl } from "./utils";

interface Arguments {
Copy link

Choose a reason for hiding this comment

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

package.json marks this argument as required: false, so the type should allow it to be missing.

Suggested change
interface Arguments {
interface Arguments {
camera?: string;
}

camera: string;
}

export default async function Command(props: LaunchProps<{ arguments: Arguments }>) {
const camera = props.arguments.camera?.trim() || null;

const url = buildDeeplinkUrl({
set_camera: {
camera: camera ? { ModelID: camera } : null,
Copy link
Contributor

Choose a reason for hiding this comment

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

ModelID expects vid:pid format, not a camera name

The Rust ModelID deserializer at crates/camera/src/lib.rs:144 splits on : with an unwrap():

let (vid, pid) = s.split_once(":").unwrap();

If a user enters a human-readable name like "FaceTime HD Camera" (which the placeholder text "Camera name or model" suggests), the deeplink will produce {"ModelID": "FaceTime HD Camera"}. Since there's no : in that string, split_once(":").unwrap() will panic and crash the desktop app.

The ModelID type requires a vendor-ID:product-ID string (e.g., "0x1234:0x5678"), not a friendly camera name. Either the Raycast command should explain this format requirement to the user, or it should use DeviceID instead if that accepts friendly names.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/set-camera.tsx
Line: 13

Comment:
**`ModelID` expects `vid:pid` format, not a camera name**

The Rust `ModelID` deserializer at `crates/camera/src/lib.rs:144` splits on `:` with an `unwrap()`:

```rust
let (vid, pid) = s.split_once(":").unwrap();
```

If a user enters a human-readable name like "FaceTime HD Camera" (which the placeholder text `"Camera name or model"` suggests), the deeplink will produce `{"ModelID": "FaceTime HD Camera"}`. Since there's no `:` in that string, `split_once(":").unwrap()` will **panic** and crash the desktop app.

The `ModelID` type requires a vendor-ID:product-ID string (e.g., `"0x1234:0x5678"`), not a friendly camera name. Either the Raycast command should explain this format requirement to the user, or it should use `DeviceID` instead if that accepts friendly names.

How can I resolve this? If you propose a fix, please make it concise.

},
});

await open(url);
await showHUD(camera ? `Switching camera to ${camera}` : "Disabling camera");
}
19 changes: 19 additions & 0 deletions apps/raycast/src/set-microphone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { LaunchProps, open, showHUD } from "@raycast/api";
import { buildDeeplinkUrl } from "./utils";

interface Arguments {
Copy link

Choose a reason for hiding this comment

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

Same as set-camera: argument is optional in package.json, so the type should reflect that.

Suggested change
interface Arguments {
interface Arguments {
microphone?: string;
}

microphone: string;
}

export default async function Command(props: LaunchProps<{ arguments: Arguments }>) {
const mic = props.arguments.microphone?.trim() || null;

const url = buildDeeplinkUrl({
set_microphone: {
mic_label: mic,
},
});

await open(url);
await showHUD(mic ? `Switching microphone to ${mic}` : "Disabling microphone");
}
59 changes: 59 additions & 0 deletions apps/raycast/src/start-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Action, ActionPanel, Form, open, showHUD } from "@raycast/api";
import { buildDeeplinkUrl } from "./utils";

export default function Command() {
return (
<Form
actions={
<ActionPanel>
<Action.SubmitForm title="Start Recording" onSubmit={handleSubmit} />
</ActionPanel>
}
>
<Form.Dropdown id="captureType" title="Capture Type" defaultValue="screen">
<Form.Dropdown.Item value="screen" title="Screen" />
<Form.Dropdown.Item value="window" title="Window" />
</Form.Dropdown>
<Form.TextField id="captureName" title="Screen/Window Name" placeholder="e.g. Built-in Retina Display" />
<Form.Dropdown id="mode" title="Recording Mode" defaultValue="studio">
<Form.Dropdown.Item value="studio" title="Studio" />
<Form.Dropdown.Item value="instant" title="Instant" />
</Form.Dropdown>
<Form.TextField id="camera" title="Camera (optional)" placeholder="Camera name or model" />
<Form.TextField id="mic" title="Microphone (optional)" placeholder="Microphone label" />
<Form.Checkbox id="systemAudio" title="System Audio" label="Capture system audio" defaultValue={false} />
</Form>
);
}

interface FormValues {
captureType: string;
captureName: string;
mode: string;
camera: string;
mic: string;
systemAudio: boolean;
}

async function handleSubmit(values: FormValues) {
const captureMode =
Copy link

Choose a reason for hiding this comment

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

Worth validating/normalizing inputs before firing the deeplink; empty captureName or whitespace camera/mic become hard-to-debug errors on the desktop side.

Suggested change
const captureMode =
async function handleSubmit(values: FormValues) {
const captureName = values.captureName.trim();
if (!captureName) {
await showHUD("Please enter a screen/window name");
return;
}
const captureMode = values.captureType === "screen" ? { screen: captureName } : { window: captureName };
const camera = values.camera.trim() ? { ModelID: values.camera.trim() } : null;
const micLabel = values.mic.trim() || null;
const url = buildDeeplinkUrl({
start_recording: {
capture_mode: captureMode,
camera,
mic_label: micLabel,
capture_system_audio: values.systemAudio,
mode: values.mode as "studio" | "instant",
},
});
await open(url);
await showHUD("Starting Cap recording");
}

values.captureType === "screen"
? { screen: values.captureName }
: { window: values.captureName };

const camera = values.camera ? { ModelID: values.camera } : null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same ModelID panic risk as set-camera

This line has the same issue as set-camera.tsx: the form placeholder says "Camera name or model" but wraps the input in { ModelID: values.camera }. The Rust ModelID deserializer expects a "vid:pid" format string and will panic (unwrap() on split_once(":")) if the user enters a human-readable camera name like "FaceTime HD Camera".

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/start-recording.tsx
Line: 44

Comment:
**Same `ModelID` panic risk as set-camera**

This line has the same issue as `set-camera.tsx`: the form placeholder says "Camera name or model" but wraps the input in `{ ModelID: values.camera }`. The Rust `ModelID` deserializer expects a `"vid:pid"` format string and will **panic** (`unwrap()` on `split_once(":")`) if the user enters a human-readable camera name like "FaceTime HD Camera".

How can I resolve this? If you propose a fix, please make it concise.

const micLabel = values.mic || null;

const url = buildDeeplinkUrl({
start_recording: {
capture_mode: captureMode,
camera,
mic_label: micLabel,
capture_system_audio: values.systemAudio,
mode: values.mode as "studio" | "instant",
},
});

await open(url);
await showHUD("Starting Cap recording");
}
7 changes: 7 additions & 0 deletions apps/raycast/src/stop-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { open, showHUD } from "@raycast/api";
import { buildDeeplinkUrl } from "./utils";

export default async function Command() {
await open(buildDeeplinkUrl("stop_recording"));
await showHUD("Stopping Cap recording");
}
7 changes: 7 additions & 0 deletions apps/raycast/src/toggle-pause.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { open, showHUD } from "@raycast/api";
import { buildDeeplinkUrl } from "./utils";

export default async function Command() {
await open(buildDeeplinkUrl("toggle_pause_recording"));
await showHUD("Toggling Cap recording pause");
}
60 changes: 60 additions & 0 deletions apps/raycast/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
Copy link

Choose a reason for hiding this comment

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

Repo-wide guideline is no code comments; can we drop the docblock + inline comment here?

Suggested change
/**
export function buildDeeplinkUrl(action: DeepLinkAction): string {
const json = JSON.stringify(action);
return `cap-desktop://action?value=${encodeURIComponent(json)}`;
}
export type DeepLinkAction =
| "stop_recording"
| "pause_recording"
| "resume_recording"
| "toggle_pause_recording"
| StartRecordingAction
| SetCameraAction
| SetMicrophoneAction
| OpenEditorAction
| OpenSettingsAction;

* Builds a Cap desktop deeplink URL.
*
* Cap desktop uses the URL scheme: cap-desktop://action?value={json_encoded_action}
* The JSON value follows the Rust serde enum serialization format:
* - Unit variants: "variant_name"
* - Struct variants: { "variant_name": { field: value } }
*/
export function buildDeeplinkUrl(action: DeepLinkAction): string {
const json = JSON.stringify(action);
return `cap-desktop://action?value=${encodeURIComponent(json)}`;
}

// Unit variants serialize as plain strings
Comment on lines +1 to +14
Copy link
Contributor

Choose a reason for hiding this comment

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

Code comments violate repo convention

Per the project's CLAUDE.md: "CRITICAL: NO CODE COMMENTS: Never add any form of comments to code." This file contains a JSDoc block (lines 1-8) and a single-line comment on line 14 (// Unit variants serialize as plain strings). These should be removed — the types and function names are already self-explanatory.

Rule Used: CLAUDE.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/utils.ts
Line: 1-14

Comment:
**Code comments violate repo convention**

Per the project's CLAUDE.md: "CRITICAL: NO CODE COMMENTS: Never add any form of comments to code." This file contains a JSDoc block (lines 1-8) and a single-line comment on line 14 (`// Unit variants serialize as plain strings`). These should be removed — the types and function names are already self-explanatory.

**Rule Used:** CLAUDE.md ([source](https://app.greptile.com/review/custom-context?memory=9a906542-f1fe-42c1-89a2-9f252d96d9f0))

How can I resolve this? If you propose a fix, please make it concise.

export type DeepLinkAction =
| "stop_recording"
| "pause_recording"
| "resume_recording"
| "toggle_pause_recording"
| StartRecordingAction
| SetCameraAction
| SetMicrophoneAction
| OpenEditorAction
| OpenSettingsAction;

export interface StartRecordingAction {
start_recording: {
capture_mode: { screen: string } | { window: string };
camera: CameraId | null;
mic_label: string | null;
capture_system_audio: boolean;
mode: "studio" | "instant";
};
}

export type CameraId = { ModelID: string } | { DeviceID: string };

export interface SetCameraAction {
set_camera: {
camera: CameraId | null;
};
}

export interface SetMicrophoneAction {
set_microphone: {
mic_label: string | null;
};
}

export interface OpenEditorAction {
open_editor: {
project_path: string;
};
}

export interface OpenSettingsAction {
open_settings: {
page: string | null;
};
}
Loading