Skip to content

feat: Extend deeplinks support + Raycast Extension#1634

Open
769066112-ops wants to merge 2 commits intoCapSoftware:mainfrom
769066112-ops:feat/deeplinks-raycast-extension
Open

feat: Extend deeplinks support + Raycast Extension#1634
769066112-ops wants to merge 2 commits intoCapSoftware:mainfrom
769066112-ops:feat/deeplinks-raycast-extension

Conversation

@769066112-ops
Copy link

@769066112-ops 769066112-ops commented Feb 27, 2026

Summary

Closes #1540

This PR adds extended deeplink support to the Cap desktop app and introduces a Raycast extension that leverages these deeplinks.

Deeplink Additions

The existing deeplink infrastructure (deeplink_actions.rs) already supported StartRecording, StopRecording, OpenEditor, and OpenSettings. This PR extends it with:

  • PauseRecording — Pause the current recording
  • ResumeRecording — Resume a paused recording
  • TogglePause — Toggle pause/resume state
  • TakeScreenshot — Capture a screenshot of a screen or window
  • SetCamera — Switch camera input (or disable)
  • SetMicrophone — Switch microphone input (or disable)

All new actions follow the existing pattern and use the same cap-desktop://action?value=<json> URL format.

Raycast Extension

A new Raycast extension is added at apps/raycast/ with the following commands:

Command Description
Start Instant Recording Start an instant screen recording
Start Studio Recording Start a studio screen recording
Stop Recording Stop the current recording
Toggle Pause Recording Pause or resume the current recording
Take Screenshot Take a screenshot
Open Settings Open Cap settings

Each command uses the cap-desktop:// deeplink scheme to communicate with the running Cap desktop app.

Testing

  1. Deeplinks: Test by opening deeplink URLs directly:
    open "cap-desktop://action?value=%7B%22stop_recording%22%3A%7B%7D%7D"
    open "cap-desktop://action?value=%7B%22toggle_pause%22%3A%7B%7D%7D"
    
  2. Raycast Extension: Install the extension locally via ray develop in apps/raycast/, then use Raycast to trigger each command while Cap is running.

Implementation Notes

  • All new deeplink actions delegate to existing recording functions (pause_recording, resume_recording, toggle_pause_recording, take_screenshot, set_camera_input, set_mic_input), ensuring consistent behavior with the UI controls.
  • The Raycast extension is a standalone package with no dependencies on the Cap monorepo build system.
  • Follows existing code style and patterns throughout.

Greptile Summary

This PR extends Cap's deeplink infrastructure with 6 new actions and adds a Raycast extension for remote control. The implementation follows existing patterns by delegating to established recording functions.

Key Changes:

  • Added PauseRecording, ResumeRecording, TogglePause, TakeScreenshot, SetCamera, and SetMicrophone deeplink actions
  • Created Raycast extension with 6 commands that trigger deeplinks
  • All new deeplink actions properly integrate with existing recording infrastructure

Issues Found:

  • JSDoc comments in utils.ts violate the NO COMMENTS repository rule
  • Code duplication in capture mode conversion logic in deeplink_actions.rs
  • Hardcoded "Main Display" in Raycast commands may fail on systems with different display names

Confidence Score: 4/5

  • This PR is safe to merge with minor style issues that should be addressed
  • The core functionality is solid and follows existing patterns. All new deeplink actions delegate to existing, tested recording functions. The issues found are non-critical: style violations (comments), code duplication that doesn't affect correctness, and hardcoded display names that may reduce portability but won't cause crashes
  • Pay attention to apps/raycast/src/utils.ts (comments removal) and consider refactoring the duplicate capture mode logic in apps/desktop/src-tauri/src/deeplink_actions.rs

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Added 6 new deeplink actions (pause, resume, toggle pause, screenshot, set camera/mic); code duplication in capture mode conversion logic
apps/raycast/src/utils.ts Utility functions for deeplink generation and execution; contains JSDoc comments violating NO COMMENTS rule
apps/raycast/src/start-instant-recording.ts Command to start instant recording; hardcodes "Main Display" which may not work on all systems
apps/raycast/src/start-studio-recording.ts Command to start studio recording; hardcodes "Main Display" which may not work on all systems
apps/raycast/src/take-screenshot.ts Command to take screenshot; hardcodes "Main Display" which may not work on all systems

Sequence Diagram

