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
90 changes: 85 additions & 5 deletions apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
use cap_recording::{
RecordingMode, feeds::camera::DeviceOrModelID, sources::screen_capture::ScreenCaptureTarget,
};
use scap_targets::Display;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tauri::{AppHandle, Manager, Url};
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
use tracing::trace;

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

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
Expand All @@ -18,6 +23,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 Down Expand Up @@ -88,10 +94,19 @@ impl TryFrom<&Url> for DeepLinkAction {
.map_err(|_| ActionParseFromUrlError::Invalid);
}

match url.domain() {
Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction),
_ => Err(ActionParseFromUrlError::Invalid),
}?;
if url.scheme() == "cap" {
return match url.host_str() {
Some("record") => Ok(Self::StartDefaultRecording),
Some("stop") => Ok(Self::StopRecording),
_ => Err(ActionParseFromUrlError::Invalid),
};
}

match url.host_str() {
Some("action") => {}
Some(_) => return Err(ActionParseFromUrlError::NotAction),
None => return Err(ActionParseFromUrlError::Invalid),
}

let params = url
.query_pairs()
Expand All @@ -108,6 +123,44 @@ impl TryFrom<&Url> for DeepLinkAction {
impl DeepLinkAction {
pub async fn execute(self, app: &AppHandle) -> Result<(), String> {
match self {
DeepLinkAction::StartDefaultRecording => {
let proceed = app
.dialog()
.message("Start a new recording from an external deep link?")
.title("Cap")
.kind(MessageDialogKind::Info)
.buttons(MessageDialogButtons::OkCancel)
.blocking_show();

if !proceed {
return Ok(());
}

let state = app.state::<ArcLock<App>>();
let 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 {
capture_target: settings.target.unwrap_or_else(|| {
ScreenCaptureTarget::Display {
id: Display::primary().id(),
}
}),
mode: settings.mode.unwrap_or_default(),
capture_system_audio: settings.system_audio,
organization_id: settings.organization_id,
};

crate::recording::start_recording(app.clone(), state, inputs)
.await
.map(|_| ())
}
DeepLinkAction::StartRecording {
capture_mode,
camera,
Expand Down Expand Up @@ -156,3 +209,30 @@ impl DeepLinkAction {
}
}
}

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

#[test]
fn parses_cap_record_as_start_default_recording() {
let url = Url::parse("cap://record").expect("valid cap record url");
let action = DeepLinkAction::try_from(&url).expect("cap record should parse");
assert!(matches!(action, DeepLinkAction::StartDefaultRecording));
}

#[test]
fn parses_cap_stop_as_stop_recording() {
let url = Url::parse("cap://stop").expect("valid cap stop url");
let action = DeepLinkAction::try_from(&url).expect("cap stop should parse");
assert!(matches!(action, DeepLinkAction::StopRecording));
}

#[test]
fn parses_existing_action_format() {
let url = Url::parse("cap-desktop://action?value=%7B%22stop_recording%22%3Anull%7D")
.expect("valid action deep link");
let action = DeepLinkAction::try_from(&url).expect("action deep link should parse");
assert!(matches!(action, DeepLinkAction::StopRecording));
}
}
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"updater": { "active": false, "pubkey": "" },
"deep-link": {
"desktop": {
"schemes": ["cap-desktop"]
"schemes": ["cap-desktop", "cap"]
}
}
},
Expand Down
14 changes: 14 additions & 0 deletions extensions/raycast/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Cap Raycast Extension

Lightweight Raycast commands for Cap Desktop.

## Commands
- Start Recording: opens `cap://record` and starts recording with your current Cap defaults.
- Stop Recording: opens `cap://stop` and stops the active recording.
- Open Dashboard: opens `https://cap.so/dashboard` in your browser.

## Local usage
1. `cd extensions/raycast`
2. `npm install`
3. `npm run dev`
4. In Raycast, import this extension from the local folder.
Binary file added extensions/raycast/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.
48 changes: 48 additions & 0 deletions extensions/raycast/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "cap",
"title": "Cap",
"description": "Control Cap Desktop recording from Raycast",
"icon": "assets/cap-icon.png",
"author": "cap-software",
"categories": ["Productivity", "Developer Tools"],
"license": "MIT",
"commands": [
{
"name": "start-recording",
"title": "Start Recording",
"description": "Start recording in Cap with your current default settings",
"mode": "no-view"
},
{
"name": "stop-recording",
"title": "Stop Recording",
"description": "Stop the current Cap recording",
"mode": "no-view"
},
{
"name": "open-dashboard",
"title": "Open Dashboard",
"description": "Open Cap dashboard in your browser",
"mode": "no-view"
}
],
"dependencies": {
"@raycast/api": "^1.83.0"
},
"devDependencies": {
"@raycast/eslint-config": "^1.0.11",
"@types/node": "20.8.10",
"eslint": "^8.57.0",
"prettier": "^3.3.3",
"typescript": "^5.4.5"
},
"scripts": {
"build": "ray build --skip-types -e dist -o dist",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"prepublishOnly": "echo \"\\n\\nUse npm run publish for Raycast Store release.\\n\" && exit 1",
"publish": "ray publish"
}
}
14 changes: 14 additions & 0 deletions extensions/raycast/src/open-dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { closeMainWindow, open, showToast, Toast } from "@raycast/api";

export default async function Command() {
try {
await closeMainWindow();
await open("https://cap.so/dashboard");
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to open dashboard",
message: String(error),
});
}
}
15 changes: 15 additions & 0 deletions extensions/raycast/src/start-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { closeMainWindow, open, showHUD, showToast, Toast } from "@raycast/api";

export default async function Command() {
try {
await closeMainWindow();
await open("cap://record");
await showHUD("Sent start request to Cap");
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to start Cap recording",
message: String(error),
});
}
}
15 changes: 15 additions & 0 deletions extensions/raycast/src/stop-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { closeMainWindow, open, showHUD, showToast, Toast } from "@raycast/api";

export default async function Command() {
try {
await closeMainWindow();
await open("cap://stop");
await showHUD("Sent stop request to Cap");
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to stop Cap recording",
message: String(error),
});
}
}
11 changes: 11 additions & 0 deletions extensions/raycast/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"strict": true,
"skipLibCheck": true
},
"include": ["src"]
}