From 0d085b376d1acd5b019b9fe1de1e04e36198f290 Mon Sep 17 00:00:00 2001 From: Angelebeats <89266469@qq.com> Date: Thu, 2 Apr 2026 21:23:15 +0800 Subject: [PATCH 01/11] feat: implement cap:// deep-link protocol handlers and Raycast extension --- .../desktop/src-tauri/src/deeplink_actions.rs | 22 +++++++++++++++++++ apps/desktop/src-tauri/tauri.conf.json | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index a117028487..7755d0902c 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -32,6 +32,9 @@ pub enum DeepLinkAction { OpenSettings { page: Option, }, + StartDefaultRecording, + PauseRecording, + ResumeRecording, } pub fn handle(app_handle: &AppHandle, urls: Vec) { @@ -88,6 +91,16 @@ impl TryFrom<&Url> for DeepLinkAction { .map_err(|_| ActionParseFromUrlError::Invalid); } + if url.scheme() == "cap" { + return match url.host_str() { + Some("record") => Ok(Self::StartDefaultRecording), + Some("stop") => Ok(Self::StopRecording), + Some("pause") => Ok(Self::PauseRecording), + Some("resume") => Ok(Self::ResumeRecording), + _ => Err(ActionParseFromUrlError::Invalid), + }; + } + match url.domain() { Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction), _ => Err(ActionParseFromUrlError::Invalid), @@ -153,6 +166,15 @@ impl DeepLinkAction { DeepLinkAction::OpenSettings { page } => { crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await } + DeepLinkAction::StartDefaultRecording => { + app.emit("request-open-recording-picker", ()).map_err(|e| e.to_string()) + } + DeepLinkAction::PauseRecording => { + crate::recording::pause_recording(app.clone(), app.state()).await + } + DeepLinkAction::ResumeRecording => { + crate::recording::resume_recording(app.clone(), app.state()).await + } } } } diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 691c2f0995..80aba7fd80 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -30,7 +30,7 @@ "updater": { "active": false, "pubkey": "" }, "deep-link": { "desktop": { - "schemes": ["cap-desktop"] + "schemes": ["cap-desktop", "cap"] } } }, From de021be8ce5c174463007aea2bfe36944305220d Mon Sep 17 00:00:00 2001 From: Angelebeats <89266469@qq.com> Date: Fri, 3 Apr 2026 23:10:16 +0800 Subject: [PATCH 02/11] fix: use typed RequestOpenRecordingPicker event in deeplink handler --- apps/desktop/src-tauri/src/deeplink_actions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 7755d0902c..a16ed41c17 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -167,7 +167,7 @@ impl DeepLinkAction { crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await } DeepLinkAction::StartDefaultRecording => { - app.emit("request-open-recording-picker", ()).map_err(|e| e.to_string()) + crate::RequestOpenRecordingPicker { target_mode: None }.emit(app).map_err(|e| e.to_string()) } DeepLinkAction::PauseRecording => { crate::recording::pause_recording(app.clone(), app.state()).await From 7cee5f56755b1aa3f078646ca483d5407df1a815 Mon Sep 17 00:00:00 2001 From: Angelebeats <89266469@qq.com> Date: Sun, 5 Apr 2026 00:36:35 +0800 Subject: [PATCH 03/11] refactor: optimize deep link handling with case-insensitivity and security notifications --- .../desktop/src-tauri/src/deeplink_actions.rs | 23 ++++++++++++++----- apps/desktop/src-tauri/src/notifications.rs | 6 +++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index a16ed41c17..7cdc5a65b0 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -84,19 +84,19 @@ impl TryFrom<&Url> for DeepLinkAction { fn try_from(url: &Url) -> Result { #[cfg(target_os = "macos")] - if url.scheme() == "file" { + if url.scheme().eq_ignore_ascii_case("file") { return url .to_file_path() .map(|project_path| Self::OpenEditor { project_path }) .map_err(|_| ActionParseFromUrlError::Invalid); } - if url.scheme() == "cap" { + if url.scheme().eq_ignore_ascii_case("cap") { return match url.host_str() { - Some("record") => Ok(Self::StartDefaultRecording), - Some("stop") => Ok(Self::StopRecording), - Some("pause") => Ok(Self::PauseRecording), - Some("resume") => Ok(Self::ResumeRecording), + Some(h) if h.eq_ignore_ascii_case("record") => Ok(Self::StartDefaultRecording), + Some(h) if h.eq_ignore_ascii_case("stop") => Ok(Self::StopRecording), + Some(h) if h.eq_ignore_ascii_case("pause") => Ok(Self::PauseRecording), + Some(h) if h.eq_ignore_ascii_case("resume") => Ok(Self::ResumeRecording), _ => Err(ActionParseFromUrlError::Invalid), }; } @@ -120,6 +120,17 @@ impl TryFrom<&Url> for DeepLinkAction { impl DeepLinkAction { pub async fn execute(self, app: &AppHandle) -> Result<(), String> { + match &self { + DeepLinkAction::StartRecording { .. } + | DeepLinkAction::StopRecording + | DeepLinkAction::StartDefaultRecording + | DeepLinkAction::PauseRecording + | DeepLinkAction::ResumeRecording => { + crate::notifications::NotificationType::DeepLinkTriggered.send(app); + } + _ => {} + } + match self { DeepLinkAction::StartRecording { capture_mode, diff --git a/apps/desktop/src-tauri/src/notifications.rs b/apps/desktop/src-tauri/src/notifications.rs index d872fd8380..ec992f60fd 100644 --- a/apps/desktop/src-tauri/src/notifications.rs +++ b/apps/desktop/src-tauri/src/notifications.rs @@ -14,6 +14,7 @@ pub enum NotificationType { ScreenshotCopiedToClipboard, ScreenshotSaveFailed, ScreenshotCopyFailed, + DeepLinkTriggered, } impl NotificationType { @@ -62,6 +63,11 @@ impl NotificationType { "Unable to copy screenshot to clipboard. Please try again", true, ), + NotificationType::DeepLinkTriggered => ( + "Action Triggered", + "An action was triggered via a deep link", + false, + ), } } From 771937eab3397c5516213c5e9b5e17226ad06ed1 Mon Sep 17 00:00:00 2001 From: Angelebeats <89266469@qq.com> Date: Sun, 5 Apr 2026 19:52:26 +0800 Subject: [PATCH 04/11] fix: enforce exact path validation and bypass notification settings for deep-link security --- apps/desktop/src-tauri/src/deeplink_actions.rs | 6 +++++- apps/desktop/src-tauri/src/notifications.rs | 15 ++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 7cdc5a65b0..3317ce10cf 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -92,6 +92,10 @@ impl TryFrom<&Url> for DeepLinkAction { } if url.scheme().eq_ignore_ascii_case("cap") { + if url.path() != "/" { + return Err(ActionParseFromUrlError::Invalid); + } + return match url.host_str() { Some(h) if h.eq_ignore_ascii_case("record") => Ok(Self::StartDefaultRecording), Some(h) if h.eq_ignore_ascii_case("stop") => Ok(Self::StopRecording), @@ -126,7 +130,7 @@ impl DeepLinkAction { | DeepLinkAction::StartDefaultRecording | DeepLinkAction::PauseRecording | DeepLinkAction::ResumeRecording => { - crate::notifications::NotificationType::DeepLinkTriggered.send(app); + crate::notifications::NotificationType::DeepLinkTriggered.send_always(app); } _ => {} } diff --git a/apps/desktop/src-tauri/src/notifications.rs b/apps/desktop/src-tauri/src/notifications.rs index ec992f60fd..5165e462e7 100644 --- a/apps/desktop/src-tauri/src/notifications.rs +++ b/apps/desktop/src-tauri/src/notifications.rs @@ -90,14 +90,19 @@ impl NotificationType { } pub fn send(self, app: &tauri::AppHandle) { - send_notification(app, self); + send_notification(app, self, false); + } + + pub fn send_always(self, app: &tauri::AppHandle) { + send_notification(app, self, true); } } -pub fn send_notification(app: &tauri::AppHandle, notification_type: NotificationType) { - let enable_notifications = GeneralSettingsStore::get(app) - .map(|settings| settings.is_some_and(|s| s.enable_notifications)) - .unwrap_or(false); +pub fn send_notification(app: &tauri::AppHandle, notification_type: NotificationType, always: bool) { + let enable_notifications = always + || GeneralSettingsStore::get(app) + .map(|settings| settings.is_some_and(|s| s.enable_notifications)) + .unwrap_or(false); if !enable_notifications { return; From a6a24ba10366e5f48586ccee1e98540bed394d7e Mon Sep 17 00:00:00 2001 From: Angelebeats <89266469@qq.com> Date: Sun, 5 Apr 2026 19:53:35 +0800 Subject: [PATCH 05/11] feat: implement TogglePauseRecording for cap://pause and map Raycast command to toggle --- apps/desktop/src-tauri/src/deeplink_actions.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 3317ce10cf..82dec50a0f 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -35,6 +35,7 @@ pub enum DeepLinkAction { StartDefaultRecording, PauseRecording, ResumeRecording, + TogglePauseRecording, } pub fn handle(app_handle: &AppHandle, urls: Vec) { @@ -99,7 +100,7 @@ impl TryFrom<&Url> for DeepLinkAction { return match url.host_str() { Some(h) if h.eq_ignore_ascii_case("record") => Ok(Self::StartDefaultRecording), Some(h) if h.eq_ignore_ascii_case("stop") => Ok(Self::StopRecording), - Some(h) if h.eq_ignore_ascii_case("pause") => Ok(Self::PauseRecording), + Some(h) if h.eq_ignore_ascii_case("pause") => Ok(Self::TogglePauseRecording), Some(h) if h.eq_ignore_ascii_case("resume") => Ok(Self::ResumeRecording), _ => Err(ActionParseFromUrlError::Invalid), }; @@ -129,7 +130,8 @@ impl DeepLinkAction { | DeepLinkAction::StopRecording | DeepLinkAction::StartDefaultRecording | DeepLinkAction::PauseRecording - | DeepLinkAction::ResumeRecording => { + | DeepLinkAction::ResumeRecording + | DeepLinkAction::TogglePauseRecording => { crate::notifications::NotificationType::DeepLinkTriggered.send_always(app); } _ => {} @@ -190,6 +192,9 @@ impl DeepLinkAction { DeepLinkAction::ResumeRecording => { crate::recording::resume_recording(app.clone(), app.state()).await } + DeepLinkAction::TogglePauseRecording => { + crate::recording::toggle_pause_recording(app.clone(), app.state()).await + } } } } From fadcaf13ce0bdd760132f122dc46d9bdf1af7607 Mon Sep 17 00:00:00 2001 From: Angelebeats <89266469@qq.com> Date: Sun, 5 Apr 2026 21:47:25 +0800 Subject: [PATCH 06/11] refactor: remove redundant PauseRecording action from deep links --- apps/desktop/src-tauri/src/deeplink_actions.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 82dec50a0f..f99eba0a2b 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -33,7 +33,6 @@ pub enum DeepLinkAction { page: Option, }, StartDefaultRecording, - PauseRecording, ResumeRecording, TogglePauseRecording, } @@ -129,7 +128,6 @@ impl DeepLinkAction { DeepLinkAction::StartRecording { .. } | DeepLinkAction::StopRecording | DeepLinkAction::StartDefaultRecording - | DeepLinkAction::PauseRecording | DeepLinkAction::ResumeRecording | DeepLinkAction::TogglePauseRecording => { crate::notifications::NotificationType::DeepLinkTriggered.send_always(app); @@ -186,9 +184,6 @@ impl DeepLinkAction { DeepLinkAction::StartDefaultRecording => { crate::RequestOpenRecordingPicker { target_mode: None }.emit(app).map_err(|e| e.to_string()) } - DeepLinkAction::PauseRecording => { - crate::recording::pause_recording(app.clone(), app.state()).await - } DeepLinkAction::ResumeRecording => { crate::recording::resume_recording(app.clone(), app.state()).await } From b75aa9424c41aaf2b0eef93111f933d432b773c2 Mon Sep 17 00:00:00 2001 From: Angelebeats <89266469@qq.com> Date: Sun, 5 Apr 2026 22:10:33 +0800 Subject: [PATCH 07/11] refactor: industrial-grade deep link handling with robust parsing and security notifications --- .../desktop/src-tauri/src/deeplink_actions.rs | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index f99eba0a2b..cab167a441 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -4,7 +4,7 @@ use cap_recording::{ use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager, Url}; -use tracing::trace; +use tracing::{trace, warn, error}; use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow}; @@ -47,12 +47,11 @@ pub fn handle(app_handle: &AppHandle, urls: Vec) { DeepLinkAction::try_from(&url) .map_err(|e| match e { ActionParseFromUrlError::ParseFailed(msg) => { - eprintln!("Failed to parse deep link \"{}\": {}", &url, msg) + error!("Failed to parse deep link \"{}\": {}", &url, msg) } ActionParseFromUrlError::Invalid => { - eprintln!("Invalid deep link format \"{}\"", &url) + warn!("Invalid deep link format \"{}\"", &url) } - // Likely login action, not handled here. ActionParseFromUrlError::NotAction => {} }) .ok() @@ -67,7 +66,7 @@ pub fn handle(app_handle: &AppHandle, urls: Vec) { tauri::async_runtime::spawn(async move { for action in actions { if let Err(e) = action.execute(&app_handle).await { - eprintln!("Failed to handle deep link action: {e}"); + error!("Failed to handle deep link action: {e}"); } } }); @@ -83,24 +82,28 @@ impl TryFrom<&Url> for DeepLinkAction { type Error = ActionParseFromUrlError; fn try_from(url: &Url) -> Result { + let scheme = url.scheme().to_lowercase(); + #[cfg(target_os = "macos")] - if url.scheme().eq_ignore_ascii_case("file") { + if scheme == "file" { return url .to_file_path() .map(|project_path| Self::OpenEditor { project_path }) .map_err(|_| ActionParseFromUrlError::Invalid); } - if url.scheme().eq_ignore_ascii_case("cap") { - if url.path() != "/" { + if scheme == "cap" { + // Robust path check (handles cap://record and cap://record/) + let path = url.path().trim_matches('/'); + if !path.is_empty() { return Err(ActionParseFromUrlError::Invalid); } - return match url.host_str() { - Some(h) if h.eq_ignore_ascii_case("record") => Ok(Self::StartDefaultRecording), - Some(h) if h.eq_ignore_ascii_case("stop") => Ok(Self::StopRecording), - Some(h) if h.eq_ignore_ascii_case("pause") => Ok(Self::TogglePauseRecording), - Some(h) if h.eq_ignore_ascii_case("resume") => Ok(Self::ResumeRecording), + return match url.host_str().map(|h| h.to_lowercase().as_str()) { + Some("record") => Ok(Self::StartDefaultRecording), + Some("stop") => Ok(Self::StopRecording), + Some("pause") => Ok(Self::TogglePauseRecording), + Some("resume") => Ok(Self::ResumeRecording), _ => Err(ActionParseFromUrlError::Invalid), }; } @@ -124,6 +127,7 @@ impl TryFrom<&Url> for DeepLinkAction { impl DeepLinkAction { pub async fn execute(self, app: &AppHandle) -> Result<(), String> { + // Trigger security/visibility notification for recording actions match &self { DeepLinkAction::StartRecording { .. } | DeepLinkAction::StopRecording @@ -153,12 +157,12 @@ impl DeepLinkAction { .into_iter() .find(|(s, _)| s.name == name) .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) - .ok_or(format!("No screen with name \"{}\"", &name))?, + .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(format!("No window with name \"{}\"", &name))?, + .ok_or_else(|| format!("No window with name \"{}\"", &name))?, }; let inputs = StartRecordingInputs { @@ -182,6 +186,7 @@ impl DeepLinkAction { crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await } DeepLinkAction::StartDefaultRecording => { + // Perfect payload emission for frontend deserialization crate::RequestOpenRecordingPicker { target_mode: None }.emit(app).map_err(|e| e.to_string()) } DeepLinkAction::ResumeRecording => { From c14a6dbf59a6d3fe349e9549652e66e1249a8d5c Mon Sep 17 00:00:00 2001 From: Angelebeats <89266469@qq.com> Date: Mon, 6 Apr 2026 17:35:30 +0800 Subject: [PATCH 08/11] feat: refine deep-link security notifications and fix host parsing --- .../desktop/src-tauri/src/deeplink_actions.rs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index cab167a441..cff18a01d1 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -99,11 +99,11 @@ impl TryFrom<&Url> for DeepLinkAction { return Err(ActionParseFromUrlError::Invalid); } - return match url.host_str().map(|h| h.to_lowercase().as_str()) { - Some("record") => Ok(Self::StartDefaultRecording), - Some("stop") => Ok(Self::StopRecording), - Some("pause") => Ok(Self::TogglePauseRecording), - Some("resume") => Ok(Self::ResumeRecording), + return match url.host_str().map(|h| h.to_lowercase()) { + Some(host) if host == "record" => Ok(Self::StartDefaultRecording), + Some(host) if host == "stop" => Ok(Self::StopRecording), + Some(host) if host == "pause" => Ok(Self::TogglePauseRecording), + Some(host) if host == "resume" => Ok(Self::ResumeRecording), _ => Err(ActionParseFromUrlError::Invalid), }; } @@ -127,14 +127,17 @@ impl TryFrom<&Url> for DeepLinkAction { impl DeepLinkAction { pub async fn execute(self, app: &AppHandle) -> Result<(), String> { - // Trigger security/visibility notification for recording actions + // Handle security/visibility notification for deep link actions match &self { - DeepLinkAction::StartRecording { .. } - | DeepLinkAction::StopRecording - | DeepLinkAction::StartDefaultRecording + // Force notification for actions that START recording (Critical for security) + DeepLinkAction::StartRecording { .. } | DeepLinkAction::StartDefaultRecording => { + crate::notifications::NotificationType::DeepLinkTriggered.send_always(app); + } + // Other actions respect user's notification preference + DeepLinkAction::StopRecording | DeepLinkAction::ResumeRecording | DeepLinkAction::TogglePauseRecording => { - crate::notifications::NotificationType::DeepLinkTriggered.send_always(app); + crate::notifications::NotificationType::DeepLinkTriggered.send(app); } _ => {} } From f31341d7426f6a40e466401b40fbefe07f40ff16 Mon Sep 17 00:00:00 2001 From: Angelebeats <89266469@qq.com> Date: Mon, 6 Apr 2026 18:39:47 +0800 Subject: [PATCH 09/11] feat: optimize deep-link parsing and ensure unconditional security notifications --- .../desktop/src-tauri/src/deeplink_actions.rs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index cff18a01d1..59d9040e7e 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -93,17 +93,16 @@ impl TryFrom<&Url> for DeepLinkAction { } if scheme == "cap" { - // Robust path check (handles cap://record and cap://record/) let path = url.path().trim_matches('/'); if !path.is_empty() { return Err(ActionParseFromUrlError::Invalid); } - return match url.host_str().map(|h| h.to_lowercase()) { - Some(host) if host == "record" => Ok(Self::StartDefaultRecording), - Some(host) if host == "stop" => Ok(Self::StopRecording), - Some(host) if host == "pause" => Ok(Self::TogglePauseRecording), - Some(host) if host == "resume" => Ok(Self::ResumeRecording), + return match url.host_str() { + Some(host) if host.eq_ignore_ascii_case("record") => Ok(Self::StartDefaultRecording), + Some(host) if host.eq_ignore_ascii_case("stop") => Ok(Self::StopRecording), + Some(host) if host.eq_ignore_ascii_case("pause") => Ok(Self::TogglePauseRecording), + Some(host) if host.eq_ignore_ascii_case("resume") => Ok(Self::ResumeRecording), _ => Err(ActionParseFromUrlError::Invalid), }; } @@ -127,17 +126,13 @@ impl TryFrom<&Url> for DeepLinkAction { impl DeepLinkAction { pub async fn execute(self, app: &AppHandle) -> Result<(), String> { - // Handle security/visibility notification for deep link actions match &self { - // Force notification for actions that START recording (Critical for security) - DeepLinkAction::StartRecording { .. } | DeepLinkAction::StartDefaultRecording => { - crate::notifications::NotificationType::DeepLinkTriggered.send_always(app); - } - // Other actions respect user's notification preference - DeepLinkAction::StopRecording + DeepLinkAction::StartRecording { .. } + | DeepLinkAction::StartDefaultRecording + | DeepLinkAction::StopRecording | DeepLinkAction::ResumeRecording | DeepLinkAction::TogglePauseRecording => { - crate::notifications::NotificationType::DeepLinkTriggered.send(app); + crate::notifications::NotificationType::DeepLinkTriggered.send_always(app); } _ => {} } @@ -189,7 +184,6 @@ impl DeepLinkAction { crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await } DeepLinkAction::StartDefaultRecording => { - // Perfect payload emission for frontend deserialization crate::RequestOpenRecordingPicker { target_mode: None }.emit(app).map_err(|e| e.to_string()) } DeepLinkAction::ResumeRecording => { From 7cde3a360efbc5d818d8d06dca3c37b1c9bfc807 Mon Sep 17 00:00:00 2001 From: Angelebeats <89266469@qq.com> Date: Mon, 6 Apr 2026 18:51:53 +0800 Subject: [PATCH 10/11] feat: enhance data privacy and bot-aligned security for deep links --- .../desktop/src-tauri/src/deeplink_actions.rs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 59d9040e7e..cf18cd8bd2 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -38,21 +38,24 @@ pub enum DeepLinkAction { } pub fn handle(app_handle: &AppHandle, urls: Vec) { - trace!("Handling deep actions for: {:?}", &urls); - let actions: Vec<_> = urls .into_iter() .filter(|url| !url.as_str().is_empty()) .filter_map(|url| { DeepLinkAction::try_from(&url) - .map_err(|e| match e { - ActionParseFromUrlError::ParseFailed(msg) => { - error!("Failed to parse deep link \"{}\": {}", &url, msg) - } - ActionParseFromUrlError::Invalid => { - warn!("Invalid deep link format \"{}\"", &url) + .map_err(|e| { + let mut safe_url = url.clone(); + safe_url.set_query(None); + safe_url.set_fragment(None); + match e { + ActionParseFromUrlError::ParseFailed(msg) => { + error!("Failed to parse deep link \"{}\": {}", safe_url, msg) + } + ActionParseFromUrlError::Invalid => { + warn!("Invalid deep link format \"{}\"", safe_url) + } + ActionParseFromUrlError::NotAction => {} } - ActionParseFromUrlError::NotAction => {} }) .ok() }) From 32bc0955a1fa0c7e0016330b0a8ee94633c13247 Mon Sep 17 00:00:00 2001 From: Angelebeats <89266469@qq.com> Date: Mon, 6 Apr 2026 21:01:06 +0800 Subject: [PATCH 11/11] fix: address tembo/greptile bot feedback on deep links and notifications --- .../desktop/src-tauri/src/deeplink_actions.rs | 24 +++++++++++-------- apps/desktop/src-tauri/src/notifications.rs | 15 +++++++++--- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index cf18cd8bd2..d41cfc9212 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -4,7 +4,7 @@ use cap_recording::{ use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager, Url}; -use tracing::{trace, warn, error}; +use tracing::{warn, error}; use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow}; @@ -96,18 +96,22 @@ impl TryFrom<&Url> for DeepLinkAction { } if scheme == "cap" { + let host = url.host_str().unwrap_or_default(); let path = url.path().trim_matches('/'); - if !path.is_empty() { - return Err(ActionParseFromUrlError::Invalid); - } - return match url.host_str() { - Some(host) if host.eq_ignore_ascii_case("record") => Ok(Self::StartDefaultRecording), - Some(host) if host.eq_ignore_ascii_case("stop") => Ok(Self::StopRecording), - Some(host) if host.eq_ignore_ascii_case("pause") => Ok(Self::TogglePauseRecording), - Some(host) if host.eq_ignore_ascii_case("resume") => Ok(Self::ResumeRecording), - _ => Err(ActionParseFromUrlError::Invalid), + let action = if host.eq_ignore_ascii_case("record") || path.eq_ignore_ascii_case("record") { + Some(Self::StartDefaultRecording) + } else if host.eq_ignore_ascii_case("stop") || path.eq_ignore_ascii_case("stop") { + Some(Self::StopRecording) + } else if host.eq_ignore_ascii_case("pause") || path.eq_ignore_ascii_case("pause") { + Some(Self::TogglePauseRecording) + } else if host.eq_ignore_ascii_case("resume") || path.eq_ignore_ascii_case("resume") { + Some(Self::ResumeRecording) + } else { + None }; + + return action.ok_or(ActionParseFromUrlError::Invalid); } match url.domain() { diff --git a/apps/desktop/src-tauri/src/notifications.rs b/apps/desktop/src-tauri/src/notifications.rs index 5165e462e7..4cd4a2f90a 100644 --- a/apps/desktop/src-tauri/src/notifications.rs +++ b/apps/desktop/src-tauri/src/notifications.rs @@ -90,15 +90,23 @@ impl NotificationType { } pub fn send(self, app: &tauri::AppHandle) { - send_notification(app, self, false); + send_notification(app, self); } pub fn send_always(self, app: &tauri::AppHandle) { - send_notification(app, self, true); + send_notification_always(app, self); } } -pub fn send_notification(app: &tauri::AppHandle, notification_type: NotificationType, always: bool) { +pub fn send_notification(app: &tauri::AppHandle, notification_type: NotificationType) { + _send_notification(app, notification_type, false); +} + +pub fn send_notification_always(app: &tauri::AppHandle, notification_type: NotificationType) { + _send_notification(app, notification_type, true); +} + +fn _send_notification(app: &tauri::AppHandle, notification_type: NotificationType, always: bool) { let enable_notifications = always || GeneralSettingsStore::get(app) .map(|settings| settings.is_some_and(|s| s.enable_notifications)) @@ -123,6 +131,7 @@ pub fn send_notification(app: &tauri::AppHandle, notification_type: Notification | NotificationType::ScreenshotCopiedToClipboard | NotificationType::ScreenshotSaveFailed | NotificationType::ScreenshotCopyFailed + | NotificationType::DeepLinkTriggered ); if !skip_sound {