GOALS.md / AGENTS.md are the source of truth. Layout apply requires ENBOXER_ALLOW_LAYOUT=1 and a GUI confirm. Example Video FX is off.
888 lines
27 KiB
Rust
888 lines
27 KiB
Rust
use crate::engine::{Action, Engine, Hold};
|
|
use crate::hotkey::{self, passthrough_id};
|
|
use crate::hypr::{self, BindSpec, Client};
|
|
use crate::profile::{runtime_dir, Mode, Profile};
|
|
use crate::vfx::{self, FeedHit};
|
|
use anyhow::{Context, Result};
|
|
use regex::Regex;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Arc;
|
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
|
use tokio::net::UnixListener;
|
|
use tokio::sync::{watch, Mutex};
|
|
use tokio::time::{sleep, Duration};
|
|
|
|
pub struct Session {
|
|
pub engine: Engine,
|
|
pub slots: Vec<(u32, Client)>,
|
|
pub binds_on: bool,
|
|
pub exe: PathBuf,
|
|
pub sock: PathBuf,
|
|
pub vfx: Vec<FeedHit>,
|
|
pub vfx_source: Option<u32>,
|
|
pub mouse_broadcast: bool,
|
|
pub mouse_follow: bool,
|
|
}
|
|
|
|
impl Session {
|
|
pub fn new(profile: Profile, exe: PathBuf, sock: PathBuf) -> Result<Self> {
|
|
Ok(Self {
|
|
engine: Engine::new(profile)?,
|
|
slots: Vec::new(),
|
|
binds_on: false,
|
|
exe,
|
|
sock,
|
|
vfx: Vec::new(),
|
|
vfx_source: None,
|
|
mouse_broadcast: false,
|
|
mouse_follow: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub async fn run(profile: Profile, sock: PathBuf) -> Result<()> {
|
|
std::fs::create_dir_all(runtime_dir()).ok();
|
|
if sock.exists() {
|
|
let _ = std::fs::remove_file(&sock);
|
|
}
|
|
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("enboxer"));
|
|
tracing::info!(
|
|
"profile {} slots={} match class={:?} title={:?}",
|
|
profile.name,
|
|
profile.slots,
|
|
profile.window_match.class,
|
|
profile.window_match.title
|
|
);
|
|
let session = Arc::new(Mutex::new(Session::new(profile, exe, sock.clone())?));
|
|
let (vfx_tx, vfx_rx) = watch::channel(Vec::new());
|
|
let hub = vfx::OverlayHub::new();
|
|
|
|
hypr::apply_vfx_window_rules().await.ok();
|
|
|
|
let listener = UnixListener::bind(&sock).with_context(|| format!("bind {}", sock.display()))?;
|
|
tracing::info!("ipc {}", sock.display());
|
|
|
|
let s1 = session.clone();
|
|
tokio::spawn(async move {
|
|
loop {
|
|
match listener.accept().await {
|
|
Ok((stream, _)) => {
|
|
let s = s1.clone();
|
|
tokio::spawn(async move {
|
|
if let Err(e) = handle_ipc(s, stream).await {
|
|
tracing::debug!("ipc: {e}");
|
|
}
|
|
});
|
|
}
|
|
Err(e) => tracing::warn!("accept: {e}"),
|
|
}
|
|
}
|
|
});
|
|
|
|
{
|
|
let hub = hub.clone();
|
|
tokio::spawn(vfx::capture_loop(vfx_rx, hub));
|
|
}
|
|
|
|
let s2 = session.clone();
|
|
let hub2 = hub.clone();
|
|
tokio::spawn(async move {
|
|
loop {
|
|
if let Err(e) = refresh_slots(&s2, &vfx_tx, &hub2).await {
|
|
tracing::warn!("refresh: {e}");
|
|
}
|
|
sleep(Duration::from_millis(400)).await;
|
|
}
|
|
});
|
|
|
|
let s3 = session.clone();
|
|
let events = hypr::listen_events(move |line| {
|
|
let s = s3.clone();
|
|
async move {
|
|
on_event(&s, &line).await;
|
|
Ok(())
|
|
}
|
|
});
|
|
|
|
tokio::select! {
|
|
r = events => {
|
|
tracing::warn!("hyprland event socket closed: {r:?}");
|
|
}
|
|
_ = tokio::signal::ctrl_c() => {
|
|
tracing::info!("ctrl-c");
|
|
}
|
|
}
|
|
let _ = hypr::clear_binds().await;
|
|
hub.kill_all().await;
|
|
Ok(())
|
|
}
|
|
|
|
async fn refresh_slots(
|
|
session: &Arc<Mutex<Session>>,
|
|
vfx_tx: &watch::Sender<Vec<FeedHit>>,
|
|
hub: &Arc<vfx::OverlayHub>,
|
|
) -> Result<()> {
|
|
let clients = hypr::clients().await?;
|
|
let aw = hypr::active_window().await.ok().flatten();
|
|
let cursor = hypr::cursor_pos().await.ok();
|
|
let mut g = session.lock().await;
|
|
let class_re = g
|
|
.engine
|
|
.profile
|
|
.window_match
|
|
.class
|
|
.as_deref()
|
|
.and_then(|p| Regex::new(p).ok());
|
|
let title_re = g
|
|
.engine
|
|
.profile
|
|
.window_match
|
|
.title
|
|
.as_deref()
|
|
.and_then(|p| Regex::new(p).ok());
|
|
|
|
let has_filter = class_re.is_some() || title_re.is_some();
|
|
let mut matched: Vec<Client> = if !has_filter {
|
|
Vec::new()
|
|
} else {
|
|
clients
|
|
.into_iter()
|
|
.filter(|c| {
|
|
if c.class == "enboxer-vfx" {
|
|
return false;
|
|
}
|
|
let class_ok = class_re
|
|
.as_ref()
|
|
.map(|r| r.is_match(&c.class))
|
|
.unwrap_or(true);
|
|
let title_ok = title_re
|
|
.as_ref()
|
|
.map(|r| r.is_match(&c.title))
|
|
.unwrap_or(true);
|
|
class_ok && title_ok && c.mapped && !c.hidden
|
|
})
|
|
.collect()
|
|
};
|
|
matched.sort_by_key(|c| (c.at[1], c.at[0], c.pid));
|
|
|
|
let n = g.engine.profile.slots as usize;
|
|
let new_slots: Vec<(u32, Client)> = matched
|
|
.into_iter()
|
|
.take(n)
|
|
.enumerate()
|
|
.map(|(i, c)| ((i as u32) + 1, c))
|
|
.collect();
|
|
let old_keys: Vec<(u32, &str)> = g
|
|
.slots
|
|
.iter()
|
|
.map(|(s, c)| (*s, c.address.as_str()))
|
|
.collect();
|
|
let new_keys: Vec<(u32, &str)> = new_slots
|
|
.iter()
|
|
.map(|(s, c)| (*s, c.address.as_str()))
|
|
.collect();
|
|
if old_keys != new_keys {
|
|
for (slot, c) in &new_slots {
|
|
tracing::info!("slot {} {} {} {}", slot, c.class, c.title, c.address);
|
|
}
|
|
}
|
|
g.slots = new_slots;
|
|
|
|
if let Some(aw) = aw {
|
|
let leader = g
|
|
.slots
|
|
.iter()
|
|
.find(|(_, c)| c.address == aw.address)
|
|
.map(|(s, _)| *s);
|
|
if let Some(slot) = leader {
|
|
g.engine.set_leader(slot);
|
|
}
|
|
let managed = g.slots.iter().any(|(_, c)| c.address == aw.address)
|
|
|| aw.class == "enboxer-vfx"
|
|
|| aw.title.starts_with("enboxer-vfx");
|
|
if managed {
|
|
if !g.binds_on {
|
|
let specs = bind_specs(&g)?;
|
|
let sock = g.sock.display().to_string();
|
|
drop(g);
|
|
hypr::replace_binds(&specs, &sock).await?;
|
|
let mut g = session.lock().await;
|
|
g.binds_on = true;
|
|
tracing::info!("routing on ({} binds)", specs.len());
|
|
return finish_vfx(g, vfx_tx, hub, cursor).await;
|
|
}
|
|
} else if g.binds_on {
|
|
g.binds_on = false;
|
|
g.vfx_source = None;
|
|
drop(g);
|
|
hypr::clear_binds().await.ok();
|
|
tracing::info!("routing off (focus left the team)");
|
|
let g = session.lock().await;
|
|
return finish_vfx(g, vfx_tx, hub, cursor).await;
|
|
}
|
|
}
|
|
|
|
finish_vfx(g, vfx_tx, hub, cursor).await
|
|
}
|
|
|
|
async fn finish_vfx(
|
|
mut g: tokio::sync::MutexGuard<'_, Session>,
|
|
vfx_tx: &watch::Sender<Vec<FeedHit>>,
|
|
hub: &Arc<vfx::OverlayHub>,
|
|
cursor: Option<(i32, i32)>,
|
|
) -> Result<()> {
|
|
let feeds = if let Some((_, primary)) = g
|
|
.slots
|
|
.iter()
|
|
.find(|(s, _)| *s == g.engine.leader_slot)
|
|
.cloned()
|
|
{
|
|
vfx::build_hits(&g.engine.profile.video_fx, &primary, &g.slots)
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
g.vfx = feeds.clone();
|
|
if let Some((x, y)) = cursor {
|
|
let src = vfx::hit_test(&g.vfx, x, y)
|
|
.filter(|h| h.pass_through)
|
|
.map(|h| h.source_slot);
|
|
if src != g.vfx_source {
|
|
g.vfx_source = src;
|
|
g.binds_on = false;
|
|
}
|
|
}
|
|
drop(g);
|
|
let _ = vfx_tx.send(feeds.clone());
|
|
hub.sync(&feeds).await.ok();
|
|
Ok(())
|
|
}
|
|
|
|
async fn on_event(session: &Arc<Mutex<Session>>, line: &str) {
|
|
if let Some(rest) = line.strip_prefix("activewindowv2>>") {
|
|
let addr = rest.trim();
|
|
let mut g = session.lock().await;
|
|
let want = addr.trim().trim_start_matches("0x");
|
|
let leader = g
|
|
.slots
|
|
.iter()
|
|
.find(|(_, c)| {
|
|
c.address
|
|
.trim()
|
|
.trim_start_matches("0x")
|
|
.eq_ignore_ascii_case(want)
|
|
})
|
|
.map(|(s, _)| *s);
|
|
if let Some(slot) = leader {
|
|
g.engine.set_leader(slot);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn bind_specs(g: &Session) -> Result<Vec<BindSpec>> {
|
|
let passthrough = g.engine.profile.passthrough_set()?;
|
|
let mut specs = Vec::new();
|
|
let bin = g.exe.display().to_string();
|
|
for m in &g.engine.profile.maps {
|
|
let id = passthrough_id(&m.hotkey.0).unwrap_or_else(|_| m.hotkey.0.clone());
|
|
if passthrough.contains(&id) {
|
|
continue;
|
|
}
|
|
let parsed = m.hotkey.parse()?;
|
|
specs.push(BindSpec {
|
|
bind: parsed.hypr_bind(),
|
|
ipc_bin: bin.clone(),
|
|
ipc_args: format!("hotkey {}", m.hotkey.0),
|
|
non_consuming: false,
|
|
release: false,
|
|
});
|
|
if m.hold {
|
|
specs.push(BindSpec {
|
|
bind: parsed.hypr_bind(),
|
|
ipc_bin: bin.clone(),
|
|
ipc_args: format!("hotkey-release {}", m.hotkey.0),
|
|
non_consuming: false,
|
|
release: true,
|
|
});
|
|
}
|
|
}
|
|
if g.engine.mode == Mode::Mirror {
|
|
let keys = if g.engine.profile.repeater.keys.is_empty() {
|
|
clone_key_set()
|
|
} else {
|
|
g.engine.profile.repeater.keys.clone()
|
|
};
|
|
for k in keys {
|
|
let id = passthrough_id(&k).unwrap_or_else(|_| k.clone());
|
|
if passthrough.contains(&id) {
|
|
continue;
|
|
}
|
|
if g.engine.profile.map_by_hotkey(&k).is_some() {
|
|
continue;
|
|
}
|
|
let parsed = hotkey::parse(&k)?;
|
|
specs.push(BindSpec {
|
|
bind: parsed.hypr_bind(),
|
|
ipc_bin: bin.clone(),
|
|
ipc_args: format!("hotkey {k}"),
|
|
non_consuming: true,
|
|
release: false,
|
|
});
|
|
}
|
|
}
|
|
let wm = [
|
|
("mode_cycle", "mode-cycle"),
|
|
("swap_next", "swap-next"),
|
|
("swap_prev", "swap-prev"),
|
|
("focus_next", "focus-next"),
|
|
("focus_prev", "focus-prev"),
|
|
("focus_main", "focus-main"),
|
|
("reset_all", "reset-all"),
|
|
("stay_on_top", "stay-on-top"),
|
|
("mouse_follow", "mouse-follow"),
|
|
("mouse_broadcast", "mouse-broadcast"),
|
|
];
|
|
for (name, ipc) in wm {
|
|
if let Some(hk) = g.engine.profile.session_hotkeys.get(name) {
|
|
let parsed = hotkey::parse(hk)?;
|
|
specs.push(BindSpec {
|
|
bind: parsed.hypr_bind(),
|
|
ipc_bin: bin.clone(),
|
|
ipc_args: ipc.into(),
|
|
non_consuming: false,
|
|
release: false,
|
|
});
|
|
}
|
|
}
|
|
for i in 1..=g.engine.profile.slots {
|
|
let focus = format!("Ctrl+F{i}");
|
|
let swap = format!("Ctrl+Shift+F{i}");
|
|
if let Ok(p) = hotkey::parse(&focus) {
|
|
specs.push(BindSpec {
|
|
bind: p.hypr_bind(),
|
|
ipc_bin: bin.clone(),
|
|
ipc_args: format!("focus {i}"),
|
|
non_consuming: false,
|
|
release: false,
|
|
});
|
|
}
|
|
if i > 1 {
|
|
if let Ok(p) = hotkey::parse(&swap) {
|
|
specs.push(BindSpec {
|
|
bind: p.hypr_bind(),
|
|
ipc_bin: bin.clone(),
|
|
ipc_args: format!("swap {i}"),
|
|
non_consuming: false,
|
|
release: false,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if g.mouse_broadcast {
|
|
for btn in ["mouse:272", "mouse:273"] {
|
|
specs.push(BindSpec {
|
|
bind: btn.to_string(),
|
|
ipc_bin: bin.clone(),
|
|
ipc_args: format!("mouse-click {btn}"),
|
|
non_consuming: true,
|
|
release: false,
|
|
});
|
|
}
|
|
}
|
|
if g.vfx_source.is_some() {
|
|
for k in vfx_hover_keys() {
|
|
if specs.iter().any(|s| s.bind == k) {
|
|
continue;
|
|
}
|
|
specs.push(BindSpec {
|
|
bind: k.clone(),
|
|
ipc_bin: bin.clone(),
|
|
ipc_args: format!("vfxkey {k}"),
|
|
non_consuming: false,
|
|
release: false,
|
|
});
|
|
}
|
|
}
|
|
Ok(specs)
|
|
}
|
|
|
|
async fn handle_ipc(session: Arc<Mutex<Session>>, stream: tokio::net::UnixStream) -> Result<()> {
|
|
let (r, mut w) = stream.into_split();
|
|
let mut lines = BufReader::new(r).lines();
|
|
while let Some(line) = lines.next_line().await? {
|
|
let reply = dispatch_cmd(&session, &line).await;
|
|
w.write_all(reply.as_bytes()).await?;
|
|
w.write_all(b"\n").await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn dispatch_cmd(session: &Arc<Mutex<Session>>, line: &str) -> String {
|
|
let mut parts = line.splitn(2, ' ');
|
|
let cmd = parts.next().unwrap_or("");
|
|
let arg = parts.next().unwrap_or("").trim();
|
|
match cmd {
|
|
"ping" => "ok".into(),
|
|
"status" => {
|
|
let g = session.lock().await;
|
|
format!(
|
|
"mode={} leader={} binds={} slots={} mouse_broadcast={} mouse_follow={} vfx_source={:?}",
|
|
g.engine.mode.as_str(),
|
|
g.engine.leader_slot,
|
|
g.binds_on,
|
|
g.slots.len(),
|
|
g.mouse_broadcast,
|
|
g.mouse_follow,
|
|
g.vfx_source
|
|
)
|
|
}
|
|
"mode-cycle" => match set_mode(session, None).await {
|
|
Ok(m) => format!("mode={}", m.as_str()),
|
|
Err(e) => format!("err {e}"),
|
|
},
|
|
"mode" => match set_mode(session, Some(arg)).await {
|
|
Ok(m) => format!("mode={}", m.as_str()),
|
|
Err(e) => format!("err {e}"),
|
|
},
|
|
"hotkey" => match fire(session, arg, Hold::Tap).await {
|
|
Ok(()) => "ok".into(),
|
|
Err(e) => format!("err {e}"),
|
|
},
|
|
"hotkey-release" => match fire(session, arg, Hold::Up).await {
|
|
Ok(()) => "ok".into(),
|
|
Err(e) => format!("err {e}"),
|
|
},
|
|
"vfxkey" => {
|
|
if let Some(rest) = arg.strip_prefix("mouse:") {
|
|
match rest.parse::<u32>() {
|
|
Ok(btn) => match fire_vfx_click(session, btn).await {
|
|
Ok(()) => "ok".into(),
|
|
Err(e) => format!("err {e}"),
|
|
},
|
|
Err(_) => format!("err bad mouse {arg}"),
|
|
}
|
|
} else {
|
|
match fire_vfx(session, arg).await {
|
|
Ok(()) => "ok".into(),
|
|
Err(e) => format!("err {e}"),
|
|
}
|
|
}
|
|
}
|
|
"swap-next" => wm_ok(swap_next(session).await),
|
|
"swap-prev" => wm_ok(swap_prev(session).await),
|
|
"swap" => match arg.parse::<u32>() {
|
|
Ok(n) => wm_ok(swap_as_main(session, n).await),
|
|
Err(_) => "err bad slot".into(),
|
|
},
|
|
"focus-next" => wm_ok(focus_step(session, 1).await),
|
|
"focus-prev" => wm_ok(focus_step(session, -1).await),
|
|
"focus-main" => wm_ok(focus_slot(session, 1).await),
|
|
"focus" => match arg.parse::<u32>() {
|
|
Ok(n) => wm_ok(focus_slot(session, n).await),
|
|
Err(_) => "err bad slot".into(),
|
|
},
|
|
"reset-all" => wm_ok(reset_layout(session).await),
|
|
"stay-on-top" => wm_ok(toggle_pin(session).await),
|
|
"mouse-follow" => wm_ok(toggle_mouse_follow(session).await),
|
|
"mouse-broadcast" => wm_ok(toggle_mouse_broadcast(session).await),
|
|
"mouse-click" => {
|
|
let btn = arg
|
|
.strip_prefix("mouse:")
|
|
.and_then(|s| s.parse::<u32>().ok())
|
|
.unwrap_or(272);
|
|
wm_ok(broadcast_click(session, btn).await)
|
|
}
|
|
"type" => wm_ok(type_to_others(session, arg).await),
|
|
_ => format!("err unknown {cmd}"),
|
|
}
|
|
}
|
|
|
|
async fn set_mode(session: &Arc<Mutex<Session>>, name: Option<&str>) -> Result<Mode> {
|
|
let aw = hypr::active_window().await.ok().flatten();
|
|
let (mode, specs, sock, managed) = {
|
|
let mut g = session.lock().await;
|
|
let mode = if let Some(n) = name {
|
|
Mode::parse_name(n).ok_or_else(|| anyhow::anyhow!("unknown mode {n}"))?
|
|
} else {
|
|
g.engine.cycle_mode()
|
|
};
|
|
g.engine.mode = mode;
|
|
let managed = aw.as_ref().is_some_and(|aw| {
|
|
g.slots.iter().any(|(_, c)| c.address == aw.address)
|
|
|| aw.class == "enboxer-vfx"
|
|
|| aw.title.starts_with("enboxer-vfx")
|
|
});
|
|
let specs = if managed { Some(bind_specs(&g)?) } else { None };
|
|
let sock = g.sock.display().to_string();
|
|
(mode, specs, sock, managed)
|
|
};
|
|
if let Some(specs) = specs {
|
|
hypr::replace_binds(&specs, &sock).await?;
|
|
let mut g = session.lock().await;
|
|
g.binds_on = managed;
|
|
}
|
|
hypr::notify(&format!("enBoxer: {}", mode.label()))
|
|
.await
|
|
.ok();
|
|
tracing::info!("mode {}", mode.as_str());
|
|
Ok(mode)
|
|
}
|
|
|
|
fn wm_ok(r: Result<()>) -> String {
|
|
match r {
|
|
Ok(()) => "ok".into(),
|
|
Err(e) => format!("err {e}"),
|
|
}
|
|
}
|
|
|
|
async fn swap_next(session: &Arc<Mutex<Session>>) -> Result<()> {
|
|
let n = {
|
|
let g = session.lock().await;
|
|
let len = g.slots.len() as u32;
|
|
if len < 2 {
|
|
return Ok(());
|
|
}
|
|
let cur = g.engine.leader_slot;
|
|
if cur >= len {
|
|
2
|
|
} else {
|
|
cur + 1
|
|
}
|
|
};
|
|
swap_as_main(session, n.max(2)).await
|
|
}
|
|
|
|
async fn swap_prev(session: &Arc<Mutex<Session>>) -> Result<()> {
|
|
let n = {
|
|
let g = session.lock().await;
|
|
let len = g.slots.len() as u32;
|
|
if len < 2 {
|
|
return Ok(());
|
|
}
|
|
let cur = g.engine.leader_slot;
|
|
if cur <= 2 {
|
|
len
|
|
} else {
|
|
cur - 1
|
|
}
|
|
};
|
|
swap_as_main(session, n).await
|
|
}
|
|
|
|
async fn swap_as_main(session: &Arc<Mutex<Session>>, n: u32) -> Result<()> {
|
|
let (wins, tiles) = {
|
|
let mut g = session.lock().await;
|
|
if n < 2 || n as usize > g.slots.len() {
|
|
return Ok(());
|
|
}
|
|
g.slots.swap(0, n as usize - 1);
|
|
for (i, (id, _)) in g.slots.iter_mut().enumerate() {
|
|
*id = i as u32 + 1;
|
|
}
|
|
g.engine.set_leader(1);
|
|
(g.slots.clone(), g.engine.profile.layout.slots.clone())
|
|
};
|
|
if !tiles.is_empty() {
|
|
crate::layout::apply(&tiles, &wins).await?;
|
|
}
|
|
if let Some((_, c)) = wins.first() {
|
|
hypr::focus_window(&c.address_selector()).await.ok();
|
|
}
|
|
hypr::notify(&format!("main is slot 1 (was {n})"))
|
|
.await
|
|
.ok();
|
|
Ok(())
|
|
}
|
|
|
|
async fn focus_step(session: &Arc<Mutex<Session>>, dir: i32) -> Result<()> {
|
|
let n = {
|
|
let g = session.lock().await;
|
|
let len = g.slots.len() as i32;
|
|
if len == 0 {
|
|
return Ok(());
|
|
}
|
|
let cur = g.engine.leader_slot as i32;
|
|
let next = ((cur - 1 + dir).rem_euclid(len)) + 1;
|
|
next as u32
|
|
};
|
|
focus_slot(session, n).await
|
|
}
|
|
|
|
async fn focus_slot(session: &Arc<Mutex<Session>>, n: u32) -> Result<()> {
|
|
let addr = {
|
|
let mut g = session.lock().await;
|
|
g.engine.set_leader(n);
|
|
g.slots
|
|
.iter()
|
|
.find(|(s, _)| *s == n)
|
|
.map(|(_, c)| c.address_selector())
|
|
};
|
|
if let Some(a) = addr {
|
|
hypr::focus_window(&a).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn reset_layout(session: &Arc<Mutex<Session>>) -> Result<()> {
|
|
let (wins, tiles) = {
|
|
let g = session.lock().await;
|
|
(g.slots.clone(), g.engine.profile.layout.slots.clone())
|
|
};
|
|
if tiles.is_empty() {
|
|
anyhow::bail!("no saved layout tiles");
|
|
}
|
|
crate::layout::apply(&tiles, &wins).await?;
|
|
hypr::notify("layout reset").await.ok();
|
|
Ok(())
|
|
}
|
|
|
|
async fn toggle_pin(session: &Arc<Mutex<Session>>) -> Result<()> {
|
|
let addr = {
|
|
let g = session.lock().await;
|
|
g.slots
|
|
.iter()
|
|
.find(|(s, _)| *s == g.engine.leader_slot)
|
|
.map(|(_, c)| c.address_selector())
|
|
};
|
|
if let Some(a) = addr {
|
|
hypr::dispatch_lua(&format!(
|
|
"hl.dsp.window.pin({{ window = {a:?}, action = \"toggle\" }})"
|
|
))
|
|
.await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn toggle_mouse_follow(session: &Arc<Mutex<Session>>) -> Result<()> {
|
|
let on = {
|
|
let mut g = session.lock().await;
|
|
g.mouse_follow = !g.mouse_follow;
|
|
g.mouse_follow
|
|
};
|
|
let v = if on { 1 } else { 0 };
|
|
let _ = hypr::hyprctl(["keyword", "input:follow_mouse", &v.to_string()]).await;
|
|
hypr::notify(&format!(
|
|
"focus follows mouse {}",
|
|
if on { "on" } else { "off" }
|
|
))
|
|
.await
|
|
.ok();
|
|
Ok(())
|
|
}
|
|
|
|
async fn toggle_mouse_broadcast(session: &Arc<Mutex<Session>>) -> Result<()> {
|
|
{
|
|
let mut g = session.lock().await;
|
|
g.mouse_broadcast = !g.mouse_broadcast;
|
|
g.binds_on = false;
|
|
hypr::notify(&format!(
|
|
"mouse broadcast {}",
|
|
if g.mouse_broadcast { "on" } else { "off" }
|
|
))
|
|
.await
|
|
.ok();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn type_to_others(session: &Arc<Mutex<Session>>, text: &str) -> Result<()> {
|
|
let others = {
|
|
let g = session.lock().await;
|
|
g.slots
|
|
.iter()
|
|
.filter(|(s, _)| *s != g.engine.leader_slot)
|
|
.map(|(_, c)| c.clone())
|
|
.collect::<Vec<_>>()
|
|
};
|
|
for key in crate::hotkey::type_keys(text) {
|
|
for c in &others {
|
|
hypr::deliver_key(c, &key, None).await.ok();
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn broadcast_click(session: &Arc<Mutex<Session>>, button: u32) -> Result<()> {
|
|
let (cx, cy) = hypr::cursor_pos().await?;
|
|
let others = {
|
|
let g = session.lock().await;
|
|
if !g.mouse_broadcast {
|
|
return Ok(());
|
|
}
|
|
let Some((_, primary)) = g.slots.iter().find(|(s, _)| *s == g.engine.leader_slot) else {
|
|
return Ok(());
|
|
};
|
|
let pw = primary.size[0].max(1) as f64;
|
|
let ph = primary.size[1].max(1) as f64;
|
|
let nx = (cx - primary.at[0]) as f64 / pw;
|
|
let ny = (cy - primary.at[1]) as f64 / ph;
|
|
g.slots
|
|
.iter()
|
|
.filter(|(s, _)| *s != g.engine.leader_slot)
|
|
.map(|(_, c)| {
|
|
let x = c.at[0] + (nx * c.size[0] as f64).round() as i32;
|
|
let y = c.at[1] + (ny * c.size[1] as f64).round() as i32;
|
|
(c.clone(), x, y)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
for (c, x, y) in others {
|
|
hypr::move_cursor(x, y).await.ok();
|
|
hypr::deliver_click(&c, button).await.ok();
|
|
}
|
|
hypr::move_cursor(cx, cy).await.ok();
|
|
Ok(())
|
|
}
|
|
|
|
fn clone_key_set() -> Vec<String> {
|
|
vfx_hover_keys()
|
|
.into_iter()
|
|
.filter(|k| !k.starts_with("mouse:"))
|
|
.collect()
|
|
}
|
|
|
|
fn vfx_hover_keys() -> Vec<String> {
|
|
let mut v: Vec<String> = [
|
|
"Space",
|
|
"Return",
|
|
"Escape",
|
|
"Tab",
|
|
"BackSpace",
|
|
"grave",
|
|
"minus",
|
|
"equal",
|
|
"mouse:272",
|
|
"mouse:273",
|
|
]
|
|
.into_iter()
|
|
.map(str::to_string)
|
|
.collect();
|
|
for c in b'a'..=b'z' {
|
|
v.push((c as char).to_string());
|
|
}
|
|
for c in b'0'..=b'9' {
|
|
v.push((c as char).to_string());
|
|
}
|
|
v
|
|
}
|
|
|
|
async fn fire_vfx(session: &Arc<Mutex<Session>>, hotkey: &str) -> Result<()> {
|
|
let (src, slots) = {
|
|
let g = session.lock().await;
|
|
let src = g
|
|
.vfx_source
|
|
.ok_or_else(|| anyhow::anyhow!("no vfx hover"))?;
|
|
(src, g.slots.clone())
|
|
};
|
|
execute(
|
|
vec![Action::Send {
|
|
key: hotkey.to_string(),
|
|
slots: vec![src],
|
|
hold: Hold::Tap,
|
|
}],
|
|
&slots,
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn fire_vfx_click(session: &Arc<Mutex<Session>>, button: u32) -> Result<()> {
|
|
let (cx, cy) = hypr::cursor_pos().await?;
|
|
let (feed, src) = {
|
|
let g = session.lock().await;
|
|
let slot = g
|
|
.vfx_source
|
|
.ok_or_else(|| anyhow::anyhow!("no vfx hover"))?;
|
|
let feed = vfx::hit_test(&g.vfx, cx, cy)
|
|
.filter(|h| h.pass_through && h.source_slot == slot)
|
|
.ok_or_else(|| anyhow::anyhow!("vfx hover lost"))?
|
|
.clone();
|
|
let src = g
|
|
.slots
|
|
.iter()
|
|
.find(|(s, _)| *s == slot)
|
|
.map(|(_, c)| c.clone())
|
|
.ok_or_else(|| anyhow::anyhow!("source slot gone"))?;
|
|
(feed, src)
|
|
};
|
|
let (sx, sy) = vfx::map_click(&feed, cx, cy);
|
|
hypr::move_cursor(sx, sy).await?;
|
|
hypr::deliver_click(&src, button).await?;
|
|
hypr::move_cursor(cx, cy).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn fire(session: &Arc<Mutex<Session>>, hotkey: &str, edge: Hold) -> Result<()> {
|
|
let (actions, slots) = {
|
|
let g = session.lock().await;
|
|
if let Some(src) = g.vfx_source {
|
|
if g.engine.profile.map_by_hotkey(hotkey).is_none()
|
|
&& g.engine
|
|
.profile
|
|
.session_hotkeys
|
|
.values()
|
|
.all(|h| !h.eq_ignore_ascii_case(hotkey))
|
|
{
|
|
let hold = if matches!(edge, Hold::Up) {
|
|
Hold::Up
|
|
} else {
|
|
Hold::Tap
|
|
};
|
|
(
|
|
vec![Action::Send {
|
|
key: hotkey.to_string(),
|
|
slots: vec![src],
|
|
hold,
|
|
}],
|
|
g.slots.clone(),
|
|
)
|
|
} else {
|
|
(g.engine.fire(hotkey, edge)?, g.slots.clone())
|
|
}
|
|
} else {
|
|
(g.engine.fire(hotkey, edge)?, g.slots.clone())
|
|
}
|
|
};
|
|
execute(actions, &slots).await
|
|
}
|
|
|
|
async fn execute(actions: Vec<Action>, slots: &[(u32, Client)]) -> Result<()> {
|
|
for a in actions {
|
|
match a {
|
|
Action::Sleep(d) => sleep(d).await,
|
|
Action::Send {
|
|
key,
|
|
slots: ids,
|
|
hold,
|
|
} => {
|
|
let parsed_state = match hold {
|
|
Hold::Down => Some("down"),
|
|
Hold::Up => Some("up"),
|
|
Hold::Tap => None,
|
|
};
|
|
for id in ids {
|
|
if let Some((_, c)) = slots.iter().find(|(s, _)| *s == id) {
|
|
hypr::deliver_key(c, &key, parsed_state).await?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn ipc_send(sock: &Path, line: &str) -> Result<String> {
|
|
use tokio::io::AsyncBufReadExt;
|
|
let mut stream = tokio::net::UnixStream::connect(sock)
|
|
.await
|
|
.with_context(|| format!("connect {}", sock.display()))?;
|
|
stream.write_all(line.as_bytes()).await?;
|
|
stream.write_all(b"\n").await?;
|
|
let mut reader = BufReader::new(stream);
|
|
let mut reply = String::new();
|
|
reader.read_line(&mut reply).await?;
|
|
Ok(reply.trim().to_string())
|
|
}
|
|
|
|
pub fn default_sock() -> PathBuf {
|
|
runtime_dir().join("enboxer.sock")
|
|
}
|