#[cfg(unix)] use std::process::Command; #[cfg(unix)] use std::env; #[cfg(unix)] use std::io::{BufRead, BufReader}; #[cfg(unix)] use std::os::unix::net::UnixStream; #[cfg(unix)] use std::path::PathBuf; use crate::config::ScytheConfig; pub type VrecConfig = ScytheConfig; pub fn is_hyprland() -> bool { #[cfg(unix)] { env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() || env::var("XDG_CURRENT_DESKTOP") .map(|d| d.to_lowercase().contains("hyprland")) .unwrap_or(false) } #[cfg(not(unix))] { false } } /// Convert standard hotkey format (e.g. "Ctrl+Shift+R") to Hyprland bind format ("CONTROL_SHIFT, R") pub fn hotkey_to_hyprland(hotkey: &str) -> Option<(String, String)> { let parts: Vec<&str> = hotkey.split('+').map(|s| s.trim()).collect(); if parts.is_empty() { return None; } let mut mods = Vec::new(); let mut key = ""; for (i, &part) in parts.iter().enumerate() { if i == parts.len() - 1 { key = part; } else { let m = match part.to_lowercase().as_str() { "ctrl" | "control" => "CONTROL", "shift" => "SHIFT", "alt" => "ALT", "super" | "win" | "meta" => "SUPER", _ => continue, }; mods.push(m); } } if key.is_empty() { return None; } let mod_str = if mods.is_empty() { "".to_string() } else { mods.join("_") }; Some((mod_str, key.to_string())) } pub fn hotkey_to_hyprland_lua(hotkey: &str) -> Option { let parts: Vec<&str> = hotkey.split('+').map(|s| s.trim()).collect(); if parts.is_empty() { return None; } let mut mods = Vec::new(); let mut key = ""; for (i, &part) in parts.iter().enumerate() { if i == parts.len() - 1 { key = part; } else { let m = match part.to_lowercase().as_str() { "ctrl" | "control" => "CONTROL", "shift" => "SHIFT", "alt" => "ALT", "super" | "win" | "meta" => "SUPER", _ => continue, }; mods.push(m); } } if key.is_empty() { return None; } if mods.is_empty() { Some(key.to_string()) } else { Some(format!("{} + {}", mods.join(" + "), key)) } } #[cfg(unix)] static RULES_INITIALIZED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); #[cfg(unix)] /// Dynamically inject binds and window rules into running Hyprland without touching hyprland.conf pub fn register_hyprland_binds(config: &ScytheConfig) { if !is_hyprland() { return; } if !RULES_INITIALIZED.swap(true, std::sync::atomic::Ordering::SeqCst) { // Register modern Hyprland (0.55+) layer rules for genuine frosted glass blur & floating window rules let _ = Command::new("hyprctl") .args(["eval", r#"hl.layer_rule({ match = { namespace = "scythe-overlay" }, blur = true, ignore_alpha = 0.1 })"#]) .output(); let _ = Command::new("hyprctl") .args(["eval", r#"hl.layer_rule({ match = { namespace = "scythe-notification" }, blur = true, ignore_alpha = 0.1 })"#]) .output(); let _ = Command::new("hyprctl") .args(["eval", r#"hl.window_rule({ match = { class = "^(scythe-overlay)$" }, float = true, pin = true, move = {"0", "0"}, size = {"100%", "100%"}, no_anim = true, border = false, shadow = false, rounding = 0 })"#]) .output(); let _ = Command::new("hyprctl") .args(["eval", r#"hl.window_rule({ match = { class = "^(scythe-toast)$" }, float = true, pin = true, move = {"(monitor_w-340)", "16"}, no_anim = true, border = false, shadow = false, rounding = 0 })"#]) .output(); let _ = Command::new("hyprctl") .args(["eval", r#"hl.window_rule({ match = { title = "Select Recordings Directory" }, float = true, pin = true, stay_focused = true, center = true })"#]) .output(); let _ = Command::new("hyprctl") .args(["eval", r#"hl.window_rule({ match = { class = "kdialog" }, float = true, pin = true, stay_focused = true, center = true })"#]) .output(); let _ = Command::new("hyprctl") .args(["eval", r#"hl.window_rule({ match = { class = "org.kde.kdialog" }, float = true, pin = true, stay_focused = true, center = true })"#]) .output(); let _ = Command::new("hyprctl") .args(["eval", r#"hl.layer_rule({ match = { namespace = "vrec-overlay" }, blur = true, ignore_alpha = 0.1 })"#]) .output(); // Also register legacy window rules for backward compatibility with older Hyprland versions let legacy_rules = [ "float, class:^(scythe-overlay)$", "move 0 0, class:^(scythe-overlay)$", "size 100% 100%, class:^(scythe-overlay)$", "pin, class:^(scythe-overlay)$", "noborder, class:^(scythe-overlay)$", "noshadow, class:^(scythe-overlay)$", "rounding 0, class:^(scythe-overlay)$", "stayfocused, class:^(scythe-overlay)$", "float, title:^(Select Recordings Directory)$", "pin, title:^(Select Recordings Directory)$", "stayfocused, title:^(Select Recordings Directory)$", "center, title:^(Select Recordings Directory)$", "float, class:^(kdialog)$", "pin, class:^(kdialog)$", "stayfocused, class:^(kdialog)$", "center, class:^(kdialog)$", "float, class:^(org.kde.kdialog)$", "pin, class:^(org.kde.kdialog)$", "stayfocused, class:^(org.kde.kdialog)$", "center, class:^(org.kde.kdialog)$", "float, class:^(scythe-hud)$", "move 0 0, class:^(scythe-hud)$", "size 100% 100%, class:^(scythe-hud)$", "pin, class:^(scythe-hud)$", "float, class:^(scythe-toast)$", "pin, class:^(scythe-toast)$", "noborder, class:^(scythe-toast)$", "move 100%-w 16, class:^(scythe-toast)$", "float, class:^(vrec-overlay)$", "move 0 0, class:^(vrec-overlay)$", "size 100% 100%, class:^(vrec-overlay)$", "pin, class:^(vrec-overlay)$", "noborder, class:^(vrec-overlay)$", ]; for rule in legacy_rules { let _ = Command::new("hyprctl") .args(["keyword", "windowrulev2", rule]) .output(); } } let binds = [ (&config.menu_hotkey, "scythe-ui --menu"), (&config.save_hotkey, "scythe-ui --save"), (&config.record_hotkey, "scythe-ui --record"), (&config.pause_hotkey, "scythe-ui --pause"), (&config.cursor_hotkey, "scythe-ui --cursor"), (&config.screenshot_hotkey, "scythe-ui --screenshot"), (&config.mute_mic_hotkey, "scythe-ui --mute-mic"), (&config.mute_sys_hotkey, "scythe-ui --mute-sys"), ]; for (hotkey, cmd) in binds { unbind_hotkey(hotkey); // Try modern Hyprland Lua eval first if let Some(lua_combo) = hotkey_to_hyprland_lua(hotkey) { let lua_script = format!(r#"hl.bind("{}", hl.dsp.exec_cmd("{}"))"#, lua_combo, cmd); if let Ok(out) = Command::new("hyprctl").args(["eval", &lua_script]).output() && out.status.success() && String::from_utf8_lossy(&out.stdout).contains("ok") { println!("Hyprland dynamic bind active (Lua): {}", lua_combo); continue; } } // Fallback to legacy keyword bind if let Some((mods, key)) = hotkey_to_hyprland(hotkey) { let bind_arg = if mods.is_empty() { format!("{}, exec, {}", key, cmd) } else { format!("{}, {}, exec, {}", mods, key, cmd) }; let res = Command::new("hyprctl") .args(["keyword", "bind", &bind_arg]) .output(); if let Ok(out) = res && out.status.success() { println!("Hyprland dynamic bind active (legacy): {}", bind_arg); } } } } #[cfg(unix)] pub fn register_hyprland_binds_async(config: &ScytheConfig) { let cfg = config.clone(); std::thread::spawn(move || { register_hyprland_binds(&cfg); }); } #[cfg(unix)] pub fn unbind_hotkey_async(hotkey: &str) { let hk = hotkey.to_string(); std::thread::spawn(move || { unbind_hotkey(&hk); }); } #[cfg(not(unix))] pub fn register_hyprland_binds(_config: &ScytheConfig) {} #[cfg(not(unix))] pub fn register_hyprland_binds_async(_config: &ScytheConfig) {} #[cfg(not(unix))] pub fn unbind_hotkey_async(_hotkey: &str) {} #[cfg(unix)] /// Unbind a specific hotkey combo from running Hyprland pub fn unbind_hotkey(hotkey: &str) { if !is_hyprland() { return; } if let Some(lua_combo) = hotkey_to_hyprland_lua(hotkey) { let lua_script = format!( r#"for i = 1, 10 do pcall(function() hl.unbind("{}") end) end"#, lua_combo ); let _ = Command::new("hyprctl") .args(["eval", &lua_script]) .output(); } if let Some((mods, key)) = hotkey_to_hyprland(hotkey) { let unbind_arg = if mods.is_empty() { key } else { format!("{}, {}", mods, key) }; let _ = Command::new("hyprctl") .args(["keyword", "unbind", &unbind_arg]) .output(); } } #[cfg(not(unix))] pub fn unbind_hotkey(_hotkey: &str) {} #[cfg(unix)] /// Dynamically remove binds from running Hyprland pub fn unregister_hyprland_binds(config: &ScytheConfig) { if !is_hyprland() { return; } let hotkeys = [ &config.menu_hotkey, &config.save_hotkey, &config.record_hotkey, &config.cursor_hotkey, ]; for hotkey in hotkeys { unbind_hotkey(hotkey); } } #[cfg(not(unix))] pub fn unregister_hyprland_binds(_config: &ScytheConfig) {} #[cfg(unix)] /// Find Hyprland socket2 for live event listening fn get_hyprland_socket2() -> Option { let sig = env::var("HYPRLAND_INSTANCE_SIGNATURE").ok()?; let xdg_runtime = env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/run/user/1000".to_string()); let path = PathBuf::from(format!("{}/hypr/{}/.socket2.sock", xdg_runtime, sig)); if path.exists() { return Some(path); } let fallback = PathBuf::from(format!("/tmp/hypr/{}/.socket2.sock", sig)); if fallback.exists() { return Some(fallback); } None } #[cfg(unix)] /// Background watcher for Hyprland config reloads so dynamic binds are never lost pub fn spawn_hyprland_reload_watcher() { if !is_hyprland() { return; } std::thread::spawn(|| { loop { if let Some(sock_path) = get_hyprland_socket2() && let Ok(stream) = UnixStream::connect(&sock_path) { let reader = BufReader::new(stream); for line in reader.lines().map_while(Result::ok) { if line.starts_with("configreloaded") { println!("Hyprland config reload detected! Re-registering scythe dynamic binds..."); let config = ScytheConfig::load(); register_hyprland_binds(&config); } } } std::thread::sleep(std::time::Duration::from_secs(3)); } }); } #[cfg(not(unix))] pub fn spawn_hyprland_reload_watcher() {} #[cfg(test)] mod tests { use super::*; #[test] fn test_hotkey_to_hyprland_conversion() { assert_eq!( hotkey_to_hyprland("Ctrl+Shift+R"), Some(("CONTROL_SHIFT".to_string(), "R".to_string())) ); assert_eq!( hotkey_to_hyprland("Alt+Z"), Some(("ALT".to_string(), "Z".to_string())) ); assert_eq!( hotkey_to_hyprland("Ctrl+Shift+F9"), Some(("CONTROL_SHIFT".to_string(), "F9".to_string())) ); assert_eq!( hotkey_to_hyprland("F12"), Some(("".to_string(), "F12".to_string())) ); assert_eq!( hotkey_to_hyprland("Super+Alt+S"), Some(("SUPER_ALT".to_string(), "S".to_string())) ); } #[test] fn test_hotkey_to_hyprland_lua_conversion() { assert_eq!( hotkey_to_hyprland_lua("Ctrl+Shift+R"), Some("CONTROL + SHIFT + R".to_string()) ); assert_eq!( hotkey_to_hyprland_lua("Alt+Z"), Some("ALT + Z".to_string()) ); assert_eq!( hotkey_to_hyprland_lua("F12"), Some("F12".to_string()) ); assert_eq!( hotkey_to_hyprland_lua("Super+Alt+S"), Some("SUPER + ALT + S".to_string()) ); } }