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
33 changes: 33 additions & 0 deletions apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ pub enum DeepLinkAction {
mode: RecordingMode,
},
StopRecording,
PauseRecording,
ResumeRecording,
SwitchCamera {
device_id: String,
},
SwitchMicrophone {
mic_label: String,
},
OpenEditor {
project_path: PathBuf,
},
Expand Down Expand Up @@ -146,6 +154,31 @@ impl DeepLinkAction {
DeepLinkAction::StopRecording => {
crate::recording::stop_recording(app.clone(), app.state()).await
}
DeepLinkAction::PauseRecording => {
let state = app.state::<ArcLock<App>>();
crate::recording::pause_recording(state)
.await
.map_err(|e| e.to_string())
}
DeepLinkAction::ResumeRecording => {
let state = app.state::<ArcLock<App>>();
crate::recording::resume_recording(state)
.await
.map_err(|e| e.to_string())
}
DeepLinkAction::SwitchCamera { device_id } => {
let state = app.state::<ArcLock<App>>();
let camera = DeviceOrModelID::ModelID(device_id.clone());
Copy link

Choose a reason for hiding this comment

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

The field is named device_id, but this constructs DeviceOrModelID::ModelID. If the deeplink is actually passing a device id (as the Raycast command suggests), this should probably use DeviceID (and avoids the clone).

Suggested change
let camera = DeviceOrModelID::ModelID(device_id.clone());
let camera = DeviceOrModelID::DeviceID(device_id);

crate::set_camera_input(app.clone(), state.clone(), Some(camera), None)
.await
.map(|_| ())
}
DeepLinkAction::SwitchMicrophone { mic_label } => {
let state = app.state::<ArcLock<App>>();
crate::set_mic_input(state, Some(mic_label))
.await
.map(|_| ())
}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
Expand Down
13 changes: 13 additions & 0 deletions packages/raycast-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "cap-recording-control",
"title": "Cap Recording Control",
"description": "Control Cap screen recording via deeplinks (pause, resume, start, stop, switch camera/microphone)",
"author": "Cap Software",
"version": "1.0.0",
"keywords": [
"cap",
"screen-recording",
"recording-control",
"deeplink"
]
}
13 changes: 13 additions & 0 deletions packages/raycast-extension/src/commands/pause-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { open, showToast, Toast } from "@raycast/api";
import { generateDeeplink } from "../utils/deeplink";

export default async function Command() {
const deeplink = generateDeeplink("pause_recording");

try {
await open(deeplink);
await showToast({ style: Toast.Style.Success, title: "Opened Cap" });
} catch (error) {
await showToast({ style: Toast.Style.Failure, title: "Error", message: String(error) });
}
}
13 changes: 13 additions & 0 deletions packages/raycast-extension/src/commands/resume-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { open, showToast, Toast } from "@raycast/api";
import { generateDeeplink } from "../utils/deeplink";

export default async function Command() {
const deeplink = generateDeeplink("resume_recording");

try {
await open(deeplink);
await showToast({ style: Toast.Style.Success, title: "Opened Cap" });
} catch (error) {
await showToast({ style: Toast.Style.Failure, title: "Error", message: String(error) });
}
}
19 changes: 19 additions & 0 deletions packages/raycast-extension/src/commands/start-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { open, showToast, Toast } from "@raycast/api";
import { generateDeeplink } from "../utils/deeplink";

export default async function Command() {
const deeplink = generateDeeplink("start_recording", {
Copy link

Choose a reason for hiding this comment

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

capture_system_audio is required on the Rust side (StartRecording { ... }), so omitting it will fail deserialization for this deeplink.

Suggested change
const deeplink = generateDeeplink("start_recording", {
const deeplink = generateDeeplink("start_recording", {
capture_mode: { screen: "default" },
capture_system_audio: false,
mode: "normal",
});

capture_mode: { screen: "default" },
camera: null,
mic_label: null,
capture_system_audio: false,
mode: "normal"
});

try {
await open(deeplink);
await showToast({ style: Toast.Style.Success, title: "Opened Cap" });
} catch (error) {
await showToast({ style: Toast.Style.Failure, title: "Error", message: String(error) });
}
}
13 changes: 13 additions & 0 deletions packages/raycast-extension/src/commands/stop-recording.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { open, showToast, Toast } from "@raycast/api";
import { generateDeeplink } from "../utils/deeplink";

export default async function Command() {
const deeplink = generateDeeplink("stop_recording");

try {
await open(deeplink);
await showToast({ style: Toast.Style.Success, title: "Opened Cap" });
} catch (error) {
await showToast({ style: Toast.Style.Failure, title: "Error", message: String(error) });
}
}
17 changes: 17 additions & 0 deletions packages/raycast-extension/src/commands/switch-camera.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { open, showToast, Toast } from "@raycast/api";
import { generateDeeplink } from "../utils/deeplink";

export default async function Command() {
// TODO: Replace "default" with actual device picker when available
// For now, this will use the default/primary camera device
const deeplink = generateDeeplink("switch_camera", {
device_id: "default"
});

try {
await open(deeplink);
await showToast({ style: Toast.Style.Success, title: "Opened Cap" });
} catch (error) {
await showToast({ style: Toast.Style.Failure, title: "Error", message: String(error) });
}
}
17 changes: 17 additions & 0 deletions packages/raycast-extension/src/commands/switch-microphone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { open, showToast, Toast } from "@raycast/api";
import { generateDeeplink } from "../utils/deeplink";

export default async function Command() {
// TODO: Replace "default" with actual mic picker when available
// For now, this will use the default/system microphone
const deeplink = generateDeeplink("switch_microphone", {
mic_label: "default"
});

try {
await open(deeplink);
await showToast({ style: Toast.Style.Success, title: "Opened Cap" });
} catch (error) {
await showToast({ style: Toast.Style.Failure, title: "Error", message: String(error) });
}
}
38 changes: 38 additions & 0 deletions packages/raycast-extension/src/utils/deeplink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Generate a deeplink for Cap recording control actions
*
* Supported actions:
* - pause_recording: {} (no params)
* - resume_recording: {} (no params)
* - stop_recording: {} (no params)
* - start_recording: { capture_mode, camera?, mic_label?, capture_system_audio, mode }
* - switch_camera: { device_id }
* - switch_microphone: { mic_label }
*/
export const generateDeeplink = (action: string, params?: Record<string, any>): string => {
Copy link

Choose a reason for hiding this comment

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

I only see src/** added for packages/raycast-extension in this PR. Is the Raycast extension manifest (e.g. package.json / raycast.json) intentionally out-of-band? Without it, this package won’t be buildable/discoverable as an extension.

const validActions = [
"pause_recording",
"resume_recording",
"stop_recording",
"start_recording",
"switch_camera",
"switch_microphone"
];

if (!validActions.includes(action)) {
throw new Error(`Invalid action: ${action}. Must be one of: ${validActions.join(", ")}`);
}

const url = new URL(`cap://action`);

const actionObj: any = {};

if (params) {
actionObj[action] = params;
} else {
actionObj[action] = {};
}

url.searchParams.append("value", JSON.stringify(actionObj));
return url.toString();
};