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: Option, } #[derive(Subcommand)] enum Cmd { /// Load a profile and route keys while a managed game is focused Run { #[arg(short, long)] config: Option, }, /// Fire a mapped hotkey (daemon must be running) Press { hotkey: String, #[arg(long)] sock: Option, }, Status { #[arg(long)] sock: Option, }, /// Set or cycle routing mode: maps | mirror | off Mode { /// maps, mirror, off, or omit to cycle which: Option, #[arg(long)] sock: Option, }, /// Print the in-game macros / binds to create Macros { #[arg(short, long)] config: Option, }, /// Check Hyprland + grim + profile Doctor { #[arg(short, long)] config: Option, }, /// Control panel (default if you run `enboxer` with no command) Gui, /// Generate and apply the window layout to captured game clients LayoutApply { #[arg(short, long)] config: Option, }, /// Called by Hyprland binds; not for humans Ipc { #[arg(long)] sock: PathBuf, verb: String, #[arg(trailing_var_arg = true)] rest: Vec, }, } #[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.unwrap_or(Cmd::Gui) { Cmd::Gui => enboxer::gui::run(), Cmd::LayoutApply { config } => { let path = config.unwrap_or_else(profile::default_config_path); let mut profile = Profile::load(&path)?; let n = enboxer::layout::apply_for_profile(&mut profile).await?; if let Ok(text) = serde_yaml::to_string(&profile) { let _ = std::fs::write(&path, text); } println!("laid out {n} windows"); Ok(()) } 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::Mode { which, sock } => { let sock = sock.unwrap_or_else(session::default_sock); let line = match which { Some(w) => format!("mode {w}"), None => "mode-cycle".into(), }; println!("{}", session::ipc_send(&sock, &line).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) -> 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)"); ok = false; } } match tokio::process::Command::new("mpv") .arg("--version") .output() .await { Ok(_) => println!("mpv: present"), Err(_) => { println!("mpv: MISSING (Video FX overlay needs mpv)"); ok = false; } } match enboxer::hypr::dispatch_lua( "hl.dsp.send_shortcut({ window = \"class:enboxer-does-not-exist\", mods = \"\", key = \"a\" })", ) .await { Ok(_) => println!("send_shortcut: compositor accepts dispatcher"), Err(e) => { let msg = e.to_string(); if msg.contains("window not found") { println!("send_shortcut: compositor accepts dispatcher"); } else { println!("send_shortcut: FAIL {e}"); ok = false; } } } 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={} vfx={}", path.display(), p.maps.len(), 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") } }