Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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,
SwitchMicrophone {
mic_label: String,
},
SwitchCamera {
camera_id: DeviceOrModelID,
},
OpenEditor {
project_path: PathBuf,
},
Expand Down Expand Up @@ -147,6 +156,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::SwitchMicrophone { mic_label } => {
let state = app.state::<ArcLock<App>>();
crate::set_mic_input(state.clone(), Some(mic_label)).await
}
DeepLinkAction::SwitchCamera { camera_id } => {
let state = app.state::<ArcLock<App>>();
crate::set_camera_input(app.clone(), state.clone(), Some(camera_id), None).await
}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
Expand Down
50 changes: 50 additions & 0 deletions apps/raycast-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Cap Raycast Extension

Control Cap screen recorder directly from Raycast.

## Features

- **Start Recording** - Begin a new screen recording
- **Stop Recording** - Stop the current recording
- **Pause Recording** - Pause an active recording
- **Resume Recording** - Resume a paused recording
- **Toggle Pause** - Toggle pause state
- **Switch Microphone** - Change the active microphone
- **Switch Camera** - Change the active camera
- **Open Settings** - Access Cap settings pages

## Installation

1. Clone the Cap repository
2. Navigate to `apps/raycast-extension`
3. Run `pnpm install`
4. Run `pnpm dev` to develop or `pnpm build` to build

## Usage

After installing the extension in Raycast, you can:

1. Open Raycast (default: `Cmd + K`)
2. Type "Cap" to see all available commands
3. Select the command you want to execute

## Deep Link Protocol

This extension uses Cap's deep link protocol (`cap-desktop://action`) to communicate with the desktop app. All commands are executed through deep links that trigger actions in the Cap desktop application.

## Development

```bash
# Install dependencies
pnpm install

# Start development mode
pnpm dev

# Build for production
pnpm build
```

## License

MIT
72 changes: 72 additions & 0 deletions apps/raycast-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "cap",
"title": "Cap",
"description": "Control Cap screen recorder - start, stop, pause recordings and switch devices",
"icon": "cap-icon.png",
"author": "BossChaos",
"license": "MIT",
"commands": [
{
"name": "start-recording",
"title": "Start Recording",
"description": "Start a new screen recording",
"mode": "view"
},
{
"name": "stop-recording",
"title": "Stop Recording",
"description": "Stop the current recording",
"mode": "view"
},
{
"name": "pause-recording",
"title": "Pause Recording",
"description": "Pause the current recording",
"mode": "view"
},
{
"name": "resume-recording",
"title": "Resume Recording",
"description": "Resume a paused recording",
"mode": "view"
},
{
"name": "toggle-pause",
"title": "Toggle Pause",
"description": "Toggle pause state of current recording",
"mode": "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"
},
{
"name": "open-settings",
"title": "Open Settings",
"description": "Open Cap settings",
"mode": "view"
}
],
"dependencies": {
"@raycast/api": "^1.55.2",
"@raycast/utils": "^1.17.0"
},
"devDependencies": {
"@types/node": "^20.10.4",
"@types/react": "^18.2.42",
"typescript": "^5.3.3"
},
"scripts": {
"build": "ray build -e dist",
"dev": "ray develop"
}
}
48 changes: 48 additions & 0 deletions apps/raycast-extension/src/deeplink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

const DEEP_LINK_BASE = "cap-desktop://action";

export function createDeepLinkUrl(action: string, params?: Record<string, string>): string {
const value = JSON.stringify({ action, ...params });
return `${DEEP_LINK_BASE}?value=${encodeURIComponent(value)}`;
}

export async function sendDeepLink(action: string, params?: Record<string, string>): Promise<void> {
const url = createDeepLinkUrl(action, params);

if (process.platform === "darwin") {
await execAsync(`open "${url}"`);
} else if (process.platform === "win32") {
await execAsync(`start "" "${url}"`);
} else {
await execAsync(`xdg-open "${url}"`);
}
}

export interface Microphone {
label: string;
}

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

export async function getAvailableMicrophones(): Promise<Microphone[]> {
return [
{ label: "Default" },
{ label: "MacBook Pro Microphone" },
{ label: "External Microphone" },
];
}

