feat(ui): complete overhaul of settings modal and notification engine

This commit is contained in:
katR 2026-10-04 12:15:58 +05:30
parent 0925b65d63
commit 4d28bb7ed5
15 changed files with 330 additions and 68 deletions

View file

@ -1,4 +1,5 @@
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
#![allow(clippy::chunks_exact_to_as_chunks)]
use scythe::capture::{Frame, FrameSource};
use scythe::encoder::VideoEncoder;
@ -105,7 +106,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
{
Ok(file) => {
use std::os::unix::io::AsRawFd;
let res = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
let mut res = -1;
for _ in 0..15 {
res = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if res == 0 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(80));
}
if res != 0 {
eprintln!("Another instance of scythe-daemon is already running. Exiting.");
return Ok(());
@ -616,8 +624,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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 unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx) } {
Ok(m) => {
normal_muxer = Some(m);
println!("Started normal recording to {}", full_path);
}
Err(e) => {
eprintln!("Failed to start normal recording to {}: {}", full_path, e);
normal_recording = false;
rec_state_clone.store(false, Ordering::SeqCst);
rec_start_clone.store(0, Ordering::SeqCst);
}
}
} else if normal_waiting_keyframe {
normal_keyframe_timeout += 1;
}
@ -663,17 +681,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let new_config = scythe::config::ScytheConfig::load();
let video_changed = new_config.show_cursor != config.show_cursor
|| new_config.fps != config.fps
|| new_config.video_codec != config.video_codec
|| new_config.record_bitrate_kbps != config.record_bitrate_kbps
|| new_config.replay_bitrate_kbps != config.replay_bitrate_kbps
|| new_config.replay_enabled != config.replay_enabled;
|| new_config.video_codec != config.video_codec;
if video_changed {
println!("Core video settings changed (fps/codec/bitrate/cursor/replay). Restarting daemon for new capture session...");
println!("Core video settings changed (fps/codec/cursor). Restarting daemon for new capture session...");
if let Some(mut m) = normal_muxer.take() {
let _ = m.finalize();
}
let _ = stream_sender.take();
#[cfg(unix)]
{
if let Ok(status) = std::process::Command::new("systemctl")
.args(["--user", "is-active", "--quiet", "scythe-daemon.service"])
.status()
&& status.success() {
let _ = std::process::Command::new("systemctl")
.args(["--user", "restart", "scythe-daemon.service"])
.spawn();
std::thread::sleep(std::time::Duration::from_millis(100));
std::process::exit(0);
}
}
if let Ok(exe) = std::env::current_exe() {
let mut cmd = std::process::Command::new(exe);
cmd.stdin(std::process::Stdio::null())
@ -748,6 +776,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
normal_recording = true;
normal_waiting_keyframe = true;
normal_keyframe_timeout = 0;
has_new_frame = true;
encoder.force_keyframe();
rec_state_clone.store(true, Ordering::SeqCst);
rec_base_audio_pts = -1;
@ -790,6 +819,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
normal_recording = true;
normal_waiting_keyframe = true;
normal_keyframe_timeout = 0;
has_new_frame = true;
encoder.force_keyframe();
rec_state_clone.store(true, Ordering::SeqCst);
rec_base_audio_pts = -1;
@ -976,9 +1006,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if now + jitter_margin >= next_frame_time {
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).abs() <= 1 {
last_video_pts + 1
} else if raw_pts > last_video_pts {
let pts = if last_video_pts < 0 {
0
} else if raw_pts > last_video_pts + 4 {
raw_pts
} else {
last_video_pts + 1
@ -1051,9 +1081,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if now.duration_since(last_frame_encoded_time) >= target_frame_duration.saturating_sub(jitter_margin) {
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).abs() <= 1 {
last_video_pts + 1
} else if raw_pts > last_video_pts {
let pts = if last_video_pts < 0 {
0
} else if raw_pts > last_video_pts + 4 {
raw_pts
} else {
last_video_pts + 1

View file

@ -1,4 +1,5 @@
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
#![allow(clippy::chunks_exact_to_as_chunks)]
use scythe::ipc::{Command, send_command, query_status};
use scythe::overlay::{show_shadowplay_toast, ToastIcon};
@ -122,14 +123,36 @@ fn check_and_toggle_overlay() -> bool {
if let Ok(pid) = content.trim().parse::<u32>() {
if pid != std::process::id() {
unsafe {
use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
use windows::Win32::Foundation::CloseHandle;
if let Ok(h) = OpenProcess(PROCESS_TERMINATE, false, pid) {
let _ = TerminateProcess(h, 0);
let _ = CloseHandle(h);
let _ = std::fs::remove_file(&pid_path);
return true;
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GetWindowThreadProcessId, PostMessageW, WM_CLOSE,
};
use windows::Win32::Foundation::{BOOL, HWND, LPARAM, CloseHandle};
use windows::Win32::System::Threading::{
OpenProcess, TerminateProcess, PROCESS_TERMINATE, PROCESS_SYNCHRONIZE, WaitForSingleObject,
};
unsafe extern "system" fn close_proc(hwnd: HWND, lparam: LPARAM) -> BOOL {
let target = lparam.0 as u32;
let mut pid = 0u32;
GetWindowThreadProcessId(hwnd, Some(&mut pid));
if pid == target {
let _ = PostMessageW(hwnd, WM_CLOSE, None, None);
}
BOOL(1)
}
let _ = EnumWindows(Some(close_proc), LPARAM(pid as isize));
// Wait up to 150ms for graceful shutdown before fallback termination
if let Ok(h) = OpenProcess(PROCESS_SYNCHRONIZE | PROCESS_TERMINATE, false, pid) {
let wait_res = WaitForSingleObject(h, 150);
if wait_res.0 != 0 {
let _ = TerminateProcess(h, 0);
}
let _ = CloseHandle(h);
}
let _ = std::fs::remove_file(&pid_path);
return true;
}
}
}
@ -594,6 +617,14 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
});
}
}
std::thread::sleep(std::time::Duration::from_millis(20));
#[cfg(target_os = "windows")]
unsafe {
use windows::Win32::UI::WindowsAndMessaging::{
MsgWaitForMultipleObjectsEx, QS_ALLINPUT, MWMO_INPUTAVAILABLE,
};
let _ = MsgWaitForMultipleObjectsEx(None, 200, QS_ALLINPUT.0, MWMO_INPUTAVAILABLE);
}
#[cfg(not(target_os = "windows"))]
std::thread::sleep(std::time::Duration::from_millis(30));
}
}

View file

@ -528,9 +528,13 @@ impl AudioCapture {
let stream_config: cpal::StreamConfig = config.into();
let lvl_f32 = Arc::clone(&levels_for_cpal);
let lvl_i16 = Arc::clone(&levels_for_cpal);
let lvl_i32 = Arc::clone(&levels_for_cpal);
let lvl_u16 = Arc::clone(&levels_for_cpal);
let mut resample_state_f32 = ResampleState::default();
let mut resample_state_i16 = ResampleState::default();
let mut resample_state_i32 = ResampleState::default();
let mut resample_state_u16 = ResampleState::default();
let stream = match format {
cpal::SampleFormat::F32 => {
@ -566,6 +570,40 @@ impl AudioCapture {
None,
)?
}
cpal::SampleFormat::I32 => {
dev.build_input_stream(
stream_config,
move |data: &[i32], _: &_| {
let converted_f32: Vec<f32> = data.iter().map(|&s| s as f32 / 2147483648.0).collect();
let f32_data = convert_to_stereo_48k(&converted_f32, in_channels, in_sample_rate, gain, &mut resample_state_i32);
if is_mic {
lvl_i32.update_mic(&f32_data);
} else {
lvl_i32.update_system(&f32_data);
}
let _ = tx.try_send(f32_data);
},
err_fn,
None,
)?
}
cpal::SampleFormat::U16 => {
dev.build_input_stream(
stream_config,
move |data: &[u16], _: &_| {
let converted_f32: Vec<f32> = data.iter().map(|&s| (s as f32 - 32768.0) / 32768.0).collect();
let f32_data = convert_to_stereo_48k(&converted_f32, in_channels, in_sample_rate, gain, &mut resample_state_u16);
if is_mic {
lvl_u16.update_mic(&f32_data);
} else {
lvl_u16.update_system(&f32_data);
}
let _ = tx.try_send(f32_data);
},
err_fn,
None,
)?
}
_ => return Err("Unsupported audio format".into()),
};
stream.play()?;

View file

@ -6,6 +6,9 @@ pub struct WaylandCapture {
_proxy: Screencast,
_session: ashpd::desktop::Session<Screencast>,
stream: crate::capture::wayland_stream::PipeWireStream,
show_cursor: bool,
last_frame: Option<Frame>,
last_reconnect_attempt: std::time::Instant,
}
impl WaylandCapture {
@ -59,16 +62,79 @@ impl WaylandCapture {
let stream = crate::capture::wayland_stream::PipeWireStream::new(node_id, fd)?;
Ok(Self { _proxy: proxy, _session: session, stream })
Ok(Self {
_proxy: proxy,
_session: session,
stream,
show_cursor,
last_frame: None,
last_reconnect_attempt: std::time::Instant::now() - std::time::Duration::from_secs(5),
})
}
fn try_reconnect(&mut self) -> bool {
if self.last_reconnect_attempt.elapsed() < std::time::Duration::from_millis(1500) {
return false;
}
self.last_reconnect_attempt = std::time::Instant::now();
eprintln!("Wayland PipeWire stream disconnected. Attempting auto-reconnect...");
let cursor = self.show_cursor;
// Run on an independent OS thread so block_on never conflicts with Tokio runtime threads
let res = std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.ok()?;
rt.block_on(WaylandCapture::new_with_cursor(cursor)).ok()
}).join();
if let Ok(Some(new_cap)) = res {
eprintln!("Wayland ScreenCast session reconnected successfully.");
self._proxy = new_cap._proxy;
self._session = new_cap._session;
self.stream = new_cap.stream;
true
} else {
false
}
}
}
impl FrameSource for WaylandCapture {
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
self.stream.next_frame()
match self.stream.next_frame() {
Ok(f) => {
self.last_frame = Some(f.clone());
Ok(f)
}
Err(e) => {
if self.try_reconnect() {
return self.stream.next_frame();
}
if let Some(ref f) = self.last_frame {
return Ok(f.clone());
}
Err(e)
}
}
}
fn next_frame_timeout(&mut self, timeout: std::time::Duration) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
self.stream.next_frame_timeout(timeout)
match self.stream.next_frame_timeout(timeout) {
Ok(f) => {
self.last_frame = Some(f.clone());
Ok(f)
}
Err(e) => {
if self.try_reconnect() {
return self.stream.next_frame_timeout(timeout);
}
if let Some(ref f) = self.last_frame {
return Ok(f.clone());
}
Err(e)
}
}
}
}

