From 6c9b8a8d8228b7df7c145a7959c09a75d96c0667 Mon Sep 17 00:00:00 2001 From: spearzificial <342299391+spearzificial@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:33:41 +0530 Subject: [PATCH] feat(ipc): implement ShowOverlay and drop settings that never worked ShowOverlay was declared in the Command enum but the daemon dispatcher had no arm for it, so it fell through to `_ => {}`. Nothing could open the HUD once it was closed except the --menu toggle path. - daemon: spawn_overlay_once() checks the overlay pid file for a live process and launches `scythe --menu` only when none is running, so repeated requests cannot stack overlays - cli: `scythe --show-overlay` sends the command, falling back to a direct launch when the daemon is unreachable Fourteen config keys were persisted and shown in the settings UI while no capture, encoder, muxer or daemon path ever read them. Removed from ScytheConfig, from the overlay state, and from their UI panels: capture_source, replay_storage, change_resolution, resolution_w/h, container_format, show_scroll_lock_led, autostart_on_game, save_folder_by_game, name_by_game, language, ui_color_theme, record_enabled. Unknown JSON keys are ignored by serde, so existing configs still load. The e2e script also unlinked a live daemon's socket: it ran rm -f on the socket path unconditionally and only stopped the systemd unit, so a daemon started by execs.lua or by hand kept its flock while losing its path entry, leaving the session broken until the process died. It now sends StopDaemon, waits for the socket to disappear, and aborts without unlinking if the daemon will not stop. --- src/config.rs | 56 ------------------ src/daemon.rs | 65 ++++++++++++++++++++ src/main.rs | 12 ++++ src/overlay_egui.rs | 140 -------------------------------------------- tests/e2e_test.sh | 18 ++++++ 5 files changed, 95 insertions(+), 196 deletions(-) diff --git a/src/config.rs b/src/config.rs index 3bf56ea..58f952b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,8 +10,6 @@ fn default_fps() -> u32 { 60 } fn default_video_codec() -> String { "h264".to_string() } fn default_audio_device() -> String { "default".to_string() } fn default_audio_mode() -> String { "system".to_string() } -fn default_lang() -> String { "en".to_string() } -fn default_theme() -> String { "dark".to_string() } fn default_save_hotkey() -> String { "Ctrl+Shift+R".to_string() } fn default_menu_hotkey() -> String { "Alt+Z".to_string() } fn default_record_hotkey() -> String { "Ctrl+Shift+F9".to_string() } @@ -24,14 +22,9 @@ fn default_stream_service() -> String { "twitch".to_string() } fn default_stream_url() -> String { "rtmp://live.twitch.tv/app/".to_string() } fn default_screenshot_hotkey() -> String { "Ctrl+Shift+S".to_string() } fn default_screenshot_format() -> String { "png".to_string() } -fn default_capture_source() -> String { "focused".to_string() } fn default_video_quality_mode() -> String { "cbr".to_string() } fn default_color_range() -> String { "limited".to_string() } fn default_audio_codec() -> String { "opus".to_string() } -fn default_replay_storage() -> String { "ram".to_string() } -fn default_container_format() -> String { "mp4".to_string() } -fn default_res_w() -> u32 { 1920 } -fn default_res_h() -> u32 { 1080 } fn default_screenshot_quality() -> u32 { 90 } fn default_screenshot_preset() -> String { "balanced".to_string() } fn default_pause_hotkey() -> String { "Ctrl+Shift+F8".to_string() } @@ -50,8 +43,6 @@ pub struct ScytheConfig { #[serde(default = "default_replay_bitrate")] pub replay_bitrate_kbps: u32, - #[serde(default)] - pub record_enabled: bool, #[serde(default = "default_record_bitrate")] pub record_bitrate_kbps: u32, #[serde(default = "default_fps")] @@ -77,10 +68,6 @@ pub struct ScytheConfig { pub autostart_replay: bool, #[serde(default)] pub autostart_overlay: bool, - #[serde(default = "default_lang")] - pub language: String, - #[serde(default = "default_theme")] - pub ui_color_theme: String, #[serde(default = "default_save_hotkey")] pub save_hotkey: String, #[serde(default = "default_menu_hotkey")] @@ -109,40 +96,20 @@ pub struct ScytheConfig { #[serde(default)] pub screenshot_cursor: bool, - #[serde(default = "default_capture_source")] - pub capture_source: String, #[serde(default = "default_video_quality_mode")] pub video_quality_mode: String, #[serde(default = "default_color_range")] pub color_range: String, #[serde(default = "default_audio_codec")] pub audio_codec: String, - #[serde(default = "default_replay_storage")] - pub replay_storage: String, #[serde(default)] pub record_low_power: bool, #[serde(default = "default_true")] pub show_replay_notifications: bool, - #[serde(default = "default_container_format")] - pub container_format: String, - #[serde(default)] - pub change_resolution: bool, - #[serde(default = "default_res_w")] - pub resolution_w: u32, - #[serde(default = "default_res_h")] - pub resolution_h: u32, - #[serde(default)] - pub save_folder_by_game: bool, - #[serde(default)] - pub name_by_game: bool, #[serde(default = "default_true")] pub restart_replay_on_save: bool, #[serde(default)] - pub show_scroll_lock_led: bool, - #[serde(default)] - pub autostart_on_game: bool, - #[serde(default)] pub autostart_power_only: bool, #[serde(default = "default_screenshot_quality")] @@ -171,7 +138,6 @@ impl Default for ScytheConfig { replay_duration_sec: 60, replay_bitrate_kbps: 18000, - record_enabled: false, record_bitrate_kbps: 20000, fps: 60, video_codec: "h264".to_string(), @@ -185,8 +151,6 @@ impl Default for ScytheConfig { autostart: false, autostart_replay: false, autostart_overlay: false, - language: "en".to_string(), - ui_color_theme: "dark".to_string(), save_hotkey: "Ctrl+Shift+R".to_string(), menu_hotkey: "Alt+Z".to_string(), record_hotkey: "Ctrl+Shift+F9".to_string(), @@ -202,23 +166,13 @@ impl Default for ScytheConfig { screenshot_format: default_screenshot_format(), screenshot_cursor: false, - capture_source: default_capture_source(), video_quality_mode: default_video_quality_mode(), color_range: default_color_range(), audio_codec: default_audio_codec(), - replay_storage: default_replay_storage(), record_low_power: false, show_replay_notifications: true, - container_format: default_container_format(), - change_resolution: false, - resolution_w: default_res_w(), - resolution_h: default_res_h(), - save_folder_by_game: false, - name_by_game: false, restart_replay_on_save: true, - show_scroll_lock_led: false, - autostart_on_game: false, autostart_power_only: false, screenshot_quality: default_screenshot_quality(), @@ -577,21 +531,11 @@ mod tests { assert_eq!(parsed.stream_url, "rtmp://live.twitch.tv/app/"); assert_eq!(parsed.stream_key, ""); - assert_eq!(parsed.container_format, "mp4"); - assert!(!parsed.change_resolution); - assert_eq!(parsed.resolution_w, 1920); - assert_eq!(parsed.resolution_h, 1080); - assert!(!parsed.save_folder_by_game); - assert!(!parsed.name_by_game); assert!(parsed.restart_replay_on_save); - assert!(!parsed.show_scroll_lock_led); - assert!(!parsed.autostart_on_game); assert!(!parsed.autostart_power_only); - assert_eq!(parsed.capture_source, "focused"); assert_eq!(parsed.video_quality_mode, "cbr"); assert_eq!(parsed.color_range, "limited"); assert_eq!(parsed.audio_codec, "opus"); - assert_eq!(parsed.replay_storage, "ram"); assert!(!parsed.record_low_power); assert!(parsed.show_replay_notifications); assert_eq!(parsed.screenshot_hotkey, "Ctrl+Shift+S"); diff --git a/src/daemon.rs b/src/daemon.rs index c05baeb..88e5b20 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -13,6 +13,67 @@ use std::io::{Read, Write}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; +/// Returns true when the overlay process recorded in the pid file is still alive. +fn overlay_alive() -> bool { + let pid_path = crate::ipc::get_overlay_pid_path(); + let Ok(content) = std::fs::read_to_string(&pid_path) else { + return false; + }; + let Ok(pid) = content.trim().parse::() else { + return false; + }; + if pid <= 0 { + return false; + } + + #[cfg(unix)] + { + unsafe { libc::kill(pid, 0) == 0 } + } + #[cfg(windows)] + { + use windows::Win32::Foundation::{CloseHandle, STILL_ACTIVE}; + use windows::Win32::System::Threading::{ + GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, + }; + unsafe { + let Ok(handle) = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid as u32) else { + return false; + }; + let mut code = 0u32; + let alive = GetExitCodeProcess(handle, &mut code).is_ok() && code == STILL_ACTIVE.0 as u32; + let _ = CloseHandle(handle); + alive + } + } + #[cfg(not(any(unix, windows)))] + { + false + } +} + +/// Opens the HUD overlay without toggling an already-visible one closed. +fn spawn_overlay_once() { + if overlay_alive() { + return; + } + let exe = std::env::current_exe() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|_| "scythe".to_string()); + + let mut cmd = std::process::Command::new(exe); + cmd.arg("--menu") + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()); + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(0x08000000); + } + let _ = cmd.spawn(); +} + fn mask_stream_url(url: &str) -> String { if let Some((base, key)) = url.rsplit_once('/') && key.len() > 4 { @@ -278,6 +339,10 @@ async fn run_daemon_async() -> Result<(), Box { + println!("ShowOverlay requested: opening HUD overlay."); + std::thread::spawn(spawn_overlay_once); + }, Command::StopDaemon => { println!("StopDaemon requested: Finalizing active recordings..."); crate::hyprland_binds::unregister_hyprland_binds(&crate::config::ScytheConfig::load()); diff --git a/src/main.rs b/src/main.rs index 357addd..4c898d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -444,6 +444,17 @@ fn run_client(args: Vec) -> Result<(), Box { return handle_toggle_recording(); } + "--show-overlay" | "--open" => { + ensure_daemon_running(); + // Daemon owns overlay lifetime; if it is unreachable, open the HUD directly. + if send_command(Command::ShowOverlay).is_err() { + ensure_hotkeys_running(); + if !check_and_toggle_overlay() { + scythe::overlay_egui::run_egui_overlay(); + } + } + return Ok(()); + } "--toast" => { let title = args.get(2).map(|s| s.as_str()).unwrap_or("SCYTHE"); let subtitle = args.get(3).map(|s| s.as_str()).unwrap_or(""); @@ -540,6 +551,7 @@ fn run_client(args: Vec) -> Result<(), Box/dev/null 2>&1; then + echo "[0/6] Stopping existing scythe daemon..." + target/release/scythe --quit >/dev/null 2>&1 || true + for _ in $(seq 1 50); do + [ -S "$SOCKET_PATH" ] || break + sleep 0.1 + done + if target/release/scythe --status >/dev/null 2>&1; then + echo "ERROR: A scythe daemon is running and did not stop cleanly." + echo " Refusing to unlink its socket. Stop it first: scythe --quit" + exit 1 + fi +fi + rm -f "$SOCKET_PATH" rm -f record_*.mp4 replay_*.mp4