Skip to content

Deep-Link commands #243

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

Merged
merged 29 commits into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ed68d8b
Setup deeplink plugin
ItsEeleeya Nov 18, 2024
6dbdd5c
Merge remote-tracking branch 'upstream/main' into deeplink-commands
ItsEeleeya Jan 18, 2025
7c1a2fd
deeplink action handler
ItsEeleeya Jan 18, 2025
ecaba71
don't add single_instance twice.
ItsEeleeya Jan 18, 2025
71008bf
Implement deep-link OAuth signin action
ItsEeleeya Jan 18, 2025
5858b6e
Only apply auth when listening
ItsEeleeya Jan 18, 2025
5d2a044
oops
ItsEeleeya Jan 18, 2025
6e9c8f8
better state handling
ItsEeleeya Jan 18, 2025
290fd19
Implement recording related commands + start listening to oauth
ItsEeleeya Jan 19, 2025
9af7489
listen to oauth command with signin action
ItsEeleeya Jan 19, 2025
d0458c6
preparation
ItsEeleeya Jan 19, 2025
106d7aa
Merge remote-tracking branch 'upstream/main' into deeplink-commands
ItsEeleeya Jan 19, 2025
43dd728
Incorporate changes with #241
ItsEeleeya Jan 19, 2025
2f4ef6b
Rename params
ItsEeleeya Jan 19, 2025
14066fd
Merge remote-tracking branch 'upstream/main' into deeplink-commands
ItsEeleeya Jan 19, 2025
a6f21d5
Update pnpm-lock.yaml
ItsEeleeya Jan 19, 2025
2440f2e
Update signin.tsx
ItsEeleeya Jan 19, 2025
1271c7f
Cleanup
ItsEeleeya Jan 19, 2025
9775899
More efficient handler
ItsEeleeya Jan 19, 2025
bc6373c
more cleanup
ItsEeleeya Jan 19, 2025
6dae59a
Merge remote-tracking branch 'upstream/main' into deeplink-commands
ItsEeleeya Jan 20, 2025
bab5f86
Use serde for deserializing deep-link actions
ItsEeleeya Jan 20, 2025
7598f07
Remove unused mobile config
ItsEeleeya Jan 20, 2025
34ab70d
Add OpenSettings action
ItsEeleeya Jan 20, 2025
3221c8c
Stop listening to OAuth from deeplinks after authenticating
ItsEeleeya Jan 20, 2025
c3f8b40
Merge remote-tracking branch 'upstream/main' into deeplink-commands
ItsEeleeya Jan 27, 2025
0f6959e
Merge remote-tracking branch 'upstream/main' into deeplink-commands
ItsEeleeya Jun 7, 2025
858834c
Fix after long awaited pull
ItsEeleeya Jun 7, 2025
d8e5e43
Updated implementation of deeplink actions
ItsEeleeya Jun 7, 2025
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
145 changes: 145 additions & 0 deletions apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use cap_recording::RecordingMode;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager, Url};

use crate::{recording::StartRecordingInputs, windows::ShowCapWindow, App, ArcLock};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CaptureMode {
Screen(String),
Window(String),
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeepLinkAction {
StartRecording {
capture_mode: CaptureMode,
camera_label: Option<String>,
mic_label: Option<String>,
capture_system_audio: bool,
mode: RecordingMode,
},
StopRecording,
OpenEditor {
project_path: String,
},
OpenSettings {
page: Option<String>,
},
}

pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) {
#[cfg(debug_assertions)]
println!("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)
}
// Likely login action, not handled here.
ActionParseFromUrlError::NotAction => {}
})
.ok()
})
.collect();

if actions.is_empty() {
return;
}

let app_handle = app_handle.clone();
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);
}
}
});
}

pub enum ActionParseFromUrlError {
ParseFailed(String),
Invalid,
NotAction,
}

impl TryFrom<&Url> for DeepLinkAction {
type Error = ActionParseFromUrlError;

fn try_from(url: &Url) -> Result<Self, Self::Error> {
match url.domain() {
Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction),
_ => Err(ActionParseFromUrlError::Invalid),
}?;

let params = url
.query_pairs()
.collect::<std::collections::HashMap<_, _>>();
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)
}
}

impl DeepLinkAction {
pub async fn execute(self, app: &AppHandle) -> Result<(), String> {
match self {
DeepLinkAction::StartRecording {
capture_mode,
camera_label,
mic_label,
capture_system_audio,
mode,
} => {
let state = app.state::<ArcLock<App>>();

crate::set_camera_input(app.clone(), state.clone(), camera_label).await?;
crate::set_mic_input(state.clone(), mic_label).await?;

use cap_media::sources::ScreenCaptureTarget;
let capture_target: ScreenCaptureTarget = match capture_mode {
CaptureMode::Screen(name) => cap_media::sources::list_screens()
.into_iter()
.find(|(s, _)| s.name == name)
.map(|(s, _)| ScreenCaptureTarget::Screen { id: s.id })
.ok_or(format!("No screen with name \"{}\"", &name))?,
CaptureMode::Window(name) => cap_media::sources::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 inputs = StartRecordingInputs {
capture_target,
capture_system_audio,
mode,
};

crate::recording::start_recording(app.clone(), state, inputs).await
}
DeepLinkAction::StopRecording => {
crate::recording::stop_recording(app.clone(), app.state()).await
}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(&project_path.into(), app.clone())
}
DeepLinkAction::OpenSettings { page } => {
crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await
}
}
}
}
Empty file.
10 changes: 10 additions & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod audio;
mod auth;
mod camera;
mod captions;
mod deeplink_actions;
mod flags;
mod general_settings;
mod hotkeys;
Expand Down Expand Up @@ -67,6 +68,7 @@ use std::{
};
use tauri::Window;
use tauri::{AppHandle, Manager, State, WindowEvent};
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_notification::{NotificationExt, PermissionState};
use tauri_plugin_opener::OpenerExt;
Expand Down Expand Up @@ -2016,6 +2018,14 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
.await;
});

// Registering deep links at runtime is not possible on macOS,
// so deep links can only be tested on the bundled application,
// which must be installed in the /Applications directory.
let app_handle = app.clone();
app.deep_link().on_open_url(move |event| {
deeplink_actions::handle(&app_handle, event.urls());
});

Ok(())
})
.on_window_event(|window, event| {
Expand Down
8 changes: 1 addition & 7 deletions apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,7 @@
"deep-link": {
"desktop": {
"schemes": ["cap-desktop"]
},
"mobile": [
{
"host": "cap.so",
"pathPrefix": ["/signin"]
}
]
}
}
},
"bundle": {
Expand Down
Loading