From c04bf36e05dd35f3dbd08a256e09065d5a8ed15f Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Tue, 8 Sep 2026 08:23:12 +0530 Subject: [PATCH] fix: resolve replay clip saving, ensure 60fps pacing and keyframes, polish audio mixing, and prevent daemon termination --- Cargo.lock | 2 +- Cargo.toml | 2 +- packaging/windows/scythe-installer.nsi | 8 +- src/bin/scythe-daemon.rs | 89 +++++++++++------ src/bin/scythe-ui.rs | 22 +++-- src/capture/audio.rs | 131 ++++++++++++++++--------- src/capture/x11.rs | 9 ++ src/config.rs | 2 +- src/hyprland_binds.rs | 75 +++++++++----- src/ipc.rs | 17 ++++ src/overlay_egui.rs | 126 +++++++++++++++--------- 11 files changed, 321 insertions(+), 162 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 693aa95..c9f0070 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3700,7 +3700,7 @@ dependencies = [ [[package]] name = "scythe" -version = "0.2.6" +version = "0.2.7" dependencies = [ "ashpd", "cpal", diff --git a/Cargo.toml b/Cargo.toml index 9584ad1..1c78dc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scythe" -version = "0.2.6" +version = "0.2.7" edition = "2024" [[bin]] diff --git a/packaging/windows/scythe-installer.nsi b/packaging/windows/scythe-installer.nsi index c439b2b..56b6093 100644 --- a/packaging/windows/scythe-installer.nsi +++ b/packaging/windows/scythe-installer.nsi @@ -29,7 +29,9 @@ Section "Scythe" SecMain ; Terminate any running instances so locked executables and DLLs can be updated cleanly nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-ui.exe /T >nul 2>&1' nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-daemon.exe /T >nul 2>&1' - Sleep 500 + nsExec::Exec 'cmd.exe /C taskkill /F /IM vrec-ui.exe /T >nul 2>&1' + nsExec::Exec 'cmd.exe /C taskkill /F /IM vrec-daemon.exe /T >nul 2>&1' + Sleep 800 SetOverwrite on SetOutPath "$INSTDIR" @@ -51,7 +53,9 @@ SectionEnd Section "Uninstall" nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-ui.exe /T >nul 2>&1' nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-daemon.exe /T >nul 2>&1' - Sleep 500 + nsExec::Exec 'cmd.exe /C taskkill /F /IM vrec-ui.exe /T >nul 2>&1' + nsExec::Exec 'cmd.exe /C taskkill /F /IM vrec-daemon.exe /T >nul 2>&1' + Sleep 800 Delete "$DESKTOP\scythe.lnk" RMDir /r "$SMPROGRAMS\scythe" diff --git a/src/bin/scythe-daemon.rs b/src/bin/scythe-daemon.rs index 8421105..1a826f1 100644 --- a/src/bin/scythe-daemon.rs +++ b/src/bin/scythe-daemon.rs @@ -156,6 +156,13 @@ async fn main() -> Result<(), Box> { if session_type.to_lowercase() == "wayland" { println!("Cursor display toggled to: {}. Restarting Wayland session...", cfg.show_cursor); let _ = cmd_tx_ipc.send(Command::StopRecording); + if let Ok(exe) = std::env::current_exe() { + let _ = std::process::Command::new(exe) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn(); + } std::thread::sleep(std::time::Duration::from_millis(150)); std::process::exit(0); } @@ -470,6 +477,13 @@ async fn main() -> Result<(), Box> { if let Some(mut m) = normal_muxer.take() { let _ = m.finalize(); } + if let Ok(exe) = std::env::current_exe() { + let _ = std::process::Command::new(exe) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn(); + } std::thread::sleep(std::time::Duration::from_millis(150)); std::process::exit(0); } @@ -620,60 +634,71 @@ async fn main() -> Result<(), Box> { latest_frame = Some(f); has_new_frame = true; } + while let Ok(f) = frame_rx.try_recv() { + latest_frame = Some(f); + has_new_frame = true; + } }, recv(ticker) -> _ => { + while ticker.try_recv().is_ok() {} + let now = std::time::Instant::now(); let elapsed = now.duration_since(stream_start); let raw_pts = (elapsed.as_secs_f64() * config.fps as f64).round() as i64; let pts = if raw_pts > last_video_pts { raw_pts } else { last_video_pts + 1 }; + last_video_pts = pts; let packets_res = if has_new_frame { has_new_frame = false; - last_video_pts = pts; if let Some(ref f) = latest_frame { encoder.encode_frame(f, pts) } else { Ok(Vec::new()) } } else { - // Heartbeat / keepalive: if no new frame for 500ms, emit cached frame to keep video track flowing smoothly - if pts - last_video_pts >= (config.fps as i64 / 2).max(1) { - last_video_pts = pts; - if let Some(ref f) = latest_frame { - encoder.encode_frame(f, pts) - } else { - encoder.encode_cached_frame(pts) + let cached = encoder.encode_cached_frame(pts); + match cached { + Ok(pkts) if !pkts.is_empty() => Ok(pkts), + _ => { + if let Some(ref f) = latest_frame { + encoder.encode_frame(f, pts) + } else { + Ok(Vec::new()) + } } - } else { - Ok(Vec::new()) } }; - if let Ok(packets) = packets_res { - for mut pkt in packets { - pkt.set_stream_index(0); - if normal_recording { - if normal_waiting_keyframe && pkt.is_keyframe() { - normal_waiting_keyframe = false; - rec_base_video_pts = pkt.pts(); - rec_base_audio_pts = -1; - let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext; - let filename = scythe::config::ScytheConfig::format_video_filename("Recording", "mp4"); - let full_path = config.resolve_save_path(&filename); - let audio_codec_ctx = audio_codec_ctx_ptr.map(|p| p as *mut ffmpeg_next::ffi::AVCodecContext); - normal_muxer = unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx).ok() }; - println!("Started normal recording to {}", full_path); + match packets_res { + Ok(packets) => { + for mut pkt in packets { + pkt.set_stream_index(0); + if normal_recording { + if normal_waiting_keyframe && pkt.is_keyframe() { + normal_waiting_keyframe = false; + rec_base_video_pts = pkt.pts(); + rec_base_audio_pts = -1; + let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext; + let filename = scythe::config::ScytheConfig::format_video_filename("Recording", "mp4"); + let full_path = config.resolve_save_path(&filename); + let audio_codec_ctx = audio_codec_ctx_ptr.map(|p| p as *mut ffmpeg_next::ffi::AVCodecContext); + normal_muxer = unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx).ok() }; + println!("Started normal recording to {}", full_path); + } + + if !normal_waiting_keyframe + && let Some(muxer) = normal_muxer.as_mut() { + let rebased = pkt.rebased(rec_base_video_pts); + let _ = muxer.write_packet(&rebased); + } } - - if !normal_waiting_keyframe - && let Some(muxer) = normal_muxer.as_mut() { - let rebased = pkt.rebased(rec_base_video_pts); - let _ = muxer.write_packet(&rebased); + if config.replay_enabled { + ring.push_overwrite(pkt); } } - if config.replay_enabled { - ring.push_overwrite(pkt); - } + } + Err(e) => { + eprintln!("Video encoding error: {}", e); } } } diff --git a/src/bin/scythe-ui.rs b/src/bin/scythe-ui.rs index b20bca3..ac577ed 100644 --- a/src/bin/scythe-ui.rs +++ b/src/bin/scythe-ui.rs @@ -242,6 +242,17 @@ fn handle_toggle_cursor() -> Result<(), Box } } +fn handle_save_replay() -> Result<(), Box> { + ensure_daemon_running(); + if let Ok(st) = query_status() { + if !st.is_replay_active { + show_shadowplay_toast("INSTANT REPLAY", "Replay is turned off", ToastIcon::Error); + return Ok(()); + } + } + send_with_notification(Command::SaveReplay, "INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay) +} + fn main() -> Result<(), Box> { ensure_wayland_env(); @@ -249,14 +260,7 @@ fn main() -> Result<(), Box> { if args.len() > 1 { match args[1].as_str() { "--save" => { - ensure_daemon_running(); - if let Ok(st) = query_status() { - if !st.is_replay_active { - show_shadowplay_toast("INSTANT REPLAY", "Replay is turned off", ToastIcon::Error); - return Ok(()); - } - } - return send_with_notification(Command::SaveReplay, "INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay); + return handle_save_replay(); } "--notify-save" => { show_shadowplay_toast("INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay); @@ -509,7 +513,7 @@ fn main() -> Result<(), Box> { }); } else if Some(event.id) == id_save { std::thread::spawn(|| { - let _ = send_with_notification(Command::SaveReplay, "INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay); + let _ = handle_save_replay(); }); } else if Some(event.id) == id_rec { std::thread::spawn(|| { diff --git a/src/capture/audio.rs b/src/capture/audio.rs index ac5b2e9..3fc068d 100644 --- a/src/capture/audio.rs +++ b/src/capture/audio.rs @@ -179,38 +179,27 @@ fn spawn_parec_stream( let mut stdout = child.stdout.take().ok_or("Failed to open parec stdout")?; std::thread::spawn(move || { - let mut remainder = Vec::with_capacity(4); + let mut remainder = Vec::with_capacity(4096); let mut read_buf = [0u8; 4096]; loop { match stdout.read(&mut read_buf) { Ok(0) => break, Ok(n) => { - let total_len = remainder.len() + n; - let full_bytes = total_len - (total_len % 4); + remainder.extend_from_slice(&read_buf[..n]); + // 2 channels * 4 bytes/sample = 8 bytes per stereo frame + let full_bytes = remainder.len() - (remainder.len() % 8); if full_bytes == 0 { - remainder.extend_from_slice(&read_buf[..n]); continue; } - let mut combined = Vec::with_capacity(total_len); - combined.extend_from_slice(&remainder); - combined.extend_from_slice(&read_buf[..n]); - let floats_count = full_bytes / 4; let mut floats = Vec::with_capacity(floats_count); - for i in 0..floats_count { - let b = [ - combined[i * 4], - combined[i * 4 + 1], - combined[i * 4 + 2], - combined[i * 4 + 3], - ]; - let raw = f32::from_le_bytes(b); + for chunk in remainder[..full_bytes].chunks_exact(4) { + let raw = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); floats.push(soft_limit(raw * gain)); } - remainder.clear(); - remainder.extend_from_slice(&combined[full_bytes..]); + remainder.drain(..full_bytes); if let Some(ref l) = levels { if is_mic { @@ -312,30 +301,57 @@ impl AudioCapture { thread::spawn(move || { let mut sys_q: VecDeque = VecDeque::with_capacity(16384); let mut mic_q: VecDeque = VecDeque::with_capacity(16384); + let mut sys_closed = false; + let mut mic_closed = false; loop { - while let Ok(chunk) = sys_rx.try_recv() { - sys_q.extend(chunk); + loop { + match sys_rx.try_recv() { + Ok(chunk) => sys_q.extend(chunk), + Err(crossbeam_channel::TryRecvError::Empty) => break, + Err(crossbeam_channel::TryRecvError::Disconnected) => { + sys_closed = true; + break; + } + } } - while let Ok(chunk) = mic_rx.try_recv() { - mic_q.extend(chunk); + loop { + match mic_rx.try_recv() { + Ok(chunk) => mic_q.extend(chunk), + Err(crossbeam_channel::TryRecvError::Empty) => break, + Err(crossbeam_channel::TryRecvError::Disconnected) => { + mic_closed = true; + break; + } + } } - let avail = sys_q.len().min(mic_q.len()); + // 2 channels stereo alignment + let avail = ((sys_q.len().min(mic_q.len())) / 2) * 2; if avail >= 480 { let mut mixed = Vec::with_capacity(avail); - for _ in 0..avail { - let s = sys_q.pop_front().unwrap_or(0.0); - let m = mic_q.pop_front().unwrap_or(0.0); + for (s, m) in sys_q.drain(..avail).zip(mic_q.drain(..avail)) { mixed.push(soft_limit(s + m)); } - let _ = out_tx.try_send(mixed); + if out_tx.try_send(mixed).is_err() && !out_tx.is_full() { + break; + } } else if sys_q.len() > 4800 { - let chunk: Vec = sys_q.drain(..960.min(sys_q.len())).collect(); - let _ = out_tx.try_send(chunk); + let drain_len = (960.min(sys_q.len()) / 2) * 2; + let chunk: Vec = sys_q.drain(..drain_len).collect(); + if out_tx.try_send(chunk).is_err() && !out_tx.is_full() { + break; + } } else if mic_q.len() > 4800 { - let chunk: Vec = mic_q.drain(..960.min(mic_q.len())).collect(); - let _ = out_tx.try_send(chunk); + let drain_len = (960.min(mic_q.len()) / 2) * 2; + let chunk: Vec = mic_q.drain(..drain_len).collect(); + if out_tx.try_send(chunk).is_err() && !out_tx.is_full() { + break; + } + } + + if sys_closed && mic_closed && sys_q.is_empty() && mic_q.is_empty() { + break; } thread::sleep(std::time::Duration::from_millis(4)); @@ -479,30 +495,57 @@ impl AudioCapture { thread::spawn(move || { let mut sys_q: VecDeque = VecDeque::with_capacity(16384); let mut mic_q: VecDeque = VecDeque::with_capacity(16384); + let mut sys_closed = false; + let mut mic_closed = false; loop { - while let Ok(chunk) = sys_rx.try_recv() { - sys_q.extend(chunk); + loop { + match sys_rx.try_recv() { + Ok(chunk) => sys_q.extend(chunk), + Err(crossbeam_channel::TryRecvError::Empty) => break, + Err(crossbeam_channel::TryRecvError::Disconnected) => { + sys_closed = true; + break; + } + } } - while let Ok(chunk) = mic_rx.try_recv() { - mic_q.extend(chunk); + loop { + match mic_rx.try_recv() { + Ok(chunk) => mic_q.extend(chunk), + Err(crossbeam_channel::TryRecvError::Empty) => break, + Err(crossbeam_channel::TryRecvError::Disconnected) => { + mic_closed = true; + break; + } + } } - let avail = sys_q.len().min(mic_q.len()); + // 2 channels stereo alignment + let avail = ((sys_q.len().min(mic_q.len())) / 2) * 2; if avail >= 480 { let mut mixed = Vec::with_capacity(avail); - for _ in 0..avail { - let s = sys_q.pop_front().unwrap_or(0.0); - let m = mic_q.pop_front().unwrap_or(0.0); + for (s, m) in sys_q.drain(..avail).zip(mic_q.drain(..avail)) { mixed.push(soft_limit(s + m)); } - let _ = out_tx.try_send(mixed); + if out_tx.try_send(mixed).is_err() && !out_tx.is_full() { + break; + } } else if sys_q.len() > 4800 { - let chunk: Vec = sys_q.drain(..960.min(sys_q.len())).collect(); - let _ = out_tx.try_send(chunk); + let drain_len = (960.min(sys_q.len()) / 2) * 2; + let chunk: Vec = sys_q.drain(..drain_len).collect(); + if out_tx.try_send(chunk).is_err() && !out_tx.is_full() { + break; + } } else if mic_q.len() > 4800 { - let chunk: Vec = mic_q.drain(..960.min(mic_q.len())).collect(); - let _ = out_tx.try_send(chunk); + let drain_len = (960.min(mic_q.len()) / 2) * 2; + let chunk: Vec = mic_q.drain(..drain_len).collect(); + if out_tx.try_send(chunk).is_err() && !out_tx.is_full() { + break; + } + } + + if sys_closed && mic_closed && sys_q.is_empty() && mic_q.is_empty() { + break; } thread::sleep(std::time::Duration::from_millis(4)); diff --git a/src/capture/x11.rs b/src/capture/x11.rs index 30e1915..99306ba 100644 --- a/src/capture/x11.rs +++ b/src/capture/x11.rs @@ -10,6 +10,7 @@ pub struct X11Capture { shm_addr: *mut u8, shm_size: usize, shmid: i32, + last_frame_instant: std::time::Instant, } unsafe impl Send for X11Capture {} @@ -63,12 +64,20 @@ impl X11Capture { shm_addr, shm_size: size, shmid, + last_frame_instant: std::time::Instant::now(), }) } } impl FrameSource for X11Capture { fn next_frame(&mut self) -> Result> { + let target_frame_time = std::time::Duration::from_millis(16); + let elapsed = self.last_frame_instant.elapsed(); + if elapsed < target_frame_time { + std::thread::sleep(target_frame_time - elapsed); + } + self.last_frame_instant = std::time::Instant::now(); + let stride = (self.width as u32) * 4; if let Some(seg) = self.shm_seg && !self.shm_addr.is_null() { diff --git a/src/config.rs b/src/config.rs index 1efb3c3..e19baf2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -132,7 +132,7 @@ impl ScytheConfig { pub fn expand_tilde(path: &str) -> PathBuf { let trimmed = path.trim(); - if let Some(sub) = trimmed.strip_prefix("~/") { + if let Some(sub) = trimmed.strip_prefix("~/").or_else(|| trimmed.strip_prefix("~\\")) { if let Some(home) = dirs::home_dir() { return home.join(sub); } diff --git a/src/hyprland_binds.rs b/src/hyprland_binds.rs index 368e064..23744e2 100644 --- a/src/hyprland_binds.rs +++ b/src/hyprland_binds.rs @@ -98,6 +98,9 @@ pub fn hotkey_to_hyprland_lua(hotkey: &str) -> Option { } } +#[cfg(unix)] +static RULES_INITIALIZED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + #[cfg(unix)] /// Dynamically inject binds and window rules into running Hyprland without touching hyprland.conf pub fn register_hyprland_binds(config: &ScytheConfig) { @@ -105,28 +108,29 @@ pub fn register_hyprland_binds(config: &ScytheConfig) { return; } - // Register modern Hyprland (0.55+) layer rules for genuine frosted glass blur & floating window rules - let _ = Command::new("hyprctl") - .args(["eval", r#"hl.layer_rule({ match = { namespace = "scythe-overlay" }, blur = true, ignore_alpha = 0.1 })"#]) - .output(); - let _ = Command::new("hyprctl") - .args(["eval", r#"hl.layer_rule({ match = { namespace = "scythe-notification" }, blur = true, ignore_alpha = 0.1 })"#]) - .output(); - let _ = Command::new("hyprctl") - .args(["eval", r#"hl.window_rule({ match = { class = "scythe-overlay" }, float = true, pin = true, move = "0 0", size = "100% 100%" })"#]) - .output(); - let _ = Command::new("hyprctl") - .args(["eval", r#"hl.window_rule({ match = { title = "Select Recordings Directory" }, float = true, pin = true, stay_focused = true, center = true })"#]) - .output(); - let _ = Command::new("hyprctl") - .args(["eval", r#"hl.window_rule({ match = { class = "kdialog" }, float = true, pin = true, stay_focused = true, center = true })"#]) - .output(); - let _ = Command::new("hyprctl") - .args(["eval", r#"hl.window_rule({ match = { class = "org.kde.kdialog" }, float = true, pin = true, stay_focused = true, center = true })"#]) - .output(); - let _ = Command::new("hyprctl") - .args(["eval", r#"hl.layer_rule({ match = { namespace = "vrec-overlay" }, blur = true, ignore_alpha = 0.1 })"#]) - .output(); + if !RULES_INITIALIZED.swap(true, std::sync::atomic::Ordering::SeqCst) { + // Register modern Hyprland (0.55+) layer rules for genuine frosted glass blur & floating window rules + let _ = Command::new("hyprctl") + .args(["eval", r#"hl.layer_rule({ match = { namespace = "scythe-overlay" }, blur = true, ignore_alpha = 0.1 })"#]) + .output(); + let _ = Command::new("hyprctl") + .args(["eval", r#"hl.layer_rule({ match = { namespace = "scythe-notification" }, blur = true, ignore_alpha = 0.1 })"#]) + .output(); + let _ = Command::new("hyprctl") + .args(["eval", r#"hl.window_rule({ match = { class = "scythe-overlay" }, float = true, pin = true, move = "0 0", size = "100% 100%" })"#]) + .output(); + let _ = Command::new("hyprctl") + .args(["eval", r#"hl.window_rule({ match = { title = "Select Recordings Directory" }, float = true, pin = true, stay_focused = true, center = true })"#]) + .output(); + let _ = Command::new("hyprctl") + .args(["eval", r#"hl.window_rule({ match = { class = "kdialog" }, float = true, pin = true, stay_focused = true, center = true })"#]) + .output(); + let _ = Command::new("hyprctl") + .args(["eval", r#"hl.window_rule({ match = { class = "org.kde.kdialog" }, float = true, pin = true, stay_focused = true, center = true })"#]) + .output(); + let _ = Command::new("hyprctl") + .args(["eval", r#"hl.layer_rule({ match = { namespace = "vrec-overlay" }, blur = true, ignore_alpha = 0.1 })"#]) + .output(); // Also register legacy window rules for backward compatibility with older Hyprland versions let legacy_rules = [ @@ -160,10 +164,11 @@ pub fn register_hyprland_binds(config: &ScytheConfig) { "pin, class:^(vrec-overlay)$", "noborder, class:^(vrec-overlay)$", ]; - for rule in legacy_rules { - let _ = Command::new("hyprctl") - .args(["keyword", "windowrulev2", rule]) - .output(); + for rule in legacy_rules { + let _ = Command::new("hyprctl") + .args(["keyword", "windowrulev2", rule]) + .output(); + } } let binds = [ @@ -203,8 +208,26 @@ pub fn register_hyprland_binds(config: &ScytheConfig) { } } +pub fn register_hyprland_binds_async(config: &ScytheConfig) { + let cfg = config.clone(); + std::thread::spawn(move || { + register_hyprland_binds(&cfg); + }); +} + +pub fn unbind_hotkey_async(hotkey: &str) { + let hk = hotkey.to_string(); + std::thread::spawn(move || { + unbind_hotkey(&hk); + }); +} + #[cfg(not(unix))] pub fn register_hyprland_binds(_config: &ScytheConfig) {} +#[cfg(not(unix))] +pub fn register_hyprland_binds_async(_config: &ScytheConfig) {} +#[cfg(not(unix))] +pub fn unbind_hotkey_async(_hotkey: &str) {} #[cfg(unix)] /// Unbind a specific hotkey combo from running Hyprland diff --git a/src/ipc.rs b/src/ipc.rs index 439b749..bf9bb1c 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -195,3 +195,20 @@ pub fn get_overlay_pid_path() -> std::path::PathBuf { pub fn clean_overlay_pid() { let _ = std::fs::remove_file(get_overlay_pid_path()); } + +pub fn get_toast_pid_path() -> std::path::PathBuf { + #[cfg(unix)] + { + let runtime_dir = std::env::var("XDG_RUNTIME_DIR") + .unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc::getuid() })); + std::path::PathBuf::from(runtime_dir).join("scythe-toast.pid") + } + #[cfg(not(unix))] + { + std::env::temp_dir().join("scythe-toast.pid") + } +} + +pub fn clean_toast_pid() { + let _ = std::fs::remove_file(get_toast_pid_path()); +} diff --git a/src/overlay_egui.rs b/src/overlay_egui.rs index 5a5fc14..f83ba56 100644 --- a/src/overlay_egui.rs +++ b/src/overlay_egui.rs @@ -207,21 +207,38 @@ fn scan_recordings(dir_str: &str) -> Vec { fn open_folder(path: &std::path::Path) { let p = path.to_path_buf(); std::thread::spawn(move || { + let is_file = p.is_file(); + let folder = if is_file { + p.parent().unwrap_or(&p).to_path_buf() + } else { + p.clone() + }; + let _ = std::fs::create_dir_all(&folder); + #[cfg(target_os = "windows")] { - 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(); + use std::os::windows::process::CommandExt; + if is_file { + let p_str = p.to_string_lossy().replace('/', "\\"); + let _ = std::process::Command::new("explorer.exe") + .arg(format!("/select,\"{}\"", p_str)) + .creation_flags(0x08000000) + .spawn(); + } else { + let f_str = folder.to_string_lossy().replace('/', "\\"); + let _ = std::process::Command::new("explorer.exe") + .arg(&f_str) + .creation_flags(0x08000000) + .spawn(); + } } #[cfg(target_os = "macos")] { - let _ = std::process::Command::new("open").arg(&p).spawn(); + let _ = std::process::Command::new("open").arg(&folder).spawn(); } #[cfg(not(any(target_os = "windows", target_os = "macos")))] { - let _ = std::process::Command::new("xdg-open").arg(&p).spawn(); + let _ = std::process::Command::new("xdg-open").arg(&folder).spawn(); } }); } @@ -234,24 +251,17 @@ 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!( - "Add-Type -AssemblyName System.Windows.Forms; \ - $f = New-Object System.Windows.Forms.FolderBrowserDialog; \ - $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 \ - }}", + "$s = (New-Object -ComObject Shell.Application).BrowseForFolder(0, 'Select Recordings Directory', 0, '{}'); \ + if ($s) {{ Write-Output $s.Self.Path }}", clean_cur ); - if let Ok(out) = std::process::Command::new("powershell.exe") - .args(["-NoProfile", "-STA", "-Command", &script]) - .output() - { + let mut cmd = std::process::Command::new("powershell.exe"); + cmd.args(["-NoProfile", "-STA", "-Command", &script]); + cmd.creation_flags(0x08000000); + if let Ok(out) = cmd.output() { let sel = String::from_utf8_lossy(&out.stdout).trim().to_string(); if !sel.is_empty() { let _ = tx.send(sel); @@ -1494,16 +1504,16 @@ impl ScytheOverlayApp { let old_save = self.config.save_hotkey.clone(); let old_rec = self.config.record_hotkey.clone(); let old_cur = self.config.cursor_hotkey.clone(); - crate::hyprland_binds::unbind_hotkey(&old_menu); - crate::hyprland_binds::unbind_hotkey(&old_save); - crate::hyprland_binds::unbind_hotkey(&old_rec); - crate::hyprland_binds::unbind_hotkey(&old_cur); + crate::hyprland_binds::unbind_hotkey_async(&old_menu); + crate::hyprland_binds::unbind_hotkey_async(&old_save); + crate::hyprland_binds::unbind_hotkey_async(&old_rec); + crate::hyprland_binds::unbind_hotkey_async(&old_cur); self.config.menu_hotkey = "Alt+Z".to_string(); self.config.save_hotkey = "Ctrl+Shift+R".to_string(); self.config.record_hotkey = "Ctrl+Shift+F9".to_string(); self.config.cursor_hotkey = "Ctrl+Shift+F10".to_string(); let _ = self.config.save(); - crate::hyprland_binds::register_hyprland_binds(&self.config); + crate::hyprland_binds::register_hyprland_binds_async(&self.config); crate::config::ScytheConfig::notify_daemon_reload(); self.show_hud_notification("KEYBINDS", "Restored default hotkeys", crate::overlay::ToastIcon::Info); } @@ -2214,9 +2224,9 @@ impl eframe::App for ScytheOverlayApp { ("Cursor Toggle", old) } }; - crate::hyprland_binds::unbind_hotkey(&old_key); + crate::hyprland_binds::unbind_hotkey_async(&old_key); let _ = self.config.save(); - crate::hyprland_binds::register_hyprland_binds(&self.config); + crate::hyprland_binds::register_hyprland_binds_async(&self.config); crate::config::ScytheConfig::notify_daemon_reload(); self.show_hud_notification("KEYBIND", &format!("Bound {}: {}", action_name, combo), crate::overlay::ToastIcon::Info); self.listening_keybind = None; @@ -2272,23 +2282,31 @@ impl eframe::App for ScytheOverlayApp { } #[cfg(target_os = "windows")] -pub fn apply_windows_transparency(title: &str) { +pub fn apply_windows_transparency(_title: &str) { unsafe { - use windows::Win32::UI::WindowsAndMessaging::FindWindowW; + use windows::Win32::UI::WindowsAndMessaging::{EnumWindows, GetWindowThreadProcessId}; + use windows::Win32::System::Threading::GetCurrentProcessId; + use windows::Win32::Foundation::{BOOL, HWND, LPARAM}; use windows::Win32::Graphics::Dwm::DwmExtendFrameIntoClientArea; use windows::Win32::UI::Controls::MARGINS; - use windows::core::HSTRING; - let htitle = HSTRING::from(title); - if let Ok(hwnd) = FindWindowW(None, &htitle) { - let margins = MARGINS { - cxLeftWidth: -1, - cxRightWidth: -1, - cyTopHeight: -1, - cyBottomHeight: -1, - }; - let _ = DwmExtendFrameIntoClientArea(hwnd, &margins); + let my_pid = GetCurrentProcessId(); + unsafe extern "system" fn enum_proc(hwnd: HWND, lparam: LPARAM) -> BOOL { + let my_pid = lparam.0 as u32; + let mut proc_id = 0u32; + GetWindowThreadProcessId(hwnd, Some(&mut proc_id)); + if proc_id == my_pid { + let margins = MARGINS { + cxLeftWidth: -1, + cxRightWidth: -1, + cyTopHeight: -1, + cyBottomHeight: -1, + }; + let _ = DwmExtendFrameIntoClientArea(hwnd, &margins); + } + BOOL(1) } + let _ = EnumWindows(Some(enum_proc), LPARAM(my_pid as isize)); } } @@ -2410,6 +2428,7 @@ impl eframe::App for ShadowPlayToastApp { let elapsed = self.created_at.elapsed().as_secs_f32(); let total_dur = self.duration.as_secs_f32(); if elapsed >= total_dur { + crate::ipc::clean_toast_pid(); ctx.send_viewport_cmd(egui::ViewportCommand::Close); std::process::exit(0); } @@ -2526,14 +2545,25 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc let toast_w: f32 = 340.0; let toast_h: f32 = 64.0; - #[cfg(target_os = "windows")] - unsafe { - use windows::Win32::UI::WindowsAndMessaging::{FindWindowW, PostMessageW, WM_CLOSE}; - if let Ok(prev) = FindWindowW(None, windows::core::w!("Scythe Notification")) { - let _ = PostMessageW(prev, WM_CLOSE, windows::Win32::Foundation::WPARAM(0), windows::Win32::Foundation::LPARAM(0)); - std::thread::sleep(Duration::from_millis(30)); + let toast_pid_path = crate::ipc::get_toast_pid_path(); + if let Ok(prev_pid_str) = std::fs::read_to_string(&toast_pid_path) { + if let Ok(prev_pid) = prev_pid_str.trim().parse::() { + #[cfg(target_os = "windows")] + unsafe { + use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE}; + if let Ok(h) = OpenProcess(PROCESS_TERMINATE, false, prev_pid) { + let _ = TerminateProcess(h, 0); + let _ = windows::Win32::Foundation::CloseHandle(h); + } + } + #[cfg(not(target_os = "windows"))] + unsafe { + let _ = libc::kill(prev_pid as i32, libc::SIGKILL); + } } } + let _ = std::fs::create_dir_all(toast_pid_path.parent().unwrap_or(std::path::Path::new("."))); + let _ = std::fs::write(&toast_pid_path, std::process::id().to_string()); #[cfg(target_os = "windows")] let (mon_x, screen_w): (f32, f32) = unsafe { @@ -2573,8 +2603,10 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc }; // Watchdog thread to guarantee exit after duration + let watchdog_pid_path = toast_pid_path.clone(); std::thread::spawn(move || { std::thread::sleep(Duration::from_millis(3200)); + let _ = std::fs::remove_file(watchdog_pid_path); std::process::exit(0); }); @@ -2583,4 +2615,6 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc options, Box::new(|_cc| Ok(Box::new(app))), ); + + crate::ipc::clean_toast_pid(); }