Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
21 changes: 11 additions & 10 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -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": []
}
}
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 cap_recording::sources::screen_capture::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 In Progress Recording",
"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()
}
5 changes: 4 additions & 1 deletion apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -2014,7 +2016,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::<cap_recording::sources::screen_capture::WindowExclusion>();

#[cfg(debug_assertions)]
specta_builder
Expand Down
15 changes: 12 additions & 3 deletions apps/desktop/src-tauri/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*,
Expand Down Expand Up @@ -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);

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
let actor = match inputs.mode {
RecordingMode::Studio => {
let mut builder = studio_recording::Actor::builder(
Expand All @@ -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);
Expand Down Expand Up @@ -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);
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
67 changes: 60 additions & 7 deletions apps/desktop/src-tauri/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ use tracing::{debug, error, warn};

use crate::{
App, ArcLock, RequestScreenCapturePrewarm, fake_window,
general_settings::{AppTheme, GeneralSettingsStore},
general_settings::{self, AppTheme, GeneralSettingsStore},
permissions,
recording_settings::RecordingTargetMode,
target_select_overlay::WindowFocusManager,
};
use cap_recording::sources::screen_capture::WindowExclusion;

#[cfg(target_os = "macos")]
const DEFAULT_TRAFFIC_LIGHTS_INSET: LogicalPosition<f64> = LogicalPosition::new(12.0, 12.0);
Expand Down Expand Up @@ -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)
Expand All @@ -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!(
"
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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();

Expand All @@ -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())
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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(),
Expand Down Expand Up @@ -840,6 +865,34 @@ fn position_traffic_lights_impl(
.ok();
}

fn should_protect_window(app: &AppHandle<Wry>, 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<Wry>) -> 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(
Expand Down
Loading
Loading