Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,91 @@ pub enum CaptureMode {
Window(String),
}

/// Response types for deeplink queries
Copy link

Choose a reason for hiding this comment

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

Repo style seems to avoid code comments; these new ////// comments in the deeplink handler might be worth removing for consistency.

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct RecordingStatusResponse {
pub is_recording: bool,
pub is_paused: bool,
pub mode: Option<RecordingMode>,
}
Comment on lines +18 to +25
Copy link
Contributor

Choose a reason for hiding this comment

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

[P1] RecordingStatusResponse/DisplayInfo/etc are never used and (more importantly) the deep link “query” actions (List*, GetRecordingStatus) don’t return anything to the caller. Right now they only trace! and Ok(()), which makes them effectively no-ops from the deeplink consumer’s perspective (Raycast can’t read stdout/stderr/tracing). This also means these new structs add dead code unless a return mechanism is introduced.

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

Comment:
[P1] `RecordingStatusResponse`/`DisplayInfo`/etc are never used and (more importantly) the deep link “query” actions (`List*`, `GetRecordingStatus`) don’t return anything to the caller. Right now they only `trace!` and `Ok(())`, which makes them effectively no-ops from the deeplink consumer’s perspective (Raycast can’t read stdout/stderr/tracing). This also means these new structs add dead code unless a return mechanism is introduced.

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


