use std::{ collections::HashMap, process::{self, ExitCode}, time::Duration, }; use anyhow::Context; use clap::Parser; use env_logger::Env; use wayvr_ipc::{ client::WayVRClient, ipc, packet_client::{self, PositionMode}, }; use crate::helper::{ WayVRClientState, wlx_device_haptics, wlx_input_state, wlx_panel_modify, wlx_show_hide, wlx_switch_set, wvr_process_get, wvr_process_launch, wvr_process_list, wvr_process_terminate, wvr_window_list, wvr_window_set_visible, }; mod helper; #[tokio::main(flavor = "current_thread")] async fn main() -> ExitCode { env_logger::init_from_env(Env::default().default_filter_or("info")); let args = Args::parse(); let mut state = WayVRClientState { wayvr_client: WayVRClient::new(&format!("wayvrctl-{}", process::id())) .await .inspect_err(|e| { log::error!("Failed to initialize WayVR connection: {e:?}"); process::exit(1); }) .unwrap(), serial_generator: ipc::SerialGenerator::new(), pretty_print: args.pretty, }; let maybe_err = if let Subcommands::Batch { fail_fast } = args.command { run_batch(&mut state, fail_fast).await } else { run_once(&mut state, args).await }; if let Err(e) = maybe_err { log::error!("{e:?}"); return ExitCode::FAILURE; } else { std::thread::sleep(Duration::from_millis(20)); } ExitCode::SUCCESS } async fn run_batch(state: &mut WayVRClientState, fail_fast: bool) -> anyhow::Result<()> { let stdin = std::io::stdin(); for (line_no, line) in stdin.lines().enumerate() { let line = line.context("error reading stdin")?; if line.trim().is_empty() || line.trim_start().starts_with('#') { continue; } if let Err(e) = parse_run_line(state, &line) .await .with_context(|| format!("error on line {}", line_no + 1)) { if fail_fast { return Err(e); } else { log::error!("{e:?}"); } } } Ok(()) } async fn parse_run_line(state: &mut WayVRClientState, line: &str) -> anyhow::Result<()> { let mut argv = shell_words::split(line).context("parse error")?; // clap expects argv[0] to be the binary name argv.insert(0, env!("CARGO_PKG_NAME").to_string()); let args = Args::try_parse_from(argv).context("invalid arguments")?; run_once(state, args).await?; Ok(()) } async fn run_once(state: &mut WayVRClientState, args: Args) -> anyhow::Result<()> { match args.command { Subcommands::Batch { .. } => { log::warn!("Ignoring recursive batch command"); } Subcommands::InputState => { wlx_input_state(state).await; } Subcommands::WindowList => { wvr_window_list(state).await; } Subcommands::WindowSetVisible { handle, visible_0_or_1, } => { let handle = serde_json::from_str(&handle).context("Invalid handle")?; wvr_window_set_visible(state, handle, visible_0_or_1 != 0).await; } Subcommands::ProcessGet { handle } => { let handle = serde_json::from_str(&handle).context("Invalid handle")?; wvr_process_get(state, handle).await; } Subcommands::ProcessList => { wvr_process_list(state).await; } Subcommands::ProcessTerminate { handle } => { let handle = serde_json::from_str(&handle).context("Invalid handle")?; wvr_process_terminate(state, handle).await; } Subcommands::ProcessLaunch { exec, name, env, resolution, pos, icon, args, } => { let env = env.split(",").map(|s| s.to_string()).collect::>(); let resolution = resolution .split_once('x') .and_then(|(x, y)| Some([x.parse::().ok()?, y.parse::().ok()?])) .context( "Invalid resolution format. Expecting x, for example: 1920x1080, 1280x720", )?; let pos_mode = match pos { PosModeEnum::Floating => PositionMode::Float, PosModeEnum::Static => PositionMode::Static, PosModeEnum::Anchored => PositionMode::Anchor, }; wvr_process_launch( state, exec, name, env, resolution, pos_mode, icon, args, HashMap::new(), ) .await; } Subcommands::Haptics { device, intensity, duration, frequency, } => { wlx_device_haptics(state, device, intensity, duration, frequency).await; } Subcommands::ShowHide {} => { wlx_show_hide(state).await; } Subcommands::PanelModify { overlay, element, command, } => { let command = match command { SubcommandPanelModify::SetText { text } => { packet_client::WlxModifyPanelCommand::SetText(text.join(" ")) } SubcommandPanelModify::SetColor { hex_color } => { packet_client::WlxModifyPanelCommand::SetColor(hex_color) } SubcommandPanelModify::SetImage { absolute_path } => { packet_client::WlxModifyPanelCommand::SetImage(absolute_path) } SubcommandPanelModify::SetVisible { visible_0_or_1 } => { packet_client::WlxModifyPanelCommand::SetVisible(visible_0_or_1 != 0) } SubcommandPanelModify::SetStickyState { sticky_state_0_or_1, } => packet_client::WlxModifyPanelCommand::SetStickyState(sticky_state_0_or_1 != 0), }; wlx_panel_modify(state, overlay, element, command).await; } Subcommands::SwitchSet { set_or_0: set } => { let set = if set <= 0 { None } else { Some((set - 1) as _) }; wlx_switch_set(state, set).await; } } Ok(()) } /// A command-line interface for WayVR IPC #[derive(clap::Parser, Debug)] #[command(version, about, long_about = None)] struct Args { /// The command to run #[command(subcommand)] command: Subcommands, /// Pretty-print JSON output #[arg(short, long)] pretty: bool, } #[derive(clap::Parser, Debug)] enum Subcommands { /// Read commands from stdout, one per line. Batch { /// Stop on the first error #[arg(short, long)] fail_fast: bool, }, /// Get the positions of HMD & controllers InputState, /// List WayVR windows WindowList, /// Delete a WayVR display // DisplaySetLayout skipped /// Change the visibility of a window on a WayVR display WindowSetVisible { /// A JSON window handle returned by DisplayWindowList handle: String, visible_0_or_1: u8, }, /// Retrieve information about a WayVR-managed process ProcessGet { /// A JSON process handle returned by ProcessList or ProcessLaunch handle: String, }, /// List all processes managed by WayVR ProcessList, /// Terminate a WayVR-managed process ProcessTerminate { /// A JSON process handle returned by ProcessList or ProcessLaunch handle: String, }, /// Launch a new process inside WayVR ProcessLaunch { /// Name for the overlay #[arg(short, long, default_value = "")] name: String, /// Enviroment variables, separated by comma #[arg(short, long, default_value = "")] env: String, /// Executable to run exec: String, #[arg(default_value = "1920x1080")] resolution: String, /// Default positioning pos: PosModeEnum, /// Absolute path to the app icon icon: Option, /// Arguments to pass to executable #[arg(default_value = "")] args: String, }, /// Trigger haptics on the user's controller Haptics { /// 0 for left, 1 for right controller device: usize, #[arg(short, long, default_value = "0.25")] intensity: f32, #[arg(short, long, default_value = "0.1")] duration: f32, #[arg(short, long, default_value = "0.1")] frequency: f32, }, /// Toggle overlay show or hide ShowHide, /// Apply a modification to a panel element PanelModify { /// The name of the overlay (XML file name without extension) overlay: String, /// The id of the element to modify, as set in the XML element: String, /// Command to execute #[command(subcommand)] command: SubcommandPanelModify, }, SwitchSet { /// Set number to switch to, 0 to hide all sets set_or_0: usize, }, } #[derive(Debug, Clone, Copy, clap::ValueEnum)] enum PosModeEnum { Floating, Anchored, Static, } #[derive(clap::Parser, Debug)] enum SubcommandPanelModify { /// Set the text of a