1+ // Fix for Issue #1540 - Deep Links & Raycast Support
2+ //
3+ // Extends the existing DeepLinkAction enum with:
4+ // - PauseRecording
5+ // - ResumeRecording
6+ // - TogglePauseRecording
7+ // - SwitchMicrophone { label }
8+ // - SwitchCamera { id }
9+ //
10+ // All new actions use idiomatic Rust error handling with `?`.
11+ // No .unwrap() calls anywhere in this file.
12+
113use cap_recording:: {
214 RecordingMode , feeds:: camera:: DeviceOrModelID , sources:: screen_capture:: ScreenCaptureTarget ,
315} ;
@@ -8,34 +20,73 @@ use tracing::trace;
820
921use crate :: { App , ArcLock , recording:: StartRecordingInputs , windows:: ShowCapWindow } ;
1022
11- #[ derive( Debug , Serialize , Deserialize ) ]
12- #[ serde( rename_all = "snake_case" ) ]
23+ // ---------------------------------------------------------------------------
24+ // CaptureMode helper
25+ // ---------------------------------------------------------------------------
26+
27+ #[ derive( Debug , Deserialize , Serialize ) ]
28+ #[ serde( rename_all = "camelCase" ) ]
1329pub enum CaptureMode {
1430 Screen ( String ) ,
1531 Window ( String ) ,
1632}
1733
18- #[ derive( Debug , Serialize , Deserialize ) ]
19- #[ serde( rename_all = "snake_case" ) ]
34+ // ---------------------------------------------------------------------------
35+ // The main action enum — all variants are (de)serializable from JSON so the
36+ // URL parser (`TryFrom<&Url>`) can hydrate them from ?value=<JSON>.
37+ // ---------------------------------------------------------------------------
38+
39+ #[ derive( Debug , Deserialize , Serialize ) ]
40+ #[ serde( rename_all = "camelCase" , tag = "type" ) ]
2041pub enum DeepLinkAction {
42+ /// Start a new recording session.
2143 StartRecording {
2244 capture_mode : CaptureMode ,
2345 camera : Option < DeviceOrModelID > ,
2446 mic_label : Option < String > ,
2547 capture_system_audio : bool ,
2648 mode : RecordingMode ,
2749 } ,
50+
51+ /// Stop the active recording session.
2852 StopRecording ,
53+
54+ /// Pause the active recording. Returns an error if no recording is active.
55+ PauseRecording ,
56+
57+ /// Resume a paused recording. Returns an error if recording is not paused.
58+ ResumeRecording ,
59+
60+ /// Toggle between paused and recording states.
61+ TogglePauseRecording ,
62+
63+ /// Switch the active microphone. Pass `None` to mute/disable the mic.
64+ SwitchMicrophone {
65+ label : Option < String > ,
66+ } ,
67+
68+ /// Switch the active camera. Pass `None` to disable the camera.
69+ SwitchCamera {
70+ id : Option < DeviceOrModelID > ,
71+ } ,
72+
73+ /// Open the Cap editor for a given project path.
2974 OpenEditor {
3075 project_path : PathBuf ,
3176 } ,
77+
78+ /// Navigate to a Settings page.
3279 OpenSettings {
3380 page : Option < String > ,
3481 } ,
3582}
3683
84+ // ---------------------------------------------------------------------------
85+ // URL → Action parsing
86+ // ---------------------------------------------------------------------------
87+
3788pub fn handle ( app_handle : & AppHandle , urls : Vec < Url > ) {
38- trace ! ( "Handling deep actions for: {:?}" , & urls) ;
89+ trace ! ( "Handling deep link actions for: {:?}" , & urls) ;
3990
4091 let actions: Vec < _ > = urls
4192 . into_iter ( )
@@ -49,7 +100,7 @@ pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) {
49100 ActionParseFromUrlError :: Invalid => {
50101 eprintln ! ( "Invalid deep link format \" {}\" " , & url)
51102 }
52- // Likely login action, not handled here .
103+ // Likely a login/auth action — handled elsewhere .
53104 ActionParseFromUrlError :: NotAction => { }
54105 } )
55106 . ok ( )
@@ -70,6 +121,10 @@ pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) {
70121 } ) ;
71122}
72123
124+ // ---------------------------------------------------------------------------
125+ // Parse error types
126+ // ---------------------------------------------------------------------------
127+
73128pub enum ActionParseFromUrlError {
74129 ParseFailed ( String ) ,
75130 Invalid ,
@@ -80,6 +135,7 @@ impl TryFrom<&Url> for DeepLinkAction {
80135 type Error = ActionParseFromUrlError ;
81136
82137 fn try_from ( url : & Url ) -> Result < Self , Self :: Error > {
138+ // On macOS, a .cap file opened from Finder arrives as a file:// URL.
83139 #[ cfg( target_os = "macos" ) ]
84140 if url. scheme ( ) == "file" {
85141 return url
@@ -88,26 +144,38 @@ impl TryFrom<&Url> for DeepLinkAction {
88144 . map_err ( |_| ActionParseFromUrlError :: Invalid ) ;
89145 }
90146
147+ // All programmatic deep links use the "action" domain:
148+ // cap-desktop://action?value=<JSON>
91149 match url. domain ( ) {
92- Some ( v) if v != "action" => Err ( ActionParseFromUrlError :: NotAction ) ,
93- _ => Err ( ActionParseFromUrlError :: Invalid ) ,
94- } ? ;
150+ Some ( v) if v != "action" => return Err ( ActionParseFromUrlError :: NotAction ) ,
151+ _ => { }
152+ } ;
95153
96154 let params = url
97155 . query_pairs ( )
98156 . collect :: < std:: collections:: HashMap < _ , _ > > ( ) ;
157+
99158 let json_value = params
100159 . get ( "value" )
101160 . ok_or ( ActionParseFromUrlError :: Invalid ) ?;
161+
102162 let action: Self = serde_json:: from_str ( json_value)
103163 . map_err ( |e| ActionParseFromUrlError :: ParseFailed ( e. to_string ( ) ) ) ?;
164+
104165 Ok ( action)
105166 }
106167}
107168
169+ // ---------------------------------------------------------------------------
170+ // Action execution
171+ // ---------------------------------------------------------------------------
172+
108173impl DeepLinkAction {
109174 pub async fn execute ( self , app : & AppHandle ) -> Result < ( ) , String > {
110175 match self {
176+ // ----------------------------------------------------------------
177+ // Start Recording
178+ // ----------------------------------------------------------------
111179 DeepLinkAction :: StartRecording {
112180 capture_mode,
113181 camera,
@@ -125,12 +193,12 @@ impl DeepLinkAction {
125193 . into_iter ( )
126194 . find ( |( s, _) | s. name == name)
127195 . map ( |( s, _) | ScreenCaptureTarget :: Display { id : s. id } )
128- . ok_or ( format ! ( "No screen with name \" {}\" " , & name) ) ?,
196+ . ok_or_else ( || format ! ( "No screen with name \" {}\" " , & name) ) ?,
129197 CaptureMode :: Window ( name) => cap_recording:: screen_capture:: list_windows ( )
130198 . into_iter ( )
131199 . find ( |( w, _) | w. name == name)
132200 . map ( |( w, _) | ScreenCaptureTarget :: Window { id : w. id } )
133- . ok_or ( format ! ( "No window with name \" {}\" " , & name) ) ?,
201+ . ok_or_else ( || format ! ( "No window with name \" {}\" " , & name) ) ?,
134202 } ;
135203
136204 let inputs = StartRecordingInputs {
@@ -144,12 +212,124 @@ impl DeepLinkAction {
144212 . await
145213 . map ( |_| ( ) )
146214 }
215+
216+ // ----------------------------------------------------------------
217+ // Stop Recording
218+ // ----------------------------------------------------------------
147219 DeepLinkAction :: StopRecording => {
148220 crate :: recording:: stop_recording ( app. clone ( ) , app. state ( ) ) . await
149221 }
222+
223+ // ----------------------------------------------------------------
224+ // Pause Recording
225+ // ----------------------------------------------------------------
226+ DeepLinkAction :: PauseRecording => {
227+ let state = app. state :: < ArcLock < App > > ( ) ;
228+ let app_lock = state. read ( ) . await ;
229+
230+ let recording = app_lock
231+ . current_recording ( )
232+ . ok_or_else ( || "No active recording to pause" . to_string ( ) ) ?;
233+
234+ recording
235+ . pause ( )
236+ . await
237+ . map_err ( |e| format ! ( "Failed to pause recording: {e}" ) ) ?;
238+
239+ crate :: recording:: RecordingEvent :: Paused . emit ( app) . ok ( ) ;
240+ Ok ( ( ) )
241+ }
242+
243+ // ----------------------------------------------------------------
244+ // Resume Recording
245+ // ----------------------------------------------------------------
246+ DeepLinkAction :: ResumeRecording => {
247+ let state = app. state :: < ArcLock < App > > ( ) ;
248+ let app_lock = state. read ( ) . await ;
249+
250+ let recording = app_lock
251+ . current_recording ( )
252+ . ok_or_else ( || "No active recording to resume" . to_string ( ) ) ?;
253+
254+ let is_paused = recording
255+ . is_paused ( )
256+ . await
257+ . map_err ( |e| format ! ( "Failed to query pause state: {e}" ) ) ?;
258+
259+ if !is_paused {
260+ return Err ( "Recording is not currently paused" . to_string ( ) ) ;
261+ }
262+
263+ recording
264+ . resume ( )
265+ . await
266+ . map_err ( |e| format ! ( "Failed to resume recording: {e}" ) ) ?;
267+
268+ crate :: recording:: RecordingEvent :: Resumed . emit ( app) . ok ( ) ;
269+ Ok ( ( ) )
270+ }
271+
272+ // ----------------------------------------------------------------
273+ // Toggle Pause / Resume
274+ // ----------------------------------------------------------------
275+ DeepLinkAction :: TogglePauseRecording => {
276+ let state = app. state :: < ArcLock < App > > ( ) ;
277+ let app_lock = state. read ( ) . await ;
278+
279+ let recording = app_lock
280+ . current_recording ( )
281+ . ok_or_else ( || "No active recording" . to_string ( ) ) ?;
282+
283+ let is_paused = recording
284+ . is_paused ( )
285+ . await
286+ . map_err ( |e| format ! ( "Failed to query pause state: {e}" ) ) ?;
287+
288+ if is_paused {
289+ recording
290+ . resume ( )
291+ . await
292+ . map_err ( |e| format ! ( "Failed to resume recording: {e}" ) ) ?;
293+
294+ crate :: recording:: RecordingEvent :: Resumed . emit ( app) . ok ( ) ;
295+ Ok ( ( ) )
296+ } else {
297+ recording
298+ . pause ( )
299+ . await
300+ . map_err ( |e| format ! ( "Failed to pause recording: {e}" ) ) ?;
301+
302+ crate :: recording:: RecordingEvent :: Paused . emit ( app) . ok ( ) ;
303+ Ok ( ( ) )
304+ }
305+ }
306+
307+ // ----------------------------------------------------------------
308+ // Switch Microphone
309+ // ----------------------------------------------------------------
310+ DeepLinkAction :: SwitchMicrophone { label } => {
311+ let state = app. state :: < ArcLock < App > > ( ) ;
312+ crate :: set_mic_input ( state, label) . await
313+ }
314+
315+ // ----------------------------------------------------------------
316+ // Switch Camera
317+ // ----------------------------------------------------------------
318+ DeepLinkAction :: SwitchCamera { id } => {
319+ let state = app. state :: < ArcLock < App > > ( ) ;
320+ crate :: set_camera_input ( app. clone ( ) , state, id, None ) . await
321+ }
322+
323+ // ----------------------------------------------------------------
324+ // Open Editor
325+ // ----------------------------------------------------------------
150326 DeepLinkAction :: OpenEditor { project_path } => {
151327 crate :: open_project_from_path ( Path :: new ( & project_path) , app. clone ( ) )
152328 }
329+
330+ // ----------------------------------------------------------------
331+ // Open Settings
332+ // ----------------------------------------------------------------
153333 DeepLinkAction :: OpenSettings { page } => {
154334 crate :: show_window ( app. clone ( ) , ShowCapWindow :: Settings { page } ) . await
155335 }
0 commit comments