enBoxer/src/main.rs
en 4daf0ea86a Initial enBoxer: mapped-key routing and Video FX on Hyprland.
Phase 1 tickets T1–T6, ponytail dead-code cuts, docs and plan in-tree.
2026-09-15 07:19:15 +02:00

165 lines
4.6 KiB
Rust

use anyhow::Result;
use clap::{Parser, Subcommand};
use enboxer::macros::print_macros;
use enboxer::profile::{self, Profile};
use enboxer::session;
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;
#[derive(Parser)]
#[command(
name = "enboxer",
version,
about = "Mapped-key routing for WoW on Hyprland"
)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Load a profile and route keys while a managed game is focused
Run {
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Fire a mapped hotkey (daemon must be running)
Press {
hotkey: String,
#[arg(long)]
sock: Option<PathBuf>,
},
Status {
#[arg(long)]
sock: Option<PathBuf>,
},
/// Print the in-game macros / binds to create
Macros {
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Check Hyprland + grim + profile
Doctor {
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Called by Hyprland binds; not for humans
Ipc {
#[arg(long)]
sock: PathBuf,
verb: String,
#[arg(trailing_var_arg = true)]
rest: Vec<String>,
},
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env().add_directive("enboxer=info".parse()?))
.init();
let cli = Cli::parse();
match cli.cmd {
Cmd::Run { config } => {
let path = config.unwrap_or_else(profile::default_config_path);
let profile = Profile::load(&path)?;
tracing::info!("profile {} ({} slots)", profile.name, profile.slots);
session::run(profile, session::default_sock()).await
}
Cmd::Press { hotkey, sock } => {
let sock = sock.unwrap_or_else(session::default_sock);
println!(
"{}",
session::ipc_send(&sock, &format!("hotkey {hotkey}")).await?
);
Ok(())
}
Cmd::Status { sock } => {
let sock = sock.unwrap_or_else(session::default_sock);
println!("{}", session::ipc_send(&sock, "status").await?);
Ok(())
}
Cmd::Macros { config } => {
let path = config.unwrap_or_else(profile::default_config_path);
let profile = Profile::load(&path)?;
print!("{}", print_macros(&profile));
Ok(())
}
Cmd::Doctor { config } => doctor(config).await,
Cmd::Ipc { sock, verb, rest } => {
let arg = rest.join(" ");
let line = if arg.is_empty() {
verb
} else {
format!("{verb} {arg}")
};
println!("{}", session::ipc_send(&sock, &line).await?);
Ok(())
}
}
}
async fn doctor(config: Option<PathBuf>) -> Result<()> {
let mut ok = true;
match std::env::var("HYPRLAND_INSTANCE_SIGNATURE") {
Ok(s) => println!("hyprland session: {s}"),
Err(_) => {
println!("hyprland session: MISSING (not in Hyprland?)");
ok = false;
}
}
match enboxer::hypr::eval_lua("return 'ok'").await {
Ok(s) => println!("hyprctl eval: {s}"),
Err(e) => {
println!("hyprctl eval: FAIL {e}");
ok = false;
}
}
match tokio::process::Command::new("grim")
.arg("-h")
.output()
.await
{
Ok(_) => println!("grim: present"),
Err(_) => println!("grim: MISSING (Video FX capture needs grim)"),
}
if let Some(path) = config.or_else(|| {
let p = profile::default_config_path();
p.exists().then_some(p)
}) {
match Profile::load(&path) {
Ok(p) => {
println!(
"profile: {} maps={} passthrough={:?} vfx={}",
path.display(),
p.maps.len(),
p.passthrough,
p.video_fx.len()
);
}
Err(e) => {
println!("profile {}: FAIL {e}", path.display());
ok = false;
}
}
} else {
println!(
"profile: none at {}",
profile::default_config_path().display()
);
}
if let Ok(clients) = enboxer::hypr::clients().await {
println!("windows: {}", clients.len());
for c in clients.iter().take(12) {
println!(" {} {} {:?}", c.address, c.class, c.title);
}
}
if ok {
println!("doctor: ok");
Ok(())
} else {
anyhow::bail!("doctor found problems")
}
}