#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct DisplayInfo {
pub name: String,
pub id: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct WindowInfo {
pub name: String,
pub owner_name: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct AudioDeviceInfo {
pub label: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct CameraInfo {
pub name: String,
pub id: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeepLinkAction {
/// Start a new recording
StartRecording {
capture_mode: CaptureMode,
camera: Option<DeviceOrModelID>,
mic_label: Option<String>,
capture_system_audio: bool,
mode: RecordingMode,
},
/// Stop the current recording
StopRecording,
/// Pause the current recording
PauseRecording,
/// Resume a paused recording
ResumeRecording,
/// Toggle pause state of the current recording
TogglePause,
/// Switch the microphone input
SetMicrophone {
label: Option<String>,
},
/// Switch the camera input
SetCamera {
device_id: Option<String>,
},
/// Take a screenshot
TakeScreenshot,
/// Open a project in the editor
OpenEditor {
project_path: PathBuf,
},
/// Open the settings window
OpenSettings {
page: Option<String>,
},
/// Show the main Cap window
ShowMainWindow,
/// List available displays
ListDisplays,
/// List available windows
ListWindows,
/// List available microphones
ListMicrophones,
/// List available cameras
ListCameras,
/// Get current recording status
GetRecordingStatus,
}

pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) {
Expand Down Expand Up @@ -146,12 +214,115 @@ impl DeepLinkAction {
DeepLinkAction::StopRecording => {
crate::recording::stop_recording(app.clone(), app.state()).await
}
DeepLinkAction::PauseRecording => {
crate::recording::pause_recording(app.clone(), app.state()).await
}
DeepLinkAction::ResumeRecording => {
crate::recording::resume_recording(app.clone(), app.state()).await
}
DeepLinkAction::TogglePause => {
crate::recording::toggle_pause_recording(app.clone(), app.state()).await
}
DeepLinkAction::SetMicrophone { label } => {
let state = app.state::<ArcLock<App>>();
crate::set_mic_input(state, label).await
}
DeepLinkAction::SetCamera { device_id } => {
let state = app.state::<ArcLock<App>>();
let camera_id = device_id.map(|id| DeviceOrModelID::DeviceID(id));
crate::set_camera_input(app.clone(), state, camera_id, None).await
}
DeepLinkAction::TakeScreenshot => {
// Take a screenshot of the primary display
let displays = cap_recording::screen_capture::list_displays();
if let Some((display, _)) = displays.into_iter().next() {
let target = ScreenCaptureTarget::Display { id: display.id };
crate::recording::take_screenshot(app.clone(), target)
.await
.map(|_| ())
} else {
Err("No display found for screenshot".to_string())
}
Comment on lines +235 to +245
Copy link
Contributor

Choose a reason for hiding this comment

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

[P2] TakeScreenshot picks list_displays().into_iter().next() as “primary display”. list_displays() ordering isn’t guaranteed, so this can capture a non-primary monitor depending on platform/ordering. If “primary” matters, it should be selected explicitly (or allow a display id/name parameter).

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

Comment:
[P2] `TakeScreenshot` picks `list_displays().into_iter().next()` as “primary display”. `list_displays()` ordering isn’t guaranteed, so this can capture a non-primary monitor depending on platform/ordering. If “primary” matters, it should be selected explicitly (or allow a display id/name parameter).

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

}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
DeepLinkAction::OpenSettings { page } => {
crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await
}
DeepLinkAction::ShowMainWindow => {
crate::show_window(app.clone(), ShowCapWindow::Main { init_target_mode: None }).await
}
DeepLinkAction::ListDisplays => {
let displays: Vec<DisplayInfo> = cap_recording::screen_capture::list_displays()
.into_iter()
.map(|(d, _)| DisplayInfo {
name: d.name.clone(),
id: format!("{:?}", d.id),
})
.collect();
// Log for debugging; in practice this could be returned via a different mechanism
trace!("Available displays: {:?}", displays);
Ok(())
Comment on lines +256 to +266
Copy link
Contributor

Choose a reason for hiding this comment

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

[P2] ListDisplays formats d.id with format!("{:?}", d.id) instead of a stable identifier. If DisplayId’s Debug impl changes across versions/platforms, the value becomes non-portable for consumers (e.g., a Raycast command that wants to persist an ID). Consider exposing the underlying id type/string in a stable way.

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

Comment:
[P2] `ListDisplays` formats `d.id` with `format!("{:?}", d.id)` instead of a stable identifier. If `DisplayId`’s `Debug` impl changes across versions/platforms, the value becomes non-portable for consumers (e.g., a Raycast command that wants to persist an ID). Consider exposing the underlying id type/string in a stable way.

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

}
DeepLinkAction::ListWindows => {
let windows: Vec<WindowInfo> = cap_recording::screen_capture::list_windows()
.into_iter()
.map(|(w, _)| WindowInfo {
name: w.name.clone(),
owner_name: w.owner_name.clone(),
})
.collect();
trace!("Available windows: {:?}", windows);
Ok(())
}
DeepLinkAction::ListMicrophones => {
use cap_recording::feeds::microphone::MicrophoneFeed;
let mics: Vec<AudioDeviceInfo> = MicrophoneFeed::list()
.keys()
.map(|label| AudioDeviceInfo {
label: label.clone(),
})
.collect();
trace!("Available microphones: {:?}", mics);
Ok(())
}
DeepLinkAction::ListCameras => {
let cameras: Vec<CameraInfo> = cap_camera::list_cameras()
.map(|c| CameraInfo {
name: c.display_name().to_string(),
id: c.device_id().to_string(),
})
.collect();
trace!("Available cameras: {:?}", cameras);
Ok(())
}
DeepLinkAction::GetRecordingStatus => {
let state = app.state::<ArcLock<App>>();
let app_state = state.read().await;
let status = match &app_state.recording_state {
crate::RecordingState::None => RecordingStatusResponse {
is_recording: false,
is_paused: false,
mode: None,
},
crate::RecordingState::Pending { mode, .. } => RecordingStatusResponse {
is_recording: false,
is_paused: false,
mode: Some(*mode),
},
crate::RecordingState::Active(recording) => {
let is_paused = recording.is_paused().await.unwrap_or(false);
RecordingStatusResponse {
is_recording: true,
is_paused,
mode: Some(recording.mode()),
}
}
};
trace!("Recording status: {:?}", status);
Ok(())
}
}
}
}
61 changes: 61 additions & 0 deletions extensions/raycast-cap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Cap Raycast Extension

Control [Cap](https://cap.so) screen recording directly from Raycast.

## Features

- **Start Recording** - Start a new screen recording
- **Stop Recording** - Stop the current recording
- **Pause Recording** - Pause the current recording
- **Resume Recording** - Resume a paused recording
- **Toggle Pause** - Toggle pause state of the current recording
- **Take Screenshot** - Capture the current screen
- **Open Cap** - Open the Cap application
- **Open Settings** - Open Cap settings
- **Recording Controls** - Quick access to all Cap commands in a list view

## Requirements

- [Cap](https://cap.so) must be installed on your Mac
- macOS 11.0 or later

## How It Works

This extension uses Cap's deeplink protocol (`cap-desktop://`) to communicate with the Cap application. When you trigger a command, it opens a deeplink URL that Cap handles to perform the requested action.

### Deeplink Format

Cap deeplinks use the following format:
```
cap-desktop://action?value=<JSON_ENCODED_ACTION>
```

Available actions:
- `start_recording` - Start recording with capture mode, camera, mic settings
- `stop_recording` - Stop the current recording
- `pause_recording` - Pause the current recording
- `resume_recording` - Resume a paused recording
- `toggle_pause` - Toggle pause state
- `take_screenshot` - Take a screenshot
- `open_settings` - Open settings with optional page parameter
- `show_main_window` - Show the main Cap window

## Development

```bash
# Install dependencies
npm install

# Start development
npm run dev

# Build the extension
npm run build

# Publish to Raycast Store
npm run publish
```

## License

AGPL-3.0 - See [LICENSE](../../LICENSE) for details.
Binary file added extensions/raycast-cap/assets/cap-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
99 changes: 99 additions & 0 deletions extensions/raycast-cap/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "cap",
"title": "Cap",
"description": "Control Cap screen recording from Raycast",
"icon": "cap-icon.png",
"author": "Cap Software",
"authorUrl": "https://cap.so",
"categories": ["Productivity", "Media"],
"license": "AGPL-3.0",
"repository": {
"type": "git",
"url": "https://github.com/CapSoftware/Cap"
},
"commands": [
{
"name": "start-recording",
"title": "Start Recording",
"subtitle": "Cap",
"description": "Start a new screen recording with Cap",
"mode": "no-view"
},
{
"name": "stop-recording",
"title": "Stop Recording",
"subtitle": "Cap",
"description": "Stop the current recording",
"mode": "no-view"
},
{
"name": "pause-recording",
"title": "Pause Recording",
"subtitle": "Cap",
"description": "Pause the current recording",
"mode": "no-view"
},
{
"name": "resume-recording",
"title": "Resume Recording",
"subtitle": "Cap",
"description": "Resume a paused recording",
"mode": "no-view"
},
{
"name": "toggle-pause",
"title": "Toggle Pause",
"subtitle": "Cap",
"description": "Toggle pause state of the current recording",
"mode": "no-view"
},
{
"name": "take-screenshot",
"title": "Take Screenshot",
"subtitle": "Cap",
"description": "Take a screenshot with Cap",
"mode": "no-view"
},
{
"name": "open-cap",
"title": "Open Cap",
"subtitle": "Cap",
"description": "Open the Cap application",
"mode": "no-view"
},
{
"name": "open-settings",
"title": "Open Settings",
"subtitle": "Cap",
"description": "Open Cap settings",
"mode": "no-view"
},
{
"name": "recording-controls",
"title": "Recording Controls",
"subtitle": "Cap",
"description": "Quick actions for Cap recording",
"mode": "view"
}
],
"dependencies": {
"@raycast/api": "^1.83.0"
},
"devDependencies": {
"@raycast/eslint-config": "^1.0.11",
"@types/node": "22.10.2",
"@types/react": "18.3.16",
"eslint": "^9.16.0",
"prettier": "^3.4.2",
"typescript": "^5.7.2"
},
"scripts": {
"build": "ray build --skip-types -e dist -o dist",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1",
"publish": "npx @raycast/api@latest publish"
}
}
5 changes: 5 additions & 0 deletions extensions/raycast-cap/src/open-cap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { openCap } from "./utils/deeplink";

export default async function Command() {
await openCap();
}
8 changes: 8 additions & 0 deletions extensions/raycast-cap/src/open-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { executeCapAction } from "./utils/deeplink";

export default async function Command() {
await executeCapAction(
{ open_settings: { page: null } },
"Opening Settings"
);
}
5 changes: 5 additions & 0 deletions extensions/raycast-cap/src/pause-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { executeCapAction } from "./utils/deeplink";

export default async function Command() {
await executeCapAction("pause_recording", "Recording Paused");
}
Loading