Skip to content

Commit cdfe952

Browse files
Den A EvDen A Ev
authored andcommitted
feat: stabilize URI remote control via direct actor management
1 parent bd987b3 commit cdfe952

File tree

5 files changed

+167
-111
lines changed

5 files changed

+167
-111
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
"@tanstack/solid-query": "^5.51.21",
4343
"@tauri-apps/api": "2.8.0",
4444
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
45-
"@tauri-apps/plugin-deep-link": "^2.4.1",
4645
"@tauri-apps/plugin-dialog": "^2.4.0",
4746
"@tauri-apps/plugin-fs": "^2.4.1",
4847
"@tauri-apps/plugin-http": "^2.5.1",

apps/desktop/src-tauri/src/deeplink_actions.rs

Lines changed: 116 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ pub enum DeepLinkAction {
2525
capture_system_audio: bool,
2626
mode: RecordingMode,
2727
},
28+
PauseRecording,
29+
ResumeRecording,
30+
TogglePauseRecording,
2831
StopRecording,
32+
TakeScreenshot,
2933
OpenEditor {
3034
project_path: PathBuf,
3135
},
@@ -34,44 +38,8 @@ pub enum DeepLinkAction {
3438
},
3539
}
3640

37-
pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) {
38-
trace!("Handling deep actions for: {:?}", &urls);
39-
40-
let actions: Vec<_> = urls
41-
.into_iter()
42-
.filter(|url| !url.as_str().is_empty())
43-
.filter_map(|url| {
44-
DeepLinkAction::try_from(&url)
45-
.map_err(|e| match e {
46-
ActionParseFromUrlError::ParseFailed(msg) => {
47-
eprintln!("Failed to parse deep link \"{}\": {}", &url, msg)
48-
}
49-
ActionParseFromUrlError::Invalid => {
50-
eprintln!("Invalid deep link format \"{}\"", &url)
51-
}
52-
// Likely login action, not handled here.
53-
ActionParseFromUrlError::NotAction => {}
54-
})
55-
.ok()
56-
})
57-
.collect();
58-
59-
if actions.is_empty() {
60-
return;
61-
}
62-
63-
let app_handle = app_handle.clone();
64-
tauri::async_runtime::spawn(async move {
65-
for action in actions {
66-
if let Err(e) = action.execute(&app_handle).await {
67-
eprintln!("Failed to handle deep link action: {e}");
68-
}
69-
}
70-
});
71-
}
72-
7341
pub enum ActionParseFromUrlError {
74-
ParseFailed(String),
42+
ParseFailed,
7543
Invalid,
7644
NotAction,
7745
}
@@ -80,35 +48,74 @@ impl TryFrom<&Url> for DeepLinkAction {
8048
type Error = ActionParseFromUrlError;
8149

8250
fn try_from(url: &Url) -> Result<Self, Self::Error> {
83-
#[cfg(target_os = "macos")]
84-
if url.scheme() == "file" {
85-
return url
86-
.to_file_path()
87-
.map(|project_path| Self::OpenEditor { project_path })
88-
.map_err(|_| ActionParseFromUrlError::Invalid);
51+
if url.scheme() != "cap-desktop" {
52+
return Err(ActionParseFromUrlError::NotAction);
8953
}
9054

9155
match url.domain() {
92-
Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction),
93-
_ => Err(ActionParseFromUrlError::Invalid),
94-
}?;
95-
96-
let params = url
97-
.query_pairs()
98-
.collect::<std::collections::HashMap<_, _>>();
99-
let json_value = params
100-
.get("value")
101-
.ok_or(ActionParseFromUrlError::Invalid)?;
102-
let action: Self = serde_json::from_str(json_value)
103-
.map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?;
104-
Ok(action)
56+
Some("pause") => Ok(Self::PauseRecording),
57+
Some("resume") => Ok(Self::ResumeRecording),
58+
Some("toggle-pause") => Ok(Self::TogglePauseRecording),
59+
Some("stop") => Ok(Self::StopRecording),
60+
Some("screenshot") => Ok(Self::TakeScreenshot),
61+
_ => {
62+
if url.domain() == Some("action") {
63+
let params = url.query_pairs().collect::<std::collections::HashMap<_, _>>();
64+
let json_value = params.get("value").ok_or(ActionParseFromUrlError::Invalid)?;
65+
let action: Self = serde_json::from_str(json_value)
66+
.map_err(|_| ActionParseFromUrlError::ParseFailed)?;
67+
Ok(action)
68+
} else {
69+
Err(ActionParseFromUrlError::Invalid)
70+
}
71+
}
72+
}
10573
}
10674
}
10775

