feat: Deeplinks support (Pause/Resume/SwitchMic/SwitchCamera) + Raycast Extension#1757
feat: Deeplinks support (Pause/Resume/SwitchMic/SwitchCamera) + Raycast Extension#1757Ojas2095 wants to merge 3 commits intoCapSoftware:mainfrom
Conversation
…ull Raycast extension - Extended DeepLinkAction enum with PauseRecording, ResumeRecording, SwitchMicrophone, SwitchCamera, GetStatus - All new actions reuse existing crate::recording helpers, zero new deps - Added complete Raycast extension under raycast-extension/ with 6 commands - Closes CapSoftware#1540
| pub enum CaptureMode { | ||
| Screen(String), | ||
| Window(String), | ||
| } |
There was a problem hiding this comment.
Missing
Deserialize derive — will not compile
The PR removes #[derive(Debug, Serialize, Deserialize)] from both CaptureMode and DeepLinkAction. The TryFrom<&Url> implementation at line 107 calls serde_json::from_str::<DeepLinkAction>(json_value), which requires DeepLinkAction: Deserialize<'_>. Without the derive, the crate will fail to compile with "the trait Deserialize<'_> is not implemented for DeepLinkAction". The original derive also covered CaptureMode, which is a nested variant field and must be deserializable as well.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 11-14
Comment:
**Missing `Deserialize` derive — will not compile**
The PR removes `#[derive(Debug, Serialize, Deserialize)]` from both `CaptureMode` and `DeepLinkAction`. The `TryFrom<&Url>` implementation at line 107 calls `serde_json::from_str::<DeepLinkAction>(json_value)`, which requires `DeepLinkAction: Deserialize<'_>`. Without the derive, the crate will fail to compile with *"the trait `Deserialize<'_>` is not implemented for `DeepLinkAction`"*. The original derive also covered `CaptureMode`, which is a nested variant field and must be deserializable as well.
How can I resolve this? If you propose a fix, please make it concise.| } | ||
| DeepLinkAction::GetStatus => { | ||
| let state = app.state::<ArcLock<App>>(); | ||
| let locked = state.lock().await; |
There was a problem hiding this comment.
RwLock has no .lock() method — compile error
state is State<'_, Arc<RwLock<App>>> (where RwLock is tokio::sync::RwLock). Tokio's RwLock exposes .read().await and .write().await, but not .lock(). Calling state.lock().await will fail to compile. Since GetStatus only needs to read current_recording, use .read().await instead.
| let locked = state.lock().await; | |
| let locked = state.read().await; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 175
Comment:
**`RwLock` has no `.lock()` method — compile error**
`state` is `State<'_, Arc<RwLock<App>>>` (where `RwLock` is `tokio::sync::RwLock`). Tokio's `RwLock` exposes `.read().await` and `.write().await`, but not `.lock()`. Calling `state.lock().await` will fail to compile. Since `GetStatus` only needs to read `current_recording`, use `.read().await` instead.
```suggestion
let locked = state.read().await;
```
How can I resolve this? If you propose a fix, please make it concise.| export default async function Command() { | ||
| await triggerDeeplink({ GetStatus: {} }); | ||
| await showHUD("📡 Status request sent to Cap"); |
There was a problem hiding this comment.
GetStatus result is never visible in Raycast
The backend emits the status as a Tauri event (app.emit("cap://status", …)), which is dispatched to the Tauri webview frontend — not to Raycast. Raycast has no listener for Tauri events, so the get-status.tsx command can never actually display whether Cap is recording. The HUD message "📡 Status request sent to Cap" confirms the event was fired, but the user sees no actual status. A functional implementation would require a different mechanism (e.g., returning state via the deeplink URL or writing it to a shared file/HTTP endpoint that Raycast can read).
Prompt To Fix With AI
This is a comment left during a code review.
Path: raycast-extension/src/get-status.tsx
Line: 4-6
Comment:
**`GetStatus` result is never visible in Raycast**
The backend emits the status as a Tauri event (`app.emit("cap://status", …)`), which is dispatched to the Tauri webview frontend — not to Raycast. Raycast has no listener for Tauri events, so the `get-status.tsx` command can never actually display whether Cap is recording. The HUD message *"📡 Status request sent to Cap"* confirms the event was fired, but the user sees no actual status. A functional implementation would require a different mechanism (e.g., returning state via the deeplink URL or writing it to a shared file/HTTP endpoint that Raycast can read).
How can I resolve this? If you propose a fix, please make it concise.| const isPaused = (await LocalStorage.getItem<string>(PAUSED_KEY)) === "true"; | ||
|
|
||
| if (isPaused) { | ||
| await triggerDeeplink({ ResumeRecording: {} }); | ||
| await LocalStorage.setItem(PAUSED_KEY, "false"); | ||
| await showHUD("▶ Cap recording resumed"); | ||
| } else { | ||
| await triggerDeeplink({ PauseRecording: {} }); | ||
| await LocalStorage.setItem(PAUSED_KEY, "true"); | ||
| await showHUD("⏸ Cap recording paused"); | ||
| } |
There was a problem hiding this comment.
LocalStorage toggle can permanently desync from real recording state
The pause/resume toggle is driven entirely by a LocalStorage flag, with no way to resynchronize against the actual Cap recording state. If the recording is stopped (or crashes) while cap_is_paused is "true", the next invocation will send ResumeRecording instead of PauseRecording, and subsequent invocations will stay permanently inverted. Since Cap doesn't push state back to Raycast, this mismatch is silent and has no recovery path other than manually toggling twice.
Prompt To Fix With AI
This is a comment left during a code review.
Path: raycast-extension/src/pause-resume-recording.tsx
Line: 8-18
Comment:
**LocalStorage toggle can permanently desync from real recording state**
The pause/resume toggle is driven entirely by a `LocalStorage` flag, with no way to resynchronize against the actual Cap recording state. If the recording is stopped (or crashes) while `cap_is_paused` is `"true"`, the next invocation will send `ResumeRecording` instead of `PauseRecording`, and subsequent invocations will stay permanently inverted. Since Cap doesn't push state back to Raycast, this mismatch is silent and has no recovery path other than manually toggling twice.
How can I resolve this? If you propose a fix, please make it concise.| // Common microphone labels — users can extend this list | ||
| const MICROPHONES = [ | ||
| "MacBook Pro Microphone", | ||
| "Built-in Microphone", | ||
| "AirPods Pro", | ||
| "AirPods Max", | ||
| "USB Audio Device", | ||
| "Rode NT-USB", | ||
| "Blue Yeti", |
There was a problem hiding this comment.
Hardcoded device list won't match users' actual hardware
MICROPHONES (and CAMERAS in switch-camera.tsx) is a fixed static array of guessed device labels. If a user's microphone label doesn't exactly match one of the hardcoded strings — including capitalisation and punctuation — the SwitchMicrophone deeplink will silently fail to match any device in Cap. Consider fetching the real list of available devices from Cap (e.g., via a dedicated deeplink query or a local HTTP endpoint) so the extension always reflects the current system state.
Prompt To Fix With AI
This is a comment left during a code review.
Path: raycast-extension/src/switch-microphone.tsx
Line: 4-12
Comment:
**Hardcoded device list won't match users' actual hardware**
`MICROPHONES` (and `CAMERAS` in `switch-camera.tsx`) is a fixed static array of guessed device labels. If a user's microphone label doesn't exactly match one of the hardcoded strings — including capitalisation and punctuation — the `SwitchMicrophone` deeplink will silently fail to match any device in Cap. Consider fetching the real list of available devices from Cap (e.g., via a dedicated deeplink query or a local HTTP endpoint) so the extension always reflects the current system state.
How can I resolve this? If you propose a fix, please make it concise.
Summary
Closes #1540
This PR implements the full bounty scope:
1. Extended Deeplink Actions (
apps/desktop/src-tauri/src/deeplink_actions.rs)Added 4 new
DeepLinkActionvariants to the existing Tauri deeplink handler:PauseRecordingcap-desktop://action?value={"PauseRecording":{}}ResumeRecordingcap-desktop://action?value={"ResumeRecording":{}}SwitchMicrophone { label }cap-desktop://action?value={"SwitchMicrophone":{"label":"..."}}SwitchCamera { label }cap-desktop://action?value={"SwitchCamera":{"label":"..."}}GetStatuscap-desktop://action?value={"GetStatus":{}}— emitscap://statuseventAll new actions reuse existing
crate::recordingandcrate::set_mic_input/crate::set_camera_inputhelpers. No new dependencies introduced.2. Raycast Extension (
raycast-extension/)A complete, publish-ready Raycast extension with 6 commands:
no-viewcommandno-viewcommandno-viewcommand withLocalStoragetoggleListviewListviewno-viewcommandAll commands use the shared
src/lib/deeplink.tsutility to keep them DRY.Testing
cap-desktop://URL scheme/claim #1540
Greptile Summary
This PR adds
PauseRecording,ResumeRecording,SwitchMicrophone,SwitchCamera, andGetStatusdeeplink actions to the Tauri backend, and ships a full Raycast extension with 6 commands that drive them via thecap-desktop://URL scheme.There are two P0 build-breaking issues in
deeplink_actions.rsthat must be fixed before this can land:#[derive(Serialize, Deserialize)]attributes were accidentally removed fromCaptureModeandDeepLinkAction, makingserde_json::from_struncompilable.GetStatushandler callsstate.lock().awaiton atokio::sync::RwLock, which has no.lock()method; it should be.read().await.Confidence Score: 2/5
Not safe to merge — two P0 compile errors in the Rust backend will break the desktop app build.
Two separate issues in deeplink_actions.rs prevent the crate from compiling at all: the removed Deserialize derive and the invalid .lock() call on RwLock. Until those are fixed the entire desktop app build is broken regardless of the Raycast extension quality.
apps/desktop/src-tauri/src/deeplink_actions.rs requires immediate fixes before merging.
Important Files Changed
.lock()on RwLock (compile failure)Sequence Diagram
sequenceDiagram participant R as Raycast Command participant OS as macOS URL Scheme participant T as Tauri deeplink_actions.rs participant App as Cap App State R->>OS: open(cap-desktop://action?value=...) OS->>T: handle(urls) T->>T: DeepLinkAction::try_from(url) [serde_json::from_str] T->>App: execute(action) alt PauseRecording App->>App: recording::pause_recording() else ResumeRecording App->>App: recording::resume_recording() else SwitchMicrophone App->>App: set_mic_input(label) else SwitchCamera App->>App: set_camera_input(label) else GetStatus App->>App: state.read() — current_recording.is_some() App-->>T: app.emit(cap://status, {status}) Note over T,R: Event goes to Tauri webview only — Raycast never receives it endPrompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat: Add Pause/Resume/SwitchMic/SwitchC..." | Re-trigger Greptile
Demo Video
https://github.com/Ojas2095/Cap/raw/feat/deeplinks-raycast-extension/pr_demo.mp4