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
177 changes: 176 additions & 1 deletion apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ use std::path::{Path, PathBuf};
use tauri::{AppHandle, Manager, Url};
use tracing::trace;

use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow};
use crate::{
App, ArcLock,
recording::StartRecordingInputs,
recording_settings::RecordingTargetMode,
windows::ShowCapWindow,
};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
Expand All @@ -25,7 +30,23 @@ pub enum DeepLinkAction {
capture_system_audio: bool,
mode: RecordingMode,
},
StartRecordingFromSettings {
mode: RecordingMode,
},
StopRecording,
RestartRecording,
PauseRecording,
ResumeRecording,
TogglePauseRecording,
SetMicrophone {
mic_label: Option<String>,
},
SetCamera {
camera: Option<DeviceOrModelID>,
},
OpenRecordingPicker {
target_mode: Option<RecordingTargetMode>,
},
OpenEditor {
project_path: PathBuf,
},
Expand Down Expand Up @@ -70,6 +91,7 @@ pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) {
});
}

#[derive(Debug)]
pub enum ActionParseFromUrlError {
ParseFailed(String),
Invalid,
Expand Down Expand Up @@ -147,6 +169,67 @@ impl DeepLinkAction {
DeepLinkAction::StopRecording => {
crate::recording::stop_recording(app.clone(), app.state()).await
}
DeepLinkAction::StartRecordingFromSettings { mode } => {
let state = app.state::<ArcLock<App>>();
let settings = crate::recording_settings::RecordingSettingsStore::get(app)
.ok()
.flatten()
.unwrap_or_default();

crate::set_mic_input(state.clone(), settings.mic_name).await?;
crate::set_camera_input(app.clone(), state.clone(), settings.camera_id, None)
.await?;

let inputs = StartRecordingInputs {
mode,
capture_target: settings.target.unwrap_or_else(|| {
ScreenCaptureTarget::Display {
id: scap_targets::Display::primary().id(),
}
}),
capture_system_audio: settings.system_audio,
organization_id: settings.organization_id,
};

crate::recording::start_recording(app.clone(), state, inputs)
.await
.map(|_| ())
}
DeepLinkAction::RestartRecording => crate::recording::restart_recording(
app.clone(),
app.state(),
)
.await
.map(|_| ()),
DeepLinkAction::PauseRecording => {
crate::recording::pause_recording(app.clone(), app.state()).await
}
DeepLinkAction::ResumeRecording => {
crate::recording::resume_recording(app.clone(), app.state()).await
}
DeepLinkAction::TogglePauseRecording => {
crate::recording::toggle_pause_recording(app.clone(), app.state()).await
}
DeepLinkAction::SetMicrophone { mic_label } => {
crate::set_mic_input(app.state(), mic_label).await
}
DeepLinkAction::SetCamera { camera } => {
crate::set_camera_input(app.clone(), app.state(), camera, None).await
}
DeepLinkAction::OpenRecordingPicker { target_mode } => {
match target_mode {
Some(target_mode) => crate::open_target_picker(app, target_mode).await,
None => {
ShowCapWindow::Main {
init_target_mode: None,
}
.show(app)
.await?;
}
Comment on lines +222 to +228
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 ShowCapWindow::show returns tauri::Result<WebviewWindow>, and tauri::Error does not implement From<_> for String. Using bare ? here won't compile because the surrounding execute function returns Result<(), String>. Every other call site in this codebase (e.g. show_window in lib.rs) uses .map_err(|e| e.to_string())? to bridge the conversion.

Suggested change
None => {
ShowCapWindow::Main {
init_target_mode: None,
}
.show(app)
.await?;
}
None => {
ShowCapWindow::Main {
init_target_mode: None,
}
.show(app)
.await
.map_err(|e| e.to_string())?;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 185-191

Comment:
`ShowCapWindow::show` returns `tauri::Result<WebviewWindow>`, and `tauri::Error` does not implement `From<_> for String`. Using bare `?` here won't compile because the surrounding `execute` function returns `Result<(), String>`. Every other call site in this codebase (e.g. `show_window` in `lib.rs`) uses `.map_err(|e| e.to_string())?` to bridge the conversion.

```suggestion
                    None => {
                        ShowCapWindow::Main {
                            init_target_mode: None,
                        }
                        .show(app)
                        .await
                        .map_err(|e| e.to_string())?;
                    }
```

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

}

Ok(())
}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
Expand All @@ -156,3 +239,95 @@ impl DeepLinkAction {
}
}
}

#[cfg(test)]
mod tests {
use super::{ActionParseFromUrlError, DeepLinkAction};
use crate::recording_settings::RecordingTargetMode;
use tauri::Url;

fn parse_action(encoded_value: &str) -> Result<DeepLinkAction, ActionParseFromUrlError> {
let url = Url::parse(&format!("cap-desktop://action?value={encoded_value}")).unwrap();

DeepLinkAction::try_from(&url)
}

#[test]
fn parses_restart_recording_action() {
assert!(matches!(
parse_action("%22restart_recording%22").unwrap(),
DeepLinkAction::RestartRecording
));
}

#[test]
fn parses_start_recording_from_settings_action() {
assert!(matches!(
parse_action(
"%7B%22start_recording_from_settings%22%3A%7B%22mode%22%3A%22studio%22%7D%7D"
)
.unwrap(),
DeepLinkAction::StartRecordingFromSettings {
mode: cap_recording::RecordingMode::Studio
}
));
}

#[test]
fn parses_pause_recording_action() {
assert!(matches!(
parse_action("%22pause_recording%22").unwrap(),
DeepLinkAction::PauseRecording
));
}

#[test]
fn parses_resume_recording_action() {
assert!(matches!(
parse_action("%22resume_recording%22").unwrap(),
DeepLinkAction::ResumeRecording
));
}

#[test]
fn parses_toggle_pause_recording_action() {
assert!(matches!(
parse_action("%22toggle_pause_recording%22").unwrap(),
DeepLinkAction::TogglePauseRecording
));
}

#[test]
fn parses_set_microphone_action() {
assert!(matches!(
parse_action("%7B%22set_microphone%22%3A%7B%22mic_label%22%3Anull%7D%7D").unwrap(),
DeepLinkAction::SetMicrophone { mic_label: None }
));
}

#[test]
fn parses_set_camera_action() {
assert!(matches!(
parse_action(
"%7B%22set_camera%22%3A%7B%22camera%22%3A%7B%22DeviceID%22%3A%22camera-device-id%22%7D%7D%7D"
)
.unwrap(),
DeepLinkAction::SetCamera {
camera: Some(cap_recording::feeds::camera::DeviceOrModelID::DeviceID(_))
}
));
}

#[test]
fn parses_open_recording_picker_action() {
assert!(matches!(
parse_action(
"%7B%22open_recording_picker%22%3A%7B%22target_mode%22%3A%22display%22%7D%7D"
)
.unwrap(),
DeepLinkAction::OpenRecordingPicker {
target_mode: Some(RecordingTargetMode::Display)
}
));
}
Comment on lines +300 to +332
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Missing test cases for SetCamera and OpenRecordingPicker { target_mode: None }

The new tests cover RestartRecording, TogglePauseRecording, SetMicrophone (null label), and OpenRecordingPicker (with a mode), but there are no round-trip parse tests for SetCamera (both null and non-null camera values) or for OpenRecordingPicker when target_mode is None. Adding these would match the test coverage level of the other new actions.

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

Comment:
**Missing test cases for `SetCamera` and `OpenRecordingPicker { target_mode: None }`**

The new tests cover `RestartRecording`, `TogglePauseRecording`, `SetMicrophone` (null label), and `OpenRecordingPicker` (with a mode), but there are no round-trip parse tests for `SetCamera` (both null and non-null camera values) or for `OpenRecordingPicker` when `target_mode` is `None`. Adding these would match the test coverage level of the other new actions.

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

}
4 changes: 4 additions & 0 deletions apps/raycast/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"useTabs": true,
"tabWidth": 2
}
35 changes: 35 additions & 0 deletions apps/raycast/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Cap Raycast Extension