export async function getAvailableCameras(): Promise<Camera[]> {
return [
{ id: "default", label: "Default" },
{ id: "faceTime", label: "FaceTime HD Camera" },
{ id: "external", label: "External Camera" },
];
}
69 changes: 69 additions & 0 deletions apps/raycast-extension/src/open-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Action, ActionPanel, Icon, List, showToast, Toast } from "@raycast/api";
import { sendDeepLink } from "../deeplink";

export default function OpenSettingsCommand() {
const handleOpen = async (page?: string) => {
try {
await showToast({
style: Toast.Style.Animated,
title: "Opening Settings...",
});

await sendDeepLink("open_settings", page ? { page } : undefined);

await showToast({
style: Toast.Style.Success,
title: "Settings Opened",
});
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to Open Settings",
message: error instanceof Error ? error.message : "Unknown error",
});
}
};

return (
<List>
<List.Section title="Cap Settings">
<List.Item
title="General"
icon={Icon.Gear}
actions={
<ActionPanel>
<Action title="Open General Settings" icon={Icon.ArrowRight} onAction={() => handleOpen("general")} />
</ActionPanel>
}
/>
<List.Item
title="Recording"
icon={Icon.Record}
actions={
<ActionPanel>
<Action title="Open Recording Settings" icon={Icon.ArrowRight} onAction={() => handleOpen("recording")} />
</ActionPanel>
}
/>
<List.Item
title="Audio"
icon={Icon.Microphone}
actions={
<ActionPanel>
<Action title="Open Audio Settings" icon={Icon.ArrowRight} onAction={() => handleOpen("audio")} />
</ActionPanel>
}
/>
<List.Item
title="Video"
icon={Icon.Camera}
actions={
<ActionPanel>
<Action title="Open Video Settings" icon={Icon.ArrowRight} onAction={() => handleOpen("video")} />
</ActionPanel>
}
/>
</List.Section>
</List>
);
}
32 changes: 32 additions & 0 deletions apps/raycast-extension/src/pause-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Action, ActionPanel, Icon, showToast, Toast } from "@raycast/api";
import { sendDeepLink } from "../deeplink";

export default function PauseRecordingCommand() {
const handlePause = async () => {
try {
await showToast({
style: Toast.Style.Animated,
title: "Pausing Recording...",
});

await sendDeepLink("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: error instanceof Error ? error.message : "Unknown error",
});
}
};

return (
<ActionPanel>
<Action title="Pause Recording" icon={Icon.Pause} onAction={handlePause} />
</ActionPanel>
);
}
32 changes: 32 additions & 0 deletions apps/raycast-extension/src/resume-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Action, ActionPanel, Icon, showToast, Toast } from "@raycast/api";
import { sendDeepLink } from "../deeplink";

export default function ResumeRecordingCommand() {
const handleResume = async () => {
try {
await showToast({
style: Toast.Style.Animated,
title: "Resuming Recording...",
});

await sendDeepLink("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: error instanceof Error ? error.message : "Unknown error",
});
}
};

return (
<ActionPanel>
<Action title="Resume Recording" icon={Icon.Play} onAction={handleResume} />
</ActionPanel>
);
}
36 changes: 36 additions & 0 deletions apps/raycast-extension/src/start-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Action, ActionPanel, confirmAlert, Icon, showToast, Toast } from "@raycast/api";
import { sendDeepLink } from "../deeplink";

export default function StartRecordingCommand() {
const handleStart = async () => {
try {
await showToast({
style: Toast.Style.Animated,
title: "Starting Recording...",
});

await sendDeepLink("start_recording", {
capture_mode: "screen",
mode: "instant",
capture_system_audio: "true",
});

await showToast({
style: Toast.Style.Success,
title: "Recording Started",
});
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to Start Recording",
message: error instanceof Error ? error.message : "Unknown error",
});
}
};

return (
<ActionPanel>
<Action title="Start Recording" icon={Icon.Record} onAction={handleStart} />
</ActionPanel>
);
}
Loading