-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Extend desktop deeplink actions for recording controls #1794
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
192cf9b
0eac839
ef32236
c5f0d87
4440487
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 |
|---|---|---|
|
|
@@ -6,7 +6,12 @@ use std::path::{Path, PathBuf}; | |
| use tauri::{AppHandle, Manager, Url}; | ||
| use tracing::trace; | ||
|
|
||
| use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow}; | ||
| use crate::{ | ||
| App, ArcLock, | ||
| recording::StartRecordingInputs, | ||
| recording_settings::RecordingTargetMode, | ||
| windows::ShowCapWindow, | ||
| }; | ||
|
|
||
| #[derive(Debug, Serialize, Deserialize)] | ||
| #[serde(rename_all = "snake_case")] | ||
|
|
@@ -25,7 +30,23 @@ pub enum DeepLinkAction { | |
| capture_system_audio: bool, | ||
| mode: RecordingMode, | ||
| }, | ||
| StartRecordingFromSettings { | ||
| mode: RecordingMode, | ||
| }, | ||
| StopRecording, | ||
| RestartRecording, | ||
| PauseRecording, | ||
| ResumeRecording, | ||
| TogglePauseRecording, | ||
| SetMicrophone { | ||
| mic_label: Option<String>, | ||
| }, | ||
| SetCamera { | ||
| camera: Option<DeviceOrModelID>, | ||
| }, | ||
| OpenRecordingPicker { | ||
| target_mode: Option<RecordingTargetMode>, | ||
| }, | ||
| OpenEditor { | ||
| project_path: PathBuf, | ||
| }, | ||
|
|
@@ -70,6 +91,7 @@ pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) { | |
| }); | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| pub enum ActionParseFromUrlError { | ||
| ParseFailed(String), | ||
| Invalid, | ||
|
|
@@ -147,6 +169,67 @@ impl DeepLinkAction { | |
| DeepLinkAction::StopRecording => { | ||
| crate::recording::stop_recording(app.clone(), app.state()).await | ||
| } | ||
| DeepLinkAction::StartRecordingFromSettings { mode } => { | ||
| let state = app.state::<ArcLock<App>>(); | ||
| let settings = crate::recording_settings::RecordingSettingsStore::get(app) | ||
| .ok() | ||
| .flatten() | ||
| .unwrap_or_default(); | ||
|
|
||
| crate::set_mic_input(state.clone(), settings.mic_name).await?; | ||
| crate::set_camera_input(app.clone(), state.clone(), settings.camera_id, None) | ||
| .await?; | ||
|
|
||
| let inputs = StartRecordingInputs { | ||
| mode, | ||
| capture_target: settings.target.unwrap_or_else(|| { | ||
| ScreenCaptureTarget::Display { | ||
| id: scap_targets::Display::primary().id(), | ||
| } | ||
| }), | ||
| capture_system_audio: settings.system_audio, | ||
| organization_id: settings.organization_id, | ||
| }; | ||
|
|
||
| crate::recording::start_recording(app.clone(), state, inputs) | ||
| .await | ||
| .map(|_| ()) | ||
| } | ||
| DeepLinkAction::RestartRecording => crate::recording::restart_recording( | ||
| app.clone(), | ||
| app.state(), | ||
| ) | ||
| .await | ||
| .map(|_| ()), | ||
| 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 { mic_label } => { | ||
| crate::set_mic_input(app.state(), mic_label).await | ||
| } | ||
| DeepLinkAction::SetCamera { camera } => { | ||
| crate::set_camera_input(app.clone(), app.state(), camera, None).await | ||
| } | ||
| DeepLinkAction::OpenRecordingPicker { target_mode } => { | ||
| match target_mode { | ||
| Some(target_mode) => crate::open_target_picker(app, target_mode).await, | ||
| None => { | ||
| ShowCapWindow::Main { | ||
| init_target_mode: None, | ||
| } | ||
| .show(app) | ||
| .await?; | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
| DeepLinkAction::OpenEditor { project_path } => { | ||
| crate::open_project_from_path(Path::new(&project_path), app.clone()) | ||
| } | ||
|
|
@@ -156,3 +239,95 @@ impl DeepLinkAction { | |
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::{ActionParseFromUrlError, DeepLinkAction}; | ||
| use crate::recording_settings::RecordingTargetMode; | ||
| use tauri::Url; | ||
|
|
||
| fn parse_action(encoded_value: &str) -> Result<DeepLinkAction, ActionParseFromUrlError> { | ||
| let url = Url::parse(&format!("cap-desktop://action?value={encoded_value}")).unwrap(); | ||
|
|
||
| DeepLinkAction::try_from(&url) | ||
| } | ||
|
|
||
| #[test] | ||
| fn parses_restart_recording_action() { | ||
| assert!(matches!( | ||
| parse_action("%22restart_recording%22").unwrap(), | ||
| DeepLinkAction::RestartRecording | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parses_start_recording_from_settings_action() { | ||
| assert!(matches!( | ||
| parse_action( | ||
| "%7B%22start_recording_from_settings%22%3A%7B%22mode%22%3A%22studio%22%7D%7D" | ||
| ) | ||
| .unwrap(), | ||
| DeepLinkAction::StartRecordingFromSettings { | ||
| mode: cap_recording::RecordingMode::Studio | ||
| } | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parses_pause_recording_action() { | ||
| assert!(matches!( | ||
| parse_action("%22pause_recording%22").unwrap(), | ||
| DeepLinkAction::PauseRecording | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parses_resume_recording_action() { | ||
| assert!(matches!( | ||
| parse_action("%22resume_recording%22").unwrap(), | ||
| DeepLinkAction::ResumeRecording | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parses_toggle_pause_recording_action() { | ||
| assert!(matches!( | ||
| parse_action("%22toggle_pause_recording%22").unwrap(), | ||
| DeepLinkAction::TogglePauseRecording | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parses_set_microphone_action() { | ||
| assert!(matches!( | ||
| parse_action("%7B%22set_microphone%22%3A%7B%22mic_label%22%3Anull%7D%7D").unwrap(), | ||
| DeepLinkAction::SetMicrophone { mic_label: None } | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parses_set_camera_action() { | ||
| assert!(matches!( | ||
| parse_action( | ||
| "%7B%22set_camera%22%3A%7B%22camera%22%3A%7B%22DeviceID%22%3A%22camera-device-id%22%7D%7D%7D" | ||
| ) | ||
| .unwrap(), | ||
| DeepLinkAction::SetCamera { | ||
| camera: Some(cap_recording::feeds::camera::DeviceOrModelID::DeviceID(_)) | ||
| } | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parses_open_recording_picker_action() { | ||
| assert!(matches!( | ||
| parse_action( | ||
| "%7B%22open_recording_picker%22%3A%7B%22target_mode%22%3A%22display%22%7D%7D" | ||
| ) | ||
| .unwrap(), | ||
| DeepLinkAction::OpenRecordingPicker { | ||
| target_mode: Some(RecordingTargetMode::Display) | ||
| } | ||
| )); | ||
| } | ||
|
Comment on lines
+300
to
+332
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.
The new tests cover Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 234-253
Comment:
**Missing test cases for `SetCamera` and `OpenRecordingPicker { target_mode: None }`**
The new tests cover `RestartRecording`, `TogglePauseRecording`, `SetMicrophone` (null label), and `OpenRecordingPicker` (with a mode), but there are no round-trip parse tests for `SetCamera` (both null and non-null camera values) or for `OpenRecordingPicker` when `target_mode` is `None`. Adding these would match the test coverage level of the other new actions.
How can I resolve this? If you propose a fix, please make it concise. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "useTabs": true, | ||
| "tabWidth": 2 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Cap Raycast Extension | ||
|
|
||
| Control Cap desktop recordings from Raycast. | ||
|
|
||
| ## Commands | ||
|
|
||
| - Start Studio Recording | ||
| - Start Instant Recording | ||
| - Stop Recording | ||
| - Restart Recording | ||
| - Pause Recording | ||
| - Resume Recording | ||
| - Pause or Resume Recording | ||
| - Open Recording Picker | ||
| - Set Microphone | ||
| - Clear Microphone | ||
| - Set Camera | ||
| - Clear Camera | ||
| - Open Settings | ||
|
|
||
| ## How It Works | ||
|
|
||
| The extension opens Cap desktop deeplinks using the `cap-desktop://action` scheme. | ||
|
|
||
| Examples: | ||
|
|
||
| - `cap-desktop://action?value=%22stop_recording%22` | ||
| - `cap-desktop://action?value=%22pause_recording%22` | ||
| - `cap-desktop://action?value=%22resume_recording%22` | ||
| - `cap-desktop://action?value=%22toggle_pause_recording%22` | ||
| - `cap-desktop://action?value=%7B%22start_recording_from_settings%22%3A%7B%22mode%22%3A%22studio%22%7D%7D` | ||
| - `cap-desktop://action?value=%7B%22set_microphone%22%3A%7B%22mic_label%22%3A%22MacBook%20Pro%20Microphone%22%7D%7D` | ||
| - `cap-desktop://action?value=%7B%22set_camera%22%3A%7B%22camera%22%3A%7B%22DeviceID%22%3A%22camera-device-id%22%7D%7D%7D` | ||
|
|
||
| The desktop app parses the `value` query parameter as JSON and executes the corresponding action. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| const parser = require("@typescript-eslint/parser"); | ||
|
|
||
| module.exports = [ | ||
| { | ||
| files: ["src/**/*.ts"], | ||
| languageOptions: { | ||
| ecmaVersion: "latest", | ||
| parser, | ||
| sourceType: "module", | ||
| }, | ||
| rules: {}, | ||
| }, | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| { | ||
| "name": "cap-raycast", | ||
| "title": "Cap", | ||
| "description": "Control Cap recordings from Raycast", | ||
| "icon": "icon.png", | ||
| "author": "cap", | ||
| "license": "MIT", | ||
| "categories": [ | ||
| "Productivity", | ||
| "Developer Tools" | ||
| ], | ||
| "platforms": [ | ||
| "macOS" | ||
| ], | ||
| "scripts": { | ||
| "dev": "ray develop", | ||
| "build": "ray build", | ||
| "lint": "ray lint" | ||
| }, | ||
| "dependencies": { | ||
| "@raycast/api": "^1.102.7" | ||
| }, | ||
| "devDependencies": { | ||
| "@raycast/eslint-config": "^2.0.4", | ||
| "@typescript-eslint/parser": "^8.57.2", | ||
| "@types/node": "22.19.17", | ||
| "eslint": "^8.57.1", | ||
| "prettier": "^3.0.0", | ||
| "typescript": "^5.8.3" | ||
| }, | ||
| "commands": [ | ||
| { | ||
| "name": "start-studio-recording", | ||
| "title": "Start Studio Recording", | ||
| "description": "Start a Cap Studio recording with saved settings", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "start-instant-recording", | ||
| "title": "Start Instant Recording", | ||
| "description": "Start a Cap Instant recording with saved settings", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "stop-recording", | ||
| "title": "Stop Recording", | ||
| "description": "Stop the current Cap recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "restart-recording", | ||
| "title": "Restart Recording", | ||
| "description": "Restart the current Cap recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "pause-recording", | ||
| "title": "Pause Recording", | ||
| "description": "Pause the current Cap recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "resume-recording", | ||
| "title": "Resume Recording", | ||
| "description": "Resume the current Cap recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "toggle-pause-recording", | ||
| "title": "Pause or Resume Recording", | ||
| "description": "Pause or resume the current Cap recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "open-recording-picker", | ||
| "title": "Open Recording Picker", | ||
| "description": "Open the Cap recording picker", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "set-microphone", | ||
| "title": "Set Microphone", | ||
| "description": "Switch Cap to a microphone by label", | ||
| "mode": "no-view", | ||
| "arguments": [ | ||
| { | ||
| "name": "micLabel", | ||
| "title": "Microphone Label", | ||
| "placeholder": "MacBook Pro Microphone", | ||
| "type": "text", | ||
| "required": true | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "name": "clear-microphone", | ||
| "title": "Clear Microphone", | ||
| "description": "Clear the selected Cap microphone", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "set-camera", | ||
| "title": "Set Camera", | ||
| "description": "Switch Cap to a camera by device ID", | ||
| "mode": "no-view", | ||
| "arguments": [ | ||
| { | ||
| "name": "deviceId", | ||
| "title": "Device ID", | ||
| "placeholder": "camera-device-id", | ||
| "type": "text", | ||
| "required": true | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "name": "clear-camera", | ||
| "title": "Clear Camera", | ||
| "description": "Clear the selected Cap camera", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "open-settings", | ||
| "title": "Open Settings", | ||
| "description": "Open Cap settings", | ||
| "mode": "no-view" | ||
| } | ||
| ] | ||
| } |
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.
ShowCapWindow::showreturnstauri::Result<WebviewWindow>, andtauri::Errordoes not implementFrom<_> for String. Using bare?here won't compile because the surroundingexecutefunction returnsResult<(), String>. Every other call site in this codebase (e.g.show_windowinlib.rs) uses.map_err(|e| e.to_string())?to bridge the conversion.Prompt To Fix With AI