Control Cap desktop recordings from Raycast.

## Commands

- Start Studio Recording
- Start Instant Recording
- Stop Recording
- Restart Recording
- Pause Recording
- Resume Recording
- Pause or Resume Recording
- Open Recording Picker
- Set Microphone
- Clear Microphone
- Set Camera
- Clear Camera
- Open Settings

## How It Works

The extension opens Cap desktop deeplinks using the `cap-desktop://action` scheme.

Examples:

- `cap-desktop://action?value=%22stop_recording%22`
- `cap-desktop://action?value=%22pause_recording%22`
- `cap-desktop://action?value=%22resume_recording%22`
- `cap-desktop://action?value=%22toggle_pause_recording%22`
- `cap-desktop://action?value=%7B%22start_recording_from_settings%22%3A%7B%22mode%22%3A%22studio%22%7D%7D`
- `cap-desktop://action?value=%7B%22set_microphone%22%3A%7B%22mic_label%22%3A%22MacBook%20Pro%20Microphone%22%7D%7D`
- `cap-desktop://action?value=%7B%22set_camera%22%3A%7B%22camera%22%3A%7B%22DeviceID%22%3A%22camera-device-id%22%7D%7D%7D`

The desktop app parses the `value` query parameter as JSON and executes the corresponding action.
Binary file added apps/raycast/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions apps/raycast/eslint.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const parser = require("@typescript-eslint/parser");

