From 5085485e67bc7e28640a9499936710693d4c6f9c Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Thu, 20 Nov 2025 03:28:15 +0000 Subject: [PATCH] Show recording overlay in macOS fullscreen apps, and when switching between spaces (#361) * Gitignore `*.local.*` Allows for local files like `CLAUDE.local.md` and `settings.local.json` * Support macOS spaces and fullscreen apps --- .gitignore | 1 + src-tauri/Cargo.lock | 19 +++++++++++ src-tauri/Cargo.toml | 3 ++ src-tauri/src/lib.rs | 11 ++++-- src-tauri/src/overlay.rs | 74 ++++++++++++++++++++++++++++++++++++++-- 5 files changed, 103 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index a342ac2..0b9df8b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +*.local.* # Editor directories and files .vscode/* diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2630a7a..396bc18 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2528,6 +2528,7 @@ dependencies = [ "tar", "tauri", "tauri-build", + "tauri-nspanel", "tauri-plugin-autostart", "tauri-plugin-clipboard-manager", "tauri-plugin-fs", @@ -4305,6 +4306,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pathdiff" version = "0.2.3" @@ -6629,6 +6636,18 @@ dependencies = [ "tauri-utils", ] +[[package]] +name = "tauri-nspanel" +version = "2.1.0" +source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2.1#da9c9a8d4eb7f0524a2508988df1a7d9585b4904" +dependencies = [ + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "pastey", + "tauri", +] + [[package]] name = "tauri-plugin" version = "2.5.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 55e21c3..fc0918b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -85,6 +85,9 @@ windows = { version = "0.61.3", features = [ "Win32_System_Variant", ] } +[target.'cfg(target_os = "macos")'.dependencies] +tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2.1" } + [profile.release] lto = true codegen-units = 1 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dc55056..ff1bf96 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -211,7 +211,7 @@ pub fn run() { // when the variable is unset let console_filter = build_console_filter(); - tauri::Builder::default() + let mut builder = tauri::Builder::default() .plugin( LogBuilder::new() .level(log::LevelFilter::Trace) // Set to most verbose level globally @@ -234,7 +234,14 @@ pub fn run() { }), ]) .build(), - ) + ); + + #[cfg(target_os = "macos")] + { + builder = builder.plugin(tauri_nspanel::init()); + } + + builder .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { show_main_window(app); })) diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index bcd2008..a5f7472 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -1,8 +1,29 @@ use crate::settings; use crate::settings::OverlayPosition; use enigo::{Enigo, Mouse}; +use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, PhysicalSize}; + +#[cfg(not(target_os = "macos"))] use log::debug; -use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, PhysicalSize, WebviewWindowBuilder}; + +#[cfg(not(target_os = "macos"))] +use tauri::WebviewWindowBuilder; + +#[cfg(target_os = "macos")] +use tauri::WebviewUrl; + +#[cfg(target_os = "macos")] +use tauri_nspanel::{tauri_panel, CollectionBehavior, PanelBuilder, PanelLevel}; + +#[cfg(target_os = "macos")] +tauri_panel! { + panel!(RecordingOverlayPanel { + config: { + can_become_key_window: false, + is_floating_panel: true + } + }) +} const OVERLAY_WIDTH: f64 = 172.0; const OVERLAY_HEIGHT: f64 = 36.0; @@ -84,6 +105,7 @@ fn calculate_overlay_position(app_handle: &AppHandle) -> Option<(f64, f64)> { } /// Creates the recording overlay window and keeps it hidden by default +#[cfg(not(target_os = "macos"))] pub fn create_recording_overlay(app_handle: &AppHandle) { if let Some((x, y)) = calculate_overlay_position(app_handle) { match WebviewWindowBuilder::new( @@ -118,6 +140,49 @@ pub fn create_recording_overlay(app_handle: &AppHandle) { } } +/// Creates the recording overlay panel and keeps it hidden by default (macOS) +#[cfg(target_os = "macos")] +pub fn create_recording_overlay(app_handle: &AppHandle) { + if let Some((x, y)) = calculate_overlay_position(app_handle) { + // PanelBuilder creates a Tauri window then converts it to NSPanel. + // The window remains registered, so get_webview_window() still works. + match PanelBuilder::<_, RecordingOverlayPanel>::new( + app_handle, + "recording_overlay" + ) + .url(WebviewUrl::App("src/overlay/index.html".into())) + .title("Recording") + .position(tauri::Position::Logical(tauri::LogicalPosition { x, y })) + .level(PanelLevel::Status) + .size(tauri::Size::Logical(tauri::LogicalSize { + width: OVERLAY_WIDTH, + height: OVERLAY_HEIGHT + })) + .has_shadow(false) + .transparent(true) + .no_activate(true) + .corner_radius(0.0) + .with_window(|w| { + w.decorations(false) + .transparent(true) + }) + .collection_behavior( + CollectionBehavior::new() + .can_join_all_spaces() + .full_screen_auxiliary() + ) + .build() + { + Ok(panel) => { + let _ = panel.hide(); + } + Err(e) => { + log::error!("Failed to create recording overlay panel: {}", e); + } + } + } +} + /// Shows the recording overlay window with fade-in animation pub fn show_recording_overlay(app_handle: &AppHandle) { // Check if overlay should be shown based on position setting @@ -126,9 +191,12 @@ pub fn show_recording_overlay(app_handle: &AppHandle) { return; } - update_overlay_position(app_handle); - if let Some(overlay_window) = app_handle.get_webview_window("recording_overlay") { + // Update position before showing to prevent flicker from position changes + if let Some((x, y)) = calculate_overlay_position(app_handle) { + let _ = overlay_window.set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y })); + } + let _ = overlay_window.show(); // Emit event to trigger fade-in animation with recording state let _ = overlay_window.emit("show-overlay", "recording");