10876
impl DeepLinkAction {
10977
pub async fn execute(self, app: &AppHandle) -> Result<(), String> {
78+
trace!("Executing deep link action: {:?}", self);
79+
11080
match self {
111-
DeepLinkAction::StartRecording {
81+
Self::PauseRecording => {
82+
let state = app.state::<ArcLock<App>>();
83+
crate::recording::pause_recording(app.clone(), state.clone()).await?;
84+
let _ = ShowCapWindow::Main { init_target_mode: None }.show(app).await;
85+
Ok(())
86+
}
87+
Self::ResumeRecording => {
88+
let state = app.state::<ArcLock<App>>();
89+
crate::recording::resume_recording(app.clone(), state.clone()).await?;
90+
let _ = ShowCapWindow::Main { init_target_mode: None }.show(app).await;
91+
Ok(())
92+
}
93+
Self::TogglePauseRecording => {
94+
let state = app.state::<ArcLock<App>>();
95+
crate::recording::toggle_pause_recording(app.clone(), state.clone()).await?;
96+
let _ = ShowCapWindow::Main { init_target_mode: None }.show(app).await;
97+
Ok(())
98+
}
99+
Self::StopRecording => {
100+
let state = app.state::<ArcLock<App>>();
101+
crate::recording::stop_recording(app.clone(), state.clone()).await?;
102+
let _ = ShowCapWindow::Main { init_target_mode: None }.show(app).await;
103+
Ok(())
104+
}
105+
Self::TakeScreenshot => {
106+
use scap_targets::Display;
107+
let display = Display::get_containing_cursor().unwrap_or_else(Display::primary);
108+
let target = ScreenCaptureTarget::Display { id: display.id() };
109+
110+
match crate::recording::take_screenshot(app.clone(), target).await {
111+
Ok(path) => {
112+
let _ = ShowCapWindow::ScreenshotEditor { path }.show(app).await;
113+
Ok(())
114+
}
115+
Err(e) => Err(format!("Failed to take screenshot: {e}")),
116+
}
117+
}
118+
Self::StartRecording {
112119
capture_mode,
113120
camera,
114121
mic_label,
@@ -117,42 +124,77 @@ impl DeepLinkAction {
117124
} => {
118125
let state = app.state::<ArcLock<App>>();
119126

120-
crate::set_camera_input(app.clone(), state.clone(), camera, None).await?;
121-
crate::set_mic_input(state.clone(), mic_label).await?;
122-
123-
let capture_target: ScreenCaptureTarget = match capture_mode {
124-
CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays()
127+
let capture_target = match capture_mode {
128+
CaptureMode::Screen(name) => cap_recording::sources::screen_capture::list_displays()
125129
.into_iter()
126-
.find(|(s, _)| s.name == name)
130+
.find(|(s, _)| s.name == *name)
127131
.map(|(s, _)| ScreenCaptureTarget::Display { id: s.id })
128132
.ok_or(format!("No screen with name \"{}\"", &name))?,
129-
CaptureMode::Window(name) => cap_recording::screen_capture::list_windows()
133+
CaptureMode::Window(name) => cap_recording::sources::screen_capture::list_windows()
130134
.into_iter()
131-
.find(|(w, _)| w.name == name)
135+
.find(|(w, _)| w.name == *name)
132136
.map(|(w, _)| ScreenCaptureTarget::Window { id: w.id })
133137
.ok_or(format!("No window with name \"{}\"", &name))?,
134138
};
135139

136140
let inputs = StartRecordingInputs {
137-
mode,
138141
capture_target,
139142
capture_system_audio,
143+
mode,
140144
organization_id: None,
141145
};
142146

143-
crate::recording::start_recording(app.clone(), state, inputs)
147+
if let Some(camera_id) = camera {
148+
crate::set_camera_input(app.clone(), state.clone(), Some(camera_id.clone()), None)
149+
.await
150+
.map_err(|e| e.to_string())?;
151+
}
152+
153+
if let Some(mic) = mic_label {
154+
crate::set_mic_input(state.clone(), Some(mic.clone()))
155+
.await
156+
.map_err(|e| e.to_string())?;
157+
}
158+
159+
crate::recording::start_recording(app.clone(), state.clone(), inputs)
144160
.await
145-
.map(|_| ())
146-
}
147-
DeepLinkAction::StopRecording => {
148-
crate::recording::stop_recording(app.clone(), app.state()).await
161+
.map_err(|e| e.to_string())?;
162+
Ok(())
149163
}
150-
DeepLinkAction::OpenEditor { project_path } => {
164+
Self::OpenEditor { project_path } => {
151165
crate::open_project_from_path(Path::new(&project_path), app.clone())
166+
.map_err(|e| e.to_string())?;
167+
Ok(())
152168
}
153-
DeepLinkAction::OpenSettings { page } => {
154-
crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await
169+
Self::OpenSettings { page } => {
170+
crate::show_window(app.clone(), ShowCapWindow::Settings { page: page.clone() })
171+
.await
172+
.map_err(|e| e.to_string())?;
173+
Ok(())
155174
}
156175
}
157176
}
158177
}
178+
179+
pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) {
180+
trace!("Handling deep actions for: {:?}", &urls);
181+
182+
let actions: Vec<_> = urls
183+
.into_iter()
184+
.filter(|url| !url.as_str().is_empty())
185+
.filter_map(|url| DeepLinkAction::try_from(&url).ok())
186+
.collect();
187+
188+
if actions.is_empty() {
189+
return;
190+
}
191+
192+
let app_handle = app_handle.clone();
193+
tauri::async_runtime::spawn(async move {
194+
for action in actions {
195+
if let Err(e) = action.execute(&app_handle).await {
196+
trace!("Failed to handle deep link action: {}", e);
197+
}
198+
}
199+
});
200+
}

apps/desktop/src-tauri/src/lib.rs

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ use std::{
8787
},
8888
time::{Duration, SystemTime, UNIX_EPOCH},
8989
};
90-
use tauri::{AppHandle, Manager, State, Window, WindowEvent, ipc::Channel};
91-
use tauri_plugin_deep_link::DeepLinkExt;
90+
use tauri::{
91+
ipc::Channel, AppHandle, Emitter, Manager, State, Url, Window, WindowEvent,
92+
};
9293
use tauri_plugin_dialog::DialogExt;
9394
use tauri_plugin_global_shortcut::GlobalShortcutExt;
9495
use tauri_plugin_notification::{NotificationExt, PermissionState};
@@ -3295,29 +3296,47 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
32953296
tauri::async_runtime::set(tokio::runtime::Handle::current());
32963297

32973298
#[allow(unused_mut)]
3298-
let mut builder =
3299-
tauri::Builder::default().plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
3300-
trace!("Single instance invoked with args {args:?}");
3301-
3302-
// This is also handled as a deeplink on some platforms (eg macOS), see deeplink_actions
3303-
let Some(cap_file) = args
3304-
.iter()
3305-
.find(|arg| arg.ends_with(".cap"))
3306-
.map(PathBuf::from)
3307-
else {
3308-
let app = app.clone();
3309-
tokio::spawn(async move {
3310-
ShowCapWindow::Main {
3311-
init_target_mode: None,
3312-
}
3313-
.show(&app)
3314-
.await
3315-
});
3316-
return;
3317-
};
3299+
let mut builder = tauri::Builder::default().plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
3300+
info!("Single instance invoked with args {:?}", &args);
3301+
let app_handle = app.clone();
3302+
3303+
// 1. Handle Remote Control URLs
3304+
if let Some(arg) = args.get(1) {
3305+
if arg.starts_with("cap-desktop://") {
3306+
if let Ok(url) = Url::parse(arg) {
3307+
crate::deeplink_actions::handle(app, vec![url]);
3308+
app.emit("tauri://deep-link", vec![arg.clone()]).ok();
3309+
3310+
let ah = app_handle.clone();
3311+
tauri::async_runtime::spawn(async move {
3312+
let _ = crate::windows::ShowCapWindow::Main {
3313+
init_target_mode: None,
3314+
}
3315+
.show(&ah)
3316+
.await;
3317+
});
3318+
}
3319+
}
3320+
}
33183321

3319-
let _ = open_project_from_path(&cap_file, app.clone());
3320-
}));
3322+
// 2. Handle .cap files
3323+
let cap_file = args.iter()
3324+
.find(|arg| arg.ends_with(".cap"))
3325+
.map(PathBuf::from);
3326+
3327+
if let Some(file) = cap_file {
3328+
let _ = open_project_from_path(&file, app.clone());
3329+
}
3330+
3331+
let ah = app_handle.clone();
3332+
tauri::async_runtime::spawn(async move {
3333+
let _ = crate::windows::ShowCapWindow::Main {
3334+
init_target_mode: None,
3335+
}
3336+
.show(&ah)
3337+
.await;
3338+
});
3339+
}));
33213340

33223341
#[cfg(target_os = "macos")]
33233342
{
@@ -3335,7 +3354,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
33353354
.plugin(tauri_plugin_updater::Builder::new().build())
33363355
.plugin(tauri_plugin_notification::init())
33373356
.plugin(flags::plugin::init())
3338-
.plugin(tauri_plugin_deep_link::init())
33393357
.plugin(tauri_plugin_clipboard_manager::init())
33403358
.plugin(tauri_plugin_fs::init())
33413359
.plugin(tauri_plugin_opener::init())
@@ -3597,11 +3615,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
35973615
prewarmer.request(event.force).await;
35983616
});
35993617

3600-
let app_handle = app.clone();
3601-
app.deep_link().on_open_url(move |event| {
3602-
deeplink_actions::handle(&app_handle, event.urls());
3603-
});
3604-
36053618
Ok(())
36063619
})
36073620
.on_window_event(|window, event| {

0 commit comments

Comments
 (0)