Fix Windows overlay transparency, global hotkeys, toast duplication, and file dialogs
Some checks are pending
Release & Packaging / Build Linux Packages (.tar.gz, .deb, .rpm, .AppImage) (push) Waiting to run
Release & Packaging / Build Windows Binaries (.exe) (push) Waiting to run
Release & Packaging / Publish GitHub Release (push) Blocked by required conditions

This commit is contained in:
katR 2026-09-06 14:04:44 +05:30
parent 3d5496d4ab
commit 0749bb2e61
8 changed files with 201 additions and 73 deletions

View file

@ -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)

2
Cargo.lock generated
View file

@ -3700,7 +3700,7 @@ dependencies = [
[[package]]
name = "scythe"
version = "0.2.1"
version = "0.2.2"
dependencies = [
"ashpd",
"cpal",

View file

@ -1,6 +1,6 @@
[package]
name = "scythe"
version = "0.2.1"
version = "0.2.2"
edition = "2024"
[[bin]]

View file

@ -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"

View file

@ -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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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<HotKey> = 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<HotKey>| -> (Option<u32>, Option<u32>, Option<u32>, Option<u32>) {
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, &current_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, &current_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));
}
}

View file

@ -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"

View file

@ -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;
}

View file

@ -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<String>, is_active: Arc<AtomicBool>
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<String>, is_active: Arc<AtomicBool>
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<String>, is_active: Arc<AtomicBool>
});
}
// 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,