scythe/src/main.rs
spearzificial 6fcdf357f7 ci: add build workflow; reject unknown args; make audio probe opt-in
GitHub Actions built only on tag pushes, so nothing verified that main
compiles. Adds ci.yml running check, unit tests, clippy and a release
build on Linux, plus a Windows MSYS2 build, with both binaries uploaded
as artifacts.

Two issues would have made that workflow hang or fail:

- Unknown CLI arguments fell through the dispatch match into the global
  hotkey loop, so `scythe --version` (a typo, or any unrecognised flag)
  silently became a background hotkey daemon that never exited. Unknown
  arguments now report the flag and exit 2.
- test_audio_probe asserts that a system audio device opens and emits
  samples. Headless runners have no audio server, so the suite could
  never pass in CI. The test is now opt-in via SCYTHE_HW_TESTS=1 and
  still runs on a real desktop session.
2026-10-20 17:10:45 +05:30

800 lines
32 KiB
Rust

#![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};
use std::env;
use global_hotkey::{GlobalHotKeyManager, hotkey::{HotKey, Modifiers, Code}, GlobalHotKeyEvent, HotKeyState};
fn ensure_wayland_env() {
scythe::overlay::ensure_wayland_env();
}
fn get_daemon_cmd() -> std::process::Command {
#[allow(unused_mut)]
let mut cmd = if let Ok(self_exe) = std::env::current_exe() {
let mut c = std::process::Command::new(self_exe);
c.arg("daemon");
c
} else {
let mut c = std::process::Command::new("scythe");
c.arg("daemon");
c
};
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
}
cmd
}
fn get_ui_cmd() -> std::process::Command {
#[allow(unused_mut)]
let mut cmd = if let Ok(path) = std::env::current_exe() {
let mut c = std::process::Command::new(&path);
if let Some(parent) = path.parent() {
c.current_dir(parent);
}
c
} else {
#[cfg(target_os = "windows")]
{
std::process::Command::new("scythe-ui.exe")
}
#[cfg(not(target_os = "windows"))]
{
std::process::Command::new("scythe-ui")
}
};
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
}
cmd
}
fn check_and_toggle_overlay() -> bool {
let pid_path = scythe::ipc::get_overlay_pid_path();
if pid_path.exists() {
if let Ok(metadata) = std::fs::metadata(&pid_path)
&& let Ok(modified) = metadata.modified()
&& let Ok(elapsed) = modified.elapsed() {
// Debounce: if the overlay was spawned less than 400ms ago, this is a
// duplicate event from concurrent triggers. Do not kill it; simply exit.
if elapsed < std::time::Duration::from_millis(400) {
return true;
}
}
if let Ok(content) = std::fs::read_to_string(&pid_path) {
#[cfg(unix)]
if let Ok(pid) = content.trim().parse::<i32>() {
let exists = unsafe { libc::kill(pid, 0) == 0 };
if exists && pid != std::process::id() as i32 {
let proc_cmd = format!("/proc/{}/cmdline", pid);
let is_scythe = std::fs::read_to_string(&proc_cmd)
.map(|cmd| cmd.contains("scythe") || cmd.contains("vrec"))
.unwrap_or(false);
if is_scythe {
// Overlay already open: toggle it closed instantly!
unsafe { libc::kill(pid, libc::SIGTERM) };
let _ = std::fs::remove_file(&pid_path);
return true;
}
}
}
#[cfg(windows)]
if let Ok(pid) = content.trim().parse::<u32>()
&& pid != std::process::id() {
unsafe {
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 {
unsafe {
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;
}
}
}
let _ = std::fs::remove_file(&pid_path);
}
let my_pid = std::process::id().to_string();
let _ = std::fs::write(&pid_path, my_pid);
false
}
fn ensure_daemon_running_async() {
#[cfg(unix)]
{
use std::os::unix::net::UnixStream;
if UnixStream::connect(scythe::ipc::get_socket_path()).is_ok()
|| UnixStream::connect(scythe::ipc::get_legacy_socket_path()).is_ok() {
return;
}
if scythe::hyprland_binds::is_hyprland() {
let _ = std::process::Command::new("systemctl")
.args(["--user", "start", "xdg-desktop-portal-hyprland"])
.status();
}
}
#[cfg(target_os = "windows")]
{
if scythe::ipc::is_daemon_running() {
return;
}
}
let _ = get_daemon_cmd()
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
fn ensure_daemon_running() {
if query_status().is_err() {
#[cfg(target_os = "linux")]
{
if scythe::hyprland_binds::is_hyprland() {
let _ = std::process::Command::new("systemctl")
.args(["--user", "start", "xdg-desktop-portal-hyprland"])
.status();
}
}
let _ = get_daemon_cmd()
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
for _ in 0..25 {
std::thread::sleep(std::time::Duration::from_millis(100));
if query_status().is_ok() {
break;
}
}
}
}
fn ensure_hotkeys_running() {
#[cfg(target_os = "windows")]
{
unsafe {
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Threading::{OpenMutexW, SYNCHRONIZATION_ACCESS_RIGHTS};
if let Ok(handle) = OpenMutexW(SYNCHRONIZATION_ACCESS_RIGHTS(0x00100000), false, windows::core::w!("Local\\scythe_hotkeys_single_instance")) {
let _ = CloseHandle(handle);
return;
}
}
let _ = get_ui_cmd()
.arg("--hotkeys")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
}
fn send_with_notification(cmd: Command, title: &str, subtitle: &str, icon: ToastIcon) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
ensure_daemon_running();
match send_command(cmd) {
Ok(()) => {
show_shadowplay_toast(title, subtitle, icon);
Ok(())
}
Err(e) => {
show_shadowplay_toast("SCYTHE", "Error: failed to connect to daemon", ToastIcon::Error);
Err(e)
}
}
}
fn handle_toggle_recording() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
ensure_daemon_running();
let status = query_status().ok();
let is_rec = status.as_ref().map(|s| s.is_recording).unwrap_or(false);
match send_command(Command::ToggleRecording) {
Ok(()) => {
if is_rec {
show_shadowplay_toast("RECORDING", "Recording saved", ToastIcon::Save);
} else {
show_shadowplay_toast("RECORDING", "Recording started", ToastIcon::Record);
}
Ok(())
}
Err(e) => {
show_shadowplay_toast("SCYTHE", "Error: failed to connect to daemon", ToastIcon::Error);
Err(e)
}
}
}
fn handle_toggle_cursor() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
ensure_daemon_running();
match send_command(Command::ToggleCursor) {
Ok(()) => {
std::thread::sleep(std::time::Duration::from_millis(120));
let next_cursor = query_status().ok().map(|s| s.show_cursor).unwrap_or_else(|| {
scythe::config::ScytheConfig::load().show_cursor
});
if next_cursor {
show_shadowplay_toast("MOUSE CURSOR", "Visible in recording", ToastIcon::Cursor);
} else {
show_shadowplay_toast("MOUSE CURSOR", "Hidden from recording", ToastIcon::Cursor);
}
Ok(())
}
Err(e) => {
show_shadowplay_toast("SCYTHE", "Error: failed to connect to daemon", ToastIcon::Error);
Err(e)
}
}
}
fn handle_save_replay() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
ensure_daemon_running();
if let Ok(st) = query_status()
&& !st.is_replay_active {
show_shadowplay_toast("INSTANT REPLAY", "Replay is turned off", ToastIcon::Error);
return Ok(());
}
send_with_notification(Command::SaveReplay, "INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay)
}
fn handle_screenshot() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let cfg = scythe::config::ScytheConfig::load();
let dir = scythe::config::ScytheConfig::expand_tilde(&cfg.screenshot_directory);
let _ = std::fs::create_dir_all(&dir);
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
let ext = if cfg.screenshot_format.to_lowercase() == "jpg" || cfg.screenshot_format.to_lowercase() == "jpeg" { "jpg" } else { "png" };
let filename = format!("Screenshot_{}.{}", now, ext);
let out_path = dir.join(&filename);
#[cfg(target_os = "linux")]
{
let mut grim = std::process::Command::new("grim");
if cfg.screenshot_cursor {
grim.arg("-c");
}
if ext == "jpg" {
grim.args(["-t", "jpeg", "-q", "90"]);
}
grim.arg(&out_path);
let _ = grim.status();
if !out_path.exists() {
let _ = std::process::Command::new("import").args(["-window", "root"]).arg(&out_path).status();
}
}
#[cfg(target_os = "windows")]
{
let ps_cmd = format!(
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing; \
$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds; \
$bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height; \
$g = [System.Drawing.Graphics]::FromImage($bmp); \
$g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size); \
$bmp.Save('{}'); $g.Dispose(); $bmp.Dispose()",
out_path.to_string_lossy().replace('/', "\\")
);
let _ = std::process::Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", &ps_cmd])
.status();
}
let scale_dims = match cfg.screenshot_resolution.as_str() {
"720p" => Some("1280:720"),
"1080p" => Some("1920:1080"),
"1440p" => Some("2560:1440"),
"2160p" => Some("3840:2160"),
_ => None,
};
if let Some(scale) = scale_dims
&& out_path.exists() {
let tmp_path = dir.join(format!("tmp_{}", filename));
if std::fs::rename(&out_path, &tmp_path).is_ok() {
let _ = std::process::Command::new("ffmpeg")
.args(["-v", "error", "-y", "-i", &tmp_path.to_string_lossy(), "-vf", &format!("scale={}:flags=lanczos", scale), &out_path.to_string_lossy()])
.status();
let _ = std::fs::remove_file(&tmp_path);
}
}
show_shadowplay_toast("SCREENSHOT", &format!("Saved to {}", filename), ToastIcon::Screenshot);
Ok(())
}
fn handle_mute_mic() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut cfg = scythe::config::ScytheConfig::load();
let is_muted = cfg.mic_volume < 0.05;
cfg.mic_volume = if is_muted { 0.60 } else { 0.0 };
let _ = cfg.save();
scythe::config::ScytheConfig::notify_daemon_reload();
show_shadowplay_toast("MICROPHONE", if is_muted { "Unmuted" } else { "Muted" }, ToastIcon::Info);
Ok(())
}
fn handle_mute_sys() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut cfg = scythe::config::ScytheConfig::load();
let is_muted = cfg.system_volume < 0.05;
cfg.system_volume = if is_muted { 1.00 } else { 0.0 };
let _ = cfg.save();
scythe::config::ScytheConfig::notify_daemon_reload();
show_shadowplay_toast("SYSTEM AUDIO", if is_muted { "Unmuted" } else { "Muted" }, ToastIcon::Info);
Ok(())
}
fn handle_toggle_streaming() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
ensure_daemon_running();
let cfg = scythe::config::ScytheConfig::load();
if cfg.stream_key.trim().is_empty() {
show_shadowplay_toast("LIVESTREAM", "Stream key is empty. Configure in Settings.", ToastIcon::Error);
return Ok(());
}
let status = query_status().ok();
let is_streaming = status.as_ref().map(|s| s.is_streaming).unwrap_or(false);
match send_command(Command::ToggleStreaming) {
Ok(()) => {
if is_streaming {
show_shadowplay_toast("LIVESTREAM", "Stream stopped", ToastIcon::Info);
} else {
show_shadowplay_toast("LIVESTREAM", "Livestream started", ToastIcon::Record);
}
Ok(())
}
Err(e) => {
show_shadowplay_toast("SCYTHE", "Error: failed to connect to daemon", ToastIcon::Error);
Err(e)
}
}
}
/// Name the binary was invoked as.
///
/// `current_exe()` reads /proc/self/exe, which resolves symlinks to the real
/// file, so the installed `scythe-daemon` / `scythe-ui` aliases would all
/// report "scythe" and dispatch to the client. argv[0] keeps the invocation
/// name, which is what multicall dispatch needs.
fn invoked_stem() -> String {
if let Some(arg0) = env::args_os().next() {
let path = std::path::Path::new(&arg0);
if let Some(stem) = path.file_stem() {
let stem = stem.to_string_lossy().to_string();
if !stem.is_empty() {
return stem;
}
}
}
env::current_exe()
.ok()
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().to_string()))
.unwrap_or_default()
}
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let args: Vec<String> = env::args().collect();
let exe_name = invoked_stem();
// 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")]
unsafe {
let is_cli = args.iter().any(|a| a == "--status" || a == "--help" || a == "-h");
if !is_cli {
use windows::Win32::System::Console::FreeConsole;
let _ = FreeConsole();
}
}
ensure_wayland_env();
if args.len() > 1 {
match args[1].as_str() {
"--save" | "--save-replay" | "save" => {
return handle_save_replay();
}
"--notify-save" => {
show_shadowplay_toast("INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay);
return Ok(());
}
"--notify-start" => {
show_shadowplay_toast("RECORDING", "Recording started", ToastIcon::Record);
return Ok(());
}
"--notify-stop" => {
show_shadowplay_toast("RECORDING", "Recording saved", ToastIcon::Save);
return Ok(());
}
"--screenshot" | "screenshot" => {
return handle_screenshot();
}
"--mute-mic" => {
return handle_mute_mic();
}
"--mute-sys" => {
return handle_mute_sys();
}
"--pause" => {
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("");
let icon_name = args.get(4).map(|s| s.as_str()).unwrap_or("info");
scythe::overlay_egui::run_egui_toast(title, subtitle, ToastIcon::from_name(icon_name));
return Ok(());
}
"--menu" | "menu" => {
ensure_daemon_running_async();
ensure_hotkeys_running();
if check_and_toggle_overlay() {
return Ok(());
}
scythe::overlay_egui::run_egui_overlay();
return Ok(());
}
"--egui" => {
ensure_daemon_running_async();
ensure_hotkeys_running();
if check_and_toggle_overlay() {
return Ok(());
}
scythe::overlay_egui::run_egui_overlay();
return Ok(());
}
"--gtk" => {
ensure_daemon_running_async();
ensure_hotkeys_running();
if check_and_toggle_overlay() {
return Ok(());
}
scythe::overlay_egui::run_egui_overlay();
return Ok(());
}
"--hotkeys" | "--background" => {
// fall through to hotkey loop
}
"--record" | "--toggle" | "record" => {
return handle_toggle_recording();
}
"--stream" | "--toggle-stream" => {
return handle_toggle_streaming();
}
"--cursor" | "--toggle-cursor" => {
return handle_toggle_cursor();
}
"--start" => {
return send_with_notification(Command::StartRecording, "RECORDING", "Recording started", ToastIcon::Record);
}
"--stop" => {
return send_with_notification(Command::StopRecording, "RECORDING", "Recording saved", ToastIcon::Save);
}
"--reload" => {
ensure_daemon_running();
return send_command(Command::ReloadConfig);
}
"--status" | "status" => {
ensure_daemon_running();
match query_status() {
Ok(st) => {
println!("Daemon Status:");
println!(" State: {}", if st.is_recording { format!("RECORDING ({}s)", st.recording_duration_sec) } else { "IDLE".to_string() });
println!(" Livestream: {}", if st.is_streaming { format!("STREAMING ({}s)", st.streaming_duration_sec) } else { "OFF".to_string() });
println!(" Instant Replay: {}", if st.is_replay_active { "ACTIVE" } else { "OFF" });
println!(" Audio Mode: {}", st.audio_mode);
println!(" Mic Volume: {:.0}%", st.mic_volume * 100.0);
println!(" System Volume: {:.0}%", st.system_volume * 100.0);
println!(" Show Cursor: {}", st.show_cursor);
return Ok(());
}
Err(e) => {
eprintln!("Failed to connect to scythe daemon: {}", e);
std::process::exit(1);
}
}
}
"--quit" | "quit" => {
return send_command(Command::StopDaemon);
}
"--help" | "-h" => {
println!("scythe {} - GPU screen recorder, instant replay and HUD overlay", scythe::updater::CURRENT_VERSION);
println!("Usage:");
println!(" scythe Open the interactive overlay UI menu (default)");
println!(" scythe --menu Open the interactive overlay UI menu");
println!(" scythe --gtk Deprecated alias for --menu (GTK backend retired)");
println!(" scythe --hotkeys Run global hotkey manager in background");
println!(" scythe --status Query current daemon status");
println!(" scythe --save Save instant replay and show notification");
println!(" scythe --save-replay Alias for --save");
println!(" scythe --notify-save Show instant replay saved notification toast");
println!(" scythe --notify-start Show recording started notification toast");
println!(" scythe --notify-stop Show recording saved notification toast");
println!(" scythe --toast <t> <s> Show custom ShadowPlay notification toast");
println!(" scythe --record Toggle normal recording on/off (with notification)");
println!(" scythe --stream Toggle livestreaming on/off (with notification)");
println!(" scythe --cursor Toggle mouse cursor recording on/off (with notification)");
println!(" scythe --show-overlay Open the HUD overlay via the daemon (no toggle)");
println!(" scythe --start Start normal recording");
println!(" scythe --stop Stop normal recording");
println!(" scythe --reload Reload daemon configuration");
println!(" scythe --quit Stop background daemon");
return Ok(());
}
other => {
eprintln!("scythe: unknown argument '{}'", other);
eprintln!("Run 'scythe --help' for usage.");
std::process::exit(2);
}
}
} else {
ensure_daemon_running_async();
ensure_hotkeys_running();
if check_and_toggle_overlay() {
return Ok(());
}
scythe::overlay_egui::run_egui_overlay();
return Ok(());
}
#[cfg(target_os = "windows")]
let _mutex = unsafe {
use windows::Win32::Foundation::{GetLastError, WIN32_ERROR};
use windows::Win32::System::Threading::CreateMutexW;
let handle = CreateMutexW(None, true, windows::core::w!("Local\\scythe_hotkeys_single_instance"));
if GetLastError() == WIN32_ERROR(183) {
return Ok(());
}
handle.ok()
};
println!("Starting scythe UI/Hotkey process...");
let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string());
let desktop = env::var("XDG_CURRENT_DESKTOP").unwrap_or_else(|_| "".to_string()).to_lowercase();
let is_gnome = desktop.contains("gnome") || desktop.contains("ubuntu");
let is_kde = desktop.contains("kde");
if session_type.to_lowercase() == "wayland" {
if is_gnome || is_kde {
println!("Note: Wayland native layer shell used. Ensure your compositor allows it.");
println!("Tip: Global hotkeys may be restricted by Wayland compositors.");
println!(" You can bind `scythe-ui --menu` or `scythe-ui --save` in your system shortcut settings.");
} else {
println!("wlroots/Hyprland compositor detected.");
scythe::hyprland_binds::register_hyprland_binds(&scythe::config::ScytheConfig::load());
scythe::hyprland_binds::spawn_hyprland_reload_watcher();
}
} else {
println!("X11 session detected.");
}
let manager = match GlobalHotKeyManager::new() {
Ok(m) => m,
Err(e) => {
eprintln!("Failed to create global hotkey manager: {}", e);
return Ok(());
}
};
#[derive(Default)]
struct RegisteredIds {
menu: Option<u32>,
save: Option<u32>,
rec: Option<u32>,
pause: Option<u32>,
cur: Option<u32>,
shot: Option<u32>,
mute_mic: Option<u32>,
mute_sys: Option<u32>,
}
let mut current_config = scythe::config::ScytheConfig::load();
let mut registered_hotkeys: Vec<HotKey> = Vec::new();
let register_all = |mgr: &GlobalHotKeyManager, cfg: &scythe::config::ScytheConfig, reg: &mut Vec<HotKey>| -> RegisteredIds {
for hk in reg.drain(..) {
let _ = mgr.unregister(hk);
}
let is_on_hyprland = scythe::hyprland_binds::is_hyprland();
let m_menu = scythe::hotkey::parse_hotkey(&cfg.menu_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::ALT), Code::KeyZ)));
let m_save = scythe::hotkey::parse_hotkey(&cfg.save_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR)));
let m_rec = scythe::hotkey::parse_hotkey(&cfg.record_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9)));
let m_pause = scythe::hotkey::parse_hotkey(&cfg.pause_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F8)));
let m_cur = scythe::hotkey::parse_hotkey(&cfg.cursor_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F10)));
let m_shot = scythe::hotkey::parse_hotkey(&cfg.screenshot_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyS)));
let m_mute_mic = scythe::hotkey::parse_hotkey(&cfg.mute_mic_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyM)));
let m_mute_sys = scythe::hotkey::parse_hotkey(&cfg.mute_sys_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyN)));
let mut ids = RegisteredIds::default();
if !is_on_hyprland
&& let Some(hk) = m_menu {
ids.menu = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
if let Some(hk) = m_save {
ids.save = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
if let Some(hk) = m_rec {
ids.rec = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
if let Some(hk) = m_pause {
ids.pause = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
if let Some(hk) = m_cur {
ids.cur = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
if let Some(hk) = m_shot {
ids.shot = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
if let Some(hk) = m_mute_mic {
ids.mute_mic = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
if let Some(hk) = m_mute_sys {
ids.mute_sys = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
ids
};
let mut current_ids = register_all(&manager, &current_config, &mut registered_hotkeys);
println!("Listening for global hotkeys (Overlay: {}, Replay: {}, Record: {}, Screenshot: {})...",
current_config.menu_hotkey, current_config.save_hotkey, current_config.record_hotkey, current_config.screenshot_hotkey);
let receiver = GlobalHotKeyEvent::receiver();
let mut last_config_check = std::time::Instant::now();
loop {
#[cfg(target_os = "windows")]
unsafe {
use windows::Win32::UI::WindowsAndMessaging::{
PeekMessageW, TranslateMessage, DispatchMessageW, MSG, PM_REMOVE,
};
let mut msg = MSG::default();
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
if last_config_check.elapsed() > std::time::Duration::from_millis(800) {
last_config_check = std::time::Instant::now();
let new_cfg = scythe::config::ScytheConfig::load();
if new_cfg.menu_hotkey != current_config.menu_hotkey
|| new_cfg.save_hotkey != current_config.save_hotkey
|| new_cfg.record_hotkey != current_config.record_hotkey
|| new_cfg.pause_hotkey != current_config.pause_hotkey
|| new_cfg.cursor_hotkey != current_config.cursor_hotkey
|| new_cfg.screenshot_hotkey != current_config.screenshot_hotkey
|| new_cfg.mute_mic_hotkey != current_config.mute_mic_hotkey
|| new_cfg.mute_sys_hotkey != current_config.mute_sys_hotkey
{
current_config = new_cfg;
current_ids = register_all(&manager, &current_config, &mut registered_hotkeys);
}
}
while let Ok(event) = receiver.try_recv() {
if event.state != HotKeyState::Pressed {
continue;
}
if Some(event.id) == current_ids.menu {
std::thread::spawn(|| {
ensure_daemon_running();
let _ = get_ui_cmd().arg("--menu").spawn();
});
} else if Some(event.id) == current_ids.save {
std::thread::spawn(|| {
let _ = handle_save_replay();
});
} else if Some(event.id) == current_ids.rec {
std::thread::spawn(|| {
let _ = handle_toggle_recording();
});
} else if Some(event.id) == current_ids.pause {
std::thread::spawn(|| {
let _ = handle_toggle_recording();
});
} else if Some(event.id) == current_ids.cur {
std::thread::spawn(|| {
let _ = handle_toggle_cursor();
});
} else if Some(event.id) == current_ids.shot {
std::thread::spawn(|| {
let _ = handle_screenshot();
});
} else if Some(event.id) == current_ids.mute_mic {
std::thread::spawn(|| {
let _ = handle_mute_mic();
});
} else if Some(event.id) == current_ids.mute_sys {
std::thread::spawn(|| {
let _ = handle_mute_sys();
});
}
}
#[cfg(target_os = "windows")]
unsafe {
use windows::Win32::UI::WindowsAndMessaging::{
MsgWaitForMultipleObjectsEx, QS_ALLINPUT, MWMO_INPUTAVAILABLE,
};
let _ = MsgWaitForMultipleObjectsEx(None, 200, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
}
#[cfg(not(target_os = "windows"))]
std::thread::sleep(std::time::Duration::from_millis(30));
}
}