228 lines
6.3 KiB
Rust
228 lines
6.3 KiB
Rust
//! Hyprland 0.56 IPC: JSON queries + Lua eval/dispatch. No game injection.
|
|
|
|
use crate::hotkey::ParsedHotkey;
|
|
use anyhow::{bail, Context, Result};
|
|
use serde::Deserialize;
|
|
use std::path::PathBuf;
|
|
use std::process::Stdio;
|
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
|
use tokio::net::UnixStream;
|
|
use tokio::process::Command;
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Client {
|
|
pub address: String,
|
|
pub class: String,
|
|
pub title: String,
|
|
pub pid: i32,
|
|
pub at: [i32; 2],
|
|
pub size: [i32; 2],
|
|
#[serde(default)]
|
|
pub mapped: bool,
|
|
#[serde(default)]
|
|
pub hidden: bool,
|
|
#[serde(default)]
|
|
pub xwayland: bool,
|
|
#[serde(rename = "focusHistoryID", default)]
|
|
pub focus_history_id: i64,
|
|
}
|
|
|
|
impl Client {
|
|
pub fn address_selector(&self) -> String {
|
|
let a = self.address.trim();
|
|
if a.starts_with("address:") {
|
|
a.to_string()
|
|
} else {
|
|
format!("address:{a}")
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn clients() -> Result<Vec<Client>> {
|
|
let raw = hyprctl(["-j", "clients"]).await?;
|
|
serde_json::from_str(&raw).context("parse hyprctl clients")
|
|
}
|
|
|
|
pub async fn cursor_pos() -> Result<(i32, i32)> {
|
|
let raw = hyprctl(["-j", "cursorpos"]).await?;
|
|
#[derive(Deserialize)]
|
|
struct P {
|
|
x: i32,
|
|
y: i32,
|
|
}
|
|
let p: P = serde_json::from_str(&raw).context("cursorpos")?;
|
|
Ok((p.x, p.y))
|
|
}
|
|
|
|
pub async fn hyprctl<I, S>(args: I) -> Result<String>
|
|
where
|
|
I: IntoIterator<Item = S>,
|
|
S: AsRef<std::ffi::OsStr>,
|
|
{
|
|
let out = Command::new("hyprctl")
|
|
.args(args)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.output()
|
|
.await
|
|
.context("spawn hyprctl")?;
|
|
if !out.status.success() {
|
|
bail!("hyprctl failed: {}", String::from_utf8_lossy(&out.stderr));
|
|
}
|
|
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
|
}
|
|
|
|
pub async fn eval_lua(code: &str) -> Result<String> {
|
|
hyprctl(["eval", code]).await
|
|
}
|
|
|
|
pub async fn dispatch_lua(expr: &str) -> Result<String> {
|
|
hyprctl(["dispatch", expr]).await
|
|
}
|
|
|
|
pub fn send_shortcut_expr(window: &str, parsed: &ParsedHotkey) -> String {
|
|
let mods = parsed.hypr_mods();
|
|
format!(
|
|
"hl.dsp.send_shortcut({{ window = {window:?}, mods = {mods:?}, key = {:?} }})",
|
|
parsed.hypr_key()
|
|
)
|
|
}
|
|
|
|
pub fn send_key_state_expr(window: &str, parsed: &ParsedHotkey, state: &str) -> String {
|
|
let mods = parsed.hypr_mods();
|
|
format!(
|
|
"hl.dsp.send_key_state({{ window = {window:?}, mods = {mods:?}, key = {:?}, state = {state:?} }})",
|
|
parsed.hypr_key()
|
|
)
|
|
}
|
|
|
|
pub async fn send_key(window: &str, combo: &str, state: Option<&str>) -> Result<()> {
|
|
let parsed = crate::hotkey::parse(combo)?;
|
|
let expr = match state {
|
|
Some(s) => send_key_state_expr(window, &parsed, s),
|
|
None => send_shortcut_expr(window, &parsed),
|
|
};
|
|
dispatch_lua(&expr).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn send_mouse_click(window: &str, button: u32) -> Result<()> {
|
|
// 272 = BTN_LEFT, 273 = BTN_RIGHT
|
|
let expr = format!(
|
|
"hl.dsp.send_shortcut({{ window = {window:?}, mods = \"\", key = \"mouse:{button}\" }})"
|
|
);
|
|
dispatch_lua(&expr).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn move_cursor(x: i32, y: i32) -> Result<()> {
|
|
dispatch_lua(&format!("hl.dsp.cursor.move({{ x = {x}, y = {y} }})")).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn move_resize_window(selector: &str, x: i32, y: i32, w: i32, h: i32) -> Result<()> {
|
|
dispatch_lua(&format!(
|
|
"hl.dsp.window.move({{ window = {selector:?}, x = {x}, y = {y}, relative = false }})"
|
|
))
|
|
.await?;
|
|
dispatch_lua(&format!(
|
|
"hl.dsp.window.resize({{ window = {selector:?}, x = {w}, y = {h}, relative = false }})"
|
|
))
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
const CLEAR_BINDS: &str = r#"
|
|
_G.enboxer = _G.enboxer or { binds = {} }
|
|
for _, b in ipairs(_G.enboxer.binds) do
|
|
pcall(function() b:remove() end)
|
|
end
|
|
_G.enboxer.binds = {}
|
|
"#;
|
|
|
|
/// Install or replace the enBoxer bind table in Hyprland's Lua VM.
|
|
pub async fn replace_binds(binds: &[BindSpec], ipc: &str) -> Result<()> {
|
|
let mut body = String::from(CLEAR_BINDS);
|
|
for b in binds {
|
|
let opts = match (b.non_consuming, b.release) {
|
|
(true, true) => "{ description = \"enboxer\", non_consuming = true, release = true }",
|
|
(true, false) => "{ description = \"enboxer\", non_consuming = true }",
|
|
(false, true) => "{ description = \"enboxer\", release = true }",
|
|
(false, false) => "{ description = \"enboxer\" }",
|
|
};
|
|
let cmd = format!(
|
|
"{} ipc --sock {} {}",
|
|
shell_single(&b.ipc_bin),
|
|
shell_single(ipc),
|
|
b.ipc_args
|
|
);
|
|
body.push_str(&format!(
|
|
r#"
|
|
do
|
|
local b = hl.bind({bind:?}, function()
|
|
hl.dispatch(hl.dsp.exec_cmd({cmd:?}))
|
|
end, {opts})
|
|
table.insert(_G.enboxer.binds, b)
|
|
end
|
|
"#,
|
|
bind = b.bind,
|
|
cmd = cmd,
|
|
opts = opts,
|
|
));
|
|
}
|
|
eval_lua(&body).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn clear_binds() -> Result<()> {
|
|
eval_lua(CLEAR_BINDS).await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct BindSpec {
|
|
pub bind: String,
|
|
pub ipc_bin: String,
|
|
pub ipc_args: String,
|
|
pub non_consuming: bool,
|
|
pub release: bool,
|
|
}
|
|
|
|
fn shell_single(s: &str) -> String {
|
|
format!("'{}'", s.replace('\'', r#"'"'"'"#))
|
|
}
|
|
|
|
pub async fn active_window() -> Result<Option<Client>> {
|
|
let raw = hyprctl(["-j", "activewindow"]).await?;
|
|
if raw.trim() == "{}" || raw.trim().is_empty() {
|
|
return Ok(None);
|
|
}
|
|
Ok(serde_json::from_str(&raw).ok())
|
|
}
|
|
|
|
pub fn socket2_path() -> Result<PathBuf> {
|
|
let sig = std::env::var("HYPRLAND_INSTANCE_SIGNATURE")
|
|
.context("HYPRLAND_INSTANCE_SIGNATURE not set — not in a Hyprland session")?;
|
|
let runtime = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".into());
|
|
Ok(PathBuf::from(runtime)
|
|
.join("hypr")
|
|
.join(sig)
|
|
.join(".socket2.sock"))
|
|
}
|
|
|
|
pub async fn listen_events<F, Fut>(mut on_line: F) -> Result<()>
|
|
where
|
|
F: FnMut(String) -> Fut,
|
|
Fut: std::future::Future<Output = Result<()>>,
|
|
{
|
|
let path = socket2_path()?;
|
|
let stream = UnixStream::connect(&path)
|
|
.await
|
|
.with_context(|| format!("connect {}", path.display()))?;
|
|
let mut lines = BufReader::new(stream).lines();
|
|
while let Some(line) = lines.next_line().await? {
|
|
on_line(line).await?;
|
|
}
|
|
Ok(())
|
|
}
|