-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: deeplinks for recording control + Raycast extension #1657
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
Changes from all commits
05df1a0
7eb3bc9
f26ad44
1457cf6
2028a25
fe24c52
77156dd
2644b34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,61 @@ | ||||||||||||||||||||||||||||||||||||
| import { onOpenUrl } from "@tauri-apps/plugin-deep-link"; | ||||||||||||||||||||||||||||||||||||
| import { invoke } from "@tauri-apps/api/core"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export type DeeplinkAction = | ||||||||||||||||||||||||||||||||||||
| | "start-recording" | ||||||||||||||||||||||||||||||||||||
| | "stop-recording" | ||||||||||||||||||||||||||||||||||||
| | "pause-recording" | ||||||||||||||||||||||||||||||||||||
| | "resume-recording" | ||||||||||||||||||||||||||||||||||||
| | "restart-recording" | ||||||||||||||||||||||||||||||||||||
| | "switch-microphone" | ||||||||||||||||||||||||||||||||||||
| | "switch-camera"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export function initDeeplinks() { | ||||||||||||||||||||||||||||||||||||
| onOpenUrl((urls) => { | ||||||||||||||||||||||||||||||||||||
| for (const url of urls) { | ||||||||||||||||||||||||||||||||||||
| handleDeeplink(url); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+18
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Additionally, the project already has a Rust-level Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src/lib/deeplinks.ts
Line: 13-18
Comment:
**`initDeeplinks()` is never called — handler is never registered**
`initDeeplinks` is exported but there is no call site anywhere in the application's startup flow (checked all of `apps/desktop/src`). The `onOpenUrl` subscription is therefore never established, so no deeplink will ever be processed. This function must be called during app initialization (e.g., in the main entry point or root component).
Additionally, the project already has a Rust-level `on_open_url` handler registered in `apps/desktop/src-tauri/src/lib.rs` (via `deeplink_actions.rs`). Adding a second handler at the TypeScript layer means both will race to handle the same URL. The two systems need to be reconciled — either use the existing Rust handler and extend it, or remove the Rust handler and use this TypeScript one exclusively.
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export async function handleDeeplink(url: string) { | ||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| const parsed = new URL(url); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrong URL scheme — handler will never fire
Suggested change
This also means the Raycast commands must use Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src/lib/deeplinks.ts
Line: 22-23
Comment:
**Wrong URL scheme — handler will never fire**
`tauri.conf.json` registers the scheme as `"cap-desktop"`, so the OS dispatches URLs like `cap-desktop://start-recording`. `new URL("cap-desktop://start-recording").protocol` returns `"cap-desktop:"`, not `"cap:"`. The guard on line 23 will always be `true`, silently swallowing every deeplink without performing any action.
```suggestion
if (parsed.protocol !== "cap-desktop:") return;
```
This also means the Raycast commands must use `cap-desktop://…` URLs instead of `cap://…`.
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||
| if (parsed.protocol !== "cap:") return; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const action = parsed.hostname as DeeplinkAction; | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor robustness: |
||||||||||||||||||||||||||||||||||||
| const params = Object.fromEntries(parsed.searchParams.entries()); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| switch (action) { | ||||||||||||||||||||||||||||||||||||
| case "start-recording": | ||||||||||||||||||||||||||||||||||||
| await invoke("start_recording", params); | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||
| case "stop-recording": | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The The URL query params should be mapped to the expected struct fields explicitly, or this action should piggyback on the existing Rust deeplink system ( Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src/lib/deeplinks.ts
Line: 31-33
Comment:
**`start_recording` receives untyped string params**
The `start_recording` command expects a typed `StartRecordingInputs` struct on the Rust side (confirmed in `apps/desktop/src-tauri/src/recording.rs`). Passing `params` — a flat `Record<string, string>` built directly from URL query parameters — will not satisfy that schema and will cause a deserialization error at runtime.
The URL query params should be mapped to the expected struct fields explicitly, or this action should piggyback on the existing Rust deeplink system (`deeplink_actions.rs`) which already handles `StartRecording` with proper JSON-encoded payloads (`?value={…}`).
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||
| await invoke("stop_recording", {}); | ||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||
| case "pause-recording": | ||||||||||||||||||||||||||||||||||||
| await invoke("pause_recording", {}); | ||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||
| case "resume-recording": | ||||||||||||||||||||||||||||||||||||
| await invoke("resume_recording", {}); | ||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||
| case "restart-recording": | ||||||||||||||||||||||||||||||||||||
| await invoke("restart_recording", {}); | ||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||
| case "switch-microphone": { | ||||||||||||||||||||||||||||||||||||
| const deviceId = params["device-id"] ?? null; | ||||||||||||||||||||||||||||||||||||
| await invoke("set_microphone", { deviceId }); | ||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| case "switch-camera": { | ||||||||||||||||||||||||||||||||||||
| const deviceId = params["device-id"] ?? null; | ||||||||||||||||||||||||||||||||||||
| await invoke("set_camera", { deviceId }); | ||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrong Tauri command names for microphone and camera The registered Tauri commands (confirmed in
Suggested change
Note: also verify the parameter key names ( Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src/lib/deeplinks.ts
Line: 47-53
Comment:
**Wrong Tauri command names for microphone and camera**
The registered Tauri commands (confirmed in `apps/desktop/src-tauri/src/lib.rs`, lines 3034–3035) are `set_mic_input` and `set_camera_input`, not `set_microphone` and `set_camera`. Invoking non-existent command names will throw a runtime error.
```suggestion
case "switch-microphone": {
const deviceId = params["device-id"] ?? null;
await invoke("set_mic_input", { label: deviceId });
break;
}
case "switch-camera": {
const deviceId = params["device-id"] ?? null;
await invoke("set_camera_input", { cameraId: deviceId });
break;
}
```
Note: also verify the parameter key names (`label` / `cameraId`) against the Rust function signatures for `set_mic_input` and `set_camera_input`.
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||
| console.warn("Unknown deeplink action:", action); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||
| console.error("Failed to handle deeplink:", url, e); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| { | ||
| "$schema": "https://www.raycast.com/schemas/extension.json", | ||
| "name": "cap", | ||
| "title": "Cap", | ||
| "description": "Control Cap screen recordings from Raycast", | ||
| "icon": "cap-icon.png", | ||
| "author": "cap", | ||
| "categories": ["Applications", "Productivity"], | ||
| "license": "MIT", | ||
| "commands": [ | ||
| { | ||
| "name": "start-recording", | ||
| "title": "Start Recording", | ||
| "description": "Start a new Cap screen recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "stop-recording", | ||
| "title": "Stop Recording", | ||
| "description": "Stop the current Cap screen recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "pause-recording", | ||
| "title": "Pause Recording", | ||
| "description": "Pause the current Cap screen recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "resume-recording", | ||
| "title": "Resume Recording", | ||
| "description": "Resume the paused Cap screen recording", | ||
| "mode": "no-view" | ||
| }, | ||
| { | ||
| "name": "restart-recording", | ||
| "title": "Restart Recording", | ||
| "description": "Restart the current Cap screen recording", | ||
| "mode": "no-view" | ||
| } | ||
| ], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PR description mentions Raycast commands for |
||
| "dependencies": { | ||
| "@raycast/api": "^1.79.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@raycast/utils": "^1.17.0", | ||
| "@types/node": "20.8.10", | ||
| "@types/react": "18.3.3", | ||
| "eslint": "^8.57.0", | ||
| "prettier": "^3.3.3", | ||
| "typescript": "^5.4.5" | ||
| }, | ||
| "scripts": { | ||
| "build": "ray build -e dist", | ||
| "dev": "ray develop", | ||
| "fix-lint": "ray lint --fix", | ||
| "lint": "ray lint", | ||
| "publish": "npx @raycast/api@latest publish" | ||
| } | ||
| } | ||
|
Comment on lines
+1
to
+60
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The PR description explicitly states that The two commands need to be added both to this Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/raycast-extension/package.json
Line: 1-60
Comment:
**`switch-microphone` and `switch-camera` commands are missing**
The PR description explicitly states that `switch-microphone` and `switch-camera` deeplinks accept an optional `?device-id=` query param and are part of the feature. However, neither command has a corresponding source file in `apps/raycast-extension/src/` nor an entry in the `commands` array here.
The two commands need to be added both to this `commands` array and as individual `.ts` source files (e.g., `src/switch-microphone.ts` and `src/switch-camera.ts`) that accept or prompt for a `device-id`.
How can I resolve this? If you propose a fix, please make it concise. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { open, showHUD } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| await open("cap://pause-recording"); | ||
| await showHUD("⏸ Pausing Cap recording…"); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { open, showHUD } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| await open("cap://restart-recording"); | ||
| await showHUD("🔄 Restarting Cap recording…"); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { open, showHUD } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| await open("cap://resume-recording"); | ||
| await showHUD("▶ Resuming Cap recording…"); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { open, showHUD } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| await open("cap://start-recording"); | ||
| await showHUD("▶ Starting Cap recording…"); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { open, showHUD } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| await open("cap://stop-recording"); | ||
| await showHUD("⏹ Stopping Cap recording…"); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,14 @@ | ||||||||||
| { | ||||||||||
| "$schema": "https://www.raycast.com/schemas/extension.json", | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrong The
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/raycast-extension/tsconfig.json
Line: 2
Comment:
**Wrong `$schema` value for a TypeScript config file**
The `$schema` field points to the Raycast *extension* schema (`extension.json`), but this is a `tsconfig.json`. That schema has no knowledge of TypeScript compiler options and will produce false validation errors in any editor that respects `$schema`.
```suggestion
"$schema": "https://json.schemastore.org/tsconfig",
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||
| "compilerOptions": { | ||||||||||
| "strict": true, | ||||||||||
| "module": "CommonJS", | ||||||||||
| "target": "ES2020", | ||||||||||
| "jsx": "react-jsx", | ||||||||||
| "jsxImportSource": "react", | ||||||||||
| "lib": ["ES2020"], | ||||||||||
| "moduleResolution": "node", | ||||||||||
| "allowSyntheticDefaultImports": true, | ||||||||||
| "isolatedModules": true | ||||||||||
| } | ||||||||||
| } | ||||||||||
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.
Dropping the promise here reads like an accident. Using
voidmakes the intent explicit (and avoids any unhandled-promise linting).