-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: Add deeplinks support for pause/resume and device switching + Raycast extension #1565
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Add deeplinks support for pause/resume and device switching + Raycast extension #1565
Conversation
…aycast extension - Add PauseRecording deeplink action - Add ResumeRecording deeplink action - Add SwitchMicrophone deeplink action with mic_label parameter - Add SwitchCamera deeplink action with camera_id parameter - Create Raycast extension with commands for all recording controls - Add commands: start, stop, pause, resume, switch microphone, switch camera Fixes CapSoftware#1540
| export default async function Command() { | ||
| const action = { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoding "Main Display" seems brittle (the app looks up displays by name and will error if it doesn't match). Might be worth picking a safer default or making this configurable in the command.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
5 files reviewed, 5 comments
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
| } | ||
| } | ||
|
|
||
| export default async function Command() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file exports default twice (lines 3 and 20), which will fail TypeScript compilation. Dropping the second export default block should fix it.
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
- Add enable_deeplink_actions setting (default: false) to gate sensitive deeplink operations - Require opt-in permission for recording control actions (start, stop, pause, resume, switch devices) - Allow OpenEditor and OpenSettings without permission (less sensitive) - Add UI toggle in Settings → General → App section - Document security implications in Raycast extension README This addresses the security concern that any app/website could trigger recording state changes via URL schemes. Users must explicitly enable this feature in settings.
| import { open, showToast, Toast } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| const action = { pause_recording: null }; | ||
|
|
||
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
|
|
||
| try { | ||
| await open(url); | ||
| await showToast({ style: Toast.Style.Success, title: "Recording paused" }); | ||
| } catch (error) { | ||
| await showToast({ | ||
| style: Toast.Style.Failure, | ||
| title: "Failed to pause recording", | ||
| message: error instanceof Error ? error.message : "Unknown error", | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| export default async function Command() { | ||
| const action = { pause_recording: null }; | ||
|
|
||
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
| await open(url); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file currently has two export default functions; TypeScript will error. Removing the second copy should fix it.
| import { open, showToast, Toast } from "@raycast/api"; | |
| export default async function Command() { | |
| const action = { pause_recording: null }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| try { | |
| await open(url); | |
| await showToast({ style: Toast.Style.Success, title: "Recording paused" }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to pause recording", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| } | |
| export default async function Command() { | |
| const action = { pause_recording: null }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| await open(url); | |
| } | |
| import { open, showToast, Toast } from "@raycast/api"; | |
| export default async function Command() { | |
| const action = { pause_recording: null }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| try { | |
| await open(url); | |
| await showToast({ style: Toast.Style.Success, title: "Recording paused" }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to pause recording", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| } |
| import { open, showToast, Toast } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| const action = { resume_recording: null }; | ||
|
|
||
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
|
|
||
| try { | ||
| await open(url); | ||
| await showToast({ style: Toast.Style.Success, title: "Recording resumed" }); | ||
| } catch (error) { | ||
| await showToast({ | ||
| style: Toast.Style.Failure, | ||
| title: "Failed to resume recording", | ||
| message: error instanceof Error ? error.message : "Unknown error", | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| export default async function Command() { | ||
| const action = { resume_recording: null }; | ||
|
|
||
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
| await open(url); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here: there are two export default functions, so this won’t compile. Dropping the second copy should fix it.
| import { open, showToast, Toast } from "@raycast/api"; | |
| export default async function Command() { | |
| const action = { resume_recording: null }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| try { | |
| await open(url); | |
| await showToast({ style: Toast.Style.Success, title: "Recording resumed" }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to resume recording", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| } | |
| export default async function Command() { | |
| const action = { resume_recording: null }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| await open(url); | |
| } | |
| import { open, showToast, Toast } from "@raycast/api"; | |
| export default async function Command() { | |
| const action = { resume_recording: null }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| try { | |
| await open(url); | |
| await showToast({ style: Toast.Style.Success, title: "Recording resumed" }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to resume recording", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| } |
| export default async function Command() { | ||
| const action = { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, | ||
| camera: null, | ||
| mic_label: null, | ||
| capture_system_audio: false, | ||
| mode: "studio", | ||
| }, | ||
| }; | ||
|
|
||
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
| await open(url); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two export default async function Command() blocks in this file; the second one overrides the intended behavior and will break TypeScript. I'd remove the duplicate block.
| export default async function Command() { | |
| const action = { | |
| start_recording: { | |
| capture_mode: { screen: "Main Display" }, | |
| camera: null, | |
| mic_label: null, | |
| capture_system_audio: false, | |
| mode: "studio", | |
| }, | |
| }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| await open(url); | |
| } |
| export default async function Command() { | ||
| const action = { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: hardcoding "Main Display" will fail on many machines because Cap matches by exact display name. Might be worth switching this command to mode: "view" and letting the user pick/enter the screen name (or at least documenting that it may need editing).
| ```bash | ||
| cd apps/raycast-extension | ||
| pnpm install | ||
| ray dev |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fenced code block here is missing the closing backticks.
| ```bash | |
| cd apps/raycast-extension | |
| pnpm install | |
| ray dev | |
| ```bash | |
| cd apps/raycast-extension | |
| pnpm install | |
| ray dev |
| } | ||
| } | ||
|
|
||
| export default async function Command() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this file accidentally includes two export default async function Command() blocks. TypeScript will error on duplicate default exports; I'd drop the second one (starting at line 28) and keep the try/catch + toast version.
| } | ||
| } | ||
|
|
||
| export default async function Command() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue here: there are two export default async function Command() blocks in one file. Dropping the second export keeps this as a valid no-view command and avoids a duplicate-default-export error.
| }); | ||
| } | ||
| } | ||
| import { open, showToast, Toast } from "@raycast/api"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file currently has a duplicated import and multiple export default command definitions. I’d trim it down to a single command export (keeping the toast/try-catch version) so it compiles and behaves consistently.
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
| // Check if deeplink actions are enabled for sensitive operations | ||
| let requires_permission = matches!( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Repo policy is no inline code comments; can we drop this one?
| // Check if deeplink actions are enabled for sensitive operations | |
| let requires_permission = matches!( | |
| let requires_permission = matches!( |
| } | ||
| } | ||
|
|
||
| export default async function Command() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file has two export default commands now, which will fail to compile in Raycast. Suggest collapsing to a single implementation.
| export default async function Command() { | |
| import { open, showToast, Toast } from "@raycast/api"; | |
| export default async function Command() { | |
| const action = { pause_recording: null }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| try { | |
| await open(url); | |
| await showToast({ style: Toast.Style.Success, title: "Recording paused" }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to pause recording", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| } |
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
|
|
||
| impl DeepLinkAction { | ||
| pub async fn execute(self, app: &AppHandle) -> Result<(), String> { | ||
| // Check if deeplink actions are enabled for sensitive operations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Repo-wide nit: we try to keep the codebase comment-free. This line can probably go since requires_permission already explains intent.
| import { open, showToast, Toast } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| const action = { resume_recording: null }; | ||
|
|
||
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
|
|
||
| try { | ||
| await open(url); | ||
| await showToast({ style: Toast.Style.Success, title: "Recording resumed" }); | ||
| } catch (error) { | ||
| await showToast({ | ||
| style: Toast.Style.Failure, | ||
| title: "Failed to resume recording", | ||
| message: error instanceof Error ? error.message : "Unknown error", | ||
| }); | ||
| } | ||
| } | ||
| import { open, showToast, Toast } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| const action = { resume_recording: null }; | ||
|
|
||
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
|
|
||
| try { | ||
| await open(url); | ||
| await showToast({ style: Toast.Style.Success, title: "Recording resumed" }); | ||
| } catch (error) { | ||
| await showToast({ | ||
| style: Toast.Style.Failure, | ||
| title: "Failed to resume recording", | ||
| message: error instanceof Error ? error.message : "Unknown error", | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a copy/paste duplication slipped in here (second import + second Command()). Suggest trimming it back to a single implementation.
| import { open, showToast, Toast } from "@raycast/api"; | |
| export default async function Command() { | |
| const action = { resume_recording: null }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| try { | |
| await open(url); | |
| await showToast({ style: Toast.Style.Success, title: "Recording resumed" }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to resume recording", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| } | |
| import { open, showToast, Toast } from "@raycast/api"; | |
| export default async function Command() { | |
| const action = { resume_recording: null }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| try { | |
| await open(url); | |
| await showToast({ style: Toast.Style.Success, title: "Recording resumed" }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to resume recording", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| import { open, showToast, Toast } from "@raycast/api"; | |
| export default async function Command() { | |
| const action = { resume_recording: null }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| try { | |
| await open(url); | |
| await showToast({ style: Toast.Style.Success, title: "Resume command sent" }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to resume recording", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| } |
| import { open, showToast, Toast } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| const action = { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, | ||
| camera: null, | ||
| mic_label: null, | ||
| capture_system_audio: false, | ||
| mode: "studio", | ||
| }, | ||
| }; | ||
|
|
||
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
|
|
||
| try { | ||
| await open(url); | ||
| await showToast({ style: Toast.Style.Success, title: "Started recording" }); | ||
| } catch (error) { | ||
| await showToast({ | ||
| style: Toast.Style.Failure, | ||
| title: "Failed to start recording", | ||
| message: error instanceof Error ? error.message : "Unknown error", | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| import { open, showToast, Toast } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| const action = { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, | ||
| camera: null, | ||
| mic_label: null, | ||
| capture_system_audio: false, | ||
| mode: "studio", | ||
| }, | ||
| }; | ||
|
|
||
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
|
|
||
| try { | ||
| await open(url); | ||
| await showToast({ style: Toast.Style.Success, title: "Started recording" }); | ||
| } catch (error) { | ||
| await showToast({ | ||
| style: Toast.Style.Failure, | ||
| title: "Failed to start recording", | ||
| message: error instanceof Error ? error.message : "Unknown error", | ||
| }); | ||
| } | ||
| } | ||
| const action = { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, | ||
| camera: null, | ||
| mic_label: null, | ||
| capture_system_audio: false, | ||
| mode: "studio", | ||
| }, | ||
| }; | ||
|
|
||
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
| await open(url); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file currently has duplicated blocks + stray statements at the end, so it won't typecheck. Also, since we can't confirm Cap executed the action, the success toast is a bit optimistic.
| import { open, showToast, Toast } from "@raycast/api"; | |
| export default async function Command() { | |
| const action = { | |
| start_recording: { | |
| capture_mode: { screen: "Main Display" }, | |
| camera: null, | |
| mic_label: null, | |
| capture_system_audio: false, | |
| mode: "studio", | |
| }, | |
| }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| try { | |
| await open(url); | |
| await showToast({ style: Toast.Style.Success, title: "Started recording" }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to start recording", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| } | |
| import { open, showToast, Toast } from "@raycast/api"; | |
| export default async function Command() { | |
| const action = { | |
| start_recording: { | |
| capture_mode: { screen: "Main Display" }, | |
| camera: null, | |
| mic_label: null, | |
| capture_system_audio: false, | |
| mode: "studio", | |
| }, | |
| }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| try { | |
| await open(url); | |
| await showToast({ style: Toast.Style.Success, title: "Started recording" }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to start recording", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| } | |
| const action = { | |
| start_recording: { | |
| capture_mode: { screen: "Main Display" }, | |
| camera: null, | |
| mic_label: null, | |
| capture_system_audio: false, | |
| mode: "studio", | |
| }, | |
| }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| await open(url); | |
| } | |
| import { open, showToast, Toast } from "@raycast/api"; | |
| export default async function Command() { | |
| const action = { | |
| start_recording: { | |
| capture_mode: { screen: "Main Display" }, | |
| camera: null, | |
| mic_label: null, | |
| capture_system_audio: false, | |
| mode: "studio", | |
| }, | |
| }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| try { | |
| await open(url); | |
| await showToast({ style: Toast.Style.Success, title: "Start command sent" }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to start recording", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| } |
Minor: hardcoding "Main Display" is pretty brittle if display names differ; a small view command to let users pick/enter the name might be safer.
| import { List, ActionPanel, Action, showToast, Toast, open } from "@raycast/api"; | ||
|
|
||
| export default function Command() { | ||
| const isLoading = false; | ||
|
|
||
|
|
||
| const handleSwitchCamera = async (cameraId: string | null) => { | ||
| try { | ||
| // Camera ID can be either a model string or a device ID object | ||
| // For simplicity, we'll use model string format | ||
| const action = { | ||
| switch_camera: { | ||
| const action = { | ||
| switch_camera: { | ||
| camera_id: cameraId ? { DeviceID: cameraId } : null, | ||
| }, | ||
| }; | ||
| }, | ||
| }; | ||
| import { Action, ActionPanel, Form, List, Toast, open, showToast } from "@raycast/api"; | ||
| import { useState } from "react"; | ||
|
|
||
| async function switchCamera(cameraId: string | null) { | ||
| const action = { | ||
| switch_camera: { | ||
| camera_id: cameraId ? { DeviceID: cameraId } : null, | ||
| }, | ||
| }; | ||
|
|
||
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
|
|
||
| try { | ||
| await open(url); | ||
| await showToast({ | ||
| style: Toast.Style.Success, | ||
| title: "Camera switched", | ||
| message: cameraId ? `Switched to ${cameraId}` : "Camera disabled", | ||
| }); | ||
| } catch (error) { | ||
| await showToast({ | ||
| style: Toast.Style.Failure, | ||
| title: "Failed to switch camera", | ||
| message: error instanceof Error ? error.message : "Unknown error", | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| function CameraIdForm() { | ||
| const [cameraId, setCameraId] = useState(""); | ||
|
|
||
| return ( | ||
| <Form | ||
| actions={ | ||
| <ActionPanel> | ||
| <Action.SubmitForm | ||
| title="Switch Camera" | ||
| onSubmit={() => switchCamera(cameraId.trim() || null)} | ||
| /> | ||
| </ActionPanel> | ||
| } | ||
| > | ||
| <Form.TextField | ||
| id="cameraId" | ||
| title="Camera Device ID" | ||
| value={cameraId} | ||
| onChange={setCameraId} | ||
| /> | ||
| </Form> | ||
| ); | ||
| } | ||
|
|
||
| export default function Command() { | ||
| return ( | ||
| <List searchBarPlaceholder="Camera..."> | ||
| <List.Item | ||
| title="Disable Camera" | ||
| subtitle="Turn off camera input" | ||
| actions={ | ||
| <ActionPanel> | ||
| <Action title="Disable Camera" onAction={() => switchCamera(null)} /> | ||
| </ActionPanel> | ||
| } | ||
| /> | ||
| <List.Item | ||
| title="Switch Camera" | ||
| subtitle="Enter a camera device ID" | ||
| actions={ | ||
| <ActionPanel> | ||
| <Action.Push title="Enter Camera ID" target={<CameraIdForm />} /> | ||
| </ActionPanel> | ||
| } | ||
| /> | ||
| </List> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like an unfinished merge/edit (duplicate imports/exports + partial function body) and currently won't compile. Also, we try to keep the repo comment-free.
| import { List, ActionPanel, Action, showToast, Toast, open } from "@raycast/api"; | |
| export default function Command() { | |
| const isLoading = false; | |
| const handleSwitchCamera = async (cameraId: string | null) => { | |
| try { | |
| // Camera ID can be either a model string or a device ID object | |
| // For simplicity, we'll use model string format | |
| const action = { | |
| switch_camera: { | |
| const action = { | |
| switch_camera: { | |
| camera_id: cameraId ? { DeviceID: cameraId } : null, | |
| }, | |
| }; | |
| }, | |
| }; | |
| import { Action, ActionPanel, Form, List, Toast, open, showToast } from "@raycast/api"; | |
| import { useState } from "react"; | |
| async function switchCamera(cameraId: string | null) { | |
| const action = { | |
| switch_camera: { | |
| camera_id: cameraId ? { DeviceID: cameraId } : null, | |
| }, | |
| }; | |
| const url = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| try { | |
| await open(url); | |
| await showToast({ | |
| style: Toast.Style.Success, | |
| title: "Camera switched", | |
| message: cameraId ? `Switched to ${cameraId}` : "Camera disabled", | |
| }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to switch camera", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| } | |
| function CameraIdForm() { | |
| const [cameraId, setCameraId] = useState(""); | |
| return ( | |
| <Form | |
| actions={ | |
| <ActionPanel> | |
| <Action.SubmitForm | |
| title="Switch Camera" | |
| onSubmit={() => switchCamera(cameraId.trim() || null)} | |
| /> | |
| </ActionPanel> | |
| } | |
| > | |
| <Form.TextField | |
| id="cameraId" | |
| title="Camera Device ID" | |
| value={cameraId} | |
| onChange={setCameraId} | |
| /> | |
| </Form> | |
| ); | |
| } | |
| export default function Command() { | |
| return ( | |
| <List searchBarPlaceholder="Camera..."> | |
| <List.Item | |
| title="Disable Camera" | |
| subtitle="Turn off camera input" | |
| actions={ | |
| <ActionPanel> | |
| <Action title="Disable Camera" onAction={() => switchCamera(null)} /> | |
| </ActionPanel> | |
| } | |
| /> | |
| <List.Item | |
| title="Switch Camera" | |
| subtitle="Enter a camera device ID" | |
| actions={ | |
| <ActionPanel> | |
| <Action.Push title="Enter Camera ID" target={<CameraIdForm />} /> | |
| </ActionPanel> | |
| } | |
| /> | |
| </List> | |
| ); | |
| } | |
| import { Action, ActionPanel, Form, List, Toast, open, showToast } from "@raycast/api"; | |
| import { useState } from "react"; | |
| function buildActionUrl(action: unknown) { | |
| return `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| } | |
| async function switchCamera(cameraId: string | null) { | |
| const action = { | |
| switch_camera: { | |
| camera_id: cameraId ? { DeviceID: cameraId } : null, | |
| }, | |
| }; | |
| try { | |
| await open(buildActionUrl(action)); | |
| await showToast({ | |
| style: Toast.Style.Success, | |
| title: "Camera switch command sent", | |
| message: cameraId ?? "Disable camera", | |
| }); | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to switch camera", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| } | |
| function CameraIdForm() { | |
| const [cameraId, setCameraId] = useState(""); | |
| return ( | |
| <Form | |
| actions={ | |
| <ActionPanel> | |
| <Action.SubmitForm title="Switch Camera" onSubmit={() => switchCamera(cameraId.trim() || null)} /> | |
| </ActionPanel> | |
| } | |
| > | |
| <Form.TextField id="cameraId" title="Camera Device ID" value={cameraId} onChange={setCameraId} /> | |
| </Form> | |
| ); | |
| } | |
| export default function Command() { | |
| return ( | |
| <List searchBarPlaceholder="Camera..."> | |
| <List.Item | |
| title="Disable Camera" | |
| subtitle="Turn off camera input" | |
| actions={ | |
| <ActionPanel> | |
| <Action title="Disable Camera" onAction={() => switchCamera(null)} /> | |
| </ActionPanel> | |
| } | |
| /> | |
| <List.Item | |
| title="Switch Camera" | |
| subtitle="Enter a camera device ID" | |
| actions={ | |
| <ActionPanel> | |
| <Action.Push title="Enter Camera ID" target={<CameraIdForm />} /> | |
| </ActionPanel> | |
| } | |
| /> | |
| </List> | |
| ); | |
| } |
/claim #1540
Description
This PR adds deeplink support for pause/resume recording and device switching, plus a Raycast extension to control Cap via these deeplinks.
Changes
Deeplinks Support
PauseRecordingdeeplink actionResumeRecordingdeeplink actionSwitchMicrophonedeeplink action withmic_labelparameterSwitchCameradeeplink action withcamera_idparameterAll new actions follow the same pattern as existing
StartRecordingandStopRecordingactions, using the existing Tauri commands.Raycast Extension
apps/raycast-extension/cap-desktop://deeplink scheme to communicate with the appTesting
Ready for testing. The implementation wires up existing functionality, so it should work with the current recording system.
Greptile Overview
Greptile Summary
This PR adds deeplink support for pause/resume recording and device switching, plus a new Raycast extension to control Cap via these deeplinks.
Key Changes:
DeepLinkActionvariants (PauseRecording,ResumeRecording,SwitchMicrophone,SwitchCamera) that call existing Tauri commandsStartRecording/StopRecordingactionsIssues Found:
icon.pngfile will cause the Raycast extension to fail loadingstart-recording.tshardcodes"Main Display"which will fail on systems with different display namesswitch-camera.tsxmay pass incorrect format forDeviceOrModelIDenum (needs{"device_id": "..."}or{"model_id": {...}}instead of plain string)The Rust deeplink implementation is solid and correctly wires up existing functionality, but the Raycast extension needs polish before it's production-ready.
Confidence Score: 3/5
apps/raycast-extension/src/start-recording.ts(hardcoded display name) andapps/raycast-extension/src/switch-camera.tsx(incorrect enum format)Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant Raycast participant DeeplinkHandler as Cap Desktop<br/>Deeplink Handler participant DeeplinkAction as DeepLinkAction<br/>Executor participant RecordingModule as Recording Module<br/>(Rust) User->>Raycast: Trigger command<br/>(e.g., Pause Recording) Raycast->>Raycast: Build deeplink URL<br/>cap-desktop://action?value={...} Raycast->>DeeplinkHandler: Open deeplink URL DeeplinkHandler->>DeeplinkHandler: Parse URL to DeepLinkAction DeeplinkHandler->>DeeplinkAction: execute() alt PauseRecording DeeplinkAction->>RecordingModule: pause_recording() else ResumeRecording DeeplinkAction->>RecordingModule: resume_recording() else SwitchMicrophone DeeplinkAction->>RecordingModule: set_mic_input(mic_label) else SwitchCamera DeeplinkAction->>RecordingModule: set_camera_input(camera_id) end RecordingModule-->>DeeplinkAction: Result<(), String> DeeplinkAction-->>DeeplinkHandler: Result DeeplinkHandler-->>Raycast: Success/Error Raycast-->>User: Show toast notification(2/5) Greptile learns from your feedback when you react with thumbs up/down!