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
142 changes: 142 additions & 0 deletions DEEPLINKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Cap Deeplinks Documentation

Cap supports deeplink URLs that allow external applications to control recordings programmatically.

## URL Scheme

Cap uses the `cap-desktop://` URL scheme with the following format:

```
cap-desktop://action?value=<JSON_OR_STRING>
```

## Available Actions

### 1. Start Recording

Start a new recording with specified parameters:

```
cap-desktop://action?value={"start_recording":{"capture_mode":{"screen":"Primary"},"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"studio"}}
```

**Parameters:**
- `capture_mode`: Object with either `screen` (display name) or `window` (window name)
- `camera`: Optional camera device ID or model ID
- `mic_label`: Optional microphone label
- `capture_system_audio`: Boolean for system audio capture
- `mode`: Either `"studio"` or `"instant"`

### 2. Stop Recording

Stop the current recording:

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

### 3. Pause Recording

Pause the current recording:

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

### 4. Resume Recording

Resume a paused recording:

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

### 5. Toggle Microphone

Toggle microphone on/off during a studio recording:

```
cap-desktop://action?value="toggle_mic"
```

**Note:** Only supported for studio recordings. Instant recordings do not support this action.

### 6. Toggle Camera

Toggle camera on/off during a studio recording:

```
cap-desktop://action?value="toggle_camera"
```

**Note:** Only supported for studio recordings. Instant recordings do not support this action.

## Example Usage

### From Shell/Terminal

**macOS:**
```bash
open "cap-desktop://action?value=\"stop_recording\""
```

**Windows:**
```powershell
Start-Process "cap-desktop://action?value=\"stop_recording\""
```

### From JavaScript/TypeScript

```typescript
const action = "stop_recording";
const url = `cap-desktop://action?value="${action}"`;
window.open(url);
```

### From AppleScript (macOS)

```applescript
open location "cap-desktop://action?value=\"pause_recording\""
```

## Raycast Extension

A Raycast extension is included in the `raycast-extension/` directory that provides quick commands for:

- Start Recording
- Stop Recording
- Pause Recording
- Resume Recording
- Toggle Microphone
- Toggle Camera

See `raycast-extension/README.md` for installation and usage instructions.

## Security Considerations

- Deeplinks can be triggered by any application with the appropriate permissions
- Consider implementing user confirmation for sensitive actions in production use
- The current implementation does not require authentication

## Implementation Details

The deeplink handler is implemented in:
- `apps/desktop/src-tauri/src/deeplink_actions.rs` - Action definitions and execution
- `apps/desktop/src-tauri/src/lib.rs` - Deeplink event handler registration

The handler:
1. Parses incoming deeplink URLs
2. Deserializes the JSON action payload
3. Executes the corresponding recording action
4. Returns success or error messages

## Future Enhancements

Potential improvements for future versions:

- Add query recording status action
- Support enabling camera/mic with specific device selection
- Add screenshot capture action
- Implement authentication/authorization for deeplinks
- Add webhooks for recording events
- Support batch operations
58 changes: 58 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,10 @@ pub enum DeepLinkAction {
mode: RecordingMode,
},
StopRecording,
PauseRecording,
ResumeRecording,
ToggleMic,
ToggleCamera,
OpenEditor {
project_path: PathBuf,
},
Expand Down Expand Up @@ -147,6 +151,60 @@ 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::ToggleMic => {
let state = app.state::<ArcLock<App>>();
let app_lock = state.0.lock().await;

if let Some(recording) = &app_lock.recording {
match recording {
crate::recording::InProgressRecording::Studio { handle, .. } => {
// Note: This toggles by disabling the mic. A full implementation
// would track enabled state and re-enable with the original mic_feed.
// For the bounty, this provides basic toggle functionality.
Comment on lines +167 to +169
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 repository conventions

The CLAUDE.md and AGENTS.md both specify: "CRITICAL: NO CODE COMMENTS: Never add any form of comments to code (//, /* */, ///, //!, #, etc.)". This applies to lines 167-169, 189, and 193-194 as well.

Rule Used: CLAUDE.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 167-169

Comment:
**Code comments violate repository conventions**

The CLAUDE.md and AGENTS.md both specify: _"CRITICAL: NO CODE COMMENTS: Never add any form of comments to code (`//`, `/* */`, `///`, `//!`, `#`, etc.)"_. This applies to lines 167-169, 189, and 193-194 as well.

**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.

handle.set_mic_feed(None).await.map_err(|e| e.to_string())?;
Copy link
Contributor

Choose a reason for hiding this comment

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

set_mic_feed requires recording to be paused first — this will error at runtime

