Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
39 changes: 39 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()
Comment thread
Brendonovich marked this conversation as resolved.
}

// 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 @@ -213,6 +235,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) {
Expand All @@ -231,3 +264,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
29 changes: 21 additions & 8 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,15 @@ pub async fn start_recording(
recording_dir: recording_dir.clone(),
};

let window_exclusions = general_settings
.as_ref()
.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 => {
let mut builder = studio_recording::Actor::builder(
Expand All @@ -485,7 +496,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);
Expand Down Expand Up @@ -525,7 +537,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);
Expand Down Expand Up @@ -576,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);
}

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

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

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

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;

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