Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ pub enum DeepLinkAction {
OpenSettings {
page: Option<String>,
},
StartDefaultRecording,
ResumeRecording,
TogglePauseRecording,
}

pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) {
Expand Down Expand Up @@ -81,13 +84,27 @@ impl TryFrom<&Url> for DeepLinkAction {

fn try_from(url: &Url) -> Result<Self, Self::Error> {
#[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().eq_ignore_ascii_case("cap") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently accepts any path after the host (e.g. cap://record/anything). If the intent is to support only the exact host actions, consider rejecting non-root paths to avoid surprising matches.

Suggested change
if url.scheme().eq_ignore_ascii_case("cap") {
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),
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),
};
}

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),
Some(h) if h.eq_ignore_ascii_case("pause") => Ok(Self::TogglePauseRecording),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: cap://pause maps to TogglePauseRecording, but the enum still has PauseRecording (and an execute arm). If it’s not used by the JSON deep-link path, consider dropping it to reduce confusion / surface area.

Some(h) if h.eq_ignore_ascii_case("resume") => Ok(Self::ResumeRecording),
_ => Err(ActionParseFromUrlError::Invalid),
};
}

match url.domain() {
Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction),
_ => Err(ActionParseFromUrlError::Invalid),
Expand All @@ -107,6 +124,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::ResumeRecording
| DeepLinkAction::TogglePauseRecording => {
crate::notifications::NotificationType::DeepLinkTriggered.send_always(app);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fire an always-on OS notification for every deep-link action (including pause/resume toggles), which feels like it could get pretty noisy for Raycast/automation and also bypasses the user's notification preference. Consider only forcing the notification for actions that start recording, and let the others respect the setting.

Suggested change
crate::notifications::NotificationType::DeepLinkTriggered.send_always(app);
match &self {
DeepLinkAction::StartRecording { .. } | DeepLinkAction::StartDefaultRecording => {
crate::notifications::NotificationType::DeepLinkTriggered.send_always(app);
}
DeepLinkAction::StopRecording
| DeepLinkAction::ResumeRecording
| DeepLinkAction::TogglePauseRecording => {
crate::notifications::NotificationType::DeepLinkTriggered.send(app);
}
_ => {}
}

}
_ => {}
}

match self {
DeepLinkAction::StartRecording {
capture_mode,
Expand Down Expand Up @@ -153,6 +181,15 @@ impl DeepLinkAction {
DeepLinkAction::OpenSettings { page } => {
crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await
}
DeepLinkAction::StartDefaultRecording => {
crate::RequestOpenRecordingPicker { target_mode: None }.emit(app).map_err(|e| e.to_string())
}
DeepLinkAction::ResumeRecording => {
crate::recording::resume_recording(app.clone(), app.state()).await
}
DeepLinkAction::TogglePauseRecording => {
crate::recording::toggle_pause_recording(app.clone(), app.state()).await
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cap://pause currently maps to TogglePauseRecording, so sending pause twice will resume. If the intent is separate pause/resume deep links, this arm probably wants to pause only.

Suggested change
crate::recording::toggle_pause_recording(app.clone(), app.state()).await
DeepLinkAction::TogglePauseRecording => {
crate::recording::pause_recording(app.clone(), app.state()).await
}

}
}
}
}
21 changes: 16 additions & 5 deletions apps/desktop/src-tauri/src/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub enum NotificationType {
ScreenshotCopiedToClipboard,
ScreenshotSaveFailed,
ScreenshotCopyFailed,
DeepLinkTriggered,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since DeepLinkTriggered is sent via send_always (bypassing user notification preference), consider also suppressing the notification sound for it so other apps can’t spam audio via deep links.

Suggested change
DeepLinkTriggered,
let skip_sound = matches!(
notification_type,
NotificationType::DeepLinkTriggered
| NotificationType::ScreenshotSaved
| NotificationType::ScreenshotCopiedToClipboard
| NotificationType::ScreenshotSaveFailed
| NotificationType::ScreenshotCopyFailed
);

}

impl NotificationType {
Expand Down Expand Up @@ -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,
),
}
}

Expand All @@ -84,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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing send_notification to require always is a breaking API change — any existing send_notification(app, ty) call sites will stop compiling. Might be cleaner to keep the 2-arg send_notification and add a separate send_notification_always (or a private inner helper) for the bypass.

let enable_notifications = always
|| GeneralSettingsStore::get(app)
.map(|settings| settings.is_some_and(|s| s.enable_notifications))
.unwrap_or(false);

if !enable_notifications {
return;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"updater": { "active": false, "pubkey": "" },
"deep-link": {
"desktop": {
"schemes": ["cap-desktop"]
"schemes": ["cap-desktop", "cap"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registering the short cap scheme makes it very easy for other local apps (and sometimes browsers) to trigger recording actions. Since this expands the surface area, consider making the deep-link notification unconditionally visible for all deep-link actions (not just start), or gating deep-link control behind an explicit user setting.

}
}
},
Expand Down