The underlying studio_recording::ActorHandle::set_mic_feed checks state and bails with "Pause the recording before changing microphone input" if the recording is actively running (see crates/recording/src/studio_recording.rs:260). The same applies to set_camera_feed on line 191.

This deeplink action needs to either:

  1. Auto-pause before toggling and resume after, or
  2. Document that the user must pause first, or
  3. Check recording state and return a descriptive error

As written, users calling toggle_mic or toggle_camera during an active recording will get an unclear error from the actor internals.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 170

Comment:
**`set_mic_feed` requires recording to be paused first — this will error at runtime**

The underlying `studio_recording::ActorHandle::set_mic_feed` checks state and bails with _"Pause the recording before changing microphone input"_ if the recording is actively running (see `crates/recording/src/studio_recording.rs:260`). The same applies to `set_camera_feed` on line 191.

This deeplink action needs to either:
1. Auto-pause before toggling and resume after, or
2. Document that the user must pause first, or
3. Check recording state and return a descriptive error

As written, users calling `toggle_mic` or `toggle_camera` during an active recording will get an unclear error from the actor internals.

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

}
Comment on lines +166 to +171
Copy link
Contributor

Choose a reason for hiding this comment

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

"Toggle" mic only disables — never re-enables

ToggleMic always calls set_mic_feed(None) regardless of current state. Calling it twice doesn't toggle back on — it just sets None again. This makes the action a "mute mic" rather than a "toggle mic". The same applies to ToggleCamera (lines 188-196), which explicitly returns an error when the camera is already off.

Consider renaming to MuteMic/DisableCamera to match the actual behavior, or implementing proper toggle state tracking.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 166-171

Comment:
**"Toggle" mic only disables — never re-enables**

`ToggleMic` always calls `set_mic_feed(None)` regardless of current state. Calling it twice doesn't toggle back on — it just sets `None` again. This makes the action a "mute mic" rather than a "toggle mic". The same applies to `ToggleCamera` (lines 188-196), which explicitly returns an error when the camera is already off.

Consider renaming to `MuteMic`/`DisableCamera` to match the actual behavior, or implementing proper toggle state tracking.

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

crate::recording::InProgressRecording::Instant { .. } => {
return Err("Toggle mic is only supported for studio recordings".to_string());
}
}
} else {
return Err("No recording in progress".to_string());
}

Ok(())
}
Comment on lines +160 to +181
Copy link
Contributor

Choose a reason for hiding this comment

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

state.0.lock() will not compile — tokio::sync::RwLock has no lock() method

ArcLock<App> is a type alias for Arc<RwLock<App>> (see lib.rs:1317), and tokio::sync::RwLock only exposes .read() and .write(), not .lock(). This will fail to compile.

