-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: add Raycast extension and new deeplink actions #1599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Cap Raycast Extension | ||
|
|
||
| Control [Cap](https://cap.so) screen recordings directly from [Raycast](https://raycast.com). | ||
|
|
||
| ## Commands | ||
|
|
||
| | Command | Description | | ||
| |---------|-------------| | ||
| | **Start Recording** | Start a new screen recording with default settings | | ||
| | **Stop Recording** | Stop the current recording | | ||
| | **Toggle Pause** | Pause or resume the current recording | | ||
| | **Open Settings** | Open Cap settings window | | ||
| | **Recent Recordings** | Browse and open recent recordings | | ||
|
|
||
| ## How It Works | ||
|
|
||
| This extension communicates with Cap using deeplinks (`cap-desktop://action?value=...`). Cap must be running for commands to work. | ||
|
|
||
| ## Deeplinks | ||
|
|
||
| Cap supports the following deeplink actions: | ||
|
|
||
| - `start_recording` — Start recording with capture mode, camera, mic, and mode options | ||
| - `stop_recording` — Stop the current recording | ||
| - `pause_recording` — Pause the current recording | ||
| - `resume_recording` — Resume a paused recording | ||
| - `toggle_pause` — Toggle pause/resume | ||
| - `set_camera` — Switch the active camera | ||
| - `set_microphone` — Switch the active microphone | ||
| - `open_editor` — Open a recording in the editor | ||
| - `open_settings` — Open the settings window | ||
|
|
||
| ### URL Format | ||
|
|
||
| ``` | ||
| cap-desktop://action?value=<url-encoded-json> | ||
| ``` | ||
|
|
||
| Unit actions (no parameters): | ||
| ``` | ||
| cap-desktop://action?value=%22stop_recording%22 | ||
| ``` | ||
|
|
||
| Actions with parameters (URL-encode the JSON): | ||
| ``` | ||
| cap-desktop://action?value=%7B%22start_recording%22%3A%7B%22capture_mode%22%3A%7B%22screen%22%3A%22Main%20Display%22%7D%2C%22camera%22%3Anull%2C%22mic_label%22%3Anull%2C%22capture_system_audio%22%3Afalse%2C%22mode%22%3A%22studio%22%7D%7D | ||
| ``` | ||
|
|
||
| ## Development | ||
|
|
||
| ```bash | ||
| cd apps/raycast-extension | ||
| pnpm install | ||
| pnpm dev | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| { | ||
| "name": "cap", | ||
| "title": "Cap", | ||
| "description": "Control Cap screen recordings from Raycast", | ||
| "icon": "extension-icon.png", | ||
| "author": "capsoftware", | ||
| "license": "AGPL-3.0", | ||
| "commands": [ | ||
| { | ||
| "name": "start-recording", | ||
| "title": "Start Recording", | ||
| "subtitle": "Cap", | ||
| "description": "Start a new screen recording with Cap", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "stop-recording", | ||
| "title": "Stop Recording", | ||
| "subtitle": "Cap", | ||
| "description": "Stop the current Cap recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "toggle-pause", | ||
| "title": "Toggle Pause", | ||
| "subtitle": "Cap", | ||
| "description": "Pause or resume the current Cap recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "open-settings", | ||
| "title": "Open Settings", | ||
| "subtitle": "Cap", | ||
| "description": "Open Cap settings", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "list-recent-recordings", | ||
| "title": "Recent Recordings", | ||
| "subtitle": "Cap", | ||
| "description": "Browse recent Cap recordings", | ||
| "mode": "view" | ||
| } | ||
| ], | ||
| "dependencies": { | ||
| "@raycast/api": "^1.93.2", | ||
| "@raycast/utils": "^1.19.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@raycast/eslint-config": "^1.0.11", | ||
| "typescript": "^5.8.3", | ||
| "@types/node": "^22.13.4", | ||
| "eslint": "^8.57.0" | ||
| }, | ||
| "scripts": { | ||
| "build": "ray build", | ||
| "dev": "ray develop", | ||
| "fix-lint": "ray lint --fix", | ||
| "lint": "ray lint" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { open, showToast, Toast } from "@raycast/api"; | ||
|
|
||
| const SCHEME = "cap-desktop"; | ||
|
|
||
| type DeepLinkAction = | ||
| | "stop_recording" | ||
| | "pause_recording" | ||
| | "resume_recording" | ||
| | "toggle_pause" | ||
| | { | ||
| start_recording: { | ||
| capture_mode: { screen: string } | { window: string }; | ||
| camera: { DeviceID: string } | { ModelID: string } | null; | ||
| mic_label: string | null; | ||
| capture_system_audio: boolean; | ||
| mode: "studio" | "instant" | "screenshot"; | ||
| }; | ||
| } | ||
| | { set_camera: { camera: { DeviceID: string } | { ModelID: string } | null } } | ||
| | { set_microphone: { mic_label: string | null } } | ||
| | { open_editor: { project_path: string } } | ||
| | { open_settings: { page: string | null } }; | ||
|
|
||
| function buildDeeplink(action: DeepLinkAction): string { | ||
| const json = JSON.stringify(action); | ||
| return `${SCHEME}://action?value=${encodeURIComponent(json)}`; | ||
| } | ||
|
|
||
| export async function openDeeplink( | ||
| action: DeepLinkAction, | ||
| successMessage?: string, | ||
| ): Promise<void> { | ||
| const url = buildDeeplink(action); | ||
| try { | ||
| await open(url); | ||
| if (successMessage) { | ||
| await showToast({ style: Toast.Style.Success, title: successMessage }); | ||
| } | ||
| } catch (error) { | ||
| await showToast({ | ||
| style: Toast.Style.Failure, | ||
| title: "Failed to communicate with Cap", | ||
| message: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,78 @@ | ||||||||||||||||||||||||||||||||||||||||
| import { Action, ActionPanel, List } from "@raycast/api"; | ||||||||||||||||||||||||||||||||||||||||
| import { usePromise } from "@raycast/utils"; | ||||||||||||||||||||||||||||||||||||||||
| import { readdirSync, statSync } from "fs"; | ||||||||||||||||||||||||||||||||||||||||
| import { join } from "path"; | ||||||||||||||||||||||||||||||||||||||||
| import { homedir } from "os"; | ||||||||||||||||||||||||||||||||||||||||
| import { openDeeplink } from "./deeplink"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function getRecordingsDir(): string { | ||||||||||||||||||||||||||||||||||||||||
| return join( | ||||||||||||||||||||||||||||||||||||||||
| homedir(), | ||||||||||||||||||||||||||||||||||||||||
| "Library", | ||||||||||||||||||||||||||||||||||||||||
| "Application Support", | ||||||||||||||||||||||||||||||||||||||||
| "so.cap.desktop", | ||||||||||||||||||||||||||||||||||||||||
| "recordings", | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+9
to
+15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hardcoded macOS path will prevent extension from working on Windows Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/raycast-extension/src/list-recent-recordings.tsx
Line: 10:16
Comment:
hardcoded macOS path will prevent extension from working on Windows
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
8
to
16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor style thing: repo has a no-comments rule, so I'd remove the inline comment here.
Suggested change
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done, stripped out the inline comments |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| interface Recording { | ||||||||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||||||||
| path: string; | ||||||||||||||||||||||||||||||||||||||||
| modifiedAt: Date; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function listRecordings(): Recording[] { | ||||||||||||||||||||||||||||||||||||||||
| const dir = getRecordingsDir(); | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| return readdirSync(dir) | ||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the recordings folder grows, doing a
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| .filter((f) => f.endsWith(".cap")) | ||||||||||||||||||||||||||||||||||||||||
| .map((f) => { | ||||||||||||||||||||||||||||||||||||||||
| const fullPath = join(dir, f); | ||||||||||||||||||||||||||||||||||||||||
| const stat = statSync(fullPath); | ||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||
| name: f.replace(/\.cap$/, ""), | ||||||||||||||||||||||||||||||||||||||||
| path: fullPath, | ||||||||||||||||||||||||||||||||||||||||
| modifiedAt: stat.mtime, | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| .sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime()); | ||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||
| return []; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export default function RecentRecordings() { | ||||||||||||||||||||||||||||||||||||||||
| const { data: recordings, isLoading } = usePromise(async () => listRecordings()); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||
| <List isLoading={isLoading} searchBarPlaceholder="Search recordings..."> | ||||||||||||||||||||||||||||||||||||||||
| {recordings?.length === 0 ? ( | ||||||||||||||||||||||||||||||||||||||||
| <List.EmptyView | ||||||||||||||||||||||||||||||||||||||||
| title="No Recordings Found" | ||||||||||||||||||||||||||||||||||||||||
| description="Record something with Cap first" | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||
| recordings?.map((rec) => ( | ||||||||||||||||||||||||||||||||||||||||
| <List.Item | ||||||||||||||||||||||||||||||||||||||||
| key={rec.path} | ||||||||||||||||||||||||||||||||||||||||
| title={rec.name} | ||||||||||||||||||||||||||||||||||||||||
| subtitle={rec.modifiedAt.toLocaleDateString()} | ||||||||||||||||||||||||||||||||||||||||
| accessories={[{ date: rec.modifiedAt }]} | ||||||||||||||||||||||||||||||||||||||||
| actions={ | ||||||||||||||||||||||||||||||||||||||||
| <ActionPanel> | ||||||||||||||||||||||||||||||||||||||||
| <Action | ||||||||||||||||||||||||||||||||||||||||
| title="Open in Editor" | ||||||||||||||||||||||||||||||||||||||||
| onAction={async () => | ||||||||||||||||||||||||||||||||||||||||
| openDeeplink({ open_editor: { project_path: rec.path } }) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
64
to
68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. makes sense, added async to the handler |
||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| <Action.ShowInFinder path={rec.path} /> | ||||||||||||||||||||||||||||||||||||||||
| </ActionPanel> | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| )) | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
| </List> | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { closeMainWindow } from "@raycast/api"; | ||
| import { openDeeplink } from "./deeplink"; | ||
|
|
||
| export default async function openSettings() { | ||
| await openDeeplink({ open_settings: { page: null } }, "Opening settings"); | ||
| await closeMainWindow(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { closeMainWindow } from "@raycast/api"; | ||
| import { openDeeplink } from "./deeplink"; | ||
|
|
||
| export default async function startRecording() { | ||
| await openDeeplink( | ||
| { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hardcoded Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/raycast-extension/src/start-recording.ts
Line: 8:8
Comment:
hardcoded `"Main Display"` will fail if user's primary display has different name
How can I resolve this? If you propose a fix, please make it concise. |
||
| camera: null, | ||
| mic_label: null, | ||
| capture_system_audio: false, | ||
| mode: "studio", | ||
| }, | ||
| }, | ||
| "Recording started", | ||
| ); | ||
| await closeMainWindow(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { closeMainWindow } from "@raycast/api"; | ||
| import { openDeeplink } from "./deeplink"; | ||
|
|
||
| export default async function stopRecording() { | ||
| await openDeeplink("stop_recording", "Recording stopped"); | ||
| await closeMainWindow(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { closeMainWindow } from "@raycast/api"; | ||
| import { openDeeplink } from "./deeplink"; | ||
|
|
||
| export default async function togglePause() { | ||
| await openDeeplink("toggle_pause", "Toggled pause"); | ||
| await closeMainWindow(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "$schema": "https://www.raycast.com/schemas/extension.json", | ||
| "compilerOptions": { | ||
| "strict": true, | ||
| "jsx": "react-jsx", | ||
| "target": "ESNext", | ||
| "module": "ESNext", | ||
| "moduleResolution": "bundler", | ||
| "resolveJsonModule": true, | ||
| "esModuleInterop": true, | ||
| "skipLibCheck": true, | ||
| "forceConsistentCasingInFileNames": true | ||
| }, | ||
| "include": ["src/**/*"] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The examples look slightly off:
valueneeds to be URL-encoded JSON (including quotes for unit actions), andmodeshould match the actual enum values (studio|instant|screenshot).cap-desktop://action?value=
cap-desktop://action?value=%22stop_recording%22
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed the examples to use proper URL encoding and corrected the mode values, thanks for catching that