From 0749bb2e61e51fd46b11f065fa8f0ed816daa38f Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Sun, 6 Sep 2026 14:04:44 +0530 Subject: [PATCH] Fix Windows overlay transparency, global hotkeys, toast duplication, and file dialogs --- .github/workflows/release.yml | 22 ++-- Cargo.lock | 2 +- Cargo.toml | 2 +- packaging/windows/scythe-installer.nsi | 3 + src/bin/scythe-ui.rs | 136 +++++++++++++++++++------ src/config.rs | 4 +- src/overlay.rs | 2 +- src/overlay_egui.rs | 103 ++++++++++++++----- 8 files changed, 201 insertions(+), 73 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b5150c..fcbb83c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -143,6 +143,8 @@ jobs: cp target/release/scythe-daemon.exe dist/bundle/ cp target/release/scythe-ui.exe dist/bundle/ python3 packaging/windows/bundle_deps.py dist/bundle + strip dist/bundle/*.exe || true + strip --strip-unneeded dist/bundle/*.dll || true - name: Package Windows Standalone (.exe), Installer, and Zip shell: pwsh @@ -181,19 +183,19 @@ jobs: - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ github.ref_name != 'main' && github.ref_name || 'v0.2.1' }} - name: "Scythe ${{ github.ref_name != 'main' && github.ref_name || 'v0.2.1' }}" + tag_name: ${{ github.ref_name != 'main' && github.ref_name || 'v0.2.2' }} + name: "Scythe ${{ github.ref_name != 'main' && github.ref_name || 'v0.2.2' }}" body: | High-performance GPU hardware screen recorder and instant replay overlay for Linux and Windows. - ### What's New in v0.2.1 - - **Windows Setup Installer (`scythe-setup.exe`)**: Fixed DXGI adapter initialization and DirectX 11 hardware capture pipeline on Windows. Full Windows NSIS installer with desktop/start menu integration. - - **System Autostart Options**: Toggle automatic startup on system login for both the overlay HUD and background replay buffer. - - **Clean Sequential Trimmer Exports**: Cleaned export naming to `clip_trimmed.mp4`, `clip_trimmed_1.mp4` avoiding repetitive suffixes. - - **Settings UI Organization**: Dedicated grouped sections for System & Startup, Storage & File Naming with real-time pattern preview, and hotkey defaults reset. - - **Auto-Update System**: Integrated background release checker with voluntary update notifications in the HUD and dedicated settings manager. - - **Standardized Datetime Filenames**: Clips and replays now save with clear human-readable timestamps (`Replay-HH-MM-SS_DD-MM-YYYY.mp4` / `Recording-HH-MM-SS_DD-MM-YYYY.mp4`). - - **NVIDIA ShadowPlay Toast Overlay**: Sleek, non-intrusive floating toast notifications in the top-right corner for Replay Saved, Recording Started/Stopped, and Mouse Cursor toggle. + ### What's New in v0.2.2 + - **Windows Overlay Transparency**: Fixed DWM opacity issue where the overlay showed an opaque black screen instead of a translucent frosted background. + - **Windows Global Hotkeys**: Added dedicated Win32 message pump so global hotkeys (`Alt+Z`, `Ctrl+Shift+R`, etc.) register and trigger reliably across all Windows applications and games. + - **Dynamic Hotkey Reloading**: Changes to hotkey bindings in Settings take effect immediately in the background without needing a restart. + - **Fixed Notification Duplication & Hang**: Eliminated duplicate in-HUD toast instances and fixed toast freeze issues by ensuring isolated processes with auto-closing watchdog timers. + - **Working Folder Options**: Fixed 'Open' and 'Change' folder dialogs on Windows with top-most STA PowerShell picker and visible Explorer launching. + - **Zero-Latency UI Buttons & Key Recording**: All daemon communication from UI buttons is now asynchronous, and key listening repaints with 0ms delay. + - **Reduced Windows Installer Size**: Enabled Solid LZMA compression in NSIS and stripped symbols from Windows binaries and DLLs. ### Packages - Windows: `scythe-setup.exe` (Official installer with Start Menu & Desktop shortcuts) diff --git a/Cargo.lock b/Cargo.lock index 0ea3d8d..ee131c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3700,7 +3700,7 @@ dependencies = [ [[package]] name = "scythe" -version = "0.2.1" +version = "0.2.2" dependencies = [ "ashpd", "cpal", diff --git a/Cargo.toml b/Cargo.toml index 5054d21..9354eb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scythe" -version = "0.2.1" +version = "0.2.2" edition = "2024" [[bin]] diff --git a/packaging/windows/scythe-installer.nsi b/packaging/windows/scythe-installer.nsi index 0d87f44..055904d 100644 --- a/packaging/windows/scythe-installer.nsi +++ b/packaging/windows/scythe-installer.nsi @@ -1,6 +1,9 @@ !include "MUI2.nsh" !include "FileFunc.nsh" +SetCompressor /SOLID lzma +SetCompressorDictSize 64 + Name "Scythe" OutFile "..\..\dist\scythe-setup.exe" InstallDir "$LOCALAPPDATA\Programs\scythe" diff --git a/src/bin/scythe-ui.rs b/src/bin/scythe-ui.rs index 69ad961..da59562 100644 --- a/src/bin/scythe-ui.rs +++ b/src/bin/scythe-ui.rs @@ -218,15 +218,12 @@ fn ensure_hotkeys_running() { #[cfg(target_os = "windows")] { unsafe { - use windows::Win32::Foundation::{CloseHandle, GetLastError, WIN32_ERROR}; - use windows::Win32::System::Threading::CreateMutexW; + use windows::Win32::Foundation::CloseHandle; + use windows::Win32::System::Threading::{OpenMutexW, SYNCHRONIZATION_ACCESS_RIGHTS}; - if let Ok(handle) = CreateMutexW(None, false, windows::core::w!("Global\\scythe_hotkeys_single_instance")) { - if GetLastError() == WIN32_ERROR(183) { // ERROR_ALREADY_EXISTS - let _ = CloseHandle(handle); - return; - } + if let Ok(handle) = OpenMutexW(SYNCHRONIZATION_ACCESS_RIGHTS(0x00100000), false, windows::core::w!("Global\\scythe_hotkeys_single_instance")) { let _ = CloseHandle(handle); + return; } } @@ -318,7 +315,7 @@ fn main() -> Result<(), Box> { let title = args.get(2).map(|s| s.as_str()).unwrap_or("SCYTHE"); let subtitle = args.get(3).map(|s| s.as_str()).unwrap_or(""); let icon_name = args.get(4).map(|s| s.as_str()).unwrap_or("info"); - show_shadowplay_toast(title, subtitle, ToastIcon::from_name(icon_name)); + scythe::overlay_egui::run_egui_toast(title, subtitle, ToastIcon::from_name(icon_name)); return Ok(()); } "--menu" => { @@ -463,35 +460,108 @@ fn main() -> Result<(), Box> { return Ok(()); } }; - let hotkey_menu = HotKey::new(Some(Modifiers::ALT), Code::KeyZ); - let hotkey_save = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR); - let hotkey_record = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9); - let hotkey_cursor = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F10); - let is_on_hyprland = scythe::hyprland_binds::is_hyprland(); - if !is_on_hyprland { - let _ = manager.register(hotkey_menu); - } - let _ = manager.register(hotkey_save); - let _ = manager.register(hotkey_record); - let _ = manager.register(hotkey_cursor); + let mut current_config = scythe::config::ScytheConfig::load(); + let mut registered_hotkeys: Vec = Vec::new(); - println!("Listening for global hotkeys (Alt+Z for overlay, Ctrl+Shift+R for replay, Ctrl+Shift+F9 for recording, Ctrl+Shift+F10 for cursor)..."); + let register_all = |mgr: &GlobalHotKeyManager, cfg: &scythe::config::ScytheConfig, reg: &mut Vec| -> (Option, Option, Option, Option) { + for hk in reg.drain(..) { + let _ = mgr.unregister(hk); + } + let is_on_hyprland = scythe::hyprland_binds::is_hyprland(); + let m_menu = scythe::hotkey::parse_hotkey(&cfg.menu_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::ALT), Code::KeyZ))); + let m_save = scythe::hotkey::parse_hotkey(&cfg.save_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR))); + let m_rec = scythe::hotkey::parse_hotkey(&cfg.record_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9))); + let m_cur = scythe::hotkey::parse_hotkey(&cfg.cursor_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F10))); - let receiver = GlobalHotKeyEvent::receiver(); - loop { - if let Ok(event) = receiver.try_recv() { - if event.id == hotkey_menu.id() { - ensure_daemon_running(); - let _ = get_ui_cmd().arg("--menu").spawn(); - } else if event.id == hotkey_save.id() { - let _ = send_with_notification(Command::SaveReplay, "INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay); - } else if event.id == hotkey_record.id() { - let _ = handle_toggle_recording(); - } else if event.id == hotkey_cursor.id() { - let _ = handle_toggle_cursor(); + let mut id_menu = None; + let mut id_save = None; + let mut id_rec = None; + let mut id_cur = None; + + if !is_on_hyprland { + if let Some(hk) = m_menu { + id_menu = Some(hk.id()); + let _ = mgr.register(hk); + reg.push(hk); } } - std::thread::sleep(std::time::Duration::from_millis(50)); + if let Some(hk) = m_save { + id_save = Some(hk.id()); + let _ = mgr.register(hk); + reg.push(hk); + } + if let Some(hk) = m_rec { + id_rec = Some(hk.id()); + let _ = mgr.register(hk); + reg.push(hk); + } + if let Some(hk) = m_cur { + id_cur = Some(hk.id()); + let _ = mgr.register(hk); + reg.push(hk); + } + (id_menu, id_save, id_rec, id_cur) + }; + + let (mut id_menu, mut id_save, mut id_rec, mut id_cur) = register_all(&manager, ¤t_config, &mut registered_hotkeys); + + println!("Listening for global hotkeys (Overlay: {}, Replay: {}, Record: {}, Cursor: {})...", + current_config.menu_hotkey, current_config.save_hotkey, current_config.record_hotkey, current_config.cursor_hotkey); + + let receiver = GlobalHotKeyEvent::receiver(); + let mut last_config_check = std::time::Instant::now(); + + loop { + #[cfg(target_os = "windows")] + unsafe { + use windows::Win32::UI::WindowsAndMessaging::{ + PeekMessageW, TranslateMessage, DispatchMessageW, MSG, PM_REMOVE, + }; + let mut msg = MSG::default(); + while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { + let _ = TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + + if last_config_check.elapsed() > std::time::Duration::from_millis(800) { + last_config_check = std::time::Instant::now(); + let new_cfg = scythe::config::ScytheConfig::load(); + if new_cfg.menu_hotkey != current_config.menu_hotkey + || new_cfg.save_hotkey != current_config.save_hotkey + || new_cfg.record_hotkey != current_config.record_hotkey + || new_cfg.cursor_hotkey != current_config.cursor_hotkey + { + current_config = new_cfg; + let (im, is, ir, ic) = register_all(&manager, ¤t_config, &mut registered_hotkeys); + id_menu = im; + id_save = is; + id_rec = ir; + id_cur = ic; + } + } + + while let Ok(event) = receiver.try_recv() { + if Some(event.id) == id_menu { + std::thread::spawn(|| { + ensure_daemon_running(); + let _ = get_ui_cmd().arg("--menu").spawn(); + }); + } else if Some(event.id) == id_save { + std::thread::spawn(|| { + let _ = send_with_notification(Command::SaveReplay, "INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay); + }); + } else if Some(event.id) == id_rec { + std::thread::spawn(|| { + let _ = handle_toggle_recording(); + }); + } else if Some(event.id) == id_cur { + std::thread::spawn(|| { + let _ = handle_toggle_cursor(); + }); + } + } + std::thread::sleep(std::time::Duration::from_millis(20)); } } diff --git a/src/config.rs b/src/config.rs index 369c2b2..1efb3c3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -291,7 +291,9 @@ X-GNOME-Autostart-enabled=true\n"; } pub fn notify_daemon_reload() { - let _ = crate::ipc::send_command(crate::ipc::Command::ReloadConfig); + std::thread::spawn(|| { + let _ = crate::ipc::send_command(crate::ipc::Command::ReloadConfig); + }); } /// Formats video filenames with informative local timestamps, e.g. "Replay-18-30-00_05-09-2026.mp4" diff --git a/src/overlay.rs b/src/overlay.rs index 4972800..3169f1e 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -86,7 +86,7 @@ pub fn show_notification(message: &str) { pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) { #[cfg(target_os = "windows")] { - crate::overlay_egui::run_egui_toast(title, subtitle, icon); + spawn_toast(title, subtitle, icon); return; } diff --git a/src/overlay_egui.rs b/src/overlay_egui.rs index 923da45..4f6b630 100644 --- a/src/overlay_egui.rs +++ b/src/overlay_egui.rs @@ -209,10 +209,10 @@ fn open_folder(path: &std::path::Path) { std::thread::spawn(move || { #[cfg(target_os = "windows")] { - use std::os::windows::process::CommandExt; - let _ = std::process::Command::new("explorer") - .arg(&p) - .creation_flags(0x08000000) + let p_str = p.to_string_lossy().replace('/', "\\"); + let _ = std::fs::create_dir_all(&p_str); + let _ = std::process::Command::new("explorer.exe") + .arg(&p_str) .spawn(); } #[cfg(target_os = "macos")] @@ -234,17 +234,22 @@ fn pick_folder(current_dir: &str, tx: Sender, is_active: Arc std::thread::spawn(move || { #[cfg(target_os = "windows")] { - use std::os::windows::process::CommandExt; + let clean_cur = cur.replace('/', "\\").replace('\'', "''"); let script = format!( - "[System.Reflection.Assembly]::LoadWithPartialName(\x27System.windows.forms\x27) | Out-Null; \ + "Add-Type -AssemblyName System.Windows.Forms; \ $f = New-Object System.Windows.Forms.FolderBrowserDialog; \ - $f.SelectedPath = \x27{}\x27; \ - if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {{ Write-Host $f.SelectedPath }}", - cur.replace("\x27", "\x27\x27") + $f.Description = 'Select Recordings Directory'; \ + $f.SelectedPath = '{}'; \ + $f.ShowNewFolderButton = $true; \ + $top = New-Object System.Windows.Forms.Form; \ + $top.TopMost = $true; \ + if ($f.ShowDialog($top) -eq [System.Windows.Forms.DialogResult]::OK) {{ \ + Write-Output $f.SelectedPath \ + }}", + clean_cur ); - if let Ok(out) = std::process::Command::new("powershell") - .args(["-NoProfile", "-WindowStyle", "Hidden", "-Command", &script]) - .creation_flags(0x08000000) + if let Ok(out) = std::process::Command::new("powershell.exe") + .args(["-NoProfile", "-STA", "-Command", &script]) .output() { let sel = String::from_utf8_lossy(&out.stdout).trim().to_string(); @@ -252,6 +257,7 @@ fn pick_folder(current_dir: &str, tx: Sender, is_active: Arc let _ = tx.send(sel); } } + flag.store(false, Ordering::SeqCst); } #[cfg(not(target_os = "windows"))] @@ -295,6 +301,13 @@ fn pick_folder(current_dir: &str, tx: Sender, is_active: Arc }); } +// Asynchronous daemon command dispatcher to avoid blocking the egui render loop +fn async_send_command(cmd: Command) { + std::thread::spawn(move || { + let _ = ipc::send_command(cmd); + }); +} + // Helper to render mechanical keyboard keycap badges #[allow(dead_code)] fn render_keycap(ui: &mut egui::Ui, text: &str) { @@ -790,7 +803,7 @@ impl ScytheOverlayApp { if let Ok(s) = ipc::query_status() { let _ = status_tx.send(s); } - std::thread::sleep(Duration::from_millis(50)); + std::thread::sleep(Duration::from_millis(150)); } }); @@ -1018,9 +1031,8 @@ impl ScytheOverlayApp { self.replay_dropdown_open = false; } if render_menu_item(ui, "Save Replay", accent, true) { - let _ = ipc::send_command(Command::SaveReplay); + async_send_command(Command::SaveReplay); self.show_hud_notification("INSTANT REPLAY", "Saved to Videos", crate::overlay::ToastIcon::Replay); - crate::overlay::spawn_toast("INSTANT REPLAY", "Saved to Videos", crate::overlay::ToastIcon::Replay); self.replay_dropdown_open = false; } }); @@ -1067,13 +1079,11 @@ impl ScytheOverlayApp { render_dropdown_menu(ui, card_w, accent, |ui| { let rec_toggle_text = if is_recording { "Stop Recording" } else { "Start Recording" }; if render_menu_item(ui, rec_toggle_text, accent, true) { - let _ = ipc::send_command(Command::ToggleRecording); + async_send_command(Command::ToggleRecording); if is_recording { self.show_hud_notification("RECORDING", "Recording saved", crate::overlay::ToastIcon::Save); - crate::overlay::spawn_toast("RECORDING", "Recording saved", crate::overlay::ToastIcon::Save); } else { self.show_hud_notification("RECORDING", "Recording started", crate::overlay::ToastIcon::Record); - crate::overlay::spawn_toast("RECORDING", "Recording started", crate::overlay::ToastIcon::Record); } self.record_dropdown_open = false; } @@ -1261,7 +1271,7 @@ impl ScytheOverlayApp { self.show_cursor = cur; self.config.show_cursor = cur; let _ = self.config.save(); - let _ = ipc::send_command(Command::ToggleCursor); + async_send_command(Command::ToggleCursor); self.show_hud_notification( "MOUSE CURSOR", if cur { "Visible in recording" } else { "Hidden from recording" }, @@ -2108,12 +2118,20 @@ impl ScytheOverlayApp { } impl eframe::App for ScytheOverlayApp { + fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { + [0.0, 0.0, 0.0, 0.0] + } + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.poll_async_events(); self.mic_vu = (self.mic_vu * 0.94).max(0.0); self.sys_vu = (self.sys_vu * 0.94).max(0.0); self.anim_time += 0.033; - ctx.request_repaint_after(Duration::from_millis(50)); + if self.listening_keybind.is_some() || self.hud_notification.is_some() { + ctx.request_repaint(); + } else { + ctx.request_repaint_after(Duration::from_millis(33)); + } // Auto-position and size to monitor on launch if !self.initial_pos_set { @@ -2242,14 +2260,24 @@ impl eframe::App for ScytheOverlayApp { } pub fn run_egui_overlay() { + #[cfg(target_os = "windows")] + let (screen_w, screen_h): (f32, f32) = unsafe { + use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN}; + let w = GetSystemMetrics(SM_CXSCREEN) as f32; + let h = GetSystemMetrics(SM_CYSCREEN) as f32; + if w > 100.0 && h > 100.0 { (w, h) } else { (1920.0, 1080.0) } + }; + #[cfg(not(target_os = "windows"))] + let (screen_w, screen_h): (f32, f32) = (1920.0, 1080.0); + let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_title("Scythe") .with_app_id("scythe-overlay") .with_position([0.0, 0.0]) - .with_inner_size([1920.0, 1080.0]) - .with_maximized(true) - .with_resizable(true) + .with_inner_size([screen_w, screen_h]) + .with_maximized(false) + .with_resizable(false) .with_decorations(false) .with_transparent(true) .with_always_on_top(), @@ -2275,6 +2303,10 @@ pub struct ShadowPlayToastApp { } impl eframe::App for ShadowPlayToastApp { + fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { + [0.0, 0.0, 0.0, 0.0] + } + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { let elapsed = self.created_at.elapsed().as_secs_f32(); let total_dur = self.duration.as_secs_f32(); @@ -2282,7 +2314,7 @@ impl eframe::App for ShadowPlayToastApp { ctx.send_viewport_cmd(egui::ViewportCommand::Close); std::process::exit(0); } - ctx.request_repaint(); + ctx.request_repaint_after(Duration::from_millis(16)); let slide_x = if elapsed < 0.35 { let t = (elapsed / 0.35).min(1.0); @@ -2392,13 +2424,26 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc let cfg = ScytheConfig::load(); let accent = resolve_accent_color(&cfg.accent_color); - let toast_w = 340.0; - let toast_h = 64.0; + let toast_w: f32 = 340.0; + let toast_h: f32 = 64.0; + + #[cfg(target_os = "windows")] + let screen_w: f32 = unsafe { + use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN}; + let w = GetSystemMetrics(SM_CXSCREEN) as f32; + if w > 100.0 { w } else { 1920.0 } + }; + #[cfg(not(target_os = "windows"))] + let screen_w: f32 = 1920.0; + + let pos_x = (screen_w - toast_w - 24.0).max(10.0); + let pos_y = 24.0; let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_title("Scythe Notification") .with_app_id("scythe-toast") + .with_position([pos_x, pos_y]) .with_inner_size([toast_w, toast_h]) .with_decorations(false) .with_transparent(true) @@ -2416,6 +2461,12 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc duration: Duration::from_millis(2800), }; + // Watchdog thread to guarantee exit after duration + std::thread::spawn(move || { + std::thread::sleep(Duration::from_millis(3200)); + std::process::exit(0); + }); + let _ = eframe::run_native( "scythe-toast", options,