1- use cap_recording:: {
2- RecordingMode , feeds:: camera:: DeviceOrModelID , sources:: screen_capture:: ScreenCaptureTarget ,
3- } ;
4- use serde:: { Deserialize , Serialize } ;
5- use std:: path:: { Path , PathBuf } ;
6- use tauri:: { AppHandle , Manager , Url } ;
7- use tracing:: trace;
8-
9- use crate :: { App , ArcLock , recording:: StartRecordingInputs , windows:: ShowCapWindow } ;
10-
11- #[ derive( Debug , Serialize , Deserialize ) ]
12- #[ serde( rename_all = "snake_case" ) ]
13- pub enum CaptureMode {
14- Screen ( String ) ,
15- Window ( String ) ,
1+ use std:: str:: FromStr ;
2+
3+ use tauri:: AppHandle ;
4+ use tauri_plugin_deep_link:: DeepLinkExt ;
5+ use tracing:: * ;
6+ use url:: Url ;
7+
8+ use crate :: { App , RecordingState , recording:: InProgressRecording } ;
9+ use crate :: windows:: { CapWindowId , ShowCapWindow } ;
10+
11+ /// Registers the deep-link handler for cap-desktop:// URLs and dispatches to
12+ /// the appropriate action handler.
13+ pub fn setup_deeplink_handler ( app_handle : AppHandle ) {
14+ app_handle. deep_link ( ) . on_open_urls ( move |event| {
15+ for raw_url in event. urls ( ) {
16+ let url_str = raw_url. to_string ( ) ;
17+ info ! ( url = %url_str, "Received deeplink" ) ;
18+ let app = app_handle. clone ( ) ;
19+ tauri:: async_runtime:: spawn ( async move {
20+ handle_deeplink_url ( & app, & url_str) . await ;
21+ } ) ;
22+ }
23+ } ) ;
1624}
1725
18- #[ derive( Debug , Serialize , Deserialize ) ]
19- #[ serde( rename_all = "snake_case" ) ]
20- pub enum DeepLinkAction {
21- StartRecording {
22- capture_mode : CaptureMode ,
23- camera : Option < DeviceOrModelID > ,
24- mic_label : Option < String > ,
25- capture_system_audio : bool ,
26- mode : RecordingMode ,
27- } ,
28- StopRecording ,
29- OpenEditor {
30- project_path : PathBuf ,
31- } ,
32- OpenSettings {
33- page : Option < String > ,
34- } ,
35- }
26+ /// Parse and dispatch a single cap-desktop:// URL.
27+ pub async fn handle_deeplink_url ( app : & AppHandle , raw_url : & str ) {
28+ let url = match Url :: from_str ( raw_url) {
29+ Ok ( u) => u,
30+ Err ( err) => {
31+ error ! ( url = %raw_url, error = %err, "Failed to parse deeplink URL" ) ;
32+ return ;
33+ }
34+ } ;
3635
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 ( ) {
36+ let scheme = url. scheme ( ) ;
37+ if scheme != "cap-desktop" {
38+ warn ! ( scheme = %scheme, "Ignoring deeplink with unexpected scheme" ) ;
6039 return ;
6140 }
6241
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- }
42+ // host() returns the "authority" segment, path() gives the rest.
43+ // For cap-desktop://recording/start the host is "recording" and path is "/start".
44+ let host = url. host_str ( ) . unwrap_or ( "" ) ;
45+ let path = url. path ( ) . trim_start_matches ( '/' ) ;
46+
47+ info ! ( host = %host, path = %path, "Dispatching deeplink action" ) ;
48+
49+ match ( host, path) {
50+ // ── Recording lifecycle ──────────────────────────────────────────────
51+ ( "recording" , "start" ) => action_start_recording ( app) . await ,
52+ ( "recording" , "stop" ) => action_stop_recording ( app) . await ,
53+ ( "recording" , "pause" ) => action_pause_recording ( app) . await ,
54+ ( "recording" , "resume" ) => action_resume_recording ( app) . await ,
55+
56+ // ── Device switching ─────────────────────────────────────────────────
57+ ( "microphone" , "switch" ) => {
58+ let name = query_param ( & url, "name" ) ;
59+ action_switch_microphone ( app, name. as_deref ( ) ) . await ;
6960 }
70- } ) ;
61+ ( "camera" , "switch" ) => {
62+ let name = query_param ( & url, "name" ) ;
63+ action_switch_camera ( app, name. as_deref ( ) ) . await ;
64+ }
65+
66+ _ => {
67+ warn ! ( host = %host, path = %path, "Unknown deeplink action – ignoring" ) ;
68+ }
69+ }
7170}
7271
73- pub enum ActionParseFromUrlError {
74- ParseFailed ( String ) ,
75- Invalid ,
76- NotAction ,
72+ // ─── Helpers ────────────────────────────────────────────────────────────────
73+
74+ fn query_param ( url : & Url , key : & str ) -> Option < String > {
75+ url. query_pairs ( )
76+ . find ( |( k, _) | k == key)
77+ . map ( |( _, v) | v. into_owned ( ) )
7778}
7879
79- impl TryFrom < & Url > for DeepLinkAction {
80- type Error = ActionParseFromUrlError ;
80+ // ─── Action handlers ────────────────────────────────────────────────────────
8181
82- 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 ) ;
89- }
82+ async fn action_start_recording ( app : & AppHandle ) {
83+ info ! ( "Deeplink: start recording" ) ;
84+ // Bring the main window to the front so the user can see the recording UI,
85+ // then invoke the existing start-recording pathway via a Tauri command event.
86+ let _ = ShowCapWindow :: Main . show ( app) ;
9087
91- 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)
105- }
88+ // Emit a frontend event that the SolidStart UI listens to.
89+ let _ = app. emit ( "deeplink-start-recording" , ( ) ) ;
10690}
10791
108- impl DeepLinkAction {
109- pub async fn execute ( self , app : & AppHandle ) -> Result < ( ) , String > {
110- match self {
111- DeepLinkAction :: StartRecording {
112- capture_mode,
113- camera,
114- mic_label,
115- capture_system_audio,
116- mode,
117- } => {
118- let state = app. state :: < ArcLock < App > > ( ) ;
119-
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 ( )
125- . into_iter ( )
126- . find ( |( s, _) | s. name == name)
127- . map ( |( s, _) | ScreenCaptureTarget :: Display { id : s. id } )
128- . ok_or ( format ! ( "No screen with name \" {}\" " , & name) ) ?,
129- CaptureMode :: Window ( name) => cap_recording:: screen_capture:: list_windows ( )
130- . into_iter ( )
131- . find ( |( w, _) | w. name == name)
132- . map ( |( w, _) | ScreenCaptureTarget :: Window { id : w. id } )
133- . ok_or ( format ! ( "No window with name \" {}\" " , & name) ) ?,
134- } ;
135-
136- let inputs = StartRecordingInputs {
137- mode,
138- capture_target,
139- capture_system_audio,
140- organization_id : None ,
141- } ;
142-
143- crate :: recording:: start_recording ( app. clone ( ) , state, inputs)
144- . await
145- . map ( |_| ( ) )
146- }
147- DeepLinkAction :: StopRecording => {
148- crate :: recording:: stop_recording ( app. clone ( ) , app. state ( ) ) . await
149- }
150- DeepLinkAction :: OpenEditor { project_path } => {
151- crate :: open_project_from_path ( Path :: new ( & project_path) , app. clone ( ) )
152- }
153- DeepLinkAction :: OpenSettings { page } => {
154- crate :: show_window ( app. clone ( ) , ShowCapWindow :: Settings { page } ) . await
155- }
156- }
157- }
92+ async fn action_stop_recording ( app : & AppHandle ) {
93+ info ! ( "Deeplink: stop recording" ) ;
94+ let _ = app. emit ( "deeplink-stop-recording" , ( ) ) ;
95+ }
96+
97+ async fn action_pause_recording ( app : & AppHandle ) {
98+ info ! ( "Deeplink: pause recording" ) ;
99+ let _ = app. emit ( "deeplink-pause-recording" , ( ) ) ;
100+ }
101+
102+ async fn action_resume_recording ( app : & AppHandle ) {
103+ info ! ( "Deeplink: resume recording" ) ;
104+ let _ = app. emit ( "deeplink-resume-recording" , ( ) ) ;
158105}
106+
107+ async fn action_switch_microphone ( app : & AppHandle , name : Option < & str > ) {
108+ info ! ( name = ?name, "Deeplink: switch microphone" ) ;
109+ let payload = serde_json:: json!( { "name" : name } ) ;
110+ let _ = app. emit ( "deeplink-switch-microphone" , payload) ;
111+ }
112+
113+ async fn action_switch_camera ( app : & AppHandle , name : Option < & str > ) {
114+ info ! ( name = ?name, "Deeplink: switch camera" ) ;
115+ let payload = serde_json:: json!( { "name" : name } ) ;
116+ let _ = app. emit ( "deeplink-switch-camera" , payload) ;
117+ }
0 commit comments