-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: Add deeplinks support and Raycast extension #1569
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 |
|---|---|---|
|
|
@@ -15,23 +15,91 @@ pub enum CaptureMode { | |
| Window(String), | ||
| } | ||
|
|
||
| /// Response types for deeplink queries | ||
| #[derive(Debug, Serialize, Deserialize)] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub struct RecordingStatusResponse { | ||
| pub is_recording: bool, | ||
| pub is_paused: bool, | ||
| pub mode: Option<RecordingMode>, | ||
| } | ||
|
Comment on lines
+18
to
+25
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. [P1] Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 18:25
Comment:
[P1] `RecordingStatusResponse`/`DisplayInfo`/etc are never used and (more importantly) the deep link “query” actions (`List*`, `GetRecordingStatus`) don’t return anything to the caller. Right now they only `trace!` and `Ok(())`, which makes them effectively no-ops from the deeplink consumer’s perspective (Raycast can’t read stdout/stderr/tracing). This also means these new structs add dead code unless a return mechanism is introduced.
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| #[derive(Debug, Serialize, Deserialize)] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub struct DisplayInfo { | ||
| pub name: String, | ||
| pub id: String, | ||
| } | ||
|
|
||
| #[derive(Debug, Serialize, Deserialize)] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub struct WindowInfo { | ||
| pub name: String, | ||
| pub owner_name: Option<String>, | ||
| } | ||
|
|
||
| #[derive(Debug, Serialize, Deserialize)] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub struct AudioDeviceInfo { | ||
| pub label: String, | ||
| } | ||
|
|
||
| #[derive(Debug, Serialize, Deserialize)] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub struct CameraInfo { | ||
| pub name: String, | ||
| pub id: String, | ||
| } | ||
|
|
||
| #[derive(Debug, Serialize, Deserialize)] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub enum DeepLinkAction { | ||
| /// Start a new recording | ||
| StartRecording { | ||
| capture_mode: CaptureMode, | ||
| camera: Option<DeviceOrModelID>, | ||
| mic_label: Option<String>, | ||
| capture_system_audio: bool, | ||
| mode: RecordingMode, | ||
| }, | ||
| /// Stop the current recording | ||
| StopRecording, | ||
| /// Pause the current recording | ||
| PauseRecording, | ||
| /// Resume a paused recording | ||
| ResumeRecording, | ||
| /// Toggle pause state of the current recording | ||
| TogglePause, | ||
| /// Switch the microphone input | ||
| SetMicrophone { | ||
| label: Option<String>, | ||
| }, | ||
| /// Switch the camera input | ||
| SetCamera { | ||
| device_id: Option<String>, | ||
| }, | ||
| /// Take a screenshot | ||
| TakeScreenshot, | ||
| /// Open a project in the editor | ||
| OpenEditor { | ||
| project_path: PathBuf, | ||
| }, | ||
| /// Open the settings window | ||
| OpenSettings { | ||
| page: Option<String>, | ||
| }, | ||
| /// Show the main Cap window | ||
| ShowMainWindow, | ||
| /// List available displays | ||
| ListDisplays, | ||
| /// List available windows | ||
| ListWindows, | ||
| /// List available microphones | ||
| ListMicrophones, | ||
| /// List available cameras | ||
| ListCameras, | ||
| /// Get current recording status | ||
| GetRecordingStatus, | ||
| } | ||
|
|
||
| pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) { | ||
|
|
@@ -146,12 +214,115 @@ 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::TogglePause => { | ||
| 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>>(); | ||
| let camera_id = device_id.map(|id| DeviceOrModelID::DeviceID(id)); | ||
| crate::set_camera_input(app.clone(), state, camera_id, None).await | ||
| } | ||
| DeepLinkAction::TakeScreenshot => { | ||
| // Take a screenshot of the primary display | ||
| let displays = cap_recording::screen_capture::list_displays(); | ||
| if let Some((display, _)) = displays.into_iter().next() { | ||
| let target = ScreenCaptureTarget::Display { id: display.id }; | ||
| crate::recording::take_screenshot(app.clone(), target) | ||
| .await | ||
| .map(|_| ()) | ||
| } else { | ||
| Err("No display found for screenshot".to_string()) | ||
| } | ||
|
Comment on lines
+235
to
+245
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. [P2] Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 235:245
Comment:
[P2] `TakeScreenshot` picks `list_displays().into_iter().next()` as “primary display”. `list_displays()` ordering isn’t guaranteed, so this can capture a non-primary monitor depending on platform/ordering. If “primary” matters, it should be selected explicitly (or allow a display id/name parameter).
How can I resolve this? If you propose a fix, please make it concise. |
||
| } | ||
| DeepLinkAction::OpenEditor { project_path } => { | ||
| crate::open_project_from_path(Path::new(&project_path), app.clone()) | ||
| } | ||
| DeepLinkAction::OpenSettings { page } => { | ||
| crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await | ||
| } | ||
| DeepLinkAction::ShowMainWindow => { | ||
| crate::show_window(app.clone(), ShowCapWindow::Main { init_target_mode: None }).await | ||
| } | ||
| DeepLinkAction::ListDisplays => { | ||
| let displays: Vec<DisplayInfo> = cap_recording::screen_capture::list_displays() | ||
| .into_iter() | ||
| .map(|(d, _)| DisplayInfo { | ||
| name: d.name.clone(), | ||
| id: format!("{:?}", d.id), | ||
| }) | ||
| .collect(); | ||
| // Log for debugging; in practice this could be returned via a different mechanism | ||
| trace!("Available displays: {:?}", displays); | ||
| Ok(()) | ||
|
Comment on lines
+256
to
+266
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. [P2] Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 256:266
Comment:
[P2] `ListDisplays` formats `d.id` with `format!("{:?}", d.id)` instead of a stable identifier. If `DisplayId`’s `Debug` impl changes across versions/platforms, the value becomes non-portable for consumers (e.g., a Raycast command that wants to persist an ID). Consider exposing the underlying id type/string in a stable way.
How can I resolve this? If you propose a fix, please make it concise. |
||
| } | ||
| DeepLinkAction::ListWindows => { | ||
| let windows: Vec<WindowInfo> = cap_recording::screen_capture::list_windows() | ||
| .into_iter() | ||
| .map(|(w, _)| WindowInfo { | ||
| name: w.name.clone(), | ||
| owner_name: w.owner_name.clone(), | ||
| }) | ||
| .collect(); | ||
| trace!("Available windows: {:?}", windows); | ||
| Ok(()) | ||
| } | ||
| DeepLinkAction::ListMicrophones => { | ||
| use cap_recording::feeds::microphone::MicrophoneFeed; | ||
| let mics: Vec<AudioDeviceInfo> = MicrophoneFeed::list() | ||
| .keys() | ||
| .map(|label| AudioDeviceInfo { | ||
| label: label.clone(), | ||
| }) | ||
| .collect(); | ||
| trace!("Available microphones: {:?}", mics); | ||
| Ok(()) | ||
| } | ||
| DeepLinkAction::ListCameras => { | ||
| let cameras: Vec<CameraInfo> = cap_camera::list_cameras() | ||
| .map(|c| CameraInfo { | ||
| name: c.display_name().to_string(), | ||
| id: c.device_id().to_string(), | ||
| }) | ||
| .collect(); | ||
| trace!("Available cameras: {:?}", cameras); | ||
| Ok(()) | ||
| } | ||
| DeepLinkAction::GetRecordingStatus => { | ||
| let state = app.state::<ArcLock<App>>(); | ||
| let app_state = state.read().await; | ||
| let status = match &app_state.recording_state { | ||
| crate::RecordingState::None => RecordingStatusResponse { | ||
| is_recording: false, | ||
| is_paused: false, | ||
| mode: None, | ||
| }, | ||
| crate::RecordingState::Pending { mode, .. } => RecordingStatusResponse { | ||
| is_recording: false, | ||
| is_paused: false, | ||
| mode: Some(*mode), | ||
| }, | ||
| crate::RecordingState::Active(recording) => { | ||
| let is_paused = recording.is_paused().await.unwrap_or(false); | ||
| RecordingStatusResponse { | ||
| is_recording: true, | ||
| is_paused, | ||
| mode: Some(recording.mode()), | ||
| } | ||
| } | ||
| }; | ||
| trace!("Recording status: {:?}", status); | ||
| Ok(()) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # Cap Raycast Extension | ||
|
|
||
| Control [Cap](https://cap.so) screen recording directly from Raycast. | ||
|
|
||
| ## Features | ||
|
|
||
| - **Start Recording** - Start a new screen recording | ||
| - **Stop Recording** - Stop the current recording | ||
| - **Pause Recording** - Pause the current recording | ||
| - **Resume Recording** - Resume a paused recording | ||
| - **Toggle Pause** - Toggle pause state of the current recording | ||
| - **Take Screenshot** - Capture the current screen | ||
| - **Open Cap** - Open the Cap application | ||
| - **Open Settings** - Open Cap settings | ||
| - **Recording Controls** - Quick access to all Cap commands in a list view | ||
|
|
||
| ## Requirements | ||
|
|
||
| - [Cap](https://cap.so) must be installed on your Mac | ||
| - macOS 11.0 or later | ||
|
|
||
| ## How It Works | ||
|
|
||
| This extension uses Cap's deeplink protocol (`cap-desktop://`) to communicate with the Cap application. When you trigger a command, it opens a deeplink URL that Cap handles to perform the requested action. | ||
|
|
||
| ### Deeplink Format | ||
|
|
||
| Cap deeplinks use the following format: | ||
| ``` | ||
| cap-desktop://action?value=<JSON_ENCODED_ACTION> | ||
| ``` | ||
|
|
||
| Available actions: | ||
| - `start_recording` - Start recording with capture mode, camera, mic settings | ||
| - `stop_recording` - Stop the current recording | ||
| - `pause_recording` - Pause the current recording | ||
| - `resume_recording` - Resume a paused recording | ||
| - `toggle_pause` - Toggle pause state | ||
| - `take_screenshot` - Take a screenshot | ||
| - `open_settings` - Open settings with optional page parameter | ||
| - `show_main_window` - Show the main Cap window | ||
|
|
||
| ## Development | ||
|
|
||
| ```bash | ||
| # Install dependencies | ||
| npm install | ||
|
|
||
| # Start development | ||
| npm run dev | ||
|
|
||
| # Build the extension | ||
| npm run build | ||
|
|
||
| # Publish to Raycast Store | ||
| npm run publish | ||
| ``` | ||
|
|
||
| ## License | ||
|
|
||
| AGPL-3.0 - See [LICENSE](../../LICENSE) for details. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| { | ||
| "$schema": "https://www.raycast.com/schemas/extension.json", | ||
| "name": "cap", | ||
| "title": "Cap", | ||
| "description": "Control Cap screen recording from Raycast", | ||
| "icon": "cap-icon.png", | ||
| "author": "Cap Software", | ||
| "authorUrl": "https://cap.so", | ||
| "categories": ["Productivity", "Media"], | ||
| "license": "AGPL-3.0", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/CapSoftware/Cap" | ||
| }, | ||
| "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 recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "pause-recording", | ||
| "title": "Pause Recording", | ||
| "subtitle": "Cap", | ||
| "description": "Pause the current recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "resume-recording", | ||
| "title": "Resume Recording", | ||
| "subtitle": "Cap", | ||
| "description": "Resume a paused recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "toggle-pause", | ||
| "title": "Toggle Pause", | ||
| "subtitle": "Cap", | ||
| "description": "Toggle pause state of the current recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "take-screenshot", | ||
| "title": "Take Screenshot", | ||
| "subtitle": "Cap", | ||
| "description": "Take a screenshot with Cap", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "open-cap", | ||
| "title": "Open Cap", | ||
| "subtitle": "Cap", | ||
| "description": "Open the Cap application", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "open-settings", | ||
| "title": "Open Settings", | ||
| "subtitle": "Cap", | ||
| "description": "Open Cap settings", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "recording-controls", | ||
| "title": "Recording Controls", | ||
| "subtitle": "Cap", | ||
| "description": "Quick actions for Cap recording", | ||
| "mode": "view" | ||
| } | ||
| ], | ||
| "dependencies": { | ||
| "@raycast/api": "^1.83.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@raycast/eslint-config": "^1.0.11", | ||
| "@types/node": "22.10.2", | ||
| "@types/react": "18.3.16", | ||
| "eslint": "^9.16.0", | ||
| "prettier": "^3.4.2", | ||
| "typescript": "^5.7.2" | ||
| }, | ||
| "scripts": { | ||
| "build": "ray build --skip-types -e dist -o dist", | ||
| "dev": "ray develop", | ||
| "fix-lint": "ray lint --fix", | ||
| "lint": "ray lint", | ||
| "prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1", | ||
| "publish": "npx @raycast/api@latest publish" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { openCap } from "./utils/deeplink"; | ||
|
|
||
| export default async function Command() { | ||
| await openCap(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { executeCapAction } from "./utils/deeplink"; | ||
|
|
||
| export default async function Command() { | ||
| await executeCapAction( | ||
| { open_settings: { page: null } }, | ||
| "Opening Settings" | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { executeCapAction } from "./utils/deeplink"; | ||
|
|
||
| export default async function Command() { | ||
| await executeCapAction("pause_recording", "Recording Paused"); | ||
| } |
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.
Repo style seems to avoid code comments; these new
//////comments in the deeplink handler might be worth removing for consistency.