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
52 changes: 52 additions & 0 deletions PR-DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Add deeplink actions for recording control + Raycast extension

Closes #1540

## Summary

This PR extends Cap's deeplink support to enable full recording control via URL schemes, and includes a complete Raycast extension for quick access.

## New Deeplink Actions

Added to `deeplink_actions.rs`:

| Action | Description | URL Example |
|--------|-------------|-------------|
| `pause_recording` | Pause current recording | `cap-desktop://action?value={"pause_recording":null}` |
| `resume_recording` | Resume paused recording | `cap-desktop://action?value={"resume_recording":null}` |
| `toggle_pause_recording` | Toggle pause/resume | `cap-desktop://action?value={"toggle_pause_recording":null}` |
| `set_microphone` | Switch microphone | `cap-desktop://action?value={"set_microphone":{"label":"MacBook Pro Microphone"}}` |
| `set_camera` | Switch camera | `cap-desktop://action?value={"set_camera":{"device_id":"..."}}}` |
Copy link

Choose a reason for hiding this comment

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

Docs: extra } in the set_camera example URL.

Suggested change
| `set_camera` | Switch camera | `cap-desktop://action?value={"set_camera":{"device_id":"..."}}}` |
| `set_camera` | Switch camera | `cap-desktop://action?value={"set_camera":{"device_id":"..."}}` |


All new actions call the existing internal functions (`crate::recording::pause_recording`, etc.), following the same pattern as `StartRecording` and `StopRecording`.

## Raycast Extension

Located in `extensions/raycast/` with commands:
- Start Recording
- Stop Recording
- Pause Recording
- Resume Recording
- Toggle Pause
- Open Settings

## Testing

```bash
# Test pause/resume
open "cap-desktop://action?value={\"toggle_pause_recording\":null}"

# Test open settings
open "cap-desktop://action?value={\"open_settings\":{\"page\":null}}"
```

## Checklist

- [x] Extended `DeepLinkAction` enum with new variants
- [x] Implemented `execute()` for each new action
- [x] Created Raycast extension with all commands
- [x] Added documentation

## Demo

