Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"allow": [
"Bash(pnpm typecheck:*)",
"Bash(pnpm lint:*)",
"Bash(pnpm build:*)"
"Bash(pnpm build:*)",
"Bash(cargo check:*)"
],
"deny": [],
"ask": []
Expand Down
28 changes: 28 additions & 0 deletions apps/desktop/src-tauri/src/general_settings.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::window_exclusion::WindowExclusion;
use serde::{Deserialize, Serialize};
use serde_json::json;
use specta::Type;
Expand Down Expand Up @@ -39,6 +40,24 @@ impl MainWindowRecordingStartBehaviour {
}
}

const DEFAULT_EXCLUDED_WINDOW_TITLES: &[&str] = &[
"Cap",
"Cap Settings",
"Cap Recording Controls",
"Cap Camera",
];

pub fn default_excluded_windows() -> Vec<WindowExclusion> {
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.
Expand Down Expand Up @@ -99,6 +118,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<WindowExclusion>,
}

fn default_enable_native_camera_preview() -> bool {
Expand Down Expand Up @@ -162,6 +183,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(),
}
}
}
Expand Down Expand Up @@ -231,3 +253,9 @@ pub fn init(app: &AppHandle) {

println!("GeneralSettingsState managed");
}

#[tauri::command]
#[specta::specta]
pub fn get_default_excluded_windows() -> Vec<WindowExclusion> {
default_excluded_windows()
}
8 changes: 6 additions & 2 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod tray;
mod upload;
mod upload_legacy;
mod web_api;
mod window_exclusion;
mod windows;

use audio::AppSounds;
Expand Down Expand Up @@ -1918,6 +1919,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,
Expand Down Expand Up @@ -2017,7 +2020,8 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
.typ::<hotkeys::HotkeysStore>()
.typ::<general_settings::GeneralSettingsStore>()
.typ::<recording_settings::RecordingSettingsStore>()
.typ::<cap_flags::Flags>();
.typ::<cap_flags::Flags>()
.typ::<crate::window_exclusion::WindowExclusion>();

#[cfg(debug_assertions)]
specta_builder
Expand Down Expand Up @@ -2117,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 {
Expand Down
35 changes: 29 additions & 6 deletions apps/desktop/src-tauri/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,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::*,
Expand Down Expand Up @@ -474,6 +476,17 @@ pub async fn start_recording(
recording_dir: recording_dir.clone(),
};

#[cfg(target_os = "macos")]
let excluded_windows = {
let window_exclusions = general_settings
.as_ref()
.map_or_else(general_settings::default_excluded_windows, |settings| {
settings.excluded_windows.clone()
});

crate::window_exclusion::resolve_window_ids(&window_exclusions)
};

let actor = match inputs.mode {
RecordingMode::Studio => {
let mut builder = studio_recording::Actor::builder(
Expand All @@ -487,6 +500,11 @@ pub async fn start_recording(
.unwrap_or_default(),
);

#[cfg(target_os = "macos")]
{
builder = builder.with_excluded_windows(excluded_windows.clone());
}

if let Some(camera_feed) = camera_feed {
builder = builder.with_camera_feed(camera_feed);
}
Expand Down Expand Up @@ -527,6 +545,11 @@ pub async fn start_recording(
)
.with_system_audio(inputs.capture_system_audio);

#[cfg(target_os = "macos")]
{
builder = builder.with_excluded_windows(excluded_windows.clone());
}

if let Some(mic_feed) = mic_feed {
builder = builder.with_mic_feed(mic_feed);
}
Expand Down Expand Up @@ -576,7 +599,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);
}

Expand Down Expand Up @@ -618,7 +641,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);
}

Expand Down Expand Up @@ -718,7 +741,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();

Expand All @@ -741,7 +764,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();
}

Expand Down Expand Up @@ -805,7 +828,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();
}

Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src-tauri/src/thumbnails/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct CaptureWindowWithThumbnail {
pub refresh_rate: u32,
pub thumbnail: Option<String>,
pub app_icon: Option<String>,
pub bundle_identifier: Option<String>,
}

pub fn normalize_thumbnail_dimensions(image: &image::RgbaImage) -> image::RgbaImage {
Expand Down Expand Up @@ -140,6 +141,7 @@ pub async fn collect_windows_with_thumbnails() -> Result<Vec<CaptureWindowWithTh
refresh_rate: capture_window.refresh_rate,
thumbnail,
app_icon,
bundle_identifier: capture_window.bundle_identifier,
});
}

Expand Down
95 changes: 95 additions & 0 deletions apps/desktop/src-tauri/src/window_exclusion.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub window_title: Option<String>,
}

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<WindowId> {
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.raw_handle().bundle_identifier();

#[cfg(not(target_os = "macos"))]
let bundle_identifier = None::<&str>;

exclusions
.iter()
.find(|entry| {
entry.matches(
bundle_identifier.as_deref(),
owner_name.as_deref(),
window_title.as_deref(),
)
})
.map(|_| window.id())
})
.collect()
}
Loading
Loading