enBoxer/src/session.rs
en ed75a8b899 Document full product goals; gate layout; named profiles; type-to-others.
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.
2026-09-15 09:11:31 +02:00

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