Skip to content
Closed
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
59 changes: 58 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,9 @@ 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, RecordingEvent, recording::StartRecordingInputs, windows::ShowCapWindow,
};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
Expand All @@ -18,6 +20,7 @@ pub enum CaptureMode {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeepLinkAction {
StartDefaultRecording,
StartRecording {
capture_mode: CaptureMode,
camera: Option<DeviceOrModelID>,
Expand All @@ -26,6 +29,14 @@ pub enum DeepLinkAction {
mode: RecordingMode,
},
StopRecording,
PauseRecording,
ResumeRecording,
SwitchMic {
mic_label: String,
},
SwitchCamera {
camera_id: DeviceOrModelID,
},
OpenEditor {
project_path: PathBuf,
},
Expand Down Expand Up @@ -108,6 +119,26 @@ impl TryFrom<&Url> for DeepLinkAction {
impl DeepLinkAction {
pub async fn execute(self, app: &AppHandle) -> Result<(), String> {
match self {
DeepLinkAction::StartDefaultRecording => {
let settings = crate::recording_settings::RecordingSettingsStore::get(app)?
.unwrap_or_default();

let target = settings.target.ok_or("No capture target selected")?;
let mode = settings.mode.unwrap_or(RecordingMode::Studio);

crate::recording::start_recording(
app.clone(),
app.state(),
crate::recording::StartRecordingInputs {
capture_target: target,
capture_system_audio: settings.system_audio,
mode,
organization_id: settings.organization_id,
},
)
.await
.map(|_| ())
}
DeepLinkAction::StartRecording {
capture_mode,
camera,
Expand Down Expand Up @@ -147,6 +178,32 @@ impl DeepLinkAction {
DeepLinkAction::StopRecording => {
crate::recording::stop_recording(app.clone(), app.state()).await
}
DeepLinkAction::PauseRecording => {
let state = app.state::<ArcLock<App>>();
let mut app_state = state.write().await;
if let Some(recording) = app_state.current_recording_mut() {
recording.pause().await.map_err(|e| e.to_string())?;
RecordingEvent::Paused.emit(app).ok();
}
Ok(())
}
DeepLinkAction::ResumeRecording => {
let state = app.state::<ArcLock<App>>();
let mut app_state = state.write().await;
if let Some(recording) = app_state.current_recording_mut() {
recording.resume().await.map_err(|e| e.to_string())?;
RecordingEvent::Resumed.emit(app).ok();
}
Ok(())
Comment on lines +181 to +197
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 Missing RecordingEvent emissions after pause/resume

The existing Tauri commands pause_recording and resume_recording in recording.rs (lines 1516, 1530) emit RecordingEvent::Paused and RecordingEvent::Resumed after the operation so the UI can update its state. The new deep link handlers call recording.pause() / recording.resume() but omit those event emissions. As a result, triggering pause or resume via a deep link will leave the UI stuck showing the pre-action recording state (e.g. the pause button will still show as active after pausing).

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

Comment:
**Missing `RecordingEvent` emissions after pause/resume**

The existing Tauri commands `pause_recording` and `resume_recording` in `recording.rs` (lines 1516, 1530) emit `RecordingEvent::Paused` and `RecordingEvent::Resumed` after the operation so the UI can update its state. The new deep link handlers call `recording.pause()` / `recording.resume()` but omit those event emissions. As a result, triggering pause or resume via a deep link will leave the UI stuck showing the pre-action recording state (e.g. the pause button will still show as active after pausing).

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

}
DeepLinkAction::SwitchMic { mic_label } => {
let state = app.state::<ArcLock<App>>();
crate::set_mic_input(state.clone(), Some(mic_label)).await
}
DeepLinkAction::SwitchCamera { camera_id } => {
let state = app.state::<ArcLock<App>>();
crate::set_camera_input(app.clone(), state.clone(), Some(camera_id), None).await
}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
Expand Down
87 changes: 87 additions & 0 deletions apps/raycast-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"name": "cap-raycast-extension",
"title": "Cap",
"description": "Control Cap recording and settings via Raycast.",
"icon": "command-icon.png",
"author": "Cap Software",
"categories": [
"Productivity"
],
"license": "MIT",
"commands": [
{
"name": "start-recording",
"title": "Start Recording",
"description": "Start a new recording in Cap.",
"mode": "no-view"
},
{
"name": "stop-recording",
"title": "Stop Recording",
"description": "Stop the current recording in Cap.",
"mode": "no-view"
},
{
"name": "pause-recording",
"title": "Pause Recording",
"description": "Pause the current recording in Cap.",
"mode": "no-view"
},
{
"name": "resume-recording",
"title": "Resume Recording",
"description": "Resume the current recording in Cap.",
"mode": "no-view"
},
{
"name": "switch-mic",
"title": "Switch Microphone",
"description": "Switch to a specific microphone by name.",
"mode": "no-view",
"arguments": [
{
"name": "mic_label",
"placeholder": "Microphone Name",
"type": "text",
"required": true
}
]
},
{
"name": "switch-camera",
"title": "Switch Camera",
"description": "Switch to a specific camera by ID.",
"mode": "no-view",
"arguments": [
{
"name": "camera_id",
"placeholder": "Camera ID (e.g. from continuity camera)",
"type": "text",
"required": true
},
{
"name": "type",
"placeholder": "ID Type (device or model)",
"type": "text",
"required": false
}
]
}
],
"dependencies": {
"@raycast/api": "^1.72.1"
},
"devDependencies": {
"@raycast/utils": "^1.14.2",
"@types/node": "20.8.10",
"@types/react": "18.2.27",
"typescript": "^5.2.2"
},
"scripts": {
"build": "ray build -e dist",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"publish": "ray publish"
}
}
5 changes: 5 additions & 0 deletions apps/raycast-extension/src/pause-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { executeCapAction } from "./utils";

export default async function Command() {
await executeCapAction("pause_recording");
}
5 changes: 5 additions & 0 deletions apps/raycast-extension/src/resume-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { executeCapAction } from "./utils";

export default async function Command() {
await executeCapAction("resume_recording");
}
5 changes: 5 additions & 0 deletions apps/raycast-extension/src/start-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { executeCapAction } from "./utils";

export default async function Command() {
await executeCapAction("start_default_recording");
}
5 changes: 5 additions & 0 deletions apps/raycast-extension/src/stop-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { executeCapAction } from "./utils";

export default async function Command() {
await executeCapAction("stop_recording");
}
12 changes: 12 additions & 0 deletions apps/raycast-extension/src/switch-camera.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { executeCapAction } from "./utils";

export default async function Command(props: { arguments: { camera_id: string; type?: string } }) {
const isModel = props.arguments.type?.toLowerCase() === "model";
await executeCapAction({
switch_camera: {
camera_id: isModel
? { ModelID: props.arguments.camera_id }
: { DeviceID: props.arguments.camera_id },
},
});
Comment on lines +5 to +11
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 DeviceID hardcoded; ModelID cameras are unreachable

The Rust enum DeviceOrModelID has two variants: DeviceID and ModelID. The Raycast command always wraps the user-supplied string as { DeviceID: ... }, so any camera that is identified by a model ID (common for virtual or continuity cameras) cannot be switched to via this command. Consider either accepting the variant as a second argument or documenting that only physical device IDs are supported.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast-extension/src/switch-camera.ts
Line: 4-8

Comment:
**`DeviceID` hardcoded; `ModelID` cameras are unreachable**

The Rust enum `DeviceOrModelID` has two variants: `DeviceID` and `ModelID`. The Raycast command always wraps the user-supplied string as `{ DeviceID: ... }`, so any camera that is identified by a model ID (common for virtual or continuity cameras) cannot be switched to via this command. Consider either accepting the variant as a second argument or documenting that only physical device IDs are supported.

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

}
9 changes: 9 additions & 0 deletions apps/raycast-extension/src/switch-mic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { executeCapAction } from "./utils";

export default async function Command(props: { arguments: { mic_label: string } }) {
await executeCapAction({
switch_mic: {
mic_label: props.arguments.mic_label,
},
});
}
12 changes: 12 additions & 0 deletions apps/raycast-extension/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { open } from "@raycast/api";

type CapAction =
| string
| { switch_mic: { mic_label: string } }
| { switch_camera: { camera_id: { DeviceID: string } | { ModelID: string } } };

export async function executeCapAction(action: CapAction) {
const json = JSON.stringify(action);
const url = `cap://action?value=${encodeURIComponent(json)}`;
await open(url);
}