sequenceDiagram
    participant User
    participant Raycast
    participant Deeplink as cap-desktop://
    participant DeeplinkActions as deeplink_actions.rs
    participant Recording as recording.rs
    participant CapApp as Cap Desktop App

    User->>Raycast: Trigger command
    Raycast->>Raycast: buildDeepLink(action)
    Raycast->>Deeplink: open(cap-desktop://action?value=...)
    Deeplink->>DeeplinkActions: handle(urls)
    DeeplinkActions->>DeeplinkActions: parse URL to DeepLinkAction
    alt StartRecording
        DeeplinkActions->>Recording: start_recording()
    else StopRecording
        DeeplinkActions->>Recording: stop_recording()
    else PauseRecording
        DeeplinkActions->>Recording: pause_recording()
    else ResumeRecording
        DeeplinkActions->>Recording: resume_recording()
    else TogglePause
        DeeplinkActions->>Recording: toggle_pause_recording()
    else TakeScreenshot
        DeeplinkActions->>Recording: take_screenshot()
    else SetCamera/SetMicrophone
        DeeplinkActions->>CapApp: set_camera_input() / set_mic_input()
    end
    Recording-->>CapApp: Update state
    CapApp-->>User: Visual feedback
Loading

Last reviewed commit: 7851676

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

Context used:

  • Context from dashboard - CLAUDE.md (source)

Extends the existing deeplink infrastructure with new actions:
- PauseRecording, ResumeRecording, TogglePause
- TakeScreenshot (with capture mode)
- SetCamera, SetMicrophone

Adds a Raycast extension (apps/raycast) with commands:
- Start Instant Recording
- Start Studio Recording
- Stop Recording
- Toggle Pause Recording
- Take Screenshot
- Open Settings

All commands communicate with Cap via the cap-desktop:// deeplink scheme.

Closes CapSoftware#1540
.into_iter()
.find(|(s, _)| s.name == name)
.map(|(s, _)| ScreenCaptureTarget::Display { id: s.id })
.ok_or(format!("No screen with name \"{}\"", &name))?,
Copy link

Choose a reason for hiding this comment

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

Minor perf thing: ok_or(format!(...)) eagerly builds the error string even when a match is found. ok_or_else keeps it lazy.

Suggested change
.ok_or(format!("No screen with name \"{}\"", &name))?,
.ok_or_else(|| format!("No screen with name \"{}\"", name))?,

.into_iter()
.find(|(w, _)| w.name == name)
.map(|(w, _)| ScreenCaptureTarget::Window { id: w.id })
.ok_or(format!("No window with name \"{}\"", &name))?,
Copy link

Choose a reason for hiding this comment

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

Same here—prefer the lazy ok_or_else so we only format on the error path.

Suggested change
.ok_or(format!("No window with name \"{}\"", &name))?,
.ok_or_else(|| format!("No window with name \"{}\"", name))?,

@@ -0,0 +1,19 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
Copy link

Choose a reason for hiding this comment

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

$schema looks like it’s pointing at the Raycast extension schema, but this is a tsconfig. Tweaking it helps editor validation/autocomplete.

Suggested change
"$schema": "https://www.raycast.com/schemas/extension.json",
"$schema": "https://json.schemastore.org/tsconfig",

Comment on lines 3 to 17
const DEEPLINK_SCHEME = "cap-desktop";

/**
* Build a Cap deeplink URL for the given action.
*
* Format: cap-desktop://action?value=<json-encoded action>
*/
export function buildDeepLink(action: Record<string, unknown>): string {
const json = JSON.stringify(action);
return `${DEEPLINK_SCHEME}://action?value=${encodeURIComponent(json)}`;
}

/**
* Open a Cap deeplink and show appropriate toast feedback.
*/
Copy link

Choose a reason for hiding this comment

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

Repo guidelines disallow code comments; can we drop the JSDoc blocks here?

Suggested change
const DEEPLINK_SCHEME = "cap-desktop";
/**
* Build a Cap deeplink URL for the given action.
*
* Format: cap-desktop://action?value=<json-encoded action>
*/
export function buildDeepLink(action: Record<string, unknown>): string {
const json = JSON.stringify(action);
return `${DEEPLINK_SCHEME}://action?value=${encodeURIComponent(json)}`;
}
/**
* Open a Cap deeplink and show appropriate toast feedback.
*/
const DEEPLINK_SCHEME = "cap-desktop";
export function buildDeepLink(action: Record<string, unknown>): string {
const json = JSON.stringify(action);
return `${DEEPLINK_SCHEME}://action?value=${encodeURIComponent(json)}`;
}

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

12 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 5 to 9
/**
* Build a Cap deeplink URL for the given action.
*
* Format: cap-desktop://action?value=<json-encoded action>
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

violates the NO COMMENTS rule from repository guidelines - remove JSDoc comments

Suggested change
/**
* Build a Cap deeplink URL for the given action.
*
* Format: cap-desktop://action?value=<json-encoded action>
*/
export function buildDeepLink(action: Record<string, unknown>): string {
const json = JSON.stringify(action);
return `${DEEPLINK_SCHEME}://action?value=${encodeURIComponent(json)}`;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/utils.ts
Line: 5-9

Comment:
violates the NO COMMENTS rule from repository guidelines - remove JSDoc comments

```suggestion
export function buildDeepLink(action: Record<string, unknown>): string {
  const json = JSON.stringify(action);
  return `${DEEPLINK_SCHEME}://action?value=${encodeURIComponent(json)}`;
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 15 to 17
/**
* Open a Cap deeplink and show appropriate toast feedback.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

violates the NO COMMENTS rule from repository guidelines - remove JSDoc comments

Suggested change
/**
* Open a Cap deeplink and show appropriate toast feedback.
*/
export async function executeDeepLink(
action: Record<string, unknown>,
successMessage: string,
): Promise<void> {
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/utils.ts
Line: 15-17

Comment:
violates the NO COMMENTS rule from repository guidelines - remove JSDoc comments

```suggestion
export async function executeDeepLink(
  action: Record<string, unknown>,
  successMessage: string,
): Promise<void> {
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 171 to 182
let capture_target: ScreenCaptureTarget = match capture_mode {
CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays()
.into_iter()
.find(|(s, _)| s.name == name)
.map(|(s, _)| ScreenCaptureTarget::Display { id: s.id })
.ok_or(format!("No screen with name \"{}\"", &name))?,
CaptureMode::Window(name) => cap_recording::screen_capture::list_windows()
.into_iter()
.find(|(w, _)| w.name == name)
.map(|(w, _)| ScreenCaptureTarget::Window { id: w.id })
.ok_or(format!("No window with name \"{}\"", &name))?,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

duplicates the CaptureMode to ScreenCaptureTarget conversion logic from lines 134-145 - consider extracting to helper function

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 171-182

Comment:
duplicates the `CaptureMode` to `ScreenCaptureTarget` conversion logic from lines 134-145 - consider extracting to helper function

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

await executeDeepLink(
{
start_recording: {
capture_mode: { screen: "Main Display" },
Copy link
Contributor

Choose a reason for hiding this comment

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

hardcoded "Main Display" may not work on all systems - consider using dynamic display detection or making it configurable

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/start-instant-recording.ts
Line: 7

Comment:
hardcoded `"Main Display"` may not work on all systems - consider using dynamic display detection or making it configurable

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

await executeDeepLink(
{
start_recording: {
capture_mode: { screen: "Main Display" },
Copy link
Contributor

Choose a reason for hiding this comment

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

hardcoded "Main Display" may not work on all systems - consider using dynamic display detection or making it configurable

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/start-studio-recording.ts
Line: 7

Comment:
hardcoded `"Main Display"` may not work on all systems - consider using dynamic display detection or making it configurable

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

await executeDeepLink(
{
take_screenshot: {
capture_mode: { screen: "Main Display" },
Copy link
Contributor

Choose a reason for hiding this comment

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

hardcoded "Main Display" may not work on all systems - consider using dynamic display detection or making it configurable

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/take-screenshot.ts
Line: 7

Comment:
hardcoded `"Main Display"` may not work on all systems - consider using dynamic display detection or making it configurable

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.


export function getDisplayName(): string {
const { displayName } = getPreferenceValues<Preferences>();
return displayName || "Main Display";
Copy link

Choose a reason for hiding this comment

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

package.json says leaving the preference empty should use the main display, but getDisplayName() defaults to the literal "Main Display". If you want empty to mean “default display”, I’d make the preference optional and return a trimmed string (possibly empty) here.

Suggested change
return displayName || "Main Display";
interface Preferences {
displayName?: string;
}
export function getDisplayName(): string {
const { displayName } = getPreferenceValues<Preferences>();
return displayName?.trim() ?? "";
}

.into_iter()
.find(|(s, _)| s.name == name)
.map(|(s, _)| ScreenCaptureTarget::Display { id: s.id })
.ok_or_else(|| format!("No screen with name \"{}\"", &name)),
Copy link

Choose a reason for hiding this comment

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

If clients send an empty screen name (Raycast pref “leave empty to use main display”), we currently error. Might be worth treating empty (and maybe "Main Display" for compatibility) as “pick a default display” so deeplinks work out of the box.

Suggested change
.ok_or_else(|| format!("No screen with name \"{}\"", &name)),
CaptureMode::Screen(name) => {
let match_name = !name.is_empty() && name != "Main Display";
let mut displays = cap_recording::screen_capture::list_displays().into_iter();
let display = if match_name {
displays.find(|(s, _)| s.name == name)
} else {
displays.next()
};
display
.map(|(s, _)| ScreenCaptureTarget::Display { id: s.id })
.ok_or_else(|| {
if match_name {
format!("No screen with name \"{}\"", &name)
} else {
"No displays found".to_string()
}
})
},

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bounty: Deeplinks support + Raycast Extension

1 participant