476 lines
18 KiB
Rust
476 lines
18 KiB
Rust
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
|
|
|
|
use scythe::ipc::{Command, send_command, query_status};
|
|
use scythe::overlay::show_notification;
|
|
use std::env;
|
|
use global_hotkey::{GlobalHotKeyManager, hotkey::{HotKey, Modifiers, Code}, GlobalHotKeyEvent};
|
|
|
|
fn ensure_wayland_env() {
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
let runtime_dir = env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc::getuid() }));
|
|
unsafe {
|
|
if env::var("WAYLAND_DISPLAY").is_err()
|
|
&& let Ok(entries) = std::fs::read_dir(&runtime_dir) {
|
|
for entry in entries.flatten() {
|
|
let name = entry.file_name().to_string_lossy().to_string();
|
|
if name.starts_with("wayland-") && !name.ends_with(".lock") {
|
|
env::set_var("WAYLAND_DISPLAY", &name);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if env::var("DISPLAY").is_err() && std::path::Path::new("/tmp/.X11-unix/X0").exists() {
|
|
env::set_var("DISPLAY", ":0");
|
|
}
|
|
if env::var("DBUS_SESSION_BUS_ADDRESS").is_err() {
|
|
let bus_path = format!("{}/bus", runtime_dir);
|
|
if std::path::Path::new(&bus_path).exists() {
|
|
env::set_var("DBUS_SESSION_BUS_ADDRESS", format!("unix:path={}", bus_path));
|
|
}
|
|
}
|
|
if env::var("HYPRLAND_INSTANCE_SIGNATURE").is_err() {
|
|
let hypr_dir = std::path::Path::new(&runtime_dir).join("hypr");
|
|
if let Ok(entries) = std::fs::read_dir(&hypr_dir) {
|
|
for entry in entries.flatten() {
|
|
if entry.path().is_dir() {
|
|
let sig = entry.file_name().to_string_lossy().to_string();
|
|
if !sig.is_empty() {
|
|
env::set_var("HYPRLAND_INSTANCE_SIGNATURE", &sig);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if env::var("XDG_CURRENT_DESKTOP").is_err() && env::var("WAYLAND_DISPLAY").is_ok() {
|
|
env::set_var("XDG_CURRENT_DESKTOP", "Hyprland");
|
|
}
|
|
if env::var("XDG_SESSION_TYPE").map(|s| s == "tty" || s.is_empty()).unwrap_or(true) {
|
|
if env::var("WAYLAND_DISPLAY").is_ok() {
|
|
env::set_var("XDG_SESSION_TYPE", "wayland");
|
|
} else if env::var("DISPLAY").is_ok() {
|
|
env::set_var("XDG_SESSION_TYPE", "x11");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_daemon_cmd() -> std::process::Command {
|
|
#[allow(unused_mut)]
|
|
let mut cmd = if let Ok(mut path) = std::env::current_exe() {
|
|
path.pop();
|
|
#[cfg(target_os = "windows")]
|
|
let primary = path.join("scythe-daemon.exe");
|
|
#[cfg(target_os = "windows")]
|
|
let fallback = path.join("vrec-daemon.exe");
|
|
#[cfg(not(target_os = "windows"))]
|
|
let primary = path.join("scythe-daemon");
|
|
#[cfg(not(target_os = "windows"))]
|
|
let fallback = path.join("vrec-daemon");
|
|
|
|
if primary.exists() {
|
|
std::process::Command::new(primary)
|
|
} else if fallback.exists() {
|
|
std::process::Command::new(fallback)
|
|
} else {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
std::process::Command::new("scythe-daemon.exe")
|
|
}
|
|
#[cfg(not(target_os = "windows"))]
|
|
{
|
|
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")]
|
|
{
|
|
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() {
|
|
std::process::Command::new(path)
|
|
} 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) {
|
|
if let Ok(modified) = metadata.modified() {
|
|
if 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) {
|
|
if let Ok(pid) = content.trim().parse::<i32>() {
|
|
#[cfg(unix)]
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
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, GetLastError, WIN32_ERROR};
|
|
use windows::Win32::System::Threading::CreateMutexW;
|
|
|
|
if let Ok(handle) = CreateMutexW(None, false, windows::core::w!("Global\\scythe_hotkeys_single_instance")) {
|
|
if GetLastError() == WIN32_ERROR(183) { // ERROR_ALREADY_EXISTS
|
|
let _ = CloseHandle(handle);
|
|
return;
|
|
}
|
|
let _ = CloseHandle(handle);
|
|
}
|
|
}
|
|
|
|
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, success_msg: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
ensure_daemon_running();
|
|
match send_command(cmd) {
|
|
Ok(()) => {
|
|
show_notification(success_msg);
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
show_notification("Error: failed to connect to scythe-daemon");
|
|
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_notification("Recording saved");
|
|
} else {
|
|
show_notification("Recording started");
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
show_notification("Error: failed to connect to scythe-daemon");
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_toggle_cursor() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
ensure_daemon_running();
|
|
let current_cursor = query_status().ok().map(|s| s.show_cursor).unwrap_or(true);
|
|
let next_cursor = !current_cursor;
|
|
match send_command(Command::ToggleCursor) {
|
|
Ok(()) => {
|
|
if next_cursor {
|
|
show_notification("Cursor: Visible in recording");
|
|
} else {
|
|
show_notification("Cursor: Hidden from recording");
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
show_notification("Error: failed to connect to scythe-daemon");
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
ensure_wayland_env();
|
|
|
|
let args: Vec<String> = env::args().collect();
|
|
if args.len() > 1 {
|
|
match args[1].as_str() {
|
|
"--save" => {
|
|
return send_with_notification(Command::SaveReplay, "Replay saved");
|
|
}
|
|
"--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();
|
|
ensure_hotkeys_running();
|
|
#[cfg(target_os = "linux")]
|
|
scythe::overlay::show_menu_overlay();
|
|
#[cfg(not(target_os = "linux"))]
|
|
scythe::overlay_egui::run_egui_overlay();
|
|
return Ok(());
|
|
}
|
|
"--hotkeys" | "--background" => {
|
|
// fall through to hotkey loop
|
|
}
|
|
"--record" | "--toggle" => {
|
|
return handle_toggle_recording();
|
|
}
|
|
"--cursor" | "--toggle-cursor" => {
|
|
return handle_toggle_cursor();
|
|
}
|
|
"--start" => {
|
|
return send_with_notification(Command::StartRecording, "Recording started");
|
|
}
|
|
"--stop" => {
|
|
return send_with_notification(Command::StopRecording, "Recording saved");
|
|
}
|
|
"--reload" => {
|
|
ensure_daemon_running();
|
|
return send_command(Command::ReloadConfig);
|
|
}
|
|
"--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!(" 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" => {
|
|
return send_command(Command::StopDaemon);
|
|
}
|
|
"--help" | "-h" => {
|
|
println!("scythe-ui - UI Overlay and Hotkey listener for scythe");
|
|
println!("Usage:");
|
|
println!(" scythe-ui Open the interactive overlay UI menu (default)");
|
|
println!(" scythe-ui --menu Open the interactive overlay UI menu");
|
|
println!(" scythe-ui --gtk Open legacy GTK layer-shell overlay (Linux)");
|
|
println!(" scythe-ui --hotkeys Run global hotkey manager in background");
|
|
println!(" scythe-ui --status Query current daemon status");
|
|
println!(" scythe-ui --save Save instant replay and show notification");
|
|
println!(" scythe-ui --record Toggle normal recording on/off (with notification)");
|
|
println!(" scythe-ui --cursor Toggle mouse cursor recording on/off (with notification)");
|
|
println!(" scythe-ui --start Start normal recording");
|
|
println!(" scythe-ui --stop Stop normal recording");
|
|
println!(" scythe-ui --reload Reload daemon configuration");
|
|
println!(" scythe-ui --quit Stop background daemon");
|
|
return Ok(());
|
|
}
|
|
_ => {}
|
|
}
|
|
} 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!("Global\\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(());
|
|
}
|
|
};
|
|
let hotkey_menu = HotKey::new(Some(Modifiers::ALT), Code::KeyZ);
|
|
let hotkey_save = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR);
|
|
let hotkey_record = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9);
|
|
let hotkey_cursor = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F10);
|
|
|
|
let is_on_hyprland = scythe::hyprland_binds::is_hyprland();
|
|
if !is_on_hyprland {
|
|
let _ = manager.register(hotkey_menu);
|
|
}
|
|
let _ = manager.register(hotkey_save);
|
|
let _ = manager.register(hotkey_record);
|
|
let _ = manager.register(hotkey_cursor);
|
|
|
|
println!("Listening for global hotkeys (Alt+Z for overlay, Ctrl+Shift+R for replay, Ctrl+Shift+F9 for recording, Ctrl+Shift+F10 for cursor)...");
|
|
|
|
let receiver = GlobalHotKeyEvent::receiver();
|
|
loop {
|
|
if let Ok(event) = receiver.try_recv() {
|
|
if event.id == hotkey_menu.id() {
|
|
ensure_daemon_running();
|
|
let _ = get_ui_cmd().arg("--menu").spawn();
|
|
} else if event.id == hotkey_save.id() {
|
|
let _ = send_with_notification(Command::SaveReplay, "Replay saved");
|
|
} else if event.id == hotkey_record.id() {
|
|
let _ = handle_toggle_recording();
|
|
} else if event.id == hotkey_cursor.id() {
|
|
let _ = handle_toggle_cursor();
|
|
}
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
}
|
|
}
|