scythe/src/hyprland_binds.rs

391 lines
13 KiB
Rust

#[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<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;
}
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<PathBuf> {
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())
);
}
}