feat(arch): consolidate daemon and UI into singular multicall binary scythe
This commit is contained in:
parent
425a9b1003
commit
3cc718fa69
8 changed files with 101 additions and 87 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
18
install.sh
18
install.sh
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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" => {
|
||||||
|
|
@ -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 {
|
} 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") }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{ std::process::Command::new("scythe-daemon.exe") }
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{ std::process::Command::new("scythe-daemon") }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue