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 ) ,
16- }
17-
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- } ,
1+ use tauri:: AppHandle ;
2+ use tauri_plugin_deep_link:: DeepLinkExt ;
3+
4+ /// Supported deeplink actions for Cap desktop
5+ /// Usage: cap-desktop://action[?param=value]
6+ ///
7+ /// Existing actions (handled elsewhere):
8+ /// cap-desktop://start-recording
9+ /// cap-desktop://stop-recording
10+ ///
11+ /// New actions added here:
12+ /// cap-desktop://pause-recording
13+ /// cap-desktop://resume-recording
14+ /// cap-desktop://toggle-recording-pause
15+ /// cap-desktop://switch-microphone?id=<device_id>
16+ /// cap-desktop://switch-camera?id=<device_id>
17+
18+ #[ derive( Debug ) ]
19+ pub enum DeeplinkAction {
20+ PauseRecording ,
21+ ResumeRecording ,
22+ ToggleRecordingPause ,
23+ SwitchMicrophone { device_id : Option < String > } ,
24+ SwitchCamera { device_id : Option < String > } ,
25+ Unknown ,
3526}
3627
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}" ) ;
28+ impl DeeplinkAction {
29+ pub fn from_url ( url : & str ) -> Self {
30+ // Parse URLs like cap-desktop://pause-recording or cap-desktop://switch-microphone?id=xyz
31+ let url = url. trim_start_matches ( "cap-desktop://" ) ;
32+
33+ let ( path, query) = match url. find ( '?' ) {
34+ Some ( pos) => ( & url[ ..pos] , Some ( & url[ pos + 1 ..] ) ) ,
35+ None => ( url, None ) ,
36+ } ;
37+
38+ // Strip trailing slash if present
39+ let path = path. trim_end_matches ( '/' ) ;
40+
41+ match path {
42+ "pause-recording" => DeeplinkAction :: PauseRecording ,
43+ "resume-recording" => DeeplinkAction :: ResumeRecording ,
44+ "toggle-recording-pause" => DeeplinkAction :: ToggleRecordingPause ,
45+ "switch-microphone" => {
46+ let device_id = query. and_then ( |q| parse_query_param ( q, "id" ) ) ;
47+ DeeplinkAction :: SwitchMicrophone { device_id }
6848 }
49+ "switch-camera" => {
50+ let device_id = query. and_then ( |q| parse_query_param ( q, "id" ) ) ;
51+ DeeplinkAction :: SwitchCamera { device_id }
52+ }
53+ _ => DeeplinkAction :: Unknown ,
6954 }
70- } ) ;
55+ }
7156}
7257
73- pub enum ActionParseFromUrlError {
74- ParseFailed ( String ) ,
75- Invalid ,
76- NotAction ,
58+ fn parse_query_param ( query : & str , key : & str ) -> Option < String > {
59+ for pair in query. split ( '&' ) {
60+ let mut parts = pair. splitn ( 2 , '=' ) ;
61+ if let ( Some ( k) , Some ( v) ) = ( parts. next ( ) , parts. next ( ) ) {
62+ if k == key {
63+ // URL decode the value (basic percent-decoding)
64+ let decoded = percent_decode ( v) ;
65+ if !decoded. is_empty ( ) {
66+ return Some ( decoded) ;
67+ }
68+ }
69+ }
70+ }
71+ None
7772}
7873
79- impl TryFrom < & Url > for DeepLinkAction {
80- type Error = ActionParseFromUrlError ;
81-
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 ) ;
74+ fn percent_decode ( input : & str ) -> String {
75+ let mut result = String :: with_capacity ( input. len ( ) ) ;
76+ let bytes = input. as_bytes ( ) ;
77+ let mut i = 0 ;
78+ while i < bytes. len ( ) {
79+ if bytes[ i] == b'%' && i + 2 < bytes. len ( ) {
80+ if let Ok ( hex_str) = std:: str:: from_utf8 ( & bytes[ i + 1 ..i + 3 ] ) {
81+ if let Ok ( byte) = u8:: from_str_radix ( hex_str, 16 ) {
82+ result. push ( byte as char ) ;
83+ i += 3 ;
84+ continue ;
85+ }
86+ }
87+ } else if bytes[ i] == b'+' {
88+ result. push ( ' ' ) ;
89+ i += 1 ;
90+ continue ;
8991 }
90-
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)
92+ result. push ( bytes[ i] as char ) ;
93+ i += 1 ;
10594 }
95+ result
10696}
10797
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- } ;
98+ /// Handle incoming deeplink URL and dispatch to the appropriate Tauri command/event.
99+ /// Call this from the deep-link plugin's `on_open_url` handler in lib.rs.
100+ pub fn handle_deeplink_url ( app : & AppHandle , url : & str ) {
101+ use tauri:: Emitter ;
135102
136- let inputs = StartRecordingInputs {
137- mode,
138- capture_target,
139- capture_system_audio,
140- organization_id : None ,
141- } ;
103+ let action = DeeplinkAction :: from_url ( url) ;
104+ tracing:: info!( "Handling deeplink action: {:?} from URL: {}" , action, url) ;
142105
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- }
106+ match action {
107+ DeeplinkAction :: PauseRecording => {
108+ let _ = app. emit ( "deeplink://pause-recording" , ( ) ) ;
109+ }
110+ DeeplinkAction :: ResumeRecording => {
111+ let _ = app. emit ( "deeplink://resume-recording" , ( ) ) ;
112+ }
113+ DeeplinkAction :: ToggleRecordingPause => {
114+ let _ = app. emit ( "deeplink://toggle-recording-pause" , ( ) ) ;
115+ }
116+ DeeplinkAction :: SwitchMicrophone { device_id } => {
117+ let _ = app. emit ( "deeplink://switch-microphone" , device_id) ;
118+ }
119+ DeeplinkAction :: SwitchCamera { device_id } => {
120+ let _ = app. emit ( "deeplink://switch-camera" , device_id) ;
121+ }
122+ DeeplinkAction :: Unknown => {
123+ tracing:: warn!( "Unrecognized deeplink URL: {}" , url) ;
156124 }
157125 }
158- }
126+ }
0 commit comments