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
26 changes: 26 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,
SwitchCamera {
camera: DeviceOrModelID,
},
SwitchMicrophone {
mic_label: String,
},
OpenEditor {
project_path: PathBuf,
},
Expand Down Expand Up @@ -146,6 +155,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::SwitchCamera { camera } => {
let state = app.state::<ArcLock<App>>();
crate::set_camera_input(app.clone(), state, Some(camera)).await
}
DeepLinkAction::SwitchMicrophone { mic_label } => {
let state = app.state::<ArcLock<App>>();
crate::set_mic_input(state, Some(mic_label)).await
}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
Expand Down
72 changes: 72 additions & 0 deletions extensions/raycast/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Cap Raycast Extension

Control Cap screen recording directly from Raycast.

## Features

- **Start Recording**: Choose between screen or window capture with Studio or Instant mode
- **Stop Recording**: Quickly stop your current recording
- **Pause Recording**: Pause the recording without stopping
- **Resume Recording**: Resume a paused recording
- **Toggle Pause**: Toggle between pause and resume states
- **Switch Camera**: Change the camera being used during recording
- **Switch Microphone**: Change the microphone being used during recording

## Installation

### From Source

1. Clone the Cap repository
2. Navigate to the extension directory:
```bash
cd extensions/raycast
```
3. Install dependencies:
```bash
npm install
```
4. Build and import to Raycast:
```bash
npm run dev
```

## Usage

Once installed, you can access all Cap commands from Raycast:

- Type "Cap" in Raycast to see all available commands
- Use keyboard shortcuts to quickly control your recordings
- Commands execute instantly without opening the Cap UI

## Requirements

- Cap desktop application must be installed and running
- macOS (Cap is currently macOS-only)
- Raycast

## Deep Link Protocol

This extension uses Cap's deep link protocol (`cap://action`) to communicate with the desktop application. All commands are executed via URL schemes that trigger the corresponding actions in Cap.

## Development

```bash
# Install dependencies
npm install

# Run in development mode
npm run dev

# Build for production
npm run build

# Lint code
npm run lint

# Fix linting issues
npm run fix-lint
```

## License

MIT
77 changes: 77 additions & 0 deletions extensions/raycast/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "cap",
"title": "Cap",
"description": "Control Cap screen recording from Raycast",
"icon": "cap-icon.png",
Copy link

Choose a reason for hiding this comment

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

Icon file cap-icon.png is referenced here but isn’t included in this PR; Raycast will fail to load the extension icon unless it exists. Consider adding it or switching to a built-in icon.

"author": "cap",
"categories": [
"Productivity",
"Media"
],
"license": "MIT",
"commands": [
{
"name": "start-recording",
"title": "Start Recording",
"description": "Start a new Cap recording",
"mode": "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 the paused recording",
"mode": "no-view"
},
{
"name": "toggle-pause",
"title": "Toggle Pause Recording",
"description": "Toggle pause/resume for the current recording",
"mode": "no-view"
},
{
"name": "switch-camera",
"title": "Switch Camera",
"description": "Switch to a different camera",
"mode": "view"
},
{
"name": "switch-microphone",
"title": "Switch Microphone",
"description": "Switch to a different microphone",
"mode": "view"
}
],
"dependencies": {
"@raycast/api": "^1.48.0"
},
"devDependencies": {
"@types/node": "18.8.3",
"@types/react": "18.0.9",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.0",
"eslint-config-prettier": "^8.3.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"
}
}
26 changes: 26 additions & 0 deletions extensions/raycast/src/pause-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { showToast, Toast, closeMainWindow } from "@raycast/api";
import { executeCapAction } from "./utils";