module.exports = [
{
files: ["src/**/*.ts"],
languageOptions: {
ecmaVersion: "latest",
parser,
sourceType: "module",
},
rules: {},
},
];
129 changes: 129 additions & 0 deletions apps/raycast/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
{
"name": "cap-raycast",
"title": "Cap",
"description": "Control Cap recordings from Raycast",
"icon": "icon.png",
"author": "cap",
"license": "MIT",
"categories": [
"Productivity",
"Developer Tools"
],
"platforms": [
"macOS"
],
"scripts": {
"dev": "ray develop",
"build": "ray build",
"lint": "ray lint"
},
"dependencies": {
"@raycast/api": "^1.102.7"
},
"devDependencies": {
"@raycast/eslint-config": "^2.0.4",
"@typescript-eslint/parser": "^8.57.2",
"@types/node": "22.19.17",
"eslint": "^8.57.1",
"prettier": "^3.0.0",
"typescript": "^5.8.3"
},
"commands": [
{
"name": "start-studio-recording",
"title": "Start Studio Recording",
"description": "Start a Cap Studio recording with saved settings",
"mode": "no-view"
},
{
"name": "start-instant-recording",
"title": "Start Instant Recording",
"description": "Start a Cap Instant recording with saved settings",
"mode": "no-view"
},
{
"name": "stop-recording",
"title": "Stop Recording",
"description": "Stop the current Cap recording",
"mode": "no-view"
},
{
"name": "restart-recording",
"title": "Restart Recording",
"description": "Restart the current Cap recording",
"mode": "no-view"
},
{
"name": "pause-recording",
"title": "Pause Recording",
"description": "Pause the current Cap recording",
"mode": "no-view"
},
{
"name": "resume-recording",
"title": "Resume Recording",
"description": "Resume the current Cap recording",
"mode": "no-view"
},
{
"name": "toggle-pause-recording",
"title": "Pause or Resume Recording",
"description": "Pause or resume the current Cap recording",
"mode": "no-view"
},
{
"name": "open-recording-picker",
"title": "Open Recording Picker",
"description": "Open the Cap recording picker",
"mode": "no-view"
},
{
"name": "set-microphone",
"title": "Set Microphone",
"description": "Switch Cap to a microphone by label",
"mode": "no-view",
"arguments": [
{
"name": "micLabel",
"title": "Microphone Label",
"placeholder": "MacBook Pro Microphone",
"type": "text",
"required": true
}
]
},
{
"name": "clear-microphone",
"title": "Clear Microphone",
"description": "Clear the selected Cap microphone",
"mode": "no-view"
},
{
"name": "set-camera",
"title": "Set Camera",
"description": "Switch Cap to a camera by device ID",
"mode": "no-view",
"arguments": [
{
"name": "deviceId",
"title": "Device ID",
"placeholder": "camera-device-id",
"type": "text",
"required": true
}
]
},
{
"name": "clear-camera",
"title": "Clear Camera",
"description": "Clear the selected Cap camera",
"mode": "no-view"
},
{
"name": "open-settings",
"title": "Open Settings",
"description": "Open Cap settings",
"mode": "no-view"
}
]
}
Loading