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
59 changes: 59 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,
SetMicrophone {
mic_label: Option<String>,
},
SetCamera {
camera: Option<DeviceOrModelID>,
},
OpenEditor {
project_path: PathBuf,
},
Expand Down Expand Up @@ -147,6 +155,18 @@ 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::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::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
Expand All @@ -156,3 +176,42 @@ impl DeepLinkAction {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

fn parse(encoded_value: &str) -> DeepLinkAction {
let url = Url::parse(&format!("cap://action?value={encoded_value}"))
.expect("test URL should parse");
DeepLinkAction::try_from(&url).expect("action should parse")
}

#[test]
fn parses_unit_recording_actions() {
assert!(matches!(
parse("%22stop_recording%22"),
DeepLinkAction::StopRecording
));
assert!(matches!(
parse("%22pause_recording%22"),
DeepLinkAction::PauseRecording
));
assert!(matches!(
parse("%22resume_recording%22"),
DeepLinkAction::ResumeRecording
));
}

#[test]
fn parses_switch_input_actions() {
assert!(matches!(
parse("%7B%22set_microphone%22%3A%7B%22mic_label%22%3A%22Studio%20Mic%22%7D%7D"),
DeepLinkAction::SetMicrophone { mic_label: Some(label) } if label == "Studio Mic"
));
assert!(matches!(
parse("%7B%22set_camera%22%3A%7B%22camera%22%3A%7B%22DeviceID%22%3A%22camera-1%22%7D%7D%7D"),
DeepLinkAction::SetCamera { camera: Some(DeviceOrModelID::DeviceID(id)) } if id == "camera-1"
));
}
}
14 changes: 14 additions & 0 deletions apps/raycast/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Cap Raycast Extension

Control Cap from Raycast through the `cap://action?value=...` deeplink handler.

Commands:

- Start Recording
- Stop Recording
- Pause Recording
- Resume Recording
- Switch Microphone
- Switch Camera

Start Recording accepts the display/window name that Cap should capture. Microphone labels and camera device IDs can be provided per command or saved as Raycast preferences.
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.
17 changes: 17 additions & 0 deletions apps/raycast/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import tsParser from "@typescript-eslint/parser";

export default [
{
ignores: ["dist/**", "node_modules/**"],
},
{
files: ["src/**/*.{ts,tsx}"],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaFeatures: { jsx: true },
sourceType: "module",
},
},
},
];
151 changes: 151 additions & 0 deletions apps/raycast/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "cap",
"title": "Cap",
"description": "Control Cap recordings with deeplinks.",
"icon": "icon.png",
"author": "thomas",
"license": "MIT",
"commands": [
{
"name": "start-recording",
"title": "Start Recording",
"description": "Start a Cap recording",
"mode": "view",
"preferences": [
{
"name": "defaultScreenName",
"description": "Screen display name used by Start Recording when Capture Source is Screen.",
"type": "textfield",
"required": false,
"title": "Default Screen Name",
"label": "Default Screen Name"
},
{
"name": "defaultWindowName",
"description": "Window title used by Start Recording when Capture Source is Window.",
"type": "textfield",
"required": false,
"title": "Default Window Name",
"label": "Default Window Name"
},
{
"name": "microphoneLabel",
"description": "Microphone label Cap should select before recording.",
"type": "textfield",
"required": false,
"title": "Microphone Label",
"label": "Microphone Label"
},
{
"name": "cameraDeviceId",
"description": "Camera device ID Cap should select before recording.",
"type": "textfield",
"required": false,
"title": "Camera Device ID",
"label": "Camera Device ID"
},
{
"name": "recordingMode",
"description": "Default Cap recording mode.",
"type": "dropdown",
"required": false,
"default": "studio",
"data": [
{
"title": "Studio",
"value": "studio"
},
{
"title": "Instant",
"value": "instant"
},
{
"title": "Screenshot",
"value": "screenshot"
}
],
"title": "Recording Mode",
"label": "Recording Mode"
},
{
"name": "captureSystemAudio",
"description": "Capture system audio by default.",
"type": "checkbox",
"required": false,
"default": true,
"title": "Capture System Audio",
"label": "Capture System Audio"
}
]
},
{
"name": "stop-recording",
"title": "Stop Recording",
"description": "Stop the active Cap recording",
"mode": "no-view"
},
{
"name": "pause-recording",
"title": "Pause Recording",
"description": "Pause the active Cap recording",
"mode": "no-view"
},
{
"name": "resume-recording",
"title": "Resume Recording",
"description": "Resume the active Cap recording",
"mode": "no-view"
},
{
"name": "switch-microphone",
"title": "Switch Microphone",
"description": "Select the microphone Cap should use",
"mode": "view",
"preferences": [
{
"name": "microphoneLabel",
"description": "Microphone label Cap should select.",
"type": "textfield",
"required": false,
"title": "Microphone Label",
"label": "Microphone Label"
}
]
},
{
"name": "switch-camera",
"title": "Switch Camera",
"description": "Select the camera Cap should use",
"mode": "view",
"preferences": [
{
"name": "cameraDeviceId",
"description": "Camera device ID Cap should select.",
"type": "textfield",
"required": false,
"title": "Camera Device ID",
"label": "Camera Device ID"
}
]
}
],
"dependencies": {
"@raycast/api": "^1.93.2"
},
"devDependencies": {
"@raycast/eslint-config": "^1.0.11",
"@types/node": "^22.13.10",
"eslint": "^9.22.0",
"prettier": "^3.5.3",
"typescript": "^5.8.2",
"@typescript-eslint/parser": "^8.33.1"
},
"scripts": {
"build": "ray build -e dist",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint"
},
"type": "module"
}
63 changes: 63 additions & 0 deletions apps/raycast/raycast-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/// <reference types="@raycast/api">

