Skip to content

Conversation

@d3vv3r
Copy link

@d3vv3r d3vv3r commented Jan 30, 2026

Greptile Overview

Greptile Summary

This PR implements Cap deep link support and a Raycast extension for controlling recordings via keyboard shortcuts. The implementation adds a new cap:// URL scheme that enables external applications to control Cap's recording functionality.

Key changes:

  • Added four new deep link actions: StartDefaultRecording, PauseRecording, ResumeRecording, and StopRecording to deeplink_actions.rs
  • Registered the cap:// URL scheme in Tauri configuration alongside the existing cap-desktop:// scheme
  • Created a Raycast extension with four commands that trigger these deep links
  • Deep link parsing now checks for cap:// scheme first before falling back to the existing action-based parsing
  • Pause and resume actions properly emit RecordingEvent::Paused and RecordingEvent::Resumed events to update the UI

Implementation quality:
The Rust implementation follows repository conventions with proper error handling and async patterns. The pause/resume handlers correctly access the current recording state and emit the appropriate events. The StartDefaultRecording action provides a sensible default (first display, Studio mode, system audio enabled) for quick recording starts.

Confidence Score: 5/5

  • This PR is safe to merge with no blocking issues
  • The implementation is clean and follows existing patterns. The deep link handlers properly reuse existing recording methods, emit correct events, and handle errors appropriately. All TypeScript code is simple and follows Raycast conventions.
  • No files require special attention

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Added deep link handlers for pause/resume/start-default recording actions with proper event emission
apps/desktop/src-tauri/tauri.conf.json Registered new cap:// URL scheme alongside existing cap-desktop:// scheme for deep links
extensions/raycast/package.json Created Raycast extension manifest with four commands for recording control
extensions/raycast/src/start-recording.ts Opens cap://record deep link to trigger default recording start
extensions/raycast/src/stop-recording.ts Opens cap://stop deep link to stop active recording
extensions/raycast/src/pause-recording.ts Opens cap://pause deep link to pause active recording
extensions/raycast/src/resume-recording.ts Opens cap://resume deep link to resume paused recording

Sequence Diagram

sequenceDiagram
    participant Raycast as Raycast Extension
    participant OS as Operating System
    participant Tauri as Tauri Deep Link Handler
    participant Parser as DeepLinkAction Parser
    participant App as Cap Desktop App
    participant Recording as Recording Module
    participant Frontend as Desktop Frontend

    Raycast->>OS: open("cap://record")
    OS->>Tauri: Route cap:// URL
    Tauri->>Parser: Parse URL to DeepLinkAction
    Parser->>Parser: Match domain (record/stop/pause/resume)
    Parser-->>Tauri: Return DeepLinkAction variant
    Tauri->>App: execute(StartDefaultRecording)
    App->>Recording: list_displays()
    Recording-->>App: Display list
    App->>App: Select first display
    App->>Recording: start_recording(inputs)
    Recording-->>App: Recording started
    
    Note over Raycast,Frontend: Pause Flow
    Raycast->>OS: open("cap://pause")
    OS->>Tauri: Route cap:// URL
    Tauri->>Parser: Parse URL
    Parser-->>Tauri: DeepLinkAction::PauseRecording
    Tauri->>App: execute(PauseRecording)
    App->>Recording: recording.pause()
    Recording-->>App: Ok()
    App->>Frontend: RecordingEvent::Paused.emit()
    
    Note over Raycast,Frontend: Resume Flow
    Raycast->>OS: open("cap://resume")
    OS->>Tauri: Route cap:// URL
    Tauri->>Parser: Parse URL
    Parser-->>Tauri: DeepLinkAction::ResumeRecording
    Tauri->>App: execute(ResumeRecording)
    App->>Recording: recording.resume()
    Recording-->>App: Ok()
    App->>Frontend: RecordingEvent::Resumed.emit()
    
    Note over Raycast,Frontend: Stop Flow
    Raycast->>OS: open("cap://stop")
    OS->>Tauri: Route cap:// URL
    Tauri->>Parser: Parse URL
    Parser-->>Tauri: DeepLinkAction::StopRecording
    Tauri->>App: execute(StopRecording)
    App->>Recording: stop_recording()
    Recording-->>App: Recording stopped
Loading

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

Bounty Claim

/claim #1540

Walkthrough

I have implemented the deep link support and Raycast extension as requested.

}

if url.scheme() == "cap" {
return match url.domain() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Url::domain() is meant for hostnames; for a custom scheme like cap://record host_str() is a better fit and avoids odd edge-cases. Also fixes the current indentation.

Suggested change
return match url.domain() {
if url.scheme() == "cap" {
return match url.host_str() {
Some("record") => Ok(Self::StartDefaultRecording),
Some("stop") => Ok(Self::StopRecording),
Some("pause") => Ok(Self::PauseRecording),
Some("resume") => Ok(Self::ResumeRecording),
_ => Err(ActionParseFromUrlError::Invalid),
};
}

let displays = cap_recording::screen_capture::list_displays();

if let Some((display, _)) = displays.into_iter().next() {
let capture_target = ScreenCaptureTarget::Display { id: display.id };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting to the first display + capture_system_audio: true + RecordingMode::Studio feels a bit opinionated for a deep link. If this is meant for Raycast “quick record”, might be worth pulling defaults from settings (or prompting) to avoid surprising behavior on multi-monitor setups.

if let Some(recording) = state_read.current_recording() {
recording.pause().await.map_err(|e| e.to_string())?;
crate::recording::RecordingEvent::Paused.emit(app).ok();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emit(app).ok() will trip unused_must_use = deny (since .ok() returns a #[must_use] Option). Also note this awaits while holding state.read(); if that lock is contended, consider exposing a cloneable handle to avoid awaiting under the lock.

Suggested change
}
recording.pause().await.map_err(|e| e.to_string())?;
let _ = crate::recording::RecordingEvent::Paused.emit(app);

if let Some(recording) = state_read.current_recording() {
recording.resume().await.map_err(|e| e.to_string())?;
crate::recording::RecordingEvent::Resumed.emit(app).ok();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same emit(app).ok() issue here (unused #[must_use]).

Suggested change
}
recording.resume().await.map_err(|e| e.to_string())?;
let _ = crate::recording::RecordingEvent::Resumed.emit(app);

await showHUD("Starting Cap recording...");
} catch (error) {
await showHUD("Failed to open Cap");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swallowing the actual open() failure makes debugging harder; showing the error string (or at least “is Cap installed?”) would help. Also these 4 command files are identical aside from the URL/message—might be worth a shared helper.

Suggested change
}
} catch (error) {
await showHUD(`Failed to open Cap: ${String(error)}`);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant