feat(arch): consolidate daemon and UI into singular multicall binary scythe

This commit is contained in:
katR 2026-10-11 14:28:14 +05:30
parent 425a9b1003
commit 3cc718fa69
8 changed files with 101 additions and 87 deletions

View file

@ -4,12 +4,8 @@ version = "0.2.18"
edition = "2024" edition = "2024"
[[bin]] [[bin]]
name = "scythe-daemon" name = "scythe"
path = "src/bin/scythe-daemon.rs" path = "src/main.rs"
[[bin]]
name = "scythe-ui"
path = "src/bin/scythe-ui.rs"
[dependencies] [dependencies]
crossbeam-channel = "0.5.16" crossbeam-channel = "0.5.16"

View file

@ -6,6 +6,14 @@ This checklist tracks known bugs, UI refinements, optimizations, and technical d
## 1. UI Refinements & Imperfections ## 1. UI Refinements & Imperfections
- [x] **Singular Multicall Binary Architecture (`scythe`)**:
- **Issue**: Distributing separate `scythe-daemon` and `scythe-ui` binaries required cross-process path discovery in `$PATH`, complicated Windows bundling with multiple executables, and prevented atomic single-file updates.
- **Resolution**:
- Consolidated daemon engine and UI overlay/CLI into a single binary (`scythe`) in [src/main.rs](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/src/main.rs) and [src/daemon.rs](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/src/daemon.rs).
- Implemented multicall dispatch: inspects `argv[0]` and subcommands (`scythe daemon`, `scythe [options]`, or symlinks `scythe-daemon` / `scythe-ui`).
- Configured [install.sh](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/install.sh) with backward-compatible symlinks.
- Updated release packages: both Linux and Windows distributions now ship a single executable.
- [x] **Screenshot Resolution Presets (Native, 1080p, 1440p, 2160p, 720p) & Lanczos Downscaling**: - [x] **Screenshot Resolution Presets (Native, 1080p, 1440p, 2160p, 720p) & Lanczos Downscaling**:
- **Issue**: Vague compression percentages and presets confused users instead of providing clear resolution controls. - **Issue**: Vague compression percentages and presets confused users instead of providing clear resolution controls.
- **Resolution**: Replaced vague compression sliders with clean resolution presets (`Native (Source)`, `1080p (FHD)`, `1440p (QHD)`, `2160p (4K UHD)`, `720p (HD)`) in [src/overlay_egui.rs](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/src/overlay_egui.rs). When a target resolution is selected, [src/bin/scythe-ui.rs](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/src/bin/scythe-ui.rs) executes pristine Lanczos hardware-accelerated scaling via FFmpeg. - **Resolution**: Replaced vague compression sliders with clean resolution presets (`Native (Source)`, `1080p (FHD)`, `1440p (QHD)`, `2160p (4K UHD)`, `720p (HD)`) in [src/overlay_egui.rs](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/src/overlay_egui.rs). When a target resolution is selected, [src/bin/scythe-ui.rs](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/src/bin/scythe-ui.rs) executes pristine Lanczos hardware-accelerated scaling via FFmpeg.

View file

