-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: extend deeplink actions for Raycast extension support #1543
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
0d34972
6aafa3a
4317144
3c145cb
ee70777
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,55 @@ 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, feeds::microphone::MicrophoneFeed, recording::StartRecordingInputs, | ||||||||||||
| windows::ShowCapWindow, | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| #[derive(Debug, Serialize, Deserialize)] | ||||||||||||
| #[serde(rename_all = "snake_case")] | ||||||||||||
| pub struct DeepLinkStartRecordingResult { | ||||||||||||
| pub success: bool, | ||||||||||||
| pub error: Option<String>, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| #[derive(Debug, Serialize, Deserialize)] | ||||||||||||
| #[serde(rename_all = "snake_case")] | ||||||||||||
| pub struct DeepLinkRecordingStatus { | ||||||||||||
| pub is_recording: bool, | ||||||||||||
| pub is_paused: bool, | ||||||||||||
| pub recording_mode: Option<String>, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| #[derive(Debug, Serialize, Deserialize)] | ||||||||||||
| #[serde(rename_all = "snake_case")] | ||||||||||||
| pub struct DeepLinkDevices { | ||||||||||||
| pub cameras: Vec<DeepLinkCamera>, | ||||||||||||
| pub microphones: Vec<String>, | ||||||||||||
| pub screens: Vec<DeepLinkScreen>, | ||||||||||||
| pub windows: Vec<DeepLinkWindow>, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| #[derive(Debug, Serialize, Deserialize)] | ||||||||||||
| #[serde(rename_all = "snake_case")] | ||||||||||||
| pub struct DeepLinkCamera { | ||||||||||||
| pub name: String, | ||||||||||||
| pub id: String, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| #[derive(Debug, Serialize, Deserialize)] | ||||||||||||
| #[serde(rename_all = "snake_case")] | ||||||||||||
| pub struct DeepLinkScreen { | ||||||||||||
| pub name: String, | ||||||||||||
| pub id: String, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| #[derive(Debug, Serialize, Deserialize)] | ||||||||||||
| #[serde(rename_all = "snake_case")] | ||||||||||||
| pub struct DeepLinkWindow { | ||||||||||||
| pub name: String, | ||||||||||||
| pub owner_name: String, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| #[derive(Debug, Serialize, Deserialize)] | ||||||||||||
| #[serde(rename_all = "snake_case")] | ||||||||||||
|
|
@@ -26,6 +74,21 @@ pub enum DeepLinkAction { | |||||||||||
| mode: RecordingMode, | ||||||||||||
| }, | ||||||||||||
| StopRecording, | ||||||||||||
| PauseRecording, | ||||||||||||
| ResumeRecording, | ||||||||||||
| TogglePauseRecording, | ||||||||||||
| RestartRecording, | ||||||||||||
| TakeScreenshot { | ||||||||||||
| capture_mode: CaptureMode, | ||||||||||||
| }, | ||||||||||||
| SetMicrophone { | ||||||||||||
| label: Option<String>, | ||||||||||||
| }, | ||||||||||||
| SetCamera { | ||||||||||||
| id: Option<DeviceOrModelID>, | ||||||||||||
| }, | ||||||||||||
| ListDevices, | ||||||||||||
| GetStatus, | ||||||||||||
| OpenEditor { | ||||||||||||
| project_path: PathBuf, | ||||||||||||
| }, | ||||||||||||
|
|
@@ -88,8 +151,9 @@ impl TryFrom<&Url> for DeepLinkAction { | |||||||||||
| } | ||||||||||||
|
|
||||||||||||
| match url.domain() { | ||||||||||||
| Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction), | ||||||||||||
| _ => Err(ActionParseFromUrlError::Invalid), | ||||||||||||
| Some("action") => Ok(()), | ||||||||||||
| Some(_) => Err(ActionParseFromUrlError::NotAction), | ||||||||||||
| None => Err(ActionParseFromUrlError::Invalid), | ||||||||||||
| }?; | ||||||||||||
|
|
||||||||||||
| let params = url | ||||||||||||
|
|
@@ -104,6 +168,21 @@ impl TryFrom<&Url> for DeepLinkAction { | |||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| fn resolve_capture_target(capture_mode: &CaptureMode) -> Result<ScreenCaptureTarget, String> { | ||||||||||||
| match capture_mode { | ||||||||||||
| CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays() | ||||||||||||
| .into_iter() | ||||||||||||
| .find(|(s, _)| s.name == *name) | ||||||||||||
| .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) | ||||||||||||
| .ok_or_else(|| format!("No screen with name \"{}\"", name)), | ||||||||||||
| CaptureMode::Window(name) => cap_recording::screen_capture::list_windows() | ||||||||||||
| .into_iter() | ||||||||||||
| .find(|(w, _)| w.name == *name) | ||||||||||||
| .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) | ||||||||||||
| .ok_or_else(|| format!("No window with name \"{}\"", name)), | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| impl DeepLinkAction { | ||||||||||||
| pub async fn execute(self, app: &AppHandle) -> Result<(), String> { | ||||||||||||
| match self { | ||||||||||||
|
|
@@ -119,18 +198,7 @@ impl DeepLinkAction { | |||||||||||
| crate::set_camera_input(app.clone(), state.clone(), camera).await?; | ||||||||||||
| crate::set_mic_input(state.clone(), mic_label).await?; | ||||||||||||
|
|
||||||||||||
| let capture_target: ScreenCaptureTarget = match capture_mode { | ||||||||||||
| CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays() | ||||||||||||
| .into_iter() | ||||||||||||
| .find(|(s, _)| s.name == name) | ||||||||||||
| .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) | ||||||||||||
| .ok_or(format!("No screen with name \"{}\"", &name))?, | ||||||||||||
| CaptureMode::Window(name) => cap_recording::screen_capture::list_windows() | ||||||||||||
| .into_iter() | ||||||||||||
| .find(|(w, _)| w.name == name) | ||||||||||||
| .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) | ||||||||||||
| .ok_or(format!("No window with name \"{}\"", &name))?, | ||||||||||||
| }; | ||||||||||||
| let capture_target = resolve_capture_target(&capture_mode)?; | ||||||||||||
|
|
||||||||||||
| let inputs = StartRecordingInputs { | ||||||||||||
| mode, | ||||||||||||
|
|
@@ -139,13 +207,98 @@ impl DeepLinkAction { | |||||||||||
| organization_id: None, | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| crate::recording::start_recording(app.clone(), state, inputs) | ||||||||||||
| .await | ||||||||||||
| .map(|_| ()) | ||||||||||||
| let result = crate::recording::start_recording(app.clone(), state, inputs).await; | ||||||||||||
| match result { | ||||||||||||
| Ok(crate::recording::RecordingAction::Started) => { | ||||||||||||
| write_deeplink_response(app, &DeepLinkStartRecordingResult { | ||||||||||||
| success: true, | ||||||||||||
| error: None, | ||||||||||||
| }) | ||||||||||||
| } | ||||||||||||
| Ok(crate::recording::RecordingAction::InvalidAuthentication) => { | ||||||||||||
| write_deeplink_response(app, &DeepLinkStartRecordingResult { | ||||||||||||
| success: false, | ||||||||||||
| error: Some("Please sign in to Cap to use instant recording".to_string()), | ||||||||||||
| }) | ||||||||||||
| } | ||||||||||||
| Ok(crate::recording::RecordingAction::UpgradeRequired) => { | ||||||||||||
| write_deeplink_response(app, &DeepLinkStartRecordingResult { | ||||||||||||
| success: false, | ||||||||||||
| error: Some("A Cap upgrade is required to use this feature".to_string()), | ||||||||||||
| }) | ||||||||||||
| } | ||||||||||||
| Err(error) => { | ||||||||||||
| let _ = write_deeplink_response(app, &DeepLinkStartRecordingResult { | ||||||||||||
| success: false, | ||||||||||||
| error: Some(error.clone()), | ||||||||||||
| }); | ||||||||||||
| Err(error) | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| 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::RestartRecording => { | ||||||||||||
| crate::recording::restart_recording(app.clone(), app.state()) | ||||||||||||
| .await | ||||||||||||
| .map(|_| ()) | ||||||||||||
| } | ||||||||||||
| DeepLinkAction::TakeScreenshot { capture_mode } => { | ||||||||||||
| let capture_target = resolve_capture_target(&capture_mode)?; | ||||||||||||
|
|
||||||||||||
| crate::recording::take_screenshot(app.clone(), capture_target) | ||||||||||||
| .await | ||||||||||||
| .map(|_| ()) | ||||||||||||
| } | ||||||||||||
| DeepLinkAction::SetMicrophone { label } => { | ||||||||||||
| let state = app.state::<ArcLock<App>>(); | ||||||||||||
| crate::set_mic_input(state, label).await | ||||||||||||
| } | ||||||||||||
| DeepLinkAction::SetCamera { id } => { | ||||||||||||
| let state = app.state::<ArcLock<App>>(); | ||||||||||||
| crate::set_camera_input(app.clone(), state, id).await | ||||||||||||
| } | ||||||||||||
| DeepLinkAction::ListDevices => { | ||||||||||||
| let devices = get_available_devices(); | ||||||||||||
| write_deeplink_response(app, &devices) | ||||||||||||
| } | ||||||||||||
| DeepLinkAction::GetStatus => { | ||||||||||||
| let state = app.state::<ArcLock<App>>(); | ||||||||||||
| let app_state = state.read().await; | ||||||||||||
| let status = if let Some(recording) = app_state.current_recording() { | ||||||||||||
| let is_paused = recording.is_paused().await.unwrap_or(false); | ||||||||||||
| let mode = match recording { | ||||||||||||
| crate::recording::InProgressRecording::Instant { .. } => { | ||||||||||||
| Some("instant".to_string()) | ||||||||||||
| } | ||||||||||||
| crate::recording::InProgressRecording::Studio { .. } => { | ||||||||||||
| Some("studio".to_string()) | ||||||||||||
| } | ||||||||||||
| }; | ||||||||||||
| DeepLinkRecordingStatus { | ||||||||||||
| is_recording: true, | ||||||||||||
| is_paused, | ||||||||||||
| recording_mode: mode, | ||||||||||||
| } | ||||||||||||
| } else { | ||||||||||||
| DeepLinkRecordingStatus { | ||||||||||||
| is_recording: false, | ||||||||||||
| is_paused: false, | ||||||||||||
| recording_mode: None, | ||||||||||||
| } | ||||||||||||
| }; | ||||||||||||
| write_deeplink_response(app, &status) | ||||||||||||
| } | ||||||||||||
| DeepLinkAction::OpenEditor { project_path } => { | ||||||||||||
| crate::open_project_from_path(Path::new(&project_path), app.clone()) | ||||||||||||
| } | ||||||||||||
|
|
@@ -155,3 +308,61 @@ impl DeepLinkAction { | |||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| fn write_deeplink_response<T: Serialize>(app: &AppHandle, data: &T) -> Result<(), String> { | ||||||||||||
| let response_dir = app | ||||||||||||
| .path() | ||||||||||||
| .app_data_dir() | ||||||||||||
| .map_err(|e| format!("Failed to get app data dir: {e}"))?; | ||||||||||||
|
|
||||||||||||
| std::fs::create_dir_all(&response_dir) | ||||||||||||
| .map_err(|e| format!("Failed to create app data dir: {e}"))?; | ||||||||||||
|
|
||||||||||||
| let response_path = response_dir.join("deeplink-response.json"); | ||||||||||||
| let temp_path = response_dir.join("deeplink-response.json.tmp"); | ||||||||||||
|
|
||||||||||||
| let json = serde_json::to_string(data).map_err(|e| e.to_string())?; | ||||||||||||
|
|
||||||||||||
| // Atomic write: write to temp file, then rename | ||||||||||||
| std::fs::write(&temp_path, &json).map_err(|e| format!("Failed to write response file: {e}"))?; | ||||||||||||
| let _ = std::fs::remove_file(&response_path); | ||||||||||||
| std::fs::rename(&temp_path, &response_path) | ||||||||||||
| .map_err(|e| format!("Failed to rename response file: {e}"))?; | ||||||||||||
|
Comment on lines
+329
to
+330
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
|
||||||||||||
|
|
||||||||||||
| trace!("Wrote deeplink response to {:?}", response_path); | ||||||||||||
| Ok(()) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| fn get_available_devices() -> DeepLinkDevices { | ||||||||||||
| let cameras: Vec<DeepLinkCamera> = cap_camera::list_cameras() | ||||||||||||
| .map(|c| DeepLinkCamera { | ||||||||||||
| name: c.display_name().to_string(), | ||||||||||||
| id: c.device_id().to_string(), | ||||||||||||
| }) | ||||||||||||
| .collect(); | ||||||||||||
|
|
||||||||||||
| let microphones: Vec<String> = MicrophoneFeed::list().keys().cloned().collect(); | ||||||||||||
|
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. Microphone list ordering can be nondeterministic (map key iteration). Sorting makes the output stable.
Suggested change
|
||||||||||||
|
|
||||||||||||
| let screens: Vec<DeepLinkScreen> = cap_recording::screen_capture::list_displays() | ||||||||||||
| .into_iter() | ||||||||||||
| .map(|(s, _)| DeepLinkScreen { | ||||||||||||
| name: s.name, | ||||||||||||
| id: s.id.to_string(), | ||||||||||||
| }) | ||||||||||||
| .collect(); | ||||||||||||
|
|
||||||||||||
| let windows: Vec<DeepLinkWindow> = cap_recording::screen_capture::list_windows() | ||||||||||||
| .into_iter() | ||||||||||||
| .map(|(w, _)| DeepLinkWindow { | ||||||||||||
| name: w.name, | ||||||||||||
| owner_name: w.owner_name, | ||||||||||||
| }) | ||||||||||||
| .collect(); | ||||||||||||
|
|
||||||||||||
| DeepLinkDevices { | ||||||||||||
| cameras, | ||||||||||||
| microphones, | ||||||||||||
| screens, | ||||||||||||
| windows, | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
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.
unwrap_or(false)will silently hide failures fromis_paused()and report an incorrect status. It might be better to surface the error through the deeplink response.