export default async function Command() {
try {
await closeMainWindow();

await showToast({
style: Toast.Style.Animated,
title: "Pausing recording...",
});

await executeCapAction({ pause_recording: {} });
Copy link

Choose a reason for hiding this comment

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

With the enum serialization, this should be a JSON string action.

Suggested change
await executeCapAction({ pause_recording: {} });
await executeCapAction("pause_recording");


await showToast({
style: Toast.Style.Success,
title: "Recording paused",
});
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to pause recording",
message: String(error),
});
}
}
26 changes: 26 additions & 0 deletions extensions/raycast/src/resume-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { showToast, Toast, closeMainWindow } from "@raycast/api";
import { executeCapAction } from "./utils";

export default async function Command() {
try {
await closeMainWindow();

await showToast({
style: Toast.Style.Animated,
title: "Resuming recording...",
});

await executeCapAction({ resume_recording: {} });
Copy link

Choose a reason for hiding this comment

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

With the enum serialization, this should be a JSON string action.

Suggested change
await executeCapAction({ resume_recording: {} });
await executeCapAction("resume_recording");


await showToast({
style: Toast.Style.Success,
title: "Recording resumed",
});
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to resume recording",
message: String(error),
});
}
}
90 changes: 90 additions & 0 deletions extensions/raycast/src/start-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ActionPanel, Action, List, showToast, Toast } from "@raycast/api";
import { executeCapAction, RecordingMode } from "./utils";

export default function Command() {
const captureOptions = [
{ title: "Full Screen", value: "screen" },
{ title: "Window", value: "window" },
];

const recordingModes: { title: string; value: RecordingMode }[] = [
{ title: "Studio Mode", value: "studio" },
{ title: "Instant Mode", value: "instant" },
];

async function startRecording(
captureType: "screen" | "window",
mode: RecordingMode,
captureSystemAudio: boolean
) {
try {
await showToast({
style: Toast.Style.Animated,
title: "Starting recording...",
});

// For simplicity, using default screen/window
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; you can drop these without losing clarity.

Suggested change
// For simplicity, using default screen/window
const capture_mode =

// In a real implementation, you'd want to list available screens/windows
const capture_mode =
captureType === "screen"
? { screen: "Default Screen" }
Copy link

Choose a reason for hiding this comment

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

capture_mode uses a display/window name and the backend matches it exactly. Default Screen / Default Window won’t exist, so this command will consistently fail unless you wire it up to real screen/window names (or adjust the deep link/backend to accept a default/ID).

: { window: "Default Window" };

await executeCapAction({
start_recording: {
capture_mode,
capture_system_audio: captureSystemAudio,
mode,
},
});

await showToast({
style: Toast.Style.Success,
title: "Recording started",
});
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to start recording",
message: String(error),
});
}
}

return (
<List>
{captureOptions.map((captureOption) =>
recordingModes.map((mode) => (
<List.Item
key={`${captureOption.value}-${mode.value}`}
title={`${captureOption.title} - ${mode.title}`}
actions={
<ActionPanel>
<Action
title="Start Recording"
onAction={() =>
startRecording(
captureOption.value as "screen" | "window",
mode.value,
false
)
}
/>
<Action
title="Start Recording (with System Audio)"
onAction={() =>
startRecording(
captureOption.value as "screen" | "window",
mode.value,
true
)
}
/>
</ActionPanel>
}
/>
))
)}
</List>
);
}
26 changes: 26 additions & 0 deletions extensions/raycast/src/stop-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { showToast, Toast, closeMainWindow } from "@raycast/api";
import { executeCapAction } from "./utils";

export default async function Command() {
try {
await closeMainWindow();

await showToast({
style: Toast.Style.Animated,
title: "Stopping recording...",
});

await executeCapAction({ stop_recording: {} });
Copy link

Choose a reason for hiding this comment

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

With the enum serialization, this should be a JSON string action.

Suggested change
await executeCapAction({ stop_recording: {} });
await executeCapAction("stop_recording");


await showToast({
style: Toast.Style.Success,
title: "Recording stopped",
});
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to stop recording",
message: String(error),
});
}
}
Loading