-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: add deep-link support and Raycast extension #1708
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
0d085b3
de021be
7cee5f5
771937e
a6a24ba
fadcaf1
b75aa94
c14a6db
f31341d
7cde3a3
32bc095
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 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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::{warn, error}; | ||||||||||
|
|
||||||||||
| use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow}; | ||||||||||
|
|
||||||||||
|
|
@@ -32,25 +32,30 @@ pub enum DeepLinkAction { | |||||||||
| OpenSettings { | ||||||||||
| page: Option<String>, | ||||||||||
| }, | ||||||||||
| StartDefaultRecording, | ||||||||||
| ResumeRecording, | ||||||||||
| TogglePauseRecording, | ||||||||||
| } | ||||||||||
|
|
||||||||||
| pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) { | ||||||||||
| 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) => { | ||||||||||
| eprintln!("Failed to parse deep link \"{}\": {}", &url, msg) | ||||||||||
| } | ||||||||||
| ActionParseFromUrlError::Invalid => { | ||||||||||
| eprintln!("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 => {} | ||||||||||
| } | ||||||||||
| // Likely login action, not handled here. | ||||||||||
| ActionParseFromUrlError::NotAction => {} | ||||||||||
| }) | ||||||||||
| .ok() | ||||||||||
| }) | ||||||||||
|
|
@@ -64,7 +69,7 @@ pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) { | |||||||||
| 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}"); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| }); | ||||||||||
|
|
@@ -80,14 +85,35 @@ impl TryFrom<&Url> for DeepLinkAction { | |||||||||
| type Error = ActionParseFromUrlError; | ||||||||||
|
|
||||||||||
| fn try_from(url: &Url) -> Result<Self, Self::Error> { | ||||||||||
| let scheme = url.scheme().to_lowercase(); | ||||||||||
|
|
||||||||||
| #[cfg(target_os = "macos")] | ||||||||||
| if url.scheme() == "file" { | ||||||||||
| if scheme == "file" { | ||||||||||
| return url | ||||||||||
| .to_file_path() | ||||||||||
| .map(|project_path| Self::OpenEditor { project_path }) | ||||||||||
| .map_err(|_| ActionParseFromUrlError::Invalid); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if scheme == "cap" { | ||||||||||
| let host = url.host_str().unwrap_or_default(); | ||||||||||
| let path = url.path().trim_matches('/'); | ||||||||||
|
|
||||||||||
| 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() { | ||||||||||
| Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction), | ||||||||||
| _ => Err(ActionParseFromUrlError::Invalid), | ||||||||||
|
|
@@ -107,6 +133,17 @@ impl TryFrom<&Url> for DeepLinkAction { | |||||||||
|
|
||||||||||
| impl DeepLinkAction { | ||||||||||
| pub async fn execute(self, app: &AppHandle) -> Result<(), String> { | ||||||||||
| match &self { | ||||||||||
| DeepLinkAction::StartRecording { .. } | ||||||||||
| | DeepLinkAction::StartDefaultRecording | ||||||||||
| | DeepLinkAction::StopRecording | ||||||||||
| | DeepLinkAction::ResumeRecording | ||||||||||
| | DeepLinkAction::TogglePauseRecording => { | ||||||||||
| crate::notifications::NotificationType::DeepLinkTriggered.send_always(app); | ||||||||||
| } | ||||||||||
| _ => {} | ||||||||||
| } | ||||||||||
|
|
||||||||||
| match self { | ||||||||||
| DeepLinkAction::StartRecording { | ||||||||||
| capture_mode, | ||||||||||
|
|
@@ -125,12 +162,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 { | ||||||||||
|
|
@@ -153,6 +190,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 | ||||||||||
|
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
|
||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,6 +14,7 @@ pub enum NotificationType { | |||||||||||||||||||
| ScreenshotCopiedToClipboard, | ||||||||||||||||||||
| ScreenshotSaveFailed, | ||||||||||||||||||||
| ScreenshotCopyFailed, | ||||||||||||||||||||
| DeepLinkTriggered, | ||||||||||||||||||||
|
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. Since
Suggested change
|
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 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, | ||||||||||||||||||||
| ), | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -86,12 +92,25 @@ impl NotificationType { | |||||||||||||||||||
| pub fn send(self, app: &tauri::AppHandle) { | ||||||||||||||||||||
| send_notification(app, self); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub fn send_always(self, app: &tauri::AppHandle) { | ||||||||||||||||||||
| send_notification_always(app, self); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 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); | ||||||||||||||||||||
| _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)) | ||||||||||||||||||||
| .unwrap_or(false); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if !enable_notifications { | ||||||||||||||||||||
| return; | ||||||||||||||||||||
|
|
@@ -112,6 +131,7 @@ pub fn send_notification(app: &tauri::AppHandle, notification_type: Notification | |||||||||||||||||||
| | NotificationType::ScreenshotCopiedToClipboard | ||||||||||||||||||||
| | NotificationType::ScreenshotSaveFailed | ||||||||||||||||||||
| | NotificationType::ScreenshotCopyFailed | ||||||||||||||||||||
| | NotificationType::DeepLinkTriggered | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if !skip_sound { | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,7 +30,7 @@ | |
| "updater": { "active": false, "pubkey": "" }, | ||
| "deep-link": { | ||
| "desktop": { | ||
| "schemes": ["cap-desktop"] | ||
| "schemes": ["cap-desktop", "cap"] | ||
|
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. Registering the short |
||
| } | ||
| } | ||
| }, | ||
|
|
||
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.
cap://recordworks, but some launchers producecap:///record(no host, path-only). Accepting a single path segment as a fallback makes this a bit more robust without changing behavior for normal URLs.