[Demo video will be added after testing on macOS]
33 changes: 33 additions & 0 deletions apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub enum CaptureMode {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeepLinkAction {
// Recording controls
Copy link

Choose a reason for hiding this comment

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

Minor: this repo avoids code comments; can we drop the new // ... / /// ... in DeepLinkAction (and the whitespace-only lines) so this stays consistent with the rest of the codebase?

StartRecording {
capture_mode: CaptureMode,
camera: Option<DeviceOrModelID>,
Expand All @@ -26,6 +27,21 @@ pub enum DeepLinkAction {
mode: RecordingMode,
},
StopRecording,
PauseRecording,
ResumeRecording,
TogglePauseRecording,

// Device switching
SetMicrophone {
/// Microphone label/name. None to disable.
label: Option<String>,
},
SetCamera {
/// Camera device ID or model ID. None to disable.
device_id: Option<DeviceOrModelID>,
},

// Navigation
OpenEditor {
project_path: PathBuf,
},
Expand Down Expand Up @@ -146,6 +162,23 @@ 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::SetMicrophone { label } => {
let state = app.state::<ArcLock<App>>();
crate::set_mic_input(state, label).await
}
DeepLinkAction::SetCamera { device_id } => {
let state = app.state::<ArcLock<App>>();
crate::set_camera_input(app.clone(), state, device_id, None).await
}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
Expand Down
1 change: 1 addition & 0 deletions extensions/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.
65 changes: 65 additions & 0 deletions extensions/raycast/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "cap",
"title": "Cap",
"description": "Control Cap screen recording from Raycast. Start, stop, pause recordings and switch devices.",
"icon": "command-icon.png",
"author": "capsoftware",
"categories": ["Applications", "Productivity"],
"license": "MIT",
"commands": [
{
"name": "start-recording",
"title": "Start Recording",
"description": "Start a new screen recording",
"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": "toggle-pause",
"title": "Toggle Pause",
"description": "Toggle pause/resume on the current recording",
"mode": "no-view"
},
{
"name": "open-settings",
"title": "Open Settings",
"description": "Open Cap settings",
"mode": "no-view"
}
],
"dependencies": {
"@raycast/api": "^1.51.2"
},
"devDependencies": {
"@raycast/eslint-config": "1.0.5",
"@types/node": "18.8.3",
"@types/react": "18.0.9",
"eslint": "^7.32.0",
"prettier": "^2.5.1",
"typescript": "^4.4.3"
},
"scripts": {
"build": "ray build -e dist",
"dev": "ray develop",
"lint": "ray lint",
"publish": "npx @raycast/api@latest publish"
}
}
5 changes: 5 additions & 0 deletions extensions/raycast/src/open-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { triggerCapAction } from "./utils";

export default async function Command() {
await triggerCapAction({ open_settings: { page: null } });
}
5 changes: 5 additions & 0 deletions extensions/raycast/src/pause-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { simpleCapAction } from "./utils";

export default async function Command() {
await simpleCapAction("pause_recording");
}
5 changes: 5 additions & 0 deletions extensions/raycast/src/resume-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { simpleCapAction } from "./utils";

export default async function Command() {
await simpleCapAction("resume_recording");
}
25 changes: 25 additions & 0 deletions extensions/raycast/src/start-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { showToast, Toast } from "@raycast/api";
import { isCapInstalled } from "./utils";
import { open, closeMainWindow } from "@raycast/api";

export default async function Command() {
const installed = await isCapInstalled();
if (!installed) {
await showToast({
style: Toast.Style.Failure,
title: "Cap not installed",
message: "Please install Cap from cap.so",
});
return;
}

await closeMainWindow();
// Open Cap app - it will show the recording interface
Copy link

Choose a reason for hiding this comment

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

Nit: same here—can we remove the new inline comment to stay consistent with the no-comments policy?

await open("cap-desktop://");

await showToast({
style: Toast.Style.Success,
title: "Cap opened",
message: "Select a screen or window to record",
});
}
5 changes: 5 additions & 0 deletions extensions/raycast/src/stop-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { simpleCapAction } from "./utils";

export default async function Command() {
await simpleCapAction("stop_recording");
}
5 changes: 5 additions & 0 deletions extensions/raycast/src/toggle-pause.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { simpleCapAction } from "./utils";

export default async function Command() {
await simpleCapAction("toggle_pause_recording");
}
32 changes: 32 additions & 0 deletions extensions/raycast/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { open, closeMainWindow, showToast, Toast, getApplications } from "@raycast/api";

const CAP_BUNDLE_ID = "so.cap.desktop";
const CAP_SCHEME = "cap-desktop";

export async function isCapInstalled(): Promise<boolean> {
const apps = await getApplications();
return apps.some(app => app.bundleId === CAP_BUNDLE_ID);
}

export async function triggerCapAction(action: object): Promise<void> {
Copy link

Choose a reason for hiding this comment

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

Small typing improvement: object is very broad; Record<string, unknown> avoids accidentally accepting arrays/functions.

Suggested change
export async function triggerCapAction(action: object): Promise<void> {
export async function triggerCapAction(action: Record<string, unknown>): Promise<void> {

const installed = await isCapInstalled();
if (!installed) {
await showToast({
style: Toast.Style.Failure,
title: "Cap not installed",
message: "Please install Cap from cap.so",
});
return;
}

const encoded = encodeURIComponent(JSON.stringify(action));
const url = `${CAP_SCHEME}://action?value=${encoded}`;

await closeMainWindow();
await open(url);
}

export async function simpleCapAction(actionName: string): Promise<void> {
const action = { [actionName]: null };
await triggerCapAction(action);
}
17 changes: 17 additions & 0 deletions extensions/raycast/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": "src",
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}