/* 🚧 🚧 🚧
* This file is auto-generated from the extension's manifest.
* Do not modify manually. Instead, update the `package.json` file.
* 🚧 🚧 🚧 */

/* eslint-disable @typescript-eslint/ban-types */

type ExtensionPreferences = {}

/** Preferences accessible in all the extension's commands */
declare type Preferences = ExtensionPreferences

declare namespace Preferences {
/** Preferences accessible in the `start-recording` command */
export type StartRecording = ExtensionPreferences & {
/** Default Screen Name - Screen display name used by Start Recording when Capture Source is Screen. */
"defaultScreenName"?: string,
/** Default Window Name - Window title used by Start Recording when Capture Source is Window. */
"defaultWindowName"?: string,
/** Microphone Label - Microphone label Cap should select before recording. */
"microphoneLabel"?: string,
/** Camera Device ID - Camera device ID Cap should select before recording. */
"cameraDeviceId"?: string,
/** Recording Mode - Default Cap recording mode. */
"recordingMode": "studio" | "instant" | "screenshot",
/** Capture System Audio - Capture system audio by default. */
"captureSystemAudio": boolean
}
/** Preferences accessible in the `stop-recording` command */
export type StopRecording = ExtensionPreferences & {}
/** Preferences accessible in the `pause-recording` command */
export type PauseRecording = ExtensionPreferences & {}
/** Preferences accessible in the `resume-recording` command */
export type ResumeRecording = ExtensionPreferences & {}
/** Preferences accessible in the `switch-microphone` command */
export type SwitchMicrophone = ExtensionPreferences & {
/** Microphone Label - Microphone label Cap should select. */
"microphoneLabel"?: string
}
/** Preferences accessible in the `switch-camera` command */
export type SwitchCamera = ExtensionPreferences & {
/** Camera Device ID - Camera device ID Cap should select. */
"cameraDeviceId"?: string
}
}

declare namespace Arguments {
/** Arguments passed to the `start-recording` command */
export type StartRecording = {}
/** Arguments passed to the `stop-recording` command */
export type StopRecording = {}
/** Arguments passed to the `pause-recording` command */
export type PauseRecording = {}
/** Arguments passed to the `resume-recording` command */
export type ResumeRecording = {}
/** Arguments passed to the `switch-microphone` command */
export type SwitchMicrophone = {}
/** Arguments passed to the `switch-camera` command */
export type SwitchCamera = {}
}

63 changes: 63 additions & 0 deletions apps/raycast/src/cap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
closeMainWindow,
environment,
getPreferenceValues,
open,
showToast,
Toast,
} from "@raycast/api";

export type RecordingMode = "studio" | "instant" | "screenshot";
export type CaptureMode = { screen: string } | { window: string };
export type CameraId = { DeviceID: string } | { ModelID: string };

type CapAction =
| "stop_recording"
| "pause_recording"
| "resume_recording"
| {
start_recording: {
capture_mode: CaptureMode;
camera: CameraId | null;
mic_label: string | null;
capture_system_audio: boolean;
mode: RecordingMode;
};
}
| { set_microphone: { mic_label: string | null } }
| { set_camera: { camera: CameraId | null } };

export type Preferences = {
defaultScreenName?: string;
defaultWindowName?: string;
microphoneLabel?: string;
cameraDeviceId?: string;
captureSystemAudio?: boolean;
recordingMode?: RecordingMode;
};

export function actionUrl(action: CapAction): string {
return `cap://action?value=${encodeURIComponent(JSON.stringify(action))}`;
}

export async function runAction(
action: CapAction,
message: string,
): Promise<void> {
await open(actionUrl(action));
if (!environment.isDevelopment)
await closeMainWindow({ clearRootSearch: true });
await showToast({ style: Toast.Style.Success, title: message });
}

export function preferences(): Preferences {
return getPreferenceValues<Preferences>();
}

export function cameraFromPreference(value?: string): CameraId | null {
return value?.trim() ? { DeviceID: value.trim() } : null;
}

export function micFromPreference(value?: string): string | null {
return value?.trim() || null;
}
5 changes: 5 additions & 0 deletions apps/raycast/src/pause-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { runAction } from "./cap";

export default async function Command() {
await runAction("pause_recording", "Pausing Cap recording");
}
Loading