fix: resolve replay clip saving, ensure 60fps pacing and keyframes, polish audio mixing, and prevent daemon termination
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-08 08:23:12 +05:30
parent 9c91dddbf5
commit c04bf36e05
11 changed files with 321 additions and 162 deletions

2
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -156,6 +156,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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);
}
}
}

View file

@ -242,6 +242,17 @@ fn handle_toggle_cursor() -> Result<(), Box<dyn std::error::Error + Send + Sync>
}
}
fn handle_save_replay() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
ensure_wayland_env();
@ -249,14 +260,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
});
} 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(|| {

View file

@ -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<f32> = VecDeque::with_capacity(16384);
let mut mic_q: VecDeque<f32> = 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<f32> = 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<f32> = 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<f32> = 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<f32> = 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<f32> = VecDeque::with_capacity(16384);
let mut mic_q: VecDeque<f32> = 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<f32> = 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<f32> = 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<f32> = 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<f32> = 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));

View file

@ -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<Frame, Box<dyn std::error::Error + Send + Sync>> {
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() {

View file

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

View file

@ -98,6 +98,9 @@ pub fn hotkey_to_hyprland_lua(hotkey: &str) -> Option<String> {
}
}
#[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

View file

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

View file

@ -207,21 +207,38 @@ fn scan_recordings(dir_str: &str) -> Vec<VideoClipInfo> {
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<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!(
"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::<u32>() {
#[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();
}