@ -15,15 +15,15 @@ APP_DIR="$HOME/.local/share/applications"
mkdir -p "$BIN_DIR" mkdir -p "$BIN_DIR"
mkdir -p "$APP_DIR" mkdir -p "$APP_DIR"
echo "[2/4] Installing binaries to $BIN_DIR..." echo "[2/4] Installing singular binary to $BIN_DIR..."
install -m 755 target/release/scythe-daemon "$BIN_DIR/scythe-daemon" install -m 755 target/release/scythe "$BIN_DIR/scythe"
install -m 755 target/release/scythe-ui "$BIN_DIR/scythe-ui"
ln -sf "$BIN_DIR/scythe-ui" "$BIN_DIR/scythe"
# Backward-compatible symlinks # Backward-compatible multicall symlinks
ln -sf "$BIN_DIR/scythe-daemon" "$BIN_DIR/vrec-daemon" ln -sf "$BIN_DIR/scythe" "$BIN_DIR/scythe-daemon"
ln -sf "$BIN_DIR/scythe-ui" "$BIN_DIR/vrec-ui" ln -sf "$BIN_DIR/scythe" "$BIN_DIR/scythe-ui"
ln -sf "$BIN_DIR/scythe-ui" "$BIN_DIR/vrec" ln -sf "$BIN_DIR/scythe" "$BIN_DIR/vrec-daemon"
ln -sf "$BIN_DIR/scythe" "$BIN_DIR/vrec-ui"
ln -sf "$BIN_DIR/scythe" "$BIN_DIR/vrec"
SYSTEMD_USER_DIR="$HOME/.config/systemd/user" SYSTEMD_USER_DIR="$HOME/.config/systemd/user"
mkdir -p "$SYSTEMD_USER_DIR" mkdir -p "$SYSTEMD_USER_DIR"
@ -35,7 +35,7 @@ Wants=xdg-desktop-portal.service
[Service] [Service]
Type=simple Type=simple
ExecStart=$BIN_DIR/scythe-daemon ExecStart=$BIN_DIR/scythe daemon
Restart=always Restart=always
RestartSec=2 RestartSec=2
Environment="XDG_RUNTIME_DIR=/run/user/1000" Environment="XDG_RUNTIME_DIR=/run/user/1000"

View file

@ -1,11 +1,9 @@
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
#![allow(clippy::chunks_exact_to_as_chunks)]
use scythe::capture::{Frame, FrameSource}; use crate::capture::{Frame, FrameSource};
use scythe::encoder::VideoEncoder; use crate::encoder::VideoEncoder;
use scythe::ring::Packet; use crate::ring::Packet;
use scythe::muxer::Muxer; use crate::muxer::Muxer;
use scythe::ipc::{Command, DaemonStatus}; use crate::ipc::{Command, DaemonStatus};
use ringbuf::HeapRb; use ringbuf::HeapRb;
use ringbuf::traits::{RingBuffer, Consumer, Observer}; use ringbuf::traits::{RingBuffer, Consumer, Observer};
use crossbeam_channel::bounded; use crossbeam_channel::bounded;
@ -79,12 +77,18 @@ fn spawn_stream_worker(
} }
fn ensure_wayland_env() { fn ensure_wayland_env() {
scythe::overlay::ensure_wayland_env(); crate::overlay::ensure_wayland_env();
}
pub fn run_daemon() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(run_daemon_async())
} }
#[tokio::main]
#[allow(unused_assignments)] #[allow(unused_assignments)]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn run_daemon_async() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
unsafe { unsafe {
use windows::Win32::System::Console::FreeConsole; use windows::Win32::System::Console::FreeConsole;
@ -123,11 +127,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Err(_) => None, Err(_) => None,
} }
}; };
let initial_config = scythe::config::ScytheConfig::load(); let initial_config = crate::config::ScytheConfig::load();
// Automatically register dynamic keybindings on Hyprland if active (no config edits needed) // Automatically register dynamic keybindings on Hyprland if active (no config edits needed)
scythe::hyprland_binds::register_hyprland_binds(&initial_config); crate::hyprland_binds::register_hyprland_binds(&initial_config);
scythe::hyprland_binds::spawn_hyprland_reload_watcher(); crate::hyprland_binds::spawn_hyprland_reload_watcher();
let (frame_tx, frame_rx) = bounded::<Frame>(2); let (frame_tx, frame_rx) = bounded::<Frame>(2);
let (cmd_tx, cmd_rx) = bounded::<Command>(32); let (cmd_tx, cmd_rx) = bounded::<Command>(32);
@ -140,11 +144,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let stream_start_state = Arc::new(AtomicU64::new(0)); let stream_start_state = Arc::new(AtomicU64::new(0));
let replay_enabled_state = Arc::new(AtomicBool::new(initial_config.replay_enabled)); let replay_enabled_state = Arc::new(AtomicBool::new(initial_config.replay_enabled));
let audio_muted_state = Arc::new(AtomicBool::new(false)); let audio_muted_state = Arc::new(AtomicBool::new(false));
let audio_levels = Arc::new(scythe::capture::audio::AudioLevels::new()); let audio_levels = Arc::new(crate::capture::audio::AudioLevels::new());
#[cfg(unix)] #[cfg(unix)]
let listener = { let listener = {
let socket_path = scythe::ipc::get_socket_path(); let socket_path = crate::ipc::get_socket_path();
let _ = std::fs::remove_file(&socket_path); let _ = std::fs::remove_file(&socket_path);
let l = std::os::unix::net::UnixListener::bind(&socket_path)?; let l = std::os::unix::net::UnixListener::bind(&socket_path)?;
println!("Daemon listening on IPC socket: {}", socket_path); println!("Daemon listening on IPC socket: {}", socket_path);
@ -162,7 +166,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
break; break;
} }
Err(_) => { Err(_) => {
if scythe::ipc::query_status_port(port).is_ok() { if crate::ipc::query_status_port(port).is_ok() {
println!("Another instance of scythe-daemon is already running on port {}. Exiting cleanly.", port); println!("Another instance of scythe-daemon is already running on port {}. Exiting cleanly.", port);
return Ok(()); return Ok(());
} }
@ -172,7 +176,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
match bound { match bound {
Some((l, p)) => { Some((l, p)) => {
println!("Daemon listening on TCP IPC: 127.0.0.1:{}", p); println!("Daemon listening on TCP IPC: 127.0.0.1:{}", p);
let _ = std::fs::write(scythe::ipc::get_ipc_port_path(), p.to_string()); let _ = std::fs::write(crate::ipc::get_ipc_port_path(), p.to_string());
let _ = std::fs::write(std::env::temp_dir().join("vrec-ipc.port"), p.to_string()); let _ = std::fs::write(std::env::temp_dir().join("vrec-ipc.port"), p.to_string());
l l
} }
@ -230,7 +234,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
} else { } else {
0 0
}; };
let cfg = scythe::config::ScytheConfig::load(); let cfg = crate::config::ScytheConfig::load();
let status = DaemonStatus { let status = DaemonStatus {
is_recording: rec, is_recording: rec,
recording_duration_sec: duration, recording_duration_sec: duration,
@ -252,7 +256,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
} }
}, },
Command::ToggleCursor => { Command::ToggleCursor => {
let mut cfg = scythe::config::ScytheConfig::load(); let mut cfg = crate::config::ScytheConfig::load();
cfg.show_cursor = !cfg.show_cursor; cfg.show_cursor = !cfg.show_cursor;
let _ = cfg.save(); let _ = cfg.save();
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@ -275,7 +279,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("Cursor display toggled to: {}", cfg.show_cursor); println!("Cursor display toggled to: {}", cfg.show_cursor);
}, },
Command::CycleAudioMode => { Command::CycleAudioMode => {
let mut cfg = scythe::config::ScytheConfig::load(); let mut cfg = crate::config::ScytheConfig::load();
cfg.audio_mode = match cfg.audio_mode.as_str() { cfg.audio_mode = match cfg.audio_mode.as_str() {
"system" => "mic", "system" => "mic",
"mic" => "both", "mic" => "both",
@ -288,7 +292,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}, },
Command::StopDaemon => { Command::StopDaemon => {
println!("StopDaemon requested: Finalizing active recordings..."); println!("StopDaemon requested: Finalizing active recordings...");
scythe::hyprland_binds::unregister_hyprland_binds(&scythe::config::ScytheConfig::load()); crate::hyprland_binds::unregister_hyprland_binds(&crate::config::ScytheConfig::load());
let _ = cmd_tx_ipc.send(Command::StopRecording); let _ = cmd_tx_ipc.send(Command::StopRecording);
std::thread::sleep(std::time::Duration::from_millis(350)); std::thread::sleep(std::time::Duration::from_millis(350));
std::process::exit(0); std::process::exit(0);
@ -314,7 +318,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("\nReceived shutdown signal. Finalizing recordings..."); println!("\nReceived shutdown signal. Finalizing recordings...");
let _ = cmd_tx_sig.send(Command::StopRecording); let _ = cmd_tx_sig.send(Command::StopRecording);
tokio::time::sleep(std::time::Duration::from_millis(350)).await; tokio::time::sleep(std::time::Duration::from_millis(350)).await;
scythe::hyprland_binds::unregister_hyprland_binds(&scythe::config::ScytheConfig::load()); crate::hyprland_binds::unregister_hyprland_binds(&crate::config::ScytheConfig::load());
std::process::exit(0); std::process::exit(0);
} }
}); });
@ -322,7 +326,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string()); let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string());
println!("Detected session type: {}", session_type); println!("Detected session type: {}", session_type);
let audio_capture_initial = scythe::capture::audio::AudioCapture::new_with_device_mode_volumes_and_levels( let audio_capture_initial = crate::capture::audio::AudioCapture::new_with_device_mode_volumes_and_levels(
audio_tx.clone(), audio_tx.clone(),
Some(&initial_config.audio_device), Some(&initial_config.audio_device),
&initial_config.audio_mode, &initial_config.audio_mode,
@ -335,12 +339,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if std::env::args().any(|a| a == "--mock") { if std::env::args().any(|a| a == "--mock") {
println!("Initializing MOCK capture..."); println!("Initializing MOCK capture...");
source_opt = Some(Box::new(scythe::capture::mock::MockCapture::new())); source_opt = Some(Box::new(crate::capture::mock::MockCapture::new()));
} else if cfg!(target_os = "windows") { } else if cfg!(target_os = "windows") {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
println!("Initializing Windows DXGI capture..."); println!("Initializing Windows DXGI capture...");
if let Ok(c) = scythe::capture::windows::WindowsCapture::new() { if let Ok(c) = crate::capture::windows::WindowsCapture::new() {
source_opt = Some(Box::new(c)); source_opt = Some(Box::new(c));
} }
} }
@ -354,7 +358,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.args(["--user", "start", "xdg-desktop-portal-hyprland"]) .args(["--user", "start", "xdg-desktop-portal-hyprland"])
.status(); .status();
match scythe::capture::wayland::WaylandCapture::new_with_cursor(initial_config.show_cursor).await { match crate::capture::wayland::WaylandCapture::new_with_cursor(initial_config.show_cursor).await {
Ok(cap) => { Ok(cap) => {
source_opt = Some(Box::new(cap)); source_opt = Some(Box::new(cap));
break; break;
@ -375,7 +379,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
println!("Initializing X11 capture..."); println!("Initializing X11 capture...");
if let Ok(c) = scythe::capture::x11::X11Capture::new() { if let Ok(c) = crate::capture::x11::X11Capture::new() {
source_opt = Some(Box::new(c)); source_opt = Some(Box::new(c));
} }
} }
@ -387,7 +391,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
None => { None => {
if is_mock_cli { if is_mock_cli {
eprintln!("Warning: Mock capture mode active via --mock flag."); eprintln!("Warning: Mock capture mode active via --mock flag.");
Box::new(scythe::capture::mock::MockCapture::new()) Box::new(crate::capture::mock::MockCapture::new())
} else { } else {
eprintln!("Error: Hardware capture failed to initialize on {} session. Exiting for restart.", session_type); eprintln!("Error: Hardware capture failed to initialize on {} session. Exiting for restart.", session_type);
std::process::exit(1); std::process::exit(1);
@ -455,7 +459,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut audio_capture = audio_capture_initial.map(|(c, _, _)| c); let mut audio_capture = audio_capture_initial.map(|(c, _, _)| c);
let recorder_handle = thread::spawn(move || { let recorder_handle = thread::spawn(move || {
let mut config = scythe::config::ScytheConfig::load(); let mut config = crate::config::ScytheConfig::load();
replay_state_clone.store(config.replay_enabled, Ordering::SeqCst); replay_state_clone.store(config.replay_enabled, Ordering::SeqCst);
let active_bitrate = if config.replay_enabled { let active_bitrate = if config.replay_enabled {
@ -477,9 +481,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("Background recorder engine active and ready ({} fps, replay: {}).", config.fps, config.replay_enabled); println!("Background recorder engine active and ready ({} fps, replay: {}).", config.fps, config.replay_enabled);
let mut audio_encoder = if let Some((sr, ch)) = audio_info { let mut audio_encoder = if let Some((sr, ch)) = audio_info {
scythe::encoder::AudioEncoder::new_with_codec(sr as i32, ch as i32, &config.audio_codec).ok() crate::encoder::AudioEncoder::new_with_codec(sr as i32, ch as i32, &config.audio_codec).ok()
} else { } else {
scythe::encoder::AudioEncoder::new_with_codec(48000, 2, &config.audio_codec).ok() crate::encoder::AudioEncoder::new_with_codec(48000, 2, &config.audio_codec).ok()
}; };
let audio_codec_ctx_ptr = audio_encoder.as_ref().map(|e| e.codec_ctx() as usize); let audio_codec_ctx_ptr = audio_encoder.as_ref().map(|e| e.codec_ctx() as usize);
@ -511,8 +515,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
thread::spawn(move || { thread::spawn(move || {
while let Ok(drain) = mux_rx.recv() { while let Ok(drain) = mux_rx.recv() {
let filename = scythe::config::ScytheConfig::format_video_filename("Replay", "mp4"); let filename = crate::config::ScytheConfig::format_video_filename("Replay", "mp4");
let full_path = scythe::config::ScytheConfig::load().resolve_save_path(&filename); let full_path = crate::config::ScytheConfig::load().resolve_save_path(&filename);
println!("Saving replay to {}...", full_path); println!("Saving replay to {}...", full_path);
// Find the first video IDR keyframe (ignore audio packets which always have keyframe flag set) // Find the first video IDR keyframe (ignore audio packets which always have keyframe flag set)
@ -639,7 +643,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
rec_base_video_pts = pkt.pts(); rec_base_video_pts = pkt.pts();
rec_base_audio_pts = -1; rec_base_audio_pts = -1;
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext; let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
let filename = scythe::config::ScytheConfig::format_video_filename("Recording", "mp4"); let filename = crate::config::ScytheConfig::format_video_filename("Recording", "mp4");
let full_path = config.resolve_save_path(&filename); 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); let audio_codec_ctx = audio_codec_ctx_ptr.map(|p| p as *mut ffmpeg_next::ffi::AVCodecContext);
match unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx) } { match unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx) } {
@ -696,7 +700,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(cmd) = cmd_res { if let Ok(cmd) = cmd_res {
match cmd { match cmd {
Command::ReloadConfig => { Command::ReloadConfig => {
let new_config = scythe::config::ScytheConfig::load(); let new_config = crate::config::ScytheConfig::load();
let video_changed = new_config.show_cursor != config.show_cursor let video_changed = new_config.show_cursor != config.show_cursor
|| new_config.fps != config.fps || new_config.fps != config.fps
|| new_config.video_codec != config.video_codec || new_config.video_codec != config.video_codec
@ -754,8 +758,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
unsafe { libc::malloc_trim(0); } unsafe { libc::malloc_trim(0); }
} }
scythe::hyprland_binds::register_hyprland_binds(&config); crate::hyprland_binds::register_hyprland_binds(&config);
audio_capture = match scythe::capture::audio::AudioCapture::new_with_device_mode_volumes_and_levels( audio_capture = match crate::capture::audio::AudioCapture::new_with_device_mode_volumes_and_levels(
audio_tx_clone.clone(), audio_tx_clone.clone(),
Some(&config.audio_device), Some(&config.audio_device),
&config.audio_mode, &config.audio_mode,
@ -856,7 +860,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("Audio mute toggled: {}", !cur); println!("Audio mute toggled: {}", !cur);
}, },
Command::StartStreaming => { Command::StartStreaming => {
let cfg = scythe::config::ScytheConfig::load(); let cfg = crate::config::ScytheConfig::load();
let key = cfg.stream_key.trim(); let key = cfg.stream_key.trim();
if key.is_empty() { if key.is_empty() {
eprintln!("Cannot start livestream: stream key is empty in config."); eprintln!("Cannot start livestream: stream key is empty in config.");
@ -903,7 +907,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
stream_start_clone.store(0, Ordering::SeqCst); stream_start_clone.store(0, Ordering::SeqCst);
println!("ToggleStreaming: Livestream stopped."); println!("ToggleStreaming: Livestream stopped.");
} else { } else {
let cfg = scythe::config::ScytheConfig::load(); let cfg = crate::config::ScytheConfig::load();
let key = cfg.stream_key.trim(); let key = cfg.stream_key.trim();
if key.is_empty() { if key.is_empty() {
eprintln!("Cannot start livestream: stream key is empty in config."); eprintln!("Cannot start livestream: stream key is empty in config.");
@ -1162,6 +1166,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let _ = recorder_handle.join(); let _ = recorder_handle.join();
#[cfg(windows)] #[cfg(windows)]
let _ = std::fs::remove_file(scythe::ipc::get_ipc_port_path()); let _ = std::fs::remove_file(crate::ipc::get_ipc_port_path());
Ok(()) Ok(())
} }

View file

@ -11,3 +11,4 @@ pub mod config;
pub mod overlay_egui; pub mod overlay_egui;
pub mod hyprland_binds; pub mod hyprland_binds;
pub mod updater; pub mod updater;
pub mod daemon;

View file

@ -414,7 +414,24 @@ fn handle_toggle_streaming() -> Result<(), Box<dyn std::error::Error + Send + Sy
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();
let exe_name = env::current_exe()
.ok()
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().to_string()))
.unwrap_or_default();
// Multicall dispatch: scythe-daemon executable, symlink, or subcommand
let is_daemon = exe_name == "scythe-daemon"
|| exe_name == "vrec-daemon"
|| (args.len() > 1 && (args[1] == "daemon" || args[1] == "--daemon" || args[1] == "--mock"));
if is_daemon {
return scythe::daemon::run_daemon();
}
run_client(args)
}
fn run_client(args: Vec<String>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
unsafe { unsafe {
let is_cli = args.iter().any(|a| a == "--status" || a == "--help" || a == "-h"); let is_cli = args.iter().any(|a| a == "--status" || a == "--help" || a == "-h");
@ -428,7 +445,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if args.len() > 1 { if args.len() > 1 {
match args[1].as_str() { match args[1].as_str() {
"--save" | "--save-replay" => { "--save" | "--save-replay" | "save" => {
return handle_save_replay(); return handle_save_replay();
} }
"--notify-save" => { "--notify-save" => {
@ -443,7 +460,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
show_shadowplay_toast("RECORDING", "Recording saved", ToastIcon::Save); show_shadowplay_toast("RECORDING", "Recording saved", ToastIcon::Save);
return Ok(()); return Ok(());
} }
"--screenshot" => { "--screenshot" | "screenshot" => {
return handle_screenshot(); return handle_screenshot();
} }
"--mute-mic" => { "--mute-mic" => {
@ -462,7 +479,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
scythe::overlay_egui::run_egui_toast(title, subtitle, ToastIcon::from_name(icon_name)); scythe::overlay_egui::run_egui_toast(title, subtitle, ToastIcon::from_name(icon_name));
return Ok(()); return Ok(());
} }
"--menu" => { "--menu" | "menu" => {
ensure_daemon_running_async(); ensure_daemon_running_async();
ensure_hotkeys_running(); ensure_hotkeys_running();
if check_and_toggle_overlay() { if check_and_toggle_overlay() {
@ -492,7 +509,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
"--hotkeys" | "--background" => { "--hotkeys" | "--background" => {
// fall through to hotkey loop // fall through to hotkey loop
} }
"--record" | "--toggle" => { "--record" | "--toggle" | "record" => {
return handle_toggle_recording(); return handle_toggle_recording();
} }
"--stream" | "--toggle-stream" => { "--stream" | "--toggle-stream" => {
@ -511,7 +528,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
ensure_daemon_running(); ensure_daemon_running();
return send_command(Command::ReloadConfig); return send_command(Command::ReloadConfig);
} }
"--status" => { "--status" | "status" => {
ensure_daemon_running(); ensure_daemon_running();
match query_status() { match query_status() {
Ok(st) => { Ok(st) => {
@ -531,7 +548,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
} }
} }
} }
"--quit" => { "--quit" | "quit" => {
return send_command(Command::StopDaemon); return send_command(Command::StopDaemon);
} }
"--help" | "-h" => { "--help" | "-h" => {

View file

@ -570,26 +570,14 @@ fn pick_folder(current_dir: &str, tx: Sender<String>, is_active: Arc<AtomicBool>
} }
fn spawn_daemon_process() { fn spawn_daemon_process() {
let mut cmd = if let Ok(mut path) = std::env::current_exe() { let mut cmd = if let Ok(self_exe) = std::env::current_exe() {
path.pop(); let mut c = std::process::Command::new(self_exe);
#[cfg(target_os = "windows")] c.arg("daemon");
let primary = path.join("scythe-daemon.exe"); c
#[cfg(not(target_os = "windows"))]
let primary = path.join("scythe-daemon");
if primary.exists() {
std::process::Command::new(primary)
} else {
#[cfg(target_os = "windows")]
{ std::process::Command::new("scythe-daemon.exe") }
#[cfg(not(target_os = "windows"))]
{ std::process::Command::new("scythe-daemon") }
}
} else { } else {
#[cfg(target_os = "windows")] let mut c = std::process::Command::new("scythe");
{ std::process::Command::new("scythe-daemon.exe") } c.arg("daemon");
#[cfg(not(target_os = "windows"))] c
{ std::process::Command::new("scythe-daemon") }
}; };
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]

View file

@ -23,13 +23,13 @@ if systemctl --user is-active --quiet scythe-daemon.service 2>/dev/null; then
sleep 0.5 sleep 0.5
fi fi
echo "[2/6] Starting scythe-daemon with mock capture..." echo "[2/6] Starting scythe daemon with mock capture..."
target/release/scythe-daemon --mock > daemon_test.log 2>&1 & target/release/scythe daemon --mock > daemon_test.log 2>&1 &
DAEMON_PID=$! DAEMON_PID=$!
cleanup() { cleanup() {
if kill -0 $DAEMON_PID 2>/dev/null; then if kill -0 $DAEMON_PID 2>/dev/null; then
target/release/scythe-ui --quit 2>/dev/null || kill -9 $DAEMON_PID 2>/dev/null || true target/release/scythe --quit 2>/dev/null || kill -9 $DAEMON_PID 2>/dev/null || true
fi fi
rm -f daemon_test.log rm -f daemon_test.log
if [ "$WAS_SERVICE_ACTIVE" -eq 1 ]; then if [ "$WAS_SERVICE_ACTIVE" -eq 1 ]; then
@ -56,23 +56,23 @@ echo "Daemon is live (PID: $DAEMON_PID, Socket: $SOCKET_PATH)"
# Test 1: Start Normal Recording # Test 1: Start Normal Recording
echo "[3/6] Testing: Start Normal Recording..." echo "[3/6] Testing: Start Normal Recording..."
target/release/scythe-ui --start target/release/scythe --start
echo "Recording in progress for 3 seconds..." echo "Recording in progress for 3 seconds..."
sleep 3 sleep 3
# Test 2: Stop Normal Recording # Test 2: Stop Normal Recording
echo "[4/6] Testing: Stop Normal Recording..." echo "[4/6] Testing: Stop Normal Recording..."
target/release/scythe-ui --stop target/release/scythe --stop
sleep 1 sleep 1
# Test 3: Save Instant Replay # Test 3: Save Instant Replay
echo "[5/6] Testing: Save Instant Replay buffer..." echo "[5/6] Testing: Save Instant Replay buffer..."
target/release/scythe-ui --save target/release/scythe --save
sleep 2 sleep 2
# Test 4: Quit Daemon # Test 4: Quit Daemon
echo "[6/6] Testing: Clean Daemon Shutdown..." echo "[6/6] Testing: Clean Daemon Shutdown..."
target/release/scythe-ui --quit target/release/scythe --quit
wait $DAEMON_PID 2>/dev/null || true wait $DAEMON_PID 2>/dev/null || true
echo "Daemon shutdown cleanly." echo "Daemon shutdown cleanly."