From 778d80f5e6724a18d22c564e3aa60771a636d768 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Mon, 13 Oct 2025 14:28:59 +0100
Subject: [PATCH 1/9] feat: Basic window excluding UI
---
.claude/settings.local.json | 21 +-
.../desktop/src-tauri/src/general_settings.rs | 45 ++
apps/desktop/src-tauri/src/lib.rs | 5 +-
apps/desktop/src-tauri/src/recording.rs | 15 +-
apps/desktop/src-tauri/src/thumbnails/mod.rs | 2 +
apps/desktop/src-tauri/src/windows.rs | 67 ++-
.../(window-chrome)/settings/general.tsx | 384 +++++++++++++++++-
apps/desktop/src/utils/tauri.ts | 13 +-
crates/recording/src/capture_pipeline.rs | 4 +-
crates/recording/src/instant_recording.rs | 13 +-
crates/recording/src/lib.rs | 6 +-
.../src/sources/screen_capture/macos.rs | 44 +-
.../src/sources/screen_capture/mod.rs | 76 +++-
crates/recording/src/studio_recording.rs | 20 +-
crates/scap-targets/src/lib.rs | 4 +
crates/scap-targets/src/platform/macos.rs | 36 +-
16 files changed, 695 insertions(+), 60 deletions(-)
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 083ec9bd45..5503f40598 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -1,11 +1,12 @@
{
- "permissions": {
- "allow": [
- "Bash(pnpm typecheck:*)",
- "Bash(pnpm lint:*)",
- "Bash(pnpm build:*)"
- ],
- "deny": [],
- "ask": []
- }
-}
+ "permissions": {
+ "allow": [
+ "Bash(pnpm typecheck:*)",
+ "Bash(pnpm lint:*)",
+ "Bash(pnpm build:*)",
+ "Bash(cargo check:*)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
\ No newline at end of file
diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs
index 771ae56a81..658dbc6545 100644
--- a/apps/desktop/src-tauri/src/general_settings.rs
+++ b/apps/desktop/src-tauri/src/general_settings.rs
@@ -1,3 +1,4 @@
+use cap_recording::sources::screen_capture::WindowExclusion;
use serde::{Deserialize, Serialize};
use serde_json::json;
use specta::Type;
@@ -39,6 +40,30 @@ impl MainWindowRecordingStartBehaviour {
}
}
+const DEFAULT_EXCLUDED_WINDOW_TITLES: &[&str] = &[
+ "Cap",
+ "Cap Setup",
+ "Cap Settings",
+ "Cap Editor",
+ "Cap Mode Selection",
+ "Cap Camera",
+ "Cap Recordings Overlay",
+ "Cap In Progress Recording",
+ "Cap Window Capture Occluder",
+ "Cap Capture Area",
+];
+
+pub fn default_excluded_windows() -> Vec {
+ DEFAULT_EXCLUDED_WINDOW_TITLES
+ .iter()
+ .map(|title| WindowExclusion {
+ bundle_identifier: None,
+ owner_name: None,
+ window_title: Some((*title).to_string()),
+ })
+ .collect()
+}
+
// When adding fields here, #[serde(default)] defines the value to use for existing configurations,
// and `Default::default` defines the value to use for new configurations.
// Things that affect the user experience should only be enabled by default for new configurations.
@@ -99,6 +124,8 @@ pub struct GeneralSettingsStore {
pub post_deletion_behaviour: PostDeletionBehaviour,
#[serde(default = "default_enable_new_uploader", skip_serializing_if = "no")]
pub enable_new_uploader: bool,
+ #[serde(default = "default_excluded_windows")]
+ pub excluded_windows: Vec,
}
fn default_enable_native_camera_preview() -> bool {
@@ -162,6 +189,7 @@ impl Default for GeneralSettingsStore {
enable_new_recording_flow: default_enable_new_recording_flow(),
post_deletion_behaviour: PostDeletionBehaviour::DoNothing,
enable_new_uploader: default_enable_new_uploader(),
+ excluded_windows: default_excluded_windows(),
}
}
}
@@ -213,6 +241,17 @@ impl GeneralSettingsStore {
store.set("general_settings", json!(self));
store.save().map_err(|e| e.to_string())
}
+
+ pub fn is_window_excluded(
+ &self,
+ bundle_identifier: Option<&str>,
+ owner_name: Option<&str>,
+ window_title: Option<&str>,
+ ) -> bool {
+ self.excluded_windows
+ .iter()
+ .any(|entry| entry.matches(bundle_identifier, owner_name, window_title))
+ }
}
pub fn init(app: &AppHandle) {
@@ -231,3 +270,9 @@ pub fn init(app: &AppHandle) {
println!("GeneralSettingsState managed");
}
+
+#[tauri::command]
+#[specta::specta]
+pub fn get_default_excluded_windows() -> Vec {
+ default_excluded_windows()
+}
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index 58ce372540..a1ed9d02ee 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -1914,6 +1914,8 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
recording::list_capture_displays,
recording::list_displays_with_thumbnails,
recording::list_windows_with_thumbnails,
+ windows::refresh_window_content_protection,
+ general_settings::get_default_excluded_windows,
take_screenshot,
list_audio_devices,
close_recordings_overlay_window,
@@ -2014,7 +2016,8 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
.typ::()
.typ::()
.typ::()
- .typ::();
+ .typ::()
+ .typ::();
#[cfg(debug_assertions)]
specta_builder
diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs
index 625f3ecf6c..7d2472ad04 100644
--- a/apps/desktop/src-tauri/src/recording.rs
+++ b/apps/desktop/src-tauri/src/recording.rs
@@ -44,7 +44,9 @@ use crate::{
audio::AppSounds,
auth::AuthStore,
create_screenshot,
- general_settings::{GeneralSettingsStore, PostDeletionBehaviour, PostStudioRecordingBehaviour},
+ general_settings::{
+ self, GeneralSettingsStore, PostDeletionBehaviour, PostStudioRecordingBehaviour,
+ },
open_external_link,
presets::PresetsStore,
thumbnails::*,
@@ -457,6 +459,11 @@ pub async fn start_recording(
recording_dir: recording_dir.clone(),
};
+ let excluded_windows = general_settings
+ .as_ref()
+ .map(|settings| settings.excluded_windows.clone())
+ .unwrap_or_else(general_settings::default_excluded_windows);
+
let actor = match inputs.mode {
RecordingMode::Studio => {
let mut builder = studio_recording::Actor::builder(
@@ -468,7 +475,8 @@ pub async fn start_recording(
general_settings
.map(|s| s.custom_cursor_capture)
.unwrap_or_default(),
- );
+ )
+ .with_excluded_windows(excluded_windows.clone());
if let Some(camera_feed) = camera_feed {
builder = builder.with_camera_feed(camera_feed);
@@ -508,7 +516,8 @@ pub async fn start_recording(
recording_dir.clone(),
inputs.capture_target.clone(),
)
- .with_system_audio(inputs.capture_system_audio);
+ .with_system_audio(inputs.capture_system_audio)
+ .with_excluded_windows(excluded_windows.clone());
if let Some(mic_feed) = mic_feed {
builder = builder.with_mic_feed(mic_feed);
diff --git a/apps/desktop/src-tauri/src/thumbnails/mod.rs b/apps/desktop/src-tauri/src/thumbnails/mod.rs
index c4dbeb9dae..66303520ea 100644
--- a/apps/desktop/src-tauri/src/thumbnails/mod.rs
+++ b/apps/desktop/src-tauri/src/thumbnails/mod.rs
@@ -33,6 +33,7 @@ pub struct CaptureWindowWithThumbnail {
pub refresh_rate: u32,
pub thumbnail: Option,
pub app_icon: Option,
+ pub bundle_identifier: Option,
}
pub fn normalize_thumbnail_dimensions(image: &image::RgbaImage) -> image::RgbaImage {
@@ -140,6 +141,7 @@ pub async fn collect_windows_with_thumbnails() -> Result = LogicalPosition::new(12.0, 12.0);
@@ -250,6 +251,9 @@ impl ShowCapWindow {
.map(|s| s.enable_new_recording_flow)
.unwrap_or_default();
+ let title = CapWindowId::Main.title();
+ let should_protect = should_protect_window(app, &title);
+
let window = self
.window_builder(app, if new_recording_flow { "/new-main" } else { "/" })
.resizable(false)
@@ -258,7 +262,7 @@ impl ShowCapWindow {
.minimizable(false)
.always_on_top(true)
.visible_on_all_workspaces(true)
- .content_protected(false)
+ .content_protected(should_protect)
.center()
.initialization_script(format!(
"
@@ -296,6 +300,12 @@ impl ShowCapWindow {
return Err(tauri::Error::WindowNotFound);
};
+ let title = CapWindowId::TargetSelectOverlay {
+ display_id: display_id.clone(),
+ }
+ .title();
+ let should_protect = should_protect_window(app, &title);
+
let mut window_builder = self
.window_builder(
app,
@@ -305,7 +315,7 @@ impl ShowCapWindow {
.resizable(false)
.fullscreen(false)
.shadow(false)
- .content_protected(true)
+ .content_protected(should_protect)
.always_on_top(true)
.visible_on_all_workspaces(true)
.skip_taskbar(true)
@@ -516,6 +526,12 @@ impl ShowCapWindow {
return Err(tauri::Error::WindowNotFound);
};
+ let title = CapWindowId::WindowCaptureOccluder {
+ screen_id: screen_id.clone(),
+ }
+ .title();
+ let should_protect = should_protect_window(app, &title);
+
#[cfg(target_os = "macos")]
let position = display.raw_handle().logical_position();
@@ -532,7 +548,7 @@ impl ShowCapWindow {
.shadow(false)
.always_on_top(true)
.visible_on_all_workspaces(true)
- .content_protected(true)
+ .content_protected(should_protect)
.skip_taskbar(true)
.inner_size(bounds.width(), bounds.height())
.position(position.x(), position.y())
@@ -550,13 +566,16 @@ impl ShowCapWindow {
window
}
Self::CaptureArea { screen_id } => {
+ let title = CapWindowId::CaptureArea.title();
+ let should_protect = should_protect_window(app, &title);
+
let mut window_builder = self
.window_builder(app, "/capture-area")
.maximized(false)
.fullscreen(false)
.shadow(false)
.always_on_top(true)
- .content_protected(true)
+ .content_protected(should_protect)
.skip_taskbar(true)
.closable(true)
.decorations(false)
@@ -604,6 +623,9 @@ impl ShowCapWindow {
let width = 250.0;
let height = 40.0;
+ let title = CapWindowId::InProgressRecording.title();
+ let should_protect = should_protect_window(app, &title);
+
let window = self
.window_builder(app, "/in-progress-recording")
.maximized(false)
@@ -613,7 +635,7 @@ impl ShowCapWindow {
.always_on_top(true)
.transparent(true)
.visible_on_all_workspaces(true)
- .content_protected(true)
+ .content_protected(should_protect)
.inner_size(width, height)
.position(
((monitor.size().width as f64) / monitor.scale_factor() - width) / 2.0,
@@ -634,6 +656,9 @@ impl ShowCapWindow {
window
}
Self::RecordingsOverlay => {
+ let title = CapWindowId::RecordingsOverlay.title();
+ let should_protect = should_protect_window(app, &title);
+
let window = self
.window_builder(app, "/recordings-overlay")
.maximized(false)
@@ -643,7 +668,7 @@ impl ShowCapWindow {
.always_on_top(true)
.visible_on_all_workspaces(true)
.accept_first_mouse(true)
- .content_protected(true)
+ .content_protected(should_protect)
.inner_size(
(monitor.size().width as f64) / monitor.scale_factor(),
(monitor.size().height as f64) / monitor.scale_factor(),
@@ -840,6 +865,34 @@ fn position_traffic_lights_impl(
.ok();
}
+fn should_protect_window(app: &AppHandle, window_title: &str) -> bool {
+ let matches = |list: &[WindowExclusion]| {
+ list.iter()
+ .any(|entry| entry.matches(None, None, Some(window_title)))
+ };
+
+ GeneralSettingsStore::get(app)
+ .ok()
+ .flatten()
+ .map(|settings| matches(&settings.excluded_windows))
+ .unwrap_or_else(|| matches(&general_settings::default_excluded_windows()))
+}
+
+#[tauri::command]
+#[specta::specta]
+pub fn refresh_window_content_protection(app: AppHandle) -> Result<(), String> {
+ for (label, window) in app.webview_windows() {
+ if let Ok(id) = CapWindowId::from_str(&label) {
+ let title = id.title();
+ window
+ .set_content_protected(should_protect_window(&app, &title))
+ .map_err(|e| e.to_string())?;
+ }
+ }
+
+ Ok(())
+}
+
// Credits: tauri-plugin-window-state
trait MonitorExt {
fn intersects(
diff --git a/apps/desktop/src/routes/(window-chrome)/settings/general.tsx b/apps/desktop/src/routes/(window-chrome)/settings/general.tsx
index 853973d0e2..dcc61dcf57 100644
--- a/apps/desktop/src/routes/(window-chrome)/settings/general.tsx
+++ b/apps/desktop/src/routes/(window-chrome)/settings/general.tsx
@@ -6,33 +6,145 @@ import {
} from "@tauri-apps/plugin-notification";
import { type OsType, type } from "@tauri-apps/plugin-os";
import "@total-typescript/ts-reset/filter-boolean";
-import { CheckMenuItem, Menu } from "@tauri-apps/api/menu";
+import { CheckMenuItem, Menu, MenuItem } from "@tauri-apps/api/menu";
import { confirm } from "@tauri-apps/plugin-dialog";
import { cx } from "cva";
-import { createResource, For, Show } from "solid-js";
-import { createStore } from "solid-js/store";
+import { createEffect, createMemo, createResource, For, Show } from "solid-js";
+import { createStore, reconcile } from "solid-js/store";
import themePreviewAuto from "~/assets/theme-previews/auto.jpg";
import themePreviewDark from "~/assets/theme-previews/dark.jpg";
import themePreviewLight from "~/assets/theme-previews/light.jpg";
import { Input } from "~/routes/editor/ui";
import { authStore, generalSettingsStore } from "~/store";
+import IconLucidePlus from "~icons/lucide/plus";
+import IconLucideX from "~icons/lucide/x";
import {
type AppTheme,
+ type CaptureWindowWithThumbnail,
commands,
type GeneralSettingsStore,
+ type WindowExclusion,
type MainWindowRecordingStartBehaviour,
type PostDeletionBehaviour,
type PostStudioRecordingBehaviour,
} from "~/utils/tauri";
import { Setting, ToggleSetting } from "./Setting";
+const getExclusionPrimaryLabel = (entry: WindowExclusion) =>
+ entry.ownerName ?? entry.windowTitle ?? entry.bundleIdentifier ?? "Unknown";
+
+const getExclusionSecondaryLabel = (entry: WindowExclusion) => {
+ if (entry.ownerName && entry.windowTitle) {
+ return entry.windowTitle;
+ }
+
+ if (entry.bundleIdentifier && (entry.ownerName || entry.windowTitle)) {
+ return entry.bundleIdentifier;
+ }
+
+ return entry.bundleIdentifier ?? null;
+};
+
+const getWindowOptionLabel = (window: CaptureWindowWithThumbnail) => {
+ const parts = [window.owner_name];
+ if (window.name && window.name !== window.owner_name) {
+ parts.push(window.name);
+ }
+ return parts.join(" • ");
+};
+
+type LegacyWindowExclusion = {
+ bundle_identifier?: string | null;
+ owner_name?: string | null;
+ window_title?: string | null;
+};
+
+const coerceWindowExclusion = (entry: WindowExclusion | LegacyWindowExclusion): WindowExclusion => {
+ if (entry && typeof entry === "object") {
+ if ("bundleIdentifier" in entry || "ownerName" in entry || "windowTitle" in entry) {
+ const current = entry as WindowExclusion;
+ return {
+ bundleIdentifier: current.bundleIdentifier ?? null,
+ ownerName: current.ownerName ?? null,
+ windowTitle: current.windowTitle ?? null,
+ };
+ }
+
+ const legacy = entry as LegacyWindowExclusion;
+ return {
+ bundleIdentifier: legacy.bundle_identifier ?? null,
+ ownerName: legacy.owner_name ?? null,
+ windowTitle: legacy.window_title ?? null,
+ };
+ }
+
+ return {
+ bundleIdentifier: null,
+ ownerName: null,
+ windowTitle: null,
+ };
+};
+
+const normalizeWindowExclusions = (
+ entries: (WindowExclusion | LegacyWindowExclusion)[],
+): WindowExclusion[] =>
+ entries.map((entry) => {
+ const coerced = coerceWindowExclusion(entry);
+ const bundleIdentifier = coerced.bundleIdentifier ?? null;
+ const ownerName = coerced.ownerName ?? null;
+ const hasBundleIdentifier =
+ typeof bundleIdentifier === "string" && bundleIdentifier.length > 0;
+ return {
+ bundleIdentifier,
+ ownerName,
+ windowTitle: hasBundleIdentifier ? null : coerced.windowTitle ?? null,
+ } satisfies WindowExclusion;
+ });
+
+const createDefaultGeneralSettings = (): GeneralSettingsStore => ({
+ uploadIndividualFiles: false,
+ hideDockIcon: false,
+ autoCreateShareableLink: false,
+ enableNotifications: true,
+ enableNativeCameraPreview: false,
+ enableNewRecordingFlow: false,
+ autoZoomOnClicks: false,
+ custom_cursor_capture2: true,
+ enableNewUploader: false,
+ excludedWindows: normalizeWindowExclusions([]),
+});
+
+const deriveInitialSettings = (
+ store: GeneralSettingsStore | null,
+): GeneralSettingsStore => {
+ const defaults = createDefaultGeneralSettings();
+ if (!store) return defaults;
+
+ const { excluded_windows, ...rest } = store as GeneralSettingsStore & {
+ excluded_windows?: LegacyWindowExclusion[];
+ };
+
+ const rawExcludedWindows = (
+ store.excludedWindows ?? excluded_windows ?? []
+ ) as (WindowExclusion | LegacyWindowExclusion)[];
+
+ return {
+ ...defaults,
+ ...rest,
+ excludedWindows: normalizeWindowExclusions(rawExcludedWindows),
+ };
+};
+
export default function GeneralSettings() {
- const [store] = createResource(() => generalSettingsStore.get());
+ const [store] = createResource(generalSettingsStore.get, {
+ initialValue: null,
+ });
return (
-
- {(store) => }
-
+
);
}
@@ -107,21 +219,22 @@ function AppearanceSection(props: {
);
}
-function Inner(props: { initialStore: GeneralSettingsStore | null }) {
+function Inner(props: {
+ initialStore: GeneralSettingsStore | null;
+ isStoreLoading: boolean;
+}) {
const [settings, setSettings] = createStore(
- props.initialStore ?? {
- uploadIndividualFiles: false,
- hideDockIcon: false,
- autoCreateShareableLink: false,
- enableNotifications: true,
- enableNativeCameraPreview: false,
- enableNewRecordingFlow: false,
- autoZoomOnClicks: false,
- custom_cursor_capture2: true,
- enableNewUploader: false,
- },
+ deriveInitialSettings(props.initialStore),
);
+ createEffect(() => {
+ setSettings(reconcile(deriveInitialSettings(props.initialStore)));
+ });
+
+ const [windows] = createResource(commands.listWindowsWithThumbnails, {
+ initialValue: [] as CaptureWindowWithThumbnail[],
+ });
+
const handleChange = async (
key: K,
value: (typeof settings)[K],
@@ -133,6 +246,92 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) {
};
const ostype: OsType = type();
+ const excludedWindows = createMemo(() => settings.excludedWindows ?? []);
+ const isExcludedWindowsLoading = createMemo(
+ () => props.isStoreLoading || windows.loading,
+ );
+
+ const matchesExclusion = (exclusion: WindowExclusion, window: CaptureWindowWithThumbnail) => {
+ const bundleMatch = exclusion.bundleIdentifier
+ ? window.bundle_identifier === exclusion.bundleIdentifier
+ : false;
+ if (bundleMatch) return true;
+
+ const ownerMatch = exclusion.ownerName
+ ? window.owner_name === exclusion.ownerName
+ : false;
+
+ if (exclusion.ownerName && exclusion.windowTitle) {
+ return ownerMatch && window.name === exclusion.windowTitle;
+ }
+
+ if (ownerMatch && exclusion.ownerName) {
+ return true;
+ }
+
+ if (exclusion.windowTitle) {
+ return window.name === exclusion.windowTitle;
+ }
+
+ return false;
+ };
+
+ const isManagedWindowsApp = (window: CaptureWindowWithThumbnail) => {
+ const bundle = window.bundle_identifier?.toLowerCase() ?? "";
+ if (bundle.includes("so.cap.desktop")) {
+ return true;
+ }
+ return window.owner_name.toLowerCase().includes("cap");
+ };
+
+ const availableWindows = createMemo(() => {
+ const data = windows() ?? [];
+ return data.filter((window) => {
+ if (excludedWindows().some((entry) => matchesExclusion(entry, window))) {
+ return false;
+ }
+ if (ostype === "windows") {
+ return isManagedWindowsApp(window);
+ }
+ return true;
+ });
+ });
+
+ const applyExcludedWindows = async (next: WindowExclusion[]) => {
+ const normalized = normalizeWindowExclusions(next);
+ setSettings("excludedWindows", normalized);
+ try {
+ await generalSettingsStore.set({ excludedWindows: normalized });
+ await commands.refreshWindowContentProtection();
+ } catch (error) {
+ console.error("Failed to update excluded windows", error);
+ }
+ };
+
+ const handleRemoveExclusion = async (index: number) => {
+ const current = [...excludedWindows()];
+ current.splice(index, 1);
+ await applyExcludedWindows(current);
+ };
+
+ const handleAddWindow = async (window: CaptureWindowWithThumbnail) => {
+ const windowTitle = window.bundle_identifier ? null : window.name;
+
+ const next = [
+ ...excludedWindows(),
+ {
+ bundleIdentifier: window.bundle_identifier ?? null,
+ ownerName: window.owner_name ?? null,
+ windowTitle,
+ },
+ ];
+ await applyExcludedWindows(next);
+ };
+
+ const handleResetExclusions = async () => {
+ const defaults = await commands.getDefaultExcludedWindows();
+ await applyExcludedWindows(defaults);
+ };
type ToggleSettingItem = {
label: string;
@@ -483,10 +682,20 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) {
-
+
)}
+
+
{
@@ -545,3 +754,138 @@ function ServerURLSetting(props: {
);
}
+
+function ExcludedWindowsCard(props: {
+ excludedWindows: WindowExclusion[];
+ availableWindows: CaptureWindowWithThumbnail[];
+ onRemove: (index: number) => Promise;
+ onAdd: (window: CaptureWindowWithThumbnail) => Promise;
+ onReset: () => Promise;
+ isLoading: boolean;
+ isWindows: boolean;
+}) {
+ const hasExclusions = () => props.excludedWindows.length > 0;
+ const canAdd = () => props.availableWindows.length > 0 && !props.isLoading;
+
+ const handleAddClick = async () => {
+ if (!canAdd()) return;
+
+ const items = await Promise.all(
+ props.availableWindows.map((window) =>
+ MenuItem.new({
+ text: getWindowOptionLabel(window),
+ action: () => {
+ void props.onAdd(window);
+ },
+ }),
+ ),
+ );
+
+ const menu = await Menu.new({ items });
+ await menu.popup();
+ await menu.close();
+ };
+
+ return (
+
+
+
+
Excluded Windows
+
+ Choose which windows Cap hides from your recordings.
+
+
+
+ Only Cap windows can be excluded on Windows.
+
+
+
+
+
+
+
+
+
}
+ >
+
+ No windows are currently excluded.
+
+ }
+ >
+
+
+ {(entry, index) => (
+
+
+
+ {getExclusionPrimaryLabel(entry)}
+
+
+ {(label) => (
+
+ {label()}
+
+ )}
+
+
+
+
+ )}
+
+
+
+
+
+ );
+}
+
+function ExcludedWindowsSkeleton() {
+ const chipWidths = ["w-32", "w-28", "w-36"] as const;
+
+ return (
+
+
+ {(width) => (
+
+ )}
+
+
+ );
+}
diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts
index cfece4f3a0..76e7f90b9b 100644
--- a/apps/desktop/src/utils/tauri.ts
+++ b/apps/desktop/src/utils/tauri.ts
@@ -44,6 +44,12 @@ async listDisplaysWithThumbnails() : Promise {
async listWindowsWithThumbnails() : Promise {
return await TAURI_INVOKE("list_windows_with_thumbnails");
},
+async refreshWindowContentProtection() : Promise {
+ return await TAURI_INVOKE("refresh_window_content_protection");
+},
+async getDefaultExcludedWindows() : Promise {
+ return await TAURI_INVOKE("get_default_excluded_windows");
+},
async takeScreenshot() : Promise {
return await TAURI_INVOKE("take_screenshot");
},
@@ -365,8 +371,8 @@ export type CaptionSettings = { enabled: boolean; font: string; size: number; co
export type CaptionsData = { segments: CaptionSegment[]; settings: CaptionSettings }
export type CaptureDisplay = { id: DisplayId; name: string; refresh_rate: number }
export type CaptureDisplayWithThumbnail = { id: DisplayId; name: string; refresh_rate: number; thumbnail: string | null }
-export type CaptureWindow = { id: WindowId; owner_name: string; name: string; bounds: LogicalBounds; refresh_rate: number }
-export type CaptureWindowWithThumbnail = { id: WindowId; owner_name: string; name: string; bounds: LogicalBounds; refresh_rate: number; thumbnail: string | null; app_icon: string | null }
+export type CaptureWindow = { id: WindowId; owner_name: string; name: string; bounds: LogicalBounds; refresh_rate: number; bundle_identifier: string | null }
+export type CaptureWindowWithThumbnail = { id: WindowId; owner_name: string; name: string; bounds: LogicalBounds; refresh_rate: number; thumbnail: string | null; app_icon: string | null; bundle_identifier: string | null }
export type ClipConfiguration = { index: number; offsets: ClipOffsets }
export type ClipOffsets = { camera?: number; mic?: number; system_audio?: number }
export type CommercialLicense = { licenseKey: string; expiryDate: number | null; refresh: number; activatedOn: number }
@@ -390,7 +396,7 @@ export type ExportSettings = ({ format: "Mp4" } & Mp4ExportSettings) | ({ format
export type FileType = "recording" | "screenshot"
export type Flags = { captions: boolean }
export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" }
-export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; custom_cursor_capture2?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour; enableNewUploader: boolean }
+export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; custom_cursor_capture2?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour; enableNewUploader: boolean; excludedWindows?: WindowExclusion[] }
export type GifExportSettings = { fps: number; resolution_base: XY; quality: GifQuality | null }
export type GifQuality = {
/**
@@ -479,6 +485,7 @@ export type VideoMeta = { path: string; fps?: number;
start_time?: number | null }
export type VideoRecordingMetadata = { duration: number; size: number }
export type VideoUploadInfo = { id: string; link: string; config: S3UploadMeta }
+export type WindowExclusion = { bundleIdentifier?: string | null; ownerName?: string | null; windowTitle?: string | null }
export type WindowId = string
export type WindowUnderCursor = { id: WindowId; app_name: string; bounds: LogicalBounds }
export type XY = { x: T; y: T }
diff --git a/crates/recording/src/capture_pipeline.rs b/crates/recording/src/capture_pipeline.rs
index de19cf4587..328d0ef325 100644
--- a/crates/recording/src/capture_pipeline.rs
+++ b/crates/recording/src/capture_pipeline.rs
@@ -3,7 +3,7 @@ use crate::{
output_pipeline::*,
sources,
sources::screen_capture::{
- self, ScreenCaptureConfig, ScreenCaptureFormat, ScreenCaptureTarget,
+ self, ScreenCaptureConfig, ScreenCaptureFormat, ScreenCaptureTarget, WindowExclusion,
},
};
use cap_timestamp::Timestamps;
@@ -134,6 +134,7 @@ pub async fn create_screen_capture(
system_audio: bool,
#[cfg(windows)] d3d_device: ::windows::Win32::Graphics::Direct3D11::ID3D11Device,
#[cfg(target_os = "macos")] shareable_content: cidre::arc::R,
+ excluded_windows: &[WindowExclusion],
) -> anyhow::Result> {
Ok(ScreenCaptureConfig::::init(
capture_target,
@@ -145,6 +146,7 @@ pub async fn create_screen_capture(
d3d_device,
#[cfg(target_os = "macos")]
shareable_content,
+ excluded_windows.to_vec(),
)
.await?)
}
diff --git a/crates/recording/src/instant_recording.rs b/crates/recording/src/instant_recording.rs
index 400dd6a27d..e29ab77b8f 100644
--- a/crates/recording/src/instant_recording.rs
+++ b/crates/recording/src/instant_recording.rs
@@ -1,9 +1,9 @@
use crate::{
RecordingBaseInputs,
capture_pipeline::{MakeCapturePipeline, ScreenCaptureMethod, Stop, create_screen_capture},
- feeds::{camera::CameraFeedLock, microphone::MicrophoneFeedLock},
+ feeds::microphone::MicrophoneFeedLock,
output_pipeline::{self, OutputPipeline},
- sources::screen_capture::{ScreenCaptureConfig, ScreenCaptureTarget},
+ sources::screen_capture::{ScreenCaptureConfig, ScreenCaptureTarget, WindowExclusion},
};
use cap_media_info::{AudioInfo, VideoInfo};
use cap_project::InstantRecordingMeta;
@@ -220,6 +220,7 @@ pub struct ActorBuilder {
capture_target: ScreenCaptureTarget,
system_audio: bool,
mic_feed: Option>,
+ excluded_windows: Vec,
}
impl ActorBuilder {
@@ -229,6 +230,7 @@ impl ActorBuilder {
capture_target,
system_audio: false,
mic_feed: None,
+ excluded_windows: Vec::new(),
}
}
@@ -242,6 +244,11 @@ impl ActorBuilder {
self
}
+ pub fn with_excluded_windows(mut self, excluded_windows: Vec) -> Self {
+ self.excluded_windows = excluded_windows;
+ self
+ }
+
pub async fn build(
self,
#[cfg(target_os = "macos")] shareable_content: cidre::arc::R,
@@ -255,6 +262,7 @@ impl ActorBuilder {
camera_feed: None,
#[cfg(target_os = "macos")]
shareable_content,
+ excluded_windows: self.excluded_windows,
},
)
.await
@@ -290,6 +298,7 @@ pub async fn spawn_instant_recording_actor(
d3d_device,
#[cfg(target_os = "macos")]
inputs.shareable_content.retained(),
+ &inputs.excluded_windows,
)
.await?;
diff --git a/crates/recording/src/lib.rs b/crates/recording/src/lib.rs
index a4315c1b06..3f3c5b125b 100644
--- a/crates/recording/src/lib.rs
+++ b/crates/recording/src/lib.rs
@@ -18,7 +18,10 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc;
use thiserror::Error;
-use crate::{feeds::camera::CameraFeedLock, sources::screen_capture::ScreenCaptureTarget};
+use crate::{
+ feeds::camera::CameraFeedLock,
+ sources::screen_capture::{ScreenCaptureTarget, WindowExclusion},
+};
#[derive(specta::Type, Serialize, Deserialize, Clone, Debug, Copy, Default)]
#[serde(rename_all = "camelCase")]
@@ -48,6 +51,7 @@ pub struct RecordingBaseInputs {
pub camera_feed: Option>,
#[cfg(target_os = "macos")]
pub shareable_content: cidre::arc::R,
+ pub excluded_windows: Vec,
}
#[derive(specta::Type, Serialize, Deserialize, Clone, Debug)]
diff --git a/crates/recording/src/sources/screen_capture/macos.rs b/crates/recording/src/sources/screen_capture/macos.rs
index 4ef009f18f..bcfec7118a 100644
--- a/crates/recording/src/sources/screen_capture/macos.rs
+++ b/crates/recording/src/sources/screen_capture/macos.rs
@@ -9,9 +9,12 @@ use crate::{
use anyhow::{Context, anyhow};
use cidre::*;
use futures::{FutureExt, channel::mpsc, future::BoxFuture};
-use std::sync::{
- Arc,
- atomic::{self, AtomicBool},
+use std::{
+ collections::HashSet,
+ sync::{
+ Arc,
+ atomic::{self, AtomicBool},
+ },
};
use tokio::sync::broadcast;
use tracing::debug;
@@ -75,9 +78,42 @@ impl ScreenCaptureConfig {
let display = Display::from_id(&self.config.display)
.ok_or_else(|| SourceError::NoDisplay(self.config.display.clone()))?;
+ let excluded_sc_windows = if self.excluded_windows.is_empty() {
+ Vec::new()
+ } else {
+ let mut collected = Vec::new();
+
+ for window in Window::list() {
+ let owner_name = window.owner_name();
+ let window_title = window.name();
+ let bundle_identifier = window.bundle_identifier();
+
+ if self.excluded_windows.iter().any(|entry| {
+ entry.matches(
+ bundle_identifier.as_deref(),
+ owner_name.as_deref(),
+ window_title.as_deref(),
+ )
+ }) {
+ if let Some(sc_window) = window
+ .raw_handle()
+ .as_sc(self.shareable_content.clone())
+ .await
+ {
+ collected.push(sc_window);
+ }
+ }
+ }
+
+ collected
+ };
+
let content_filter = display
.raw_handle()
- .as_content_filter(self.shareable_content.clone())
+ .as_content_filter_excluding_windows(
+ self.shareable_content.clone(),
+ excluded_sc_windows,
+ )
.await
.ok_or_else(|| SourceError::AsContentFilter)?;
diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs
index ba42979807..af82aef42b 100644
--- a/crates/recording/src/sources/screen_capture/mod.rs
+++ b/crates/recording/src/sources/screen_capture/mod.rs
@@ -32,6 +32,7 @@ pub struct CaptureWindow {
pub name: String,
pub bounds: LogicalBounds,
pub refresh_rate: u32,
+ pub bundle_identifier: Option,
}
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
@@ -47,6 +48,66 @@ pub struct CaptureArea {
pub bounds: LogicalBounds,
}
+#[derive(Debug, Clone, Serialize, Deserialize, Type, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct WindowExclusion {
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub bundle_identifier: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub owner_name: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub window_title: Option,
+}
+
+impl WindowExclusion {
+ pub fn matches(
+ &self,
+ bundle_identifier: Option<&str>,
+ owner_name: Option<&str>,
+ window_title: Option<&str>,
+ ) -> bool {
+ if let Some(identifier) = self.bundle_identifier.as_deref() {
+ if bundle_identifier
+ .map(|candidate| candidate == identifier)
+ .unwrap_or(false)
+ {
+ return true;
+ }
+ }
+
+ if let Some(expected_owner) = self.owner_name.as_deref() {
+ let owner_matches = owner_name
+ .map(|candidate| candidate == expected_owner)
+ .unwrap_or(false);
+
+ if self.window_title.is_some() {
+ return owner_matches
+ && self
+ .window_title
+ .as_deref()
+ .map(|expected_title| {
+ window_title
+ .map(|candidate| candidate == expected_title)
+ .unwrap_or(false)
+ })
+ .unwrap_or(false);
+ }
+
+ if owner_matches {
+ return true;
+ }
+ }
+
+ if let Some(expected_title) = self.window_title.as_deref() {
+ return window_title
+ .map(|candidate| candidate == expected_title)
+ .unwrap_or(false);
+ }
+
+ false
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase", tag = "variant")]
pub enum ScreenCaptureTarget {
@@ -198,6 +259,7 @@ pub struct ScreenCaptureConfig {
d3d_device: ::windows::Win32::Graphics::Direct3D11::ID3D11Device,
#[cfg(target_os = "macos")]
shareable_content: cidre::arc::R,
+ pub excluded_windows: Vec,
}
impl std::fmt::Debug for ScreenCaptureConfig {
@@ -234,6 +296,7 @@ impl Clone for ScreenCaptureConfig ScreenCaptureConfig {
system_audio: bool,
#[cfg(windows)] d3d_device: ::windows::Win32::Graphics::Direct3D11::ID3D11Device,
#[cfg(target_os = "macos")] shareable_content: cidre::arc::R,
+ excluded_windows: Vec,
) -> Result {
cap_fail::fail!("ScreenCaptureSource::init");
@@ -401,6 +465,7 @@ impl ScreenCaptureConfig {
d3d_device,
#[cfg(target_os = "macos")]
shareable_content: shareable_content.retained(),
+ excluded_windows,
})
}
@@ -464,13 +529,22 @@ pub fn list_windows() -> Vec<(CaptureWindow, Window)> {
}
}
+ let owner_name = v.owner_name()?;
+
+ #[cfg(target_os = "macos")]
+ let bundle_identifier = v.raw_handle().bundle_identifier();
+
+ #[cfg(not(target_os = "macos"))]
+ let bundle_identifier = None;
+
Some((
CaptureWindow {
id: v.id(),
name,
- owner_name: v.owner_name()?,
+ owner_name,
bounds: v.display_relative_logical_bounds()?,
refresh_rate: v.display()?.raw_handle().refresh_rate() as u32,
+ bundle_identifier,
},
v,
))
diff --git a/crates/recording/src/studio_recording.rs b/crates/recording/src/studio_recording.rs
index dfe1d4c4f1..f1fd85f692 100644
--- a/crates/recording/src/studio_recording.rs
+++ b/crates/recording/src/studio_recording.rs
@@ -4,18 +4,14 @@ use crate::{
cursor::{CursorActor, Cursors, spawn_cursor_recorder},
feeds::{camera::CameraFeedLock, microphone::MicrophoneFeedLock},
ffmpeg::{Mp4Muxer, OggMuxer},
- output_pipeline::{
- AudioFrame, DoneFut, FinishedOutputPipeline, OutputPipeline, PipelineDoneError,
- },
+ output_pipeline::{DoneFut, FinishedOutputPipeline, OutputPipeline, PipelineDoneError},
sources::{self, screen_capture},
};
use anyhow::{Context as _, anyhow};
use cap_media_info::VideoInfo;
use cap_project::{CursorEvents, StudioRecordingMeta};
use cap_timestamp::{Timestamp, Timestamps};
-use futures::{
- FutureExt, StreamExt, channel::mpsc, future::OptionFuture, stream::FuturesUnordered,
-};
+use futures::{FutureExt, StreamExt, future::OptionFuture, stream::FuturesUnordered};
use kameo::{Actor as _, prelude::*};
use relative_path::RelativePathBuf;
use std::{
@@ -348,6 +344,7 @@ pub struct ActorBuilder {
mic_feed: Option>,
camera_feed: Option>,
custom_cursor: bool,
+ excluded_windows: Vec,
}
impl ActorBuilder {
@@ -359,6 +356,7 @@ impl ActorBuilder {
mic_feed: None,
camera_feed: None,
custom_cursor: false,
+ excluded_windows: Vec::new(),
}
}
@@ -382,6 +380,14 @@ impl ActorBuilder {
self
}
+ pub fn with_excluded_windows(
+ mut self,
+ excluded_windows: Vec,
+ ) -> Self {
+ self.excluded_windows = excluded_windows;
+ self
+ }
+
pub async fn build(
self,
#[cfg(target_os = "macos")] shareable_content: cidre::arc::R,
@@ -395,6 +401,7 @@ impl ActorBuilder {
camera_feed: self.camera_feed,
#[cfg(target_os = "macos")]
shareable_content,
+ excluded_windows: self.excluded_windows,
},
self.custom_cursor,
)
@@ -693,6 +700,7 @@ async fn create_segment_pipeline(
d3d_device,
#[cfg(target_os = "macos")]
base_inputs.shareable_content,
+ &base_inputs.excluded_windows,
)
.await
.unwrap();
diff --git a/crates/scap-targets/src/lib.rs b/crates/scap-targets/src/lib.rs
index be57e60736..357dd23aed 100644
--- a/crates/scap-targets/src/lib.rs
+++ b/crates/scap-targets/src/lib.rs
@@ -139,6 +139,10 @@ impl Window {
self.0.app_icon()
}
+ pub fn bundle_identifier(&self) -> Option {
+ self.0.bundle_identifier()
+ }
+
pub fn raw_handle(&self) -> &WindowImpl {
&self.0
}
diff --git a/crates/scap-targets/src/platform/macos.rs b/crates/scap-targets/src/platform/macos.rs
index 8201014158..e3b87326b5 100644
--- a/crates/scap-targets/src/platform/macos.rs
+++ b/crates/scap-targets/src/platform/macos.rs
@@ -20,7 +20,6 @@ use core_graphics::{
};
use crate::bounds::{LogicalBounds, LogicalPosition, LogicalSize, PhysicalSize};
-use tracing::trace;
#[derive(Clone, Copy)]
pub struct DisplayImpl(CGDisplay);
@@ -348,6 +347,41 @@ impl WindowImpl {
}
}
+ pub fn bundle_identifier(&self) -> Option {
+ let pid = self.owner_pid()?;
+
+ unsafe {
+ use cocoa::base::id;
+ use cocoa::foundation::NSString;
+ use objc::{class, msg_send, sel, sel_impl};
+
+ let app: id = msg_send![
+ class!(NSRunningApplication),
+ runningApplicationWithProcessIdentifier: pid
+ ];
+
+ if app.is_null() {
+ return None;
+ }
+
+ let bundle_identifier: id = msg_send![app, bundleIdentifier];
+ if bundle_identifier.is_null() {
+ return None;
+ }
+
+ let cstr = NSString::UTF8String(bundle_identifier);
+ if cstr.is_null() {
+ return None;
+ }
+
+ Some(
+ std::ffi::CStr::from_ptr(cstr)
+ .to_string_lossy()
+ .into_owned(),
+ )
+ }
+ }
+
pub fn name(&self) -> Option {
let windows =
core_graphics::window::copy_window_info(kCGWindowListOptionIncludingWindow, self.0)?;
From 8d4239fa6551eb71e52160189958fc2adbfaec40 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Mon, 13 Oct 2025 16:08:23 +0100
Subject: [PATCH 2/9] feat: Cleanup bits
---
.../desktop/src-tauri/src/general_settings.rs | 8 +-
.../(window-chrome)/settings/general.tsx | 182 ++++++++++++------
2 files changed, 129 insertions(+), 61 deletions(-)
diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs
index 658dbc6545..9ffe29856a 100644
--- a/apps/desktop/src-tauri/src/general_settings.rs
+++ b/apps/desktop/src-tauri/src/general_settings.rs
@@ -42,15 +42,9 @@ impl MainWindowRecordingStartBehaviour {
const DEFAULT_EXCLUDED_WINDOW_TITLES: &[&str] = &[
"Cap",
- "Cap Setup",
"Cap Settings",
- "Cap Editor",
- "Cap Mode Selection",
- "Cap Camera",
- "Cap Recordings Overlay",
"Cap In Progress Recording",
- "Cap Window Capture Occluder",
- "Cap Capture Area",
+ "Cap Camera",
];
pub fn default_excluded_windows() -> Vec {
diff --git a/apps/desktop/src/routes/(window-chrome)/settings/general.tsx b/apps/desktop/src/routes/(window-chrome)/settings/general.tsx
index dcc61dcf57..b860d788db 100644
--- a/apps/desktop/src/routes/(window-chrome)/settings/general.tsx
+++ b/apps/desktop/src/routes/(window-chrome)/settings/general.tsx
@@ -9,25 +9,33 @@ import "@total-typescript/ts-reset/filter-boolean";
import { CheckMenuItem, Menu, MenuItem } from "@tauri-apps/api/menu";
import { confirm } from "@tauri-apps/plugin-dialog";
import { cx } from "cva";
-import { createEffect, createMemo, createResource, For, Show } from "solid-js";
+import {
+ createEffect,
+ createMemo,
+ createResource,
+ createSignal,
+ For,
+ Show,
+} from "solid-js";
import { createStore, reconcile } from "solid-js/store";
import themePreviewAuto from "~/assets/theme-previews/auto.jpg";
import themePreviewDark from "~/assets/theme-previews/dark.jpg";
import themePreviewLight from "~/assets/theme-previews/light.jpg";
import { Input } from "~/routes/editor/ui";
import { authStore, generalSettingsStore } from "~/store";
-import IconLucidePlus from "~icons/lucide/plus";
-import IconLucideX from "~icons/lucide/x";
import {
type AppTheme,
- type CaptureWindowWithThumbnail,
+ type CaptureWindow,
commands,
+ events,
type GeneralSettingsStore,
- type WindowExclusion,
type MainWindowRecordingStartBehaviour,
type PostDeletionBehaviour,
type PostStudioRecordingBehaviour,
+ type WindowExclusion,
} from "~/utils/tauri";
+import IconLucidePlus from "~icons/lucide/plus";
+import IconLucideX from "~icons/lucide/x";
import { Setting, ToggleSetting } from "./Setting";
const getExclusionPrimaryLabel = (entry: WindowExclusion) =>
@@ -45,7 +53,7 @@ const getExclusionSecondaryLabel = (entry: WindowExclusion) => {
return entry.bundleIdentifier ?? null;
};
-const getWindowOptionLabel = (window: CaptureWindowWithThumbnail) => {
+const getWindowOptionLabel = (window: CaptureWindow) => {
const parts = [window.owner_name];
if (window.name && window.name !== window.owner_name) {
parts.push(window.name);
@@ -59,9 +67,15 @@ type LegacyWindowExclusion = {
window_title?: string | null;
};
-const coerceWindowExclusion = (entry: WindowExclusion | LegacyWindowExclusion): WindowExclusion => {
+const coerceWindowExclusion = (
+ entry: WindowExclusion | LegacyWindowExclusion,
+): WindowExclusion => {
if (entry && typeof entry === "object") {
- if ("bundleIdentifier" in entry || "ownerName" in entry || "windowTitle" in entry) {
+ if (
+ "bundleIdentifier" in entry ||
+ "ownerName" in entry ||
+ "windowTitle" in entry
+ ) {
const current = entry as WindowExclusion;
return {
bundleIdentifier: current.bundleIdentifier ?? null,
@@ -97,7 +111,7 @@ const normalizeWindowExclusions = (
return {
bundleIdentifier,
ownerName,
- windowTitle: hasBundleIdentifier ? null : coerced.windowTitle ?? null,
+ windowTitle: hasBundleIdentifier ? null : (coerced.windowTitle ?? null),
} satisfies WindowExclusion;
});
@@ -124,9 +138,9 @@ const deriveInitialSettings = (
excluded_windows?: LegacyWindowExclusion[];
};
- const rawExcludedWindows = (
- store.excludedWindows ?? excluded_windows ?? []
- ) as (WindowExclusion | LegacyWindowExclusion)[];
+ const rawExcludedWindows = (store.excludedWindows ??
+ excluded_windows ??
+ []) as (WindowExclusion | LegacyWindowExclusion)[];
return {
...defaults,
@@ -137,7 +151,7 @@ const deriveInitialSettings = (
export default function GeneralSettings() {
const [store] = createResource(generalSettingsStore.get, {
- initialValue: null,
+ initialValue: undefined,
});
return (
@@ -231,9 +245,16 @@ function Inner(props: {
setSettings(reconcile(deriveInitialSettings(props.initialStore)));
});
- const [windows] = createResource(commands.listWindowsWithThumbnails, {
- initialValue: [] as CaptureWindowWithThumbnail[],
- });
+ const [windows, { refetch: refetchWindows }] = createResource(
+ async () => {
+ // Fetch windows with a small delay to avoid blocking initial render
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ return commands.listCaptureWindows();
+ },
+ {
+ initialValue: [] as CaptureWindow[],
+ },
+ );
const handleChange = async (
key: K,
@@ -251,7 +272,10 @@ function Inner(props: {
() => props.isStoreLoading || windows.loading,
);
- const matchesExclusion = (exclusion: WindowExclusion, window: CaptureWindowWithThumbnail) => {
+ const matchesExclusion = (
+ exclusion: WindowExclusion,
+ window: CaptureWindow,
+ ) => {
const bundleMatch = exclusion.bundleIdentifier
? window.bundle_identifier === exclusion.bundleIdentifier
: false;
@@ -276,7 +300,7 @@ function Inner(props: {
return false;
};
- const isManagedWindowsApp = (window: CaptureWindowWithThumbnail) => {
+ const isManagedWindowsApp = (window: CaptureWindow) => {
const bundle = window.bundle_identifier?.toLowerCase() ?? "";
if (bundle.includes("so.cap.desktop")) {
return true;
@@ -284,25 +308,40 @@ function Inner(props: {
return window.owner_name.toLowerCase().includes("cap");
};
+ const isWindowAvailable = (window: CaptureWindow) => {
+ if (excludedWindows().some((entry) => matchesExclusion(entry, window))) {
+ return false;
+ }
+ if (ostype === "windows") {
+ return isManagedWindowsApp(window);
+ }
+ return true;
+ };
+
const availableWindows = createMemo(() => {
const data = windows() ?? [];
- return data.filter((window) => {
- if (excludedWindows().some((entry) => matchesExclusion(entry, window))) {
- return false;
- }
- if (ostype === "windows") {
- return isManagedWindowsApp(window);
- }
- return true;
- });
+ return data.filter(isWindowAvailable);
});
+ const refreshAvailableWindows = async (): Promise => {
+ try {
+ const refreshed = (await refetchWindows()) ?? windows() ?? [];
+ return refreshed.filter(isWindowAvailable);
+ } catch (error) {
+ console.error("Failed to refresh available windows", error);
+ return availableWindows();
+ }
+ };
+
const applyExcludedWindows = async (next: WindowExclusion[]) => {
const normalized = normalizeWindowExclusions(next);
setSettings("excludedWindows", normalized);
try {
await generalSettingsStore.set({ excludedWindows: normalized });
await commands.refreshWindowContentProtection();
+ if (ostype === "macos") {
+ await events.requestScreenCapturePrewarm.emit({ force: true });
+ }
} catch (error) {
console.error("Failed to update excluded windows", error);
}
@@ -314,7 +353,7 @@ function Inner(props: {
await applyExcludedWindows(current);
};
- const handleAddWindow = async (window: CaptureWindowWithThumbnail) => {
+ const handleAddWindow = async (window: CaptureWindow) => {
const windowTitle = window.bundle_identifier ? null : window.name;
const next = [
@@ -682,13 +721,14 @@ function Inner(props: {
-
+
)}
Promise;
onRemove: (index: number) => Promise;
- onAdd: (window: CaptureWindowWithThumbnail) => Promise;
+ onAdd: (window: CaptureWindow) => Promise;
onReset: () => Promise;
isLoading: boolean;
isWindows: boolean;
}) {
const hasExclusions = () => props.excludedWindows.length > 0;
- const canAdd = () => props.availableWindows.length > 0 && !props.isLoading;
+ const canAdd = () => !props.isLoading;
+
+ const handleAddClick = async (event: MouseEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
- const handleAddClick = async () => {
if (!canAdd()) return;
- const items = await Promise.all(
- props.availableWindows.map((window) =>
- MenuItem.new({
- text: getWindowOptionLabel(window),
- action: () => {
- void props.onAdd(window);
- },
- }),
- ),
- );
+ // Use available windows if we have them, otherwise fetch
+ let windows = props.availableWindows;
- const menu = await Menu.new({ items });
- await menu.popup();
- await menu.close();
+ // Only refresh if we don't have any windows cached
+ if (!windows.length) {
+ try {
+ windows = await props.onRequestAvailableWindows();
+ } catch (error) {
+ console.error("Failed to fetch windows:", error);
+ return;
+ }
+ }
+
+ if (!windows.length) {
+ console.log("No available windows to exclude");
+ return;
+ }
+
+ try {
+ const items = await Promise.all(
+ windows.map((window) =>
+ MenuItem.new({
+ text: getWindowOptionLabel(window),
+ action: () => {
+ void props.onAdd(window);
+ },
+ }),
+ ),
+ );
+
+ const menu = await Menu.new({ items });
+
+ // Save scroll position before popup
+ const scrollPos = window.scrollY;
+
+ await menu.popup();
+ await menu.close();
+
+ // Restore scroll position after menu closes
+ requestAnimationFrame(() => {
+ window.scrollTo(0, scrollPos);
+ });
+ } catch (error) {
+ console.error("Error showing window menu:", error);
+ }
};
return (
@@ -796,13 +871,15 @@ function ExcludedWindowsCard(props: {
- Only Cap windows can be excluded on Windows.
+ Note: Only Cap
+ related windows can be excluded on Windows due to technical
+ limitations.
- }
- >
+ }>
Date: Mon, 13 Oct 2025 17:07:22 +0100
Subject: [PATCH 3/9] fix: bundle_identifier on Windows
---
crates/scap-targets/src/platform/win.rs | 39 +++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/crates/scap-targets/src/platform/win.rs b/crates/scap-targets/src/platform/win.rs
index 817a230dd6..4d8521e9ab 100644
--- a/crates/scap-targets/src/platform/win.rs
+++ b/crates/scap-targets/src/platform/win.rs
@@ -482,6 +482,45 @@ impl WindowImpl {
}
}
+ pub fn bundle_identifier(&self) -> Option {
+ // On Windows, use the executable name as the bundle identifier
+ // This is similar to the macOS bundle identifier concept
+ unsafe {
+ let mut process_id = 0u32;
+ GetWindowThreadProcessId(self.0, Some(&mut process_id));
+
+ if process_id == 0 {
+ return None;
+ }
+
+ let process_handle =
+ OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, process_id).ok()?;
+
+ let mut buffer = [0u16; 1024];
+ let mut buffer_size = buffer.len() as u32;
+
+ let result = QueryFullProcessImageNameW(
+ process_handle,
+ PROCESS_NAME_FORMAT::default(),
+ PWSTR(buffer.as_mut_ptr()),
+ &mut buffer_size,
+ );
+
+ let _ = CloseHandle(process_handle);
+
+ if result.is_ok() && buffer_size > 0 {
+ let path_str = String::from_utf16_lossy(&buffer[..buffer_size as usize]);
+
+ // Return the executable name (without extension) as the bundle identifier
+ std::path::Path::new(&path_str)
+ .file_stem()
+ .map(|stem| stem.to_string_lossy().into_owned())
+ } else {
+ None
+ }
+ }
+ }
+
fn get_file_description(&self, file_path: &str) -> Option {
unsafe {
let wide_path: Vec = file_path.encode_utf16().chain(std::iter::once(0)).collect();
From cc9aa2ed053d94756216962f9389814c9020c729 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Mon, 13 Oct 2025 17:45:24 +0100
Subject: [PATCH 4/9] feat: Implement CR suggestions + move definition to
desktop
---
.claude/settings.local.json | 22 ++---
.../desktop/src-tauri/src/general_settings.rs | 2 +-
apps/desktop/src-tauri/src/lib.rs | 3 +-
apps/desktop/src-tauri/src/recording.rs | 12 ++-
.../desktop/src-tauri/src/window_exclusion.rs | 95 +++++++++++++++++++
apps/desktop/src-tauri/src/windows.rs | 2 +-
crates/recording/src/capture_pipeline.rs | 5 +-
crates/recording/src/instant_recording.rs | 7 +-
crates/recording/src/lib.rs | 9 +-
.../src/sources/screen_capture/macos.rs | 41 +++-----
.../src/sources/screen_capture/mod.rs | 64 +------------
crates/recording/src/studio_recording.rs | 8 +-
crates/scap-targets/src/platform/macos.rs | 19 ++--
13 files changed, 155 insertions(+), 134 deletions(-)
create mode 100644 apps/desktop/src-tauri/src/window_exclusion.rs
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 5503f40598..ce1d28dd79 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -1,12 +1,12 @@
{
- "permissions": {
- "allow": [
- "Bash(pnpm typecheck:*)",
- "Bash(pnpm lint:*)",
- "Bash(pnpm build:*)",
- "Bash(cargo check:*)"
- ],
- "deny": [],
- "ask": []
- }
-}
\ No newline at end of file
+ "permissions": {
+ "allow": [
+ "Bash(pnpm typecheck:*)",
+ "Bash(pnpm lint:*)",
+ "Bash(pnpm build:*)",
+ "Bash(cargo check:*)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs
index 9ffe29856a..283beb1202 100644
--- a/apps/desktop/src-tauri/src/general_settings.rs
+++ b/apps/desktop/src-tauri/src/general_settings.rs
@@ -1,4 +1,4 @@
-use cap_recording::sources::screen_capture::WindowExclusion;
+use crate::window_exclusion::WindowExclusion;
use serde::{Deserialize, Serialize};
use serde_json::json;
use specta::Type;
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index a1ed9d02ee..b65c9edc67 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -25,6 +25,7 @@ mod tray;
mod upload;
mod upload_legacy;
mod web_api;
+mod window_exclusion;
mod windows;
use audio::AppSounds;
@@ -2017,7 +2018,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
.typ::()
.typ::()
.typ::()
- .typ::();
+ .typ::();
#[cfg(debug_assertions)]
specta_builder
diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs
index 7d2472ad04..efa61b5710 100644
--- a/apps/desktop/src-tauri/src/recording.rs
+++ b/apps/desktop/src-tauri/src/recording.rs
@@ -459,10 +459,14 @@ pub async fn start_recording(
recording_dir: recording_dir.clone(),
};
- let excluded_windows = general_settings
+ let window_exclusions = general_settings
.as_ref()
- .map(|settings| settings.excluded_windows.clone())
- .unwrap_or_else(general_settings::default_excluded_windows);
+ .map_or_else(general_settings::default_excluded_windows, |settings| {
+ settings.excluded_windows.clone()
+ });
+
+ let excluded_windows =
+ crate::window_exclusion::resolve_window_ids(&window_exclusions);
let actor = match inputs.mode {
RecordingMode::Studio => {
@@ -707,7 +711,7 @@ pub async fn delete_recording(app: AppHandle, state: MutableState<'_, App>) -> R
}
};
- if let Some((recording, recording_dir, video_id)) = recording_data {
+ if let Some((_, recording_dir, video_id)) = recording_data {
CurrentRecordingChanged.emit(&app).ok();
RecordingStopped {}.emit(&app).ok();
diff --git a/apps/desktop/src-tauri/src/window_exclusion.rs b/apps/desktop/src-tauri/src/window_exclusion.rs
new file mode 100644
index 0000000000..08981aa207
--- /dev/null
+++ b/apps/desktop/src-tauri/src/window_exclusion.rs
@@ -0,0 +1,95 @@
+use scap_targets::Window;
+use scap_targets::WindowId;
+use serde::{Deserialize, Serialize};
+use specta::Type;
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct WindowExclusion {
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub bundle_identifier: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub owner_name: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub window_title: Option,
+}
+
+impl WindowExclusion {
+ pub fn matches(
+ &self,
+ bundle_identifier: Option<&str>,
+ owner_name: Option<&str>,
+ window_title: Option<&str>,
+ ) -> bool {
+ if let Some(identifier) = self.bundle_identifier.as_deref() {
+ if bundle_identifier
+ .map(|candidate| candidate == identifier)
+ .unwrap_or(false)
+ {
+ return true;
+ }
+ }
+
+ if let Some(expected_owner) = self.owner_name.as_deref() {
+ let owner_matches = owner_name
+ .map(|candidate| candidate == expected_owner)
+ .unwrap_or(false);
+
+ if self.window_title.is_some() {
+ return owner_matches
+ && self
+ .window_title
+ .as_deref()
+ .map(|expected_title| {
+ window_title
+ .map(|candidate| candidate == expected_title)
+ .unwrap_or(false)
+ })
+ .unwrap_or(false);
+ }
+
+ if owner_matches {
+ return true;
+ }
+ }
+
+ if let Some(expected_title) = self.window_title.as_deref() {
+ return window_title
+ .map(|candidate| candidate == expected_title)
+ .unwrap_or(false);
+ }
+
+ false
+ }
+}
+
+pub fn resolve_window_ids(exclusions: &[WindowExclusion]) -> Vec {
+ if exclusions.is_empty() {
+ return Vec::new();
+ }
+
+ Window::list()
+ .into_iter()
+ .filter_map(|window| {
+ let owner_name = window.owner_name();
+ let window_title = window.name();
+
+ #[cfg(target_os = "macos")]
+ let bundle_identifier = window.bundle_identifier();
+
+ #[cfg(not(target_os = "macos"))]
+ let bundle_identifier = None;
+
+ exclusions
+ .iter()
+ .find(|entry| {
+ entry.matches(
+ bundle_identifier.as_deref(),
+ owner_name.as_deref(),
+ window_title.as_deref(),
+ )
+ })
+ .map(|_| window.id())
+ })
+ .collect()
+}
diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs
index 4954b09009..b5d5f2d030 100644
--- a/apps/desktop/src-tauri/src/windows.rs
+++ b/apps/desktop/src-tauri/src/windows.rs
@@ -26,8 +26,8 @@ use crate::{
permissions,
recording_settings::RecordingTargetMode,
target_select_overlay::WindowFocusManager,
+ window_exclusion::WindowExclusion,
};
-use cap_recording::sources::screen_capture::WindowExclusion;
#[cfg(target_os = "macos")]
const DEFAULT_TRAFFIC_LIGHTS_INSET: LogicalPosition = LogicalPosition::new(12.0, 12.0);
diff --git a/crates/recording/src/capture_pipeline.rs b/crates/recording/src/capture_pipeline.rs
index 328d0ef325..33d12c3a20 100644
--- a/crates/recording/src/capture_pipeline.rs
+++ b/crates/recording/src/capture_pipeline.rs
@@ -3,10 +3,11 @@ use crate::{
output_pipeline::*,
sources,
sources::screen_capture::{
- self, ScreenCaptureConfig, ScreenCaptureFormat, ScreenCaptureTarget, WindowExclusion,
+ self, ScreenCaptureConfig, ScreenCaptureFormat, ScreenCaptureTarget,
},
};
use cap_timestamp::Timestamps;
+use scap_targets::WindowId;
use std::{path::PathBuf, sync::Arc, time::SystemTime};
pub trait MakeCapturePipeline: ScreenCaptureFormat + std::fmt::Debug + 'static {
@@ -134,7 +135,7 @@ pub async fn create_screen_capture(
system_audio: bool,
#[cfg(windows)] d3d_device: ::windows::Win32::Graphics::Direct3D11::ID3D11Device,
#[cfg(target_os = "macos")] shareable_content: cidre::arc::R,
- excluded_windows: &[WindowExclusion],
+ excluded_windows: &[WindowId],
) -> anyhow::Result> {
Ok(ScreenCaptureConfig::::init(
capture_target,
diff --git a/crates/recording/src/instant_recording.rs b/crates/recording/src/instant_recording.rs
index e29ab77b8f..86f22e44cb 100644
--- a/crates/recording/src/instant_recording.rs
+++ b/crates/recording/src/instant_recording.rs
@@ -3,12 +3,13 @@ use crate::{
capture_pipeline::{MakeCapturePipeline, ScreenCaptureMethod, Stop, create_screen_capture},
feeds::microphone::MicrophoneFeedLock,
output_pipeline::{self, OutputPipeline},
- sources::screen_capture::{ScreenCaptureConfig, ScreenCaptureTarget, WindowExclusion},
+ sources::screen_capture::{ScreenCaptureConfig, ScreenCaptureTarget},
};
use cap_media_info::{AudioInfo, VideoInfo};
use cap_project::InstantRecordingMeta;
use cap_utils::ensure_dir;
use kameo::{Actor as _, prelude::*};
+use scap_targets::WindowId;
use std::{
path::PathBuf,
sync::Arc,
@@ -220,7 +221,7 @@ pub struct ActorBuilder {
capture_target: ScreenCaptureTarget,
system_audio: bool,
mic_feed: Option>,
- excluded_windows: Vec,
+ excluded_windows: Vec,
}
impl ActorBuilder {
@@ -244,7 +245,7 @@ impl ActorBuilder {
self
}
- pub fn with_excluded_windows(mut self, excluded_windows: Vec) -> Self {
+ pub fn with_excluded_windows(mut self, excluded_windows: Vec) -> Self {
self.excluded_windows = excluded_windows;
self
}
diff --git a/crates/recording/src/lib.rs b/crates/recording/src/lib.rs
index 3f3c5b125b..b9a12a8f61 100644
--- a/crates/recording/src/lib.rs
+++ b/crates/recording/src/lib.rs
@@ -13,15 +13,12 @@ pub use sources::screen_capture;
use cap_media::MediaError;
use feeds::microphone::MicrophoneFeedLock;
-use scap_targets::bounds::LogicalBounds;
+use scap_targets::{WindowId, bounds::LogicalBounds};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use thiserror::Error;
-use crate::{
- feeds::camera::CameraFeedLock,
- sources::screen_capture::{ScreenCaptureTarget, WindowExclusion},
-};
+use crate::{feeds::camera::CameraFeedLock, sources::screen_capture::ScreenCaptureTarget};
#[derive(specta::Type, Serialize, Deserialize, Clone, Debug, Copy, Default)]
#[serde(rename_all = "camelCase")]
@@ -51,7 +48,7 @@ pub struct RecordingBaseInputs {
pub camera_feed: Option>,
#[cfg(target_os = "macos")]
pub shareable_content: cidre::arc::R,
- pub excluded_windows: Vec,
+ pub excluded_windows: Vec,
}
#[derive(specta::Type, Serialize, Deserialize, Clone, Debug)]
diff --git a/crates/recording/src/sources/screen_capture/macos.rs b/crates/recording/src/sources/screen_capture/macos.rs
index bcfec7118a..c860b0cf92 100644
--- a/crates/recording/src/sources/screen_capture/macos.rs
+++ b/crates/recording/src/sources/screen_capture/macos.rs
@@ -9,12 +9,9 @@ use crate::{
use anyhow::{Context, anyhow};
use cidre::*;
use futures::{FutureExt, channel::mpsc, future::BoxFuture};
-use std::{
- collections::HashSet,
- sync::{
- Arc,
- atomic::{self, AtomicBool},
- },
+use std::sync::{
+ Arc,
+ atomic::{self, AtomicBool},
};
use tokio::sync::broadcast;
use tracing::debug;
@@ -67,7 +64,7 @@ impl ScreenCaptureConfig {
&self,
) -> anyhow::Result<(VideoSourceConfig, Option)> {
let (error_tx, error_rx) = broadcast::channel(1);
- let (mut video_tx, video_rx) = flume::bounded(4);
+ let (video_tx, video_rx) = flume::bounded(4);
let (mut audio_tx, audio_rx) = if self.system_audio {
let (tx, rx) = mpsc::channel(32);
(Some(tx), Some(rx))
@@ -83,25 +80,17 @@ impl ScreenCaptureConfig {
} else {
let mut collected = Vec::new();
- for window in Window::list() {
- let owner_name = window.owner_name();
- let window_title = window.name();
- let bundle_identifier = window.bundle_identifier();
-
- if self.excluded_windows.iter().any(|entry| {
- entry.matches(
- bundle_identifier.as_deref(),
- owner_name.as_deref(),
- window_title.as_deref(),
- )
- }) {
- if let Some(sc_window) = window
- .raw_handle()
- .as_sc(self.shareable_content.clone())
- .await
- {
- collected.push(sc_window);
- }
+ for window_id in &self.excluded_windows {
+ let Some(window) = Window::from_id(window_id) else {
+ continue;
+ };
+
+ if let Some(sc_window) = window
+ .raw_handle()
+ .as_sc(self.shareable_content.clone())
+ .await
+ {
+ collected.push(sc_window);
}
}
diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs
index af82aef42b..526e366dc4 100644
--- a/crates/recording/src/sources/screen_capture/mod.rs
+++ b/crates/recording/src/sources/screen_capture/mod.rs
@@ -48,66 +48,6 @@ pub struct CaptureArea {
pub bounds: LogicalBounds,
}
-#[derive(Debug, Clone, Serialize, Deserialize, Type, PartialEq, Eq)]
-#[serde(rename_all = "camelCase")]
-pub struct WindowExclusion {
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub bundle_identifier: Option,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub owner_name: Option,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub window_title: Option,
-}
-
-impl WindowExclusion {
- pub fn matches(
- &self,
- bundle_identifier: Option<&str>,
- owner_name: Option<&str>,
- window_title: Option<&str>,
- ) -> bool {
- if let Some(identifier) = self.bundle_identifier.as_deref() {
- if bundle_identifier
- .map(|candidate| candidate == identifier)
- .unwrap_or(false)
- {
- return true;
- }
- }
-
- if let Some(expected_owner) = self.owner_name.as_deref() {
- let owner_matches = owner_name
- .map(|candidate| candidate == expected_owner)
- .unwrap_or(false);
-
- if self.window_title.is_some() {
- return owner_matches
- && self
- .window_title
- .as_deref()
- .map(|expected_title| {
- window_title
- .map(|candidate| candidate == expected_title)
- .unwrap_or(false)
- })
- .unwrap_or(false);
- }
-
- if owner_matches {
- return true;
- }
- }
-
- if let Some(expected_title) = self.window_title.as_deref() {
- return window_title
- .map(|candidate| candidate == expected_title)
- .unwrap_or(false);
- }
-
- false
- }
-}
-
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase", tag = "variant")]
pub enum ScreenCaptureTarget {
@@ -259,7 +199,7 @@ pub struct ScreenCaptureConfig {
d3d_device: ::windows::Win32::Graphics::Direct3D11::ID3D11Device,
#[cfg(target_os = "macos")]
shareable_content: cidre::arc::R,
- pub excluded_windows: Vec,
+ pub excluded_windows: Vec,
}
impl std::fmt::Debug for ScreenCaptureConfig {
@@ -338,7 +278,7 @@ impl ScreenCaptureConfig {
system_audio: bool,
#[cfg(windows)] d3d_device: ::windows::Win32::Graphics::Direct3D11::ID3D11Device,
#[cfg(target_os = "macos")] shareable_content: cidre::arc::R,
- excluded_windows: Vec,
+ excluded_windows: Vec,
) -> Result {
cap_fail::fail!("ScreenCaptureSource::init");
diff --git a/crates/recording/src/studio_recording.rs b/crates/recording/src/studio_recording.rs
index f1fd85f692..c9420f5daa 100644
--- a/crates/recording/src/studio_recording.rs
+++ b/crates/recording/src/studio_recording.rs
@@ -14,6 +14,7 @@ use cap_timestamp::{Timestamp, Timestamps};
use futures::{FutureExt, StreamExt, future::OptionFuture, stream::FuturesUnordered};
use kameo::{Actor as _, prelude::*};
use relative_path::RelativePathBuf;
+use scap_targets::WindowId;
use std::{
path::{Path, PathBuf},
sync::Arc,
@@ -344,7 +345,7 @@ pub struct ActorBuilder {
mic_feed: Option>,
camera_feed: Option>,
custom_cursor: bool,
- excluded_windows: Vec,
+ excluded_windows: Vec,
}
impl ActorBuilder {
@@ -380,10 +381,7 @@ impl ActorBuilder {
self
}
- pub fn with_excluded_windows(
- mut self,
- excluded_windows: Vec,
- ) -> Self {
+ pub fn with_excluded_windows(mut self, excluded_windows: Vec) -> Self {
self.excluded_windows = excluded_windows;
self
}
diff --git a/crates/scap-targets/src/platform/macos.rs b/crates/scap-targets/src/platform/macos.rs
index e3b87326b5..c1a66b1328 100644
--- a/crates/scap-targets/src/platform/macos.rs
+++ b/crates/scap-targets/src/platform/macos.rs
@@ -350,7 +350,9 @@ impl WindowImpl {
pub fn bundle_identifier(&self) -> Option {
let pid = self.owner_pid()?;
- unsafe {
+ use objc::rc::autoreleasepool;
+
+ autoreleasepool(|| unsafe {
use cocoa::base::id;
use cocoa::foundation::NSString;
use objc::{class, msg_send, sel, sel_impl};
@@ -359,27 +361,20 @@ impl WindowImpl {
class!(NSRunningApplication),
runningApplicationWithProcessIdentifier: pid
];
-
- if app.is_null() {
- return None;
- }
+ let app = (!app.is_null()).then_some(app)?;
let bundle_identifier: id = msg_send![app, bundleIdentifier];
- if bundle_identifier.is_null() {
- return None;
- }
+ let bundle_identifier = (!bundle_identifier.is_null()).then_some(bundle_identifier)?;
let cstr = NSString::UTF8String(bundle_identifier);
- if cstr.is_null() {
- return None;
- }
+ let cstr = (!cstr.is_null()).then_some(cstr)?;
Some(
std::ffi::CStr::from_ptr(cstr)
.to_string_lossy()
.into_owned(),
)
- }
+ })
}
pub fn name(&self) -> Option {
From d5ddfd05fd9668ed04c796ee5000a19812267d5a Mon Sep 17 00:00:00 2001
From: Brendan Allan
Date: Tue, 14 Oct 2025 02:06:17 +0800
Subject: [PATCH 5/9] cleanup
---
.../desktop/src-tauri/src/general_settings.rs | 2 +-
apps/desktop/src-tauri/src/lib.rs | 2 +-
apps/desktop/src-tauri/src/recording.rs | 8 +-
apps/desktop/src-tauri/src/windows.rs | 15 +--
.../(window-chrome)/settings/general.tsx | 97 +++----------------
apps/desktop/src/routes/debug.tsx | 2 +-
crates/recording/src/capture_pipeline.rs | 5 +-
crates/recording/src/instant_recording.rs | 7 +-
crates/recording/src/lib.rs | 1 +
.../src/sources/screen_capture/mod.rs | 5 +-
crates/recording/src/studio_recording.rs | 7 +-
crates/scap-targets/src/lib.rs | 4 -
crates/scap-targets/src/platform/win.rs | 39 --------
13 files changed, 47 insertions(+), 147 deletions(-)
diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs
index 283beb1202..8d2595d5aa 100644
--- a/apps/desktop/src-tauri/src/general_settings.rs
+++ b/apps/desktop/src-tauri/src/general_settings.rs
@@ -43,7 +43,7 @@ impl MainWindowRecordingStartBehaviour {
const DEFAULT_EXCLUDED_WINDOW_TITLES: &[&str] = &[
"Cap",
"Cap Settings",
- "Cap In Progress Recording",
+ "Cap Recording Controls",
"Cap Camera",
];
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index 4ae59a4930..05289bd2d9 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -2121,7 +2121,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
CapWindowId::CaptureArea.label().as_str(),
CapWindowId::Camera.label().as_str(),
CapWindowId::RecordingsOverlay.label().as_str(),
- CapWindowId::InProgressRecording.label().as_str(),
+ CapWindowId::RecordingControls.label().as_str(),
CapWindowId::Upgrade.label().as_str(),
])
.map_label(|label| match label {
diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs
index f73ea56504..863c07fecf 100644
--- a/apps/desktop/src-tauri/src/recording.rs
+++ b/apps/desktop/src-tauri/src/recording.rs
@@ -589,7 +589,7 @@ pub async fn start_recording(
)
.kind(tauri_plugin_dialog::MessageDialogKind::Error);
- if let Some(window) = CapWindowId::InProgressRecording.get(&app) {
+ if let Some(window) = CapWindowId::RecordingControls.get(&app) {
dialog = dialog.parent(&window);
}
@@ -631,7 +631,7 @@ pub async fn start_recording(
)
.kind(tauri_plugin_dialog::MessageDialogKind::Error);
- if let Some(window) = CapWindowId::InProgressRecording.get(&app) {
+ if let Some(window) = CapWindowId::RecordingControls.get(&app) {
dialog = dialog.parent(&window);
}
@@ -754,7 +754,7 @@ pub async fn delete_recording(app: AppHandle, state: MutableState<'_, App>) -> R
.flatten()
.unwrap_or_default();
- if let Some(window) = CapWindowId::InProgressRecording.get(&app) {
+ if let Some(window) = CapWindowId::RecordingControls.get(&app) {
let _ = window.close();
}
@@ -818,7 +818,7 @@ async fn handle_recording_end(
let _ = app.recording_logging_handle.reload(None);
- if let Some(window) = CapWindowId::InProgressRecording.get(&handle) {
+ if let Some(window) = CapWindowId::RecordingControls.get(&handle) {
let _ = window.close();
}
diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs
index b5d5f2d030..5a5b113974 100644
--- a/apps/desktop/src-tauri/src/windows.rs
+++ b/apps/desktop/src-tauri/src/windows.rs
@@ -44,7 +44,7 @@ pub enum CapWindowId {
TargetSelectOverlay { display_id: DisplayId },
CaptureArea,
Camera,
- InProgressRecording,
+ RecordingControls,
Upgrade,
ModeSelect,
Debug,
@@ -60,7 +60,8 @@ impl FromStr for CapWindowId {
"settings" => Self::Settings,
"camera" => Self::Camera,
"capture-area" => Self::CaptureArea,
- "in-progress-recording" => Self::InProgressRecording,
+ // legacy identifier
+ "in-progress-recording" => Self::RecordingControls,
"recordings-overlay" => Self::RecordingsOverlay,
"upgrade" => Self::Upgrade,
"mode-select" => Self::ModeSelect,
@@ -102,7 +103,7 @@ impl std::fmt::Display for CapWindowId {
Self::TargetSelectOverlay { display_id } => {
write!(f, "target-select-overlay-{display_id}")
}
- Self::InProgressRecording => write!(f, "in-progress-recording"),
+ Self::RecordingControls => write!(f, "in-progress-recording"), // legacy identifier
Self::RecordingsOverlay => write!(f, "recordings-overlay"),
Self::Upgrade => write!(f, "upgrade"),
Self::ModeSelect => write!(f, "mode-select"),
@@ -123,7 +124,7 @@ impl CapWindowId {
Self::Settings => "Cap Settings".to_string(),
Self::WindowCaptureOccluder { .. } => "Cap Window Capture Occluder".to_string(),
Self::CaptureArea => "Cap Capture Area".to_string(),
- Self::InProgressRecording => "Cap In Progress Recording".to_string(),
+ Self::RecordingControls => "Cap Recording Controls".to_string(),
Self::Editor { .. } => "Cap Editor".to_string(),
Self::ModeSelect => "Cap Mode Selection".to_string(),
Self::Camera => "Cap Camera".to_string(),
@@ -153,7 +154,7 @@ impl CapWindowId {
pub fn traffic_lights_position(&self) -> Option