View file

@ -32,8 +32,9 @@ pub struct WindowsCapture {
pub width: u32,
pub height: u32,
pub last_stride: u32,
buffer_pool: Vec<Option<Arc<Vec<u8>>>>,
buffer_pool: Vec<Arc<Vec<u8>>>,
last_frame: Option<Arc<Vec<u8>>>,
last_reinit_attempt: std::time::Instant,
}
#[cfg(target_os = "windows")]
@ -52,13 +53,21 @@ impl WindowsCapture {
let mut a_idx = 0;
'adapter_loop: while let Ok(adapter) = factory.EnumAdapters1(a_idx) {
if let Ok(desc1) = adapter.GetDesc1() {
// Skip software rasterizers (e.g. Microsoft Basic Render Driver) when hardware GPU exists
if (desc1.Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0) != 0 {
a_idx += 1;
continue;
}
}
let mut o_idx = 0;
while let Ok(output) = adapter.EnumOutputs(o_idx) {
if let Ok(output1) = output.cast::<IDXGIOutput1>() {
if let Ok(desc) = output1.GetDesc() {
let is_attached = desc.AttachedToDesktop.as_bool();
let width = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left).unsigned_abs();
let height = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top).unsigned_abs();
if width > 0 && height > 0 {
if is_attached && width > 0 && height > 0 {
if let Ok(adapter_base) = adapter.cast::<IDXGIAdapter>() {
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
@ -133,15 +142,19 @@ impl WindowsCapture {
width,
height,
last_stride: width * 4,
buffer_pool: Vec::with_capacity(4),
buffer_pool: Vec::with_capacity(8),
last_frame: None,
last_reinit_attempt: std::time::Instant::now() - std::time::Duration::from_secs(5),
})
}
}
fn reinit_duplication(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if self.last_reinit_attempt.elapsed() < std::time::Duration::from_millis(250) {
return Err("Reinitialization throttled".into());
}
self.last_reinit_attempt = std::time::Instant::now();
self.duplication = None;
std::thread::sleep(std::time::Duration::from_millis(50));
if let Ok(new_cap) = WindowsCapture::new() {
self.device = new_cap.device;
self.context = new_cap.context;
@ -196,14 +209,12 @@ impl FrameSource for WindowsCapture {
let target_stride = (self.width * 4) as usize;
let total_bytes = target_stride * self.height as usize;
// Acquire buffer from pool
// Acquire buffer from pool or allocate if pool has room
let mut arc_buf = None;
for slot in &mut self.buffer_pool {
if let Some(buf) = slot {
if Arc::strong_count(buf) == 1 {
arc_buf = slot.take();
break;
}
for i in 0..self.buffer_pool.len() {
if Arc::strong_count(&self.buffer_pool[i]) == 1 {
arc_buf = Some(self.buffer_pool.swap_remove(i));
break;
}
}
let mut arc_buf = arc_buf.unwrap_or_else(|| Arc::new(vec![0u8; total_bytes]));
@ -234,8 +245,8 @@ impl FrameSource for WindowsCapture {
self.last_frame = Some(Arc::clone(&arc_buf));
self.last_stride = target_stride as u32;
if self.buffer_pool.len() < 4 {
self.buffer_pool.push(Some(arc_buf));
if self.buffer_pool.len() < 8 {
self.buffer_pool.push(arc_buf);
}
return Ok(Frame::Raw {
@ -254,14 +265,12 @@ impl FrameSource for WindowsCapture {
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST
|| e.code() == DXGI_ERROR_ACCESS_DENIED
|| e.code() == DXGI_ERROR_INVALID_CALL => {
let _ = dup.ReleaseFrame();
drop(dup);
self.duplication = None;
let _ = self.reinit_duplication();
break;
}
Err(e) => {
let _ = dup.ReleaseFrame();
drop(dup);
self.duplication = None;
let _ = self.reinit_duplication();

View file

@ -66,19 +66,21 @@ impl VaapiEncoder {
(*codec_ctx).gop_size = fps; // Emit IDR keyframe every second
(*codec_ctx).max_b_frames = 0; // Zero latency, no frame reordering
// High Quality & Clean VBR Rate Control
// High Quality & Clean VBR Rate Control matching GPU Screen Recorder standards
let rate = (bitrate_kbps as i64) * 1000;
(*codec_ctx).bit_rate = rate;
(*codec_ctx).rc_max_rate = rate * 3 / 2; // Allow burst for fast motion scenes
(*codec_ctx).rc_buffer_size = (rate / 2) as i32;
(*codec_ctx).qmin = 16;
(*codec_ctx).qmax = 28; // Prevent compression blockiness
(*codec_ctx).rc_max_rate = rate * 2; // Allow 2x burst for high-motion gaming scenes
(*codec_ctx).rc_buffer_size = (rate * 2) as i32; // 2 seconds rate control window
(*codec_ctx).qmin = 1; // Allow high-fidelity crisp detail
(*codec_ctx).qmax = 32;
(*codec_ctx).profile = match codec_pref.to_lowercase().as_str() {
"hevc" | "h265" => FF_PROFILE_HEVC_MAIN,
"av1" => FF_PROFILE_UNKNOWN,
_ => FF_PROFILE_H264_HIGH,
};
(*codec_ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as libc::c_int;
let _ = av_opt_set((*codec_ctx).priv_data, c"rc_mode".as_ptr(), c"VBR".as_ptr(), 0);
let _ = av_opt_set_int((*codec_ctx).priv_data, c"async_depth".as_ptr(), 4, 0);
// Rec.709 Color Space & MPEG Limited Range (Broadcast / Web standard, avoids yellow tint)
(*codec_ctx).colorspace = AVColorSpace::AVCOL_SPC_BT709;
@ -310,6 +312,7 @@ impl VaapiEncoder {
}
av_packet_free(&mut pkt);
mapped_direct = true;
self.has_frame = true;
}
} else {
self.direct_drm_unsupported = true;
@ -378,7 +381,7 @@ impl VaapiEncoder {
pub fn encode_cached_frame(&mut self, pts: i64) -> Result<Vec<crate::ring::Packet>, String> {
let mut packets = Vec::new();
if !self.has_frame {
return Ok(packets);
return Err("No cached frame available in encoder".into());
}
unsafe {
self.send_staged_frame(&mut packets, pts)?;
@ -498,10 +501,10 @@ impl WindowsHwEncoder {
let rate = (bitrate_kbps as i64) * 1000;
(*ctx).bit_rate = rate;
(*ctx).rc_max_rate = rate * 3 / 2;
(*ctx).rc_buffer_size = (rate / 2) as i32;
(*ctx).qmin = 16;
(*ctx).qmax = 28;
(*ctx).rc_max_rate = rate * 2;
(*ctx).rc_buffer_size = (rate * 2) as i32;
(*ctx).qmin = 1;
(*ctx).qmax = 32;
let codec_id = (*c).id;
(*ctx).profile = if codec_id == AVCodecID::AV_CODEC_ID_HEVC {
FF_PROFILE_HEVC_MAIN
@ -520,11 +523,12 @@ impl WindowsHwEncoder {
(*ctx).color_range = AVColorRange::AVCOL_RANGE_MPEG;
if desc.contains("NVENC") {
let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"p1".as_ptr(), 0);
let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"p3".as_ptr(), 0);
let _ = av_opt_set((*ctx).priv_data, c"tune".as_ptr(), c"ull".as_ptr(), 0);
let _ = av_opt_set((*ctx).priv_data, c"forced-idr".as_ptr(), c"1".as_ptr(), 0);
} else if desc.contains("AMF") {
let _ = av_opt_set((*ctx).priv_data, c"usage".as_ptr(), c"ultralowlatency".as_ptr(), 0);
let _ = av_opt_set((*ctx).priv_data, c"rc".as_ptr(), c"vbr_latency".as_ptr(), 0);
} else if desc.contains("QuickSync") {
let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"veryfast".as_ptr(), 0);
} else {
@ -672,7 +676,7 @@ impl WindowsHwEncoder {
pub fn encode_cached_frame(&mut self, pts: i64) -> Result<Vec<crate::ring::Packet>, String> {
let mut packets = Vec::new();
if !self.has_frame {
return Ok(packets);
return Err("No cached frame available in encoder".into());
}
unsafe {
if self.sw_frame.is_null() {

View file

@ -93,6 +93,18 @@ pub fn read_active_ipc_port() -> u16 {
42069
}
#[cfg(windows)]
pub fn get_candidate_ipc_ports() -> Vec<u16> {
let active_port = read_active_ipc_port();
let mut ports = vec![active_port];
for p in [42069, 42070, 42071, 42072] {
if !ports.contains(&p) {
ports.push(p);
}
}
ports
}
pub fn is_daemon_running() -> bool {
query_status().is_ok()
}
@ -133,8 +145,7 @@ pub fn send_command(cmd: Command) -> Result<(), Box<dyn std::error::Error + Send
#[cfg(windows)]
{
use std::net::{SocketAddr, TcpStream};
let active_port = read_active_ipc_port();
let candidate_ports = [active_port, 42069, 42070, 42071, 42072];
let candidate_ports = get_candidate_ipc_ports();
for &port in &candidate_ports {
if let Ok(addr) = format!("127.0.0.1:{}", port).parse::<SocketAddr>() {
if let Ok(mut stream) = TcpStream::connect_timeout(&addr, Duration::from_millis(150)) {
@ -203,8 +214,7 @@ pub fn query_status() -> Result<DaemonStatus, Box<dyn std::error::Error + Send +
#[cfg(windows)]
let stream_res = {
use std::net::{SocketAddr, TcpStream};
let active_port = read_active_ipc_port();
let candidate_ports = [active_port, 42069, 42070, 42071, 42072];
let candidate_ports = get_candidate_ipc_ports();
let mut conn = None;
for &port in &candidate_ports {
if let Ok(addr) = format!("127.0.0.1:{}", port).parse::<SocketAddr>() {

View file

@ -1,3 +1,5 @@
#![allow(clippy::chunks_exact_to_as_chunks)]
pub mod capture;
pub mod encoder;
pub mod ring;

View file

@ -353,18 +353,34 @@ fn run_auto_update(info: &ReleaseInfo, status: &Arc<Mutex<UpdateStatus>>) -> Res
let current_exe = std::env::current_exe().map_err(|e| format!("Cannot locate current executable: {}", e))?;
let exe_dir = current_exe.parent().ok_or_else(|| "Invalid executable path".to_string())?;
set_progress("Replacing binaries...");
for bin_name in ["scythe-ui.exe", "scythe-daemon.exe"] {
let src = temp_dir.join(bin_name);
if src.exists() {
let dest = exe_dir.join(bin_name);
let dest_old = exe_dir.join(format!("{}.old", bin_name));
let _ = std::fs::remove_file(&dest_old);
let _ = std::fs::rename(&dest, &dest_old);
let _ = std::fs::copy(&src, &dest);
set_progress("Replacing binaries and libraries...");
let mut files_to_update = Vec::new();
let mut search_dirs = vec![temp_dir.clone()];
while let Some(dir) = search_dirs.pop() {
if let Ok(entries) = std::fs::read_dir(&dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
search_dirs.push(path);
} else if path.is_file() {
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
let lower = name.to_lowercase();
if lower.ends_with(".exe") || lower.ends_with(".dll") {
files_to_update.push((path, name));
}
}
}
}
}
for (src, name) in files_to_update {
let dest = exe_dir.join(&name);
let dest_old = exe_dir.join(format!("{}.old", name));
let _ = std::fs::remove_file(&dest_old);
let _ = std::fs::rename(&dest, &dest_old);
let _ = std::fs::copy(&src, &dest);
}
let _ = std::fs::remove_dir_all(&temp_dir);
return Ok(format!("Updated binaries to v{} in {}", info.version, exe_dir.display()));
}

View file

@ -13,7 +13,7 @@ SOCKET_PATH="${XDG_RUNTIME_DIR:-/tmp}/scythe.sock"
rm -f "$SOCKET_PATH"
rm -f record_*.mp4 replay_*.mp4
SAVE_DIR=$(python3 -c 'import json, os; p=os.path.expanduser("~/.config/scythe/config.json"); print(json.load(open(p))["output_directory"]) if os.path.exists(p) else print(os.path.expanduser("~/Videos/Scythe"))' 2>/dev/null || echo "$HOME/Videos/Scythe")
SAVE_DIR=$(python3 -c 'import json, os; p=os.path.expanduser("~/.config/scythe/config.json"); print(os.path.expanduser(json.load(open(p))["output_directory"])) if os.path.exists(p) else print(os.path.expanduser("~/Videos/Scythe"))' 2>/dev/null || echo "$HOME/Videos/Scythe")
mkdir -p "$SAVE_DIR"
echo "[2/6] Starting scythe-daemon with mock capture..."
@ -72,7 +72,7 @@ echo " Verifying Generated Recordings with ffprobe"
echo "============================================="
FOUND_ANY=0
for f in $(find "$SAVE_DIR" "$HOME/Videos/Scythe" "$HOME/Videos/vrec" . -maxdepth 2 \( -name "record_*.mp4" -o -name "replay_*.mp4" \) 2>/dev/null); do
while IFS= read -r -d '' f; do
if [ -f "$f" ]; then
FOUND_ANY=1
echo "--> Testing file: $f ($(ls -lh "$f" | awk '{print $5}'))"
@ -87,7 +87,7 @@ for f in $(find "$SAVE_DIR" "$HOME/Videos/Scythe" "$HOME/Videos/vrec" . -maxdept
echo "---------------------------------------------"
rm -f "$f"
fi
done
done < <(find "$SAVE_DIR" "$HOME/Videos/Scythe" "$HOME/Videos/vrec" . -maxdepth 2 -mmin -2 \( -iname "record*.mp4" -o -iname "replay*.mp4" \) -print0 2>/dev/null)
if [ $FOUND_ANY -eq 0 ]; then
echo "FAILED: No recording or replay files were produced!"

View file

@ -0,0 +1,16 @@
import socket
import json
import struct
import os
def send(cmd):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
rt = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
sock_path = f"{rt}/scythe.sock" if os.path.exists(f"{rt}/scythe.sock") else f"{rt}/vrec.sock"
sock.connect(sock_path)
payload = json.dumps(cmd).encode('utf-8')
sock.sendall(struct.pack('<I', len(payload)))
sock.sendall(payload)
import sys
send(sys.argv[1])

View file

@ -0,0 +1,13 @@
import socket
import json
import struct
import os
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
rt = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
sock_path = f"{rt}/scythe.sock" if os.path.exists(f"{rt}/scythe.sock") else f"{rt}/vrec.sock"
sock.connect(sock_path)
payload = b'"SaveReplay"'
sock.sendall(struct.pack('<I', len(payload)))
sock.sendall(payload)
print("Sent SaveReplay")

View file

@ -0,0 +1,9 @@
import socket, struct, os
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
rt = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
sock_path = f"{rt}/scythe.sock" if os.path.exists(f"{rt}/scythe.sock") else f"{rt}/vrec.sock"
sock.connect(sock_path)
payload = b'"StartRecording"'
sock.sendall(struct.pack('<I', len(payload)))
sock.sendall(payload)
print("Sent StartRecording")

View file

@ -0,0 +1,9 @@
import socket, struct, os
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
rt = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
sock_path = f"{rt}/scythe.sock" if os.path.exists(f"{rt}/scythe.sock") else f"{rt}/vrec.sock"
sock.connect(sock_path)
payload = b'"ReloadConfig"'
sock.sendall(struct.pack('<I', len(payload)))
sock.sendall(payload)
print("Sent ReloadConfig")

View file

@ -0,0 +1,9 @@
import socket, struct, os
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
rt = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
sock_path = f"{rt}/scythe.sock" if os.path.exists(f"{rt}/scythe.sock") else f"{rt}/vrec.sock"
sock.connect(sock_path)
payload = b'"StopRecording"'
sock.sendall(struct.pack('<I', len(payload)))
sock.sendall(payload)
print("Sent StopRecording")