scythe/src/bin/scythe-ui.rs

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));
}
}