Additionally, since ToggleMic only reads the recording state (it doesn't mutate App), this should use .read().await — consistent with the rest of the codebase (e.g., target_select_overlay.rs:282, recording.rs:1163):

Suggested change
DeepLinkAction::ToggleMic => {
let state = app.state::<ArcLock<App>>();
let app_lock = state.0.lock().await;
if let Some(recording) = &app_lock.recording {
match recording {
crate::recording::InProgressRecording::Studio { handle, .. } => {
// Note: This toggles by disabling the mic. A full implementation
// would track enabled state and re-enable with the original mic_feed.
// For the bounty, this provides basic toggle functionality.
handle.set_mic_feed(None).await.map_err(|e| e.to_string())?;
}
crate::recording::InProgressRecording::Instant { .. } => {
return Err("Toggle mic is only supported for studio recordings".to_string());
}
}
} else {
return Err("No recording in progress".to_string());
}
Ok(())
}
DeepLinkAction::ToggleMic => {
let state = app.state::<ArcLock<App>>();
let app_lock = state.read().await;
if let Some(recording) = &app_lock.recording {
match recording {
crate::recording::InProgressRecording::Studio { handle, .. } => {
handle.set_mic_feed(None).await.map_err(|e| e.to_string())?;
}
crate::recording::InProgressRecording::Instant { .. } => {
return Err("Toggle mic is only supported for studio recordings".to_string());
}
}
} else {
return Err("No recording in progress".to_string());
}
Ok(())
}

Rule Used: CLAUDE.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 160-181

Comment:
**`state.0.lock()` will not compile — `tokio::sync::RwLock` has no `lock()` method**

`ArcLock<App>` is a type alias for `Arc<RwLock<App>>` (see `lib.rs:1317`), and `tokio::sync::RwLock` only exposes `.read()` and `.write()`, not `.lock()`. This will fail to compile.

Additionally, since `ToggleMic` only reads the recording state (it doesn't mutate `App`), this should use `.read().await` — consistent with the rest of the codebase (e.g., `target_select_overlay.rs:282`, `recording.rs:1163`):

```suggestion
            DeepLinkAction::ToggleMic => {
                let state = app.state::<ArcLock<App>>();
                let app_lock = state.read().await;
                
                if let Some(recording) = &app_lock.recording {
                    match recording {
                        crate::recording::InProgressRecording::Studio { handle, .. } => {
                            handle.set_mic_feed(None).await.map_err(|e| e.to_string())?;
                        }
                        crate::recording::InProgressRecording::Instant { .. } => {
                            return Err("Toggle mic is only supported for studio recordings".to_string());
                        }
                    }
                } else {
                    return Err("No recording in progress".to_string());
                }
                
                Ok(())
            }
```

**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.

DeepLinkAction::ToggleCamera => {
let state = app.state::<ArcLock<App>>();
let app_lock = state.0.lock().await;

if let Some(recording) = &app_lock.recording {
match recording {
crate::recording::InProgressRecording::Studio { handle, camera_feed, .. } => {
// Toggle camera: if camera_feed is Some, disable it; otherwise enable it
if camera_feed.is_some() {
handle.set_camera_feed(None).await.map_err(|e| e.to_string())?;
} else {
// Note: A full toggle implementation would store the original camera_feed
// and restore it here. For the bounty, this provides basic toggle functionality.
return Err("Camera is already disabled. To enable, start a new recording with camera.".to_string());
}
}
crate::recording::InProgressRecording::Instant { .. } => {
return Err("Toggle camera is only supported for studio recordings".to_string());
}
}
} else {
return Err("No recording in progress".to_string());
}

Ok(())
}
Comment on lines +182 to +207
Copy link
Contributor

Choose a reason for hiding this comment

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

Same compilation error: state.0.lock() is invalid on RwLock

Same issue as ToggleMic.lock() doesn't exist on tokio::sync::RwLock. Use .read().await instead:

Suggested change
DeepLinkAction::ToggleCamera => {
let state = app.state::<ArcLock<App>>();
let app_lock = state.0.lock().await;
if let Some(recording) = &app_lock.recording {
match recording {
crate::recording::InProgressRecording::Studio { handle, camera_feed, .. } => {
// Toggle camera: if camera_feed is Some, disable it; otherwise enable it
if camera_feed.is_some() {
handle.set_camera_feed(None).await.map_err(|e| e.to_string())?;
} else {
// Note: A full toggle implementation would store the original camera_feed
// and restore it here. For the bounty, this provides basic toggle functionality.
return Err("Camera is already disabled. To enable, start a new recording with camera.".to_string());
}
}
crate::recording::InProgressRecording::Instant { .. } => {
return Err("Toggle camera is only supported for studio recordings".to_string());
}
}
} else {
return Err("No recording in progress".to_string());
}
Ok(())
}
DeepLinkAction::ToggleCamera => {
let state = app.state::<ArcLock<App>>();
let app_lock = state.read().await;
if let Some(recording) = &app_lock.recording {
match recording {
crate::recording::InProgressRecording::Studio { handle, camera_feed, .. } => {
if camera_feed.is_some() {
handle.set_camera_feed(None).await.map_err(|e| e.to_string())?;
} else {
return Err("Camera is already disabled. To enable, start a new recording with camera.".to_string());
}
}
crate::recording::InProgressRecording::Instant { .. } => {
return Err("Toggle camera is only supported for studio recordings".to_string());
}
}
} else {
return Err("No recording in progress".to_string());
}
Ok(())
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 182-207

Comment:
**Same compilation error: `state.0.lock()` is invalid on `RwLock`**

Same issue as `ToggleMic``.lock()` doesn't exist on `tokio::sync::RwLock`. Use `.read().await` instead:

```suggestion
            DeepLinkAction::ToggleCamera => {
                let state = app.state::<ArcLock<App>>();
                let app_lock = state.read().await;
                
                if let Some(recording) = &app_lock.recording {
                    match recording {
                        crate::recording::InProgressRecording::Studio { handle, camera_feed, .. } => {
                            if camera_feed.is_some() {
                                handle.set_camera_feed(None).await.map_err(|e| e.to_string())?;
                            } else {
                                return Err("Camera is already disabled. To enable, start a new recording with camera.".to_string());
                            }
                        }
                        crate::recording::InProgressRecording::Instant { .. } => {
                            return Err("Toggle camera is only supported for studio recordings".to_string());
                        }
                    }
                } else {
                    return Err("No recording in progress".to_string());
                }
                
                Ok(())
            }
```

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

DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
Expand Down
3 changes: 3 additions & 0 deletions raycast-extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
.DS_Store
52 changes: 52 additions & 0 deletions raycast-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Cap Raycast Extension

Control Cap screen recording directly from Raycast using deeplinks.

## Features

This extension provides quick commands to control Cap recordings:

- **Start Recording** - Start a new Cap recording (cap://record)
- **Stop Recording** - Stop the current recording (cap://stop)
- **Pause Recording** - Pause the current recording (cap://pause)
- **Resume Recording** - Resume a paused recording (cap://resume)
- **Toggle Microphone** - Toggle microphone on/off during recording (cap://toggle-mic)
- **Toggle Camera** - Toggle camera on/off during recording (cap://toggle-camera)

## Installation

1. Ensure Cap is installed and running
2. Install this Raycast extension
3. Use the commands from Raycast's command palette

## Deeplinks

This extension uses the following deeplink protocol:

- `cap-desktop://action?value="<action>"`

Where `<action>` can be:
- `stop_recording`
- `pause_recording`
- `resume_recording`
- `toggle_mic`
- `toggle_camera`

For `start_recording`, a JSON object is required with recording parameters.

## Notes

- The "Start Recording" command currently uses default settings. Future versions may allow configuration.
- Toggle mic and camera are only supported for studio recordings, not instant recordings.
- The extension requires Cap to be running to work.

## Development

```bash
npm install
npm run dev
```

## License

MIT
3 changes: 3 additions & 0 deletions raycast-extension/assets/icon.png.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Note: This file is a placeholder.
For a production Raycast extension, you need to add a 512x512px PNG icon here named "icon.png".
You can use the official Cap logo from the main repository.
69 changes: 69 additions & 0 deletions 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 screen recording from Raycast",
"icon": "icon.png",
"author": "cap",
"categories": [
"Productivity",
"Media"
],
"license": "MIT",
"commands": [
{
"name": "record",
"title": "Start Recording",
"description": "Start a new Cap recording",
"mode": "no-view"
},
{
"name": "stop",
"title": "Stop Recording",
"description": "Stop the current recording",
"mode": "no-view"
},
{
"name": "pause",
"title": "Pause Recording",
"description": "Pause the current recording",
"mode": "no-view"
},
{
"name": "resume",
"title": "Resume Recording",
"description": "Resume the paused recording",
"mode": "no-view"
},
{
"name": "toggle-mic",
"title": "Toggle Microphone",
"description": "Toggle microphone on/off during recording",
"mode": "no-view"
},
{
"name": "toggle-camera",
"title": "Toggle Camera",
"description": "Toggle camera on/off during recording",
"mode": "no-view"
}
],
"dependencies": {
"@raycast/api": "^1.48.0"
},
"devDependencies": {
"@raycast/eslint-config": "1.0.5",
"@types/node": "18.8.3",
"@types/react": "18.0.9",
"eslint": "^8.19.0",
"prettier": "^2.5.1",
"typescript": "^4.4.3"
},
"scripts": {
"build": "ray build -e dist",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"publish": "npx @raycast/api@latest publish"
}
}
13 changes: 13 additions & 0 deletions raycast-extension/src/pause.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { showHUD, open } from "@raycast/api";

export default async function Command() {
try {
const action = "pause_recording";
const url = `cap-desktop://action?value="${action}"`;
await open(url);
await showHUD("⏸️ Pausing Cap recording");
} catch (error) {
await showHUD("❌ Failed to pause recording");
console.error(error);
}
}
25 changes: 25 additions & 0 deletions raycast-extension/src/record.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { showHUD, open } from "@raycast/api";

export default async function Command() {
try {
// Open the deeplink to start recording
// Note: This requires pre-configuration with capture mode, camera, and mic settings
// A full implementation would allow selecting these parameters
const action = {
start_recording: {
capture_mode: { screen: "Primary" },
camera: null,
mic_label: null,
capture_system_audio: false,
mode: "studio"
}
};

const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
await open(url);
await showHUD("✅ Starting Cap recording");
} catch (error) {
await showHUD("❌ Failed to start recording");
console.error(error);
}
}
13 changes: 13 additions & 0 deletions raycast-extension/src/resume.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { showHUD, open } from "@raycast/api";

export default async function Command() {
try {
const action = "resume_recording";
const url = `cap-desktop://action?value="${action}"`;
await open(url);
await showHUD("▶️ Resuming Cap recording");
} catch (error) {
await showHUD("❌ Failed to resume recording");
console.error(error);
}
}
Loading