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
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:
parent
9c91dddbf5
commit
c04bf36e05
11 changed files with 321 additions and 162 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -3700,7 +3700,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "scythe"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"cpal",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "scythe"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,36 +634,43 @@ 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;
|
||||
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 {
|
||||
encoder.encode_cached_frame(pts)
|
||||
}
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(packets) = packets_res {
|
||||
match packets_res {
|
||||
Ok(packets) => {
|
||||
for mut pkt in packets {
|
||||
pkt.set_stream_index(0);
|
||||
if normal_recording {
|
||||
|
|
@ -676,6 +697,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Video encoding error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,13 +242,7 @@ fn handle_toggle_cursor() -> Result<(), Box<dyn std::error::Error + Send + Sync>
|
|||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
ensure_wayland_env();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() > 1 {
|
||||
match args[1].as_str() {
|
||||
"--save" => {
|
||||
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 {
|
||||
|
|
@ -256,7 +250,17 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
return Ok(());
|
||||
}
|
||||
}
|
||||
return send_with_notification(Command::SaveReplay, "INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay);
|
||||
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();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() > 1 {
|
||||
match args[1].as_str() {
|
||||
"--save" => {
|
||||
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(|| {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
if full_bytes == 0 {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
while let Ok(chunk) = mic_rx.try_recv() {
|
||||
mic_q.extend(chunk);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
while let Ok(chunk) = mic_rx.try_recv() {
|
||||
mic_q.extend(chunk);
|
||||
}
|
||||
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,6 +108,7 @@ pub fn register_hyprland_binds(config: &ScytheConfig) {
|
|||
return;
|
||||
}
|
||||
|
||||
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 })"#])
|
||||
|
|
@ -165,6 +169,7 @@ pub fn register_hyprland_binds(config: &ScytheConfig) {
|
|||
.args(["keyword", "windowrulev2", rule])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
|
||||
let binds = [
|
||||
(&config.menu_hotkey, "scythe-ui --menu"),
|
||||
|
|
@ -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
|
||||
|
|
|
|||
17
src/ipc.rs
17
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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
if is_file {
|
||||
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)
|
||||
.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,15 +2282,20 @@ 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 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,
|
||||
|
|
@ -2289,6 +2304,9 @@ pub fn apply_windows_transparency(title: &str) {
|
|||
};
|
||||
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;
|
||||
|
||||
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::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));
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue