From 332beac175e91d06635aa6698d16e11ce443aa4f Mon Sep 17 00:00:00 2001 From: BMNTR Date: Wed, 29 Jul 2026 19:05:41 +0700 Subject: [PATCH 01/10] fix: expose deeplinks for pause, resume, mic and camera switching (#1540) --- .../desktop/src-tauri/src/deeplink_actions.rs | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 6eb7f4c8939..b9d36132d28 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -46,15 +46,14 @@ pub enum DeepLinkAction { mode: RecordingMode, }, StopRecording, - #[cfg(debug_assertions)] PauseRecording, - #[cfg(debug_assertions)] ResumeRecording, - #[cfg(debug_assertions)] + SwitchMicrophone { + mic_label: Option, + }, OpenCamera { camera: DeviceOrModelID, }, - #[cfg(debug_assertions)] SetCameraPreviewState { state: CameraPreviewState, }, @@ -165,20 +164,46 @@ impl TryFrom<&Url> for DeepLinkAction { } match url.domain() { - Some("action") => {} - Some(_) => return Err(ActionParseFromUrlError::NotAction), - None => return Err(ActionParseFromUrlError::Invalid), + Some("action") => { + let params = url + .query_pairs() + .collect::>(); + let json_value = params + .get("value") + .ok_or(ActionParseFromUrlError::Invalid)?; + let action: Self = serde_json::from_str(json_value) + .map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?; + Ok(action) + } + Some("stop" | "stop-recording") => Ok(Self::StopRecording), + Some("pause" | "pause-recording") => Ok(Self::PauseRecording), + Some("resume" | "resume-recording") => Ok(Self::ResumeRecording), + Some("switch-mic" | "switch-microphone") => { + let params = url + .query_pairs() + .collect::>(); + let mic_label = params + .get("mic_label") + .or_else(|| params.get("label")) + .map(|s| s.to_string()); + Ok(Self::SwitchMicrophone { mic_label }) + } + Some("open-camera" | "switch-camera") => { + let params = url + .query_pairs() + .collect::>(); + let device_id = params + .get("id") + .or_else(|| params.get("camera")) + .map(|s| s.to_string()) + .ok_or(ActionParseFromUrlError::Invalid)?; + Ok(Self::OpenCamera { + camera: DeviceOrModelID::DeviceID(device_id), + }) + } + Some(_) => Err(ActionParseFromUrlError::NotAction), + None => Err(ActionParseFromUrlError::Invalid), } - - let params = url - .query_pairs() - .collect::>(); - let json_value = params - .get("value") - .ok_or(ActionParseFromUrlError::Invalid)?; - let action: Self = serde_json::from_str(json_value) - .map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?; - Ok(action) } } @@ -244,15 +269,18 @@ impl DeepLinkAction { DeepLinkAction::StopRecording => { crate::recording::stop_recording(app.clone(), app.state()).await } - #[cfg(debug_assertions)] DeepLinkAction::PauseRecording => { crate::recording::pause_recording(app.clone(), app.state()).await } - #[cfg(debug_assertions)] DeepLinkAction::ResumeRecording => { crate::recording::resume_recording(app.clone(), app.state()).await } - #[cfg(debug_assertions)] + DeepLinkAction::SwitchMicrophone { mic_label } => { + let state = app.state::>(); + crate::set_mic_input(state, mic_label) + .await + .map_err(|e| e.to_string()) + } DeepLinkAction::OpenCamera { camera } => { crate::set_camera_input( app.clone(), @@ -282,7 +310,6 @@ impl DeepLinkAction { Ok(()) } - #[cfg(debug_assertions)] DeepLinkAction::SetCameraPreviewState { state } => { crate::set_camera_preview_state(app.state(), state).await } @@ -358,7 +385,6 @@ mod tests { ); } - #[cfg(debug_assertions)] #[test] fn parses_pause_and_resume_action_urls() { let pause_url = Url::parse("cap-desktop://action?value=%22pause_recording%22").unwrap(); From 8e9fb902547729e12e655d9a331ad9db6936edea Mon Sep 17 00:00:00 2001 From: BMNTR Date: Wed, 29 Jul 2026 19:14:56 +0700 Subject: [PATCH 02/10] fix: register cap scheme alongside cap-desktop in tauri.conf.json --- apps/desktop/src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 9af8659e426..fd6c01b6122 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -36,7 +36,7 @@ "updater": { "active": false, "pubkey": "" }, "deep-link": { "desktop": { - "schemes": ["cap-desktop"] + "schemes": ["cap-desktop", "cap"] } } }, From 691fe87224cd16780f9e280a6059cfb27a8378ec Mon Sep 17 00:00:00 2001 From: BMNTR <251698926+BMNTR@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:15:39 +0700 Subject: [PATCH 03/10] Update apps/desktop/src-tauri/src/deeplink_actions.rs Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com> --- apps/desktop/src-tauri/src/deeplink_actions.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index b9d36132d28..24a975b0c98 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -55,7 +55,9 @@ pub enum DeepLinkAction { camera: DeviceOrModelID, }, SetCameraPreviewState { - state: CameraPreviewState, + SetCameraPreviewState { + state: crate::camera::CameraPreviewState, + }, }, OpenEditor { project_path: PathBuf, From cb0ea6d43122df7e58dfb53288426df5dd7e4d4a Mon Sep 17 00:00:00 2001 From: BMNTR <251698926+BMNTR@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:15:53 +0700 Subject: [PATCH 04/10] Update apps/desktop/src-tauri/src/deeplink_actions.rs Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com> --- apps/desktop/src-tauri/src/deeplink_actions.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 24a975b0c98..1c05a1186c2 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -185,6 +185,9 @@ impl TryFrom<&Url> for DeepLinkAction { .query_pairs() .collect::>(); let mic_label = params + .get("mic_label") + .or_else(|| params.get("label")) + .and_then(|s| (!s.is_empty()).then(|| s.to_string())); .get("mic_label") .or_else(|| params.get("label")) .map(|s| s.to_string()); From 2479097253e45a2e99e6c34ba33907497a502b77 Mon Sep 17 00:00:00 2001 From: BMNTR <251698926+BMNTR@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:18:30 +0700 Subject: [PATCH 05/10] Update apps/desktop/src-tauri/src/deeplink_actions.rs Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com> --- apps/desktop/src-tauri/src/deeplink_actions.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 1c05a1186c2..da004ce7e2a 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -188,9 +188,6 @@ impl TryFrom<&Url> for DeepLinkAction { .get("mic_label") .or_else(|| params.get("label")) .and_then(|s| (!s.is_empty()).then(|| s.to_string())); - .get("mic_label") - .or_else(|| params.get("label")) - .map(|s| s.to_string()); Ok(Self::SwitchMicrophone { mic_label }) } Some("open-camera" | "switch-camera") => { From 2c45cd17eb4b65a279bffee9625858b623c48d50 Mon Sep 17 00:00:00 2001 From: BMNTR Date: Wed, 29 Jul 2026 19:19:34 +0700 Subject: [PATCH 06/10] fix: clean up duplicate enum variant syntax in deeplink_actions.rs --- apps/desktop/src-tauri/src/deeplink_actions.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index da004ce7e2a..f1b11b2cff6 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -54,11 +54,9 @@ pub enum DeepLinkAction { OpenCamera { camera: DeviceOrModelID, }, - SetCameraPreviewState { SetCameraPreviewState { state: crate::camera::CameraPreviewState, }, - }, OpenEditor { project_path: PathBuf, }, From 1a5b94368603de2f84bbefb83a8c56007797cf0a Mon Sep 17 00:00:00 2001 From: BMNTR Date: Wed, 29 Jul 2026 19:26:02 +0700 Subject: [PATCH 07/10] fix: use host_str(), empty string device_id guard, and add direct scheme unit tests --- .../desktop/src-tauri/src/deeplink_actions.rs | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index f1b11b2cff6..69eab66f6db 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -163,7 +163,7 @@ impl TryFrom<&Url> for DeepLinkAction { .map_err(|_| ActionParseFromUrlError::Invalid); } - match url.domain() { + match url.host_str() { Some("action") => { let params = url .query_pairs() @@ -195,7 +195,7 @@ impl TryFrom<&Url> for DeepLinkAction { let device_id = params .get("id") .or_else(|| params.get("camera")) - .map(|s| s.to_string()) + .and_then(|s| (!s.is_empty()).then(|| s.to_string())) .ok_or(ActionParseFromUrlError::Invalid)?; Ok(Self::OpenCamera { camera: DeviceOrModelID::DeviceID(device_id), @@ -400,6 +400,35 @@ mod tests { ); } + #[test] + fn parses_direct_scheme_urls() { + let pause_url = Url::parse("cap://pause").unwrap(); + let resume_url = Url::parse("cap://resume").unwrap(); + let mic_url = Url::parse("cap://switch-mic?label=Shure%20MV7%2B").unwrap(); + let camera_url = Url::parse("cap://switch-camera?id=cam-1").unwrap(); + + assert_eq!( + DeepLinkAction::try_from(&pause_url), + Ok(DeepLinkAction::PauseRecording) + ); + assert_eq!( + DeepLinkAction::try_from(&resume_url), + Ok(DeepLinkAction::ResumeRecording) + ); + assert_eq!( + DeepLinkAction::try_from(&mic_url), + Ok(DeepLinkAction::SwitchMicrophone { + mic_label: Some("Shure MV7+".to_string()) + }) + ); + assert_eq!( + DeepLinkAction::try_from(&camera_url), + Ok(DeepLinkAction::OpenCamera { + camera: DeviceOrModelID::DeviceID("cam-1".to_string()) + }) + ); + } + #[cfg(debug_assertions)] #[test] fn parses_area_recording_action_url() { From 550921864aa9338a8beee83da625c46695cea82f Mon Sep 17 00:00:00 2001 From: BMNTR Date: Wed, 29 Jul 2026 19:30:33 +0700 Subject: [PATCH 08/10] fix: restrict deep link scheme parsing to cap and cap-desktop --- apps/desktop/src-tauri/src/deeplink_actions.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 69eab66f6db..008aa35c04a 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -163,6 +163,10 @@ impl TryFrom<&Url> for DeepLinkAction { .map_err(|_| ActionParseFromUrlError::Invalid); } + if !matches!(url.scheme(), "cap" | "cap-desktop") { + return Err(ActionParseFromUrlError::NotAction); + } + match url.host_str() { Some("action") => { let params = url From bf8266294867f6ca264b9a771ff21ce8bfa37363 Mon Sep 17 00:00:00 2001 From: BMNTR Date: Wed, 29 Jul 2026 19:48:23 +0700 Subject: [PATCH 09/10] chore: sync PR status From d1230f44b0d87347a6670ccc0b07b3ed08249ede Mon Sep 17 00:00:00 2001 From: BMNTR Date: Wed, 29 Jul 2026 23:42:08 +0700 Subject: [PATCH 10/10] fix: ensure clean single try_from implementation and deeplink routes