wayvrctl + panel-modify ipc

This commit is contained in:
galister
2025-12-23 18:20:27 +09:00
parent 5287b659e8
commit f038ad8a87
19 changed files with 697 additions and 59 deletions

246
wayvrctl/src/helper.rs Normal file
View File

@@ -0,0 +1,246 @@
use std::{collections::HashMap};
use anyhow::Context;
use serde::Serialize;
use wayvr_ipc::{client::{WayVRClient, WayVRClientMutex}, ipc, packet_client, packet_server};
pub struct WayVRClientState {
pub wayvr_client: WayVRClientMutex,
pub serial_generator: ipc::SerialGenerator,
pub pretty_print: bool,
}
fn handle_empty_result(result: anyhow::Result<()>) {
if let Err(e) = result {
log::error!("{e:?}");
}
}
fn handle_result<T: Serialize>(pretty_print: bool, result: anyhow::Result<T>) {
match result {
Ok(t) => {
let maybe_json = if pretty_print {
serde_json::to_string_pretty(&t)
} else {
serde_json::to_string(&t)
};
match maybe_json {
Ok(json_string) => println!("{}", json_string),
Err(e) => log::error!("Failed to serialize JSON: {e:?}")
}
}
Err(e) => log::error!("{e:?}")
}
}
pub async fn wvr_display_create(
state: &mut WayVRClientState,
width: u16,
height: u16,
name: String,
scale: Option<f32>,
attach_to: packet_client::AttachTo,
) {
handle_result(state.pretty_print,
WayVRClient::fn_wvr_display_create(
state.wayvr_client.clone(),
state.serial_generator.increment_get(),
packet_client::WvrDisplayCreateParams {
width,
height,
name,
scale,
attach_to,
},
)
.await.context("failed to create display")
);
}
pub async fn wvr_display_list(
state: &mut WayVRClientState,
) {
handle_result(state.pretty_print,
WayVRClient::fn_wvr_display_list(
state.wayvr_client.clone(),
state.serial_generator.increment_get(),
)
.await.context("failed to fetch displays")
);
}
pub async fn wvr_display_get(
state: &mut WayVRClientState,
handle: packet_server::WvrDisplayHandle,
) {
handle_result(state.pretty_print,
WayVRClient::fn_wvr_display_get(
state.wayvr_client.clone(),
state.serial_generator.increment_get(),
handle,
)
.await.context("failed to fetch display")
);
}
pub async fn wvr_display_window_list(
state: &mut WayVRClientState,
handle: packet_server::WvrDisplayHandle,
) {
handle_result(state.pretty_print,
WayVRClient::fn_wvr_display_window_list(
state.wayvr_client.clone(),
state.serial_generator.increment_get(),
handle,
)
.await.context("failed to list window displays")
);
}
pub async fn wvr_display_remove(
state: &mut WayVRClientState,
handle: packet_server::WvrDisplayHandle,
) {
handle_result(state.pretty_print,
WayVRClient::fn_wvr_display_remove(
state.wayvr_client.clone(),
state.serial_generator.increment_get(),
handle,
)
.await.context("failed to remove display")
);
}
pub async fn wvr_display_set_visible(
state: &mut WayVRClientState,
handle: packet_server::WvrDisplayHandle,
visible: bool,
) {
handle_empty_result(
WayVRClient::fn_wvr_display_set_visible(state.wayvr_client.clone(), handle, visible).await.context("failed to set display visibility"),
)
}
pub async fn wvr_window_set_visible(
state: &mut WayVRClientState,
handle: packet_server::WvrWindowHandle,
visible: bool,
) {
handle_empty_result(
WayVRClient::fn_wvr_window_set_visible(state.wayvr_client.clone(), handle, visible).await.context("failed to set window visibility"),
)
}
pub async fn wvr_process_get(
state: &mut WayVRClientState,
handle: packet_server::WvrProcessHandle,
) {
handle_result(state.pretty_print,
WayVRClient::fn_wvr_process_get(
state.wayvr_client.clone(),
state.serial_generator.increment_get(),
handle,
)
.await.context("failed to get process"),
);
}
pub async fn wvr_process_list(
state: &mut WayVRClientState,
) {
handle_result(state.pretty_print,
WayVRClient::fn_wvr_process_list(
state.wayvr_client.clone(),
state.serial_generator.increment_get(),
)
.await.context("failed to list processes"),
)
}
pub async fn wvr_process_terminate(
state: &mut WayVRClientState,
handle: packet_server::WvrProcessHandle,
) {
handle_empty_result(
WayVRClient::fn_wvr_process_terminate(state.wayvr_client.clone(), handle).await.context("failed to terminate process"),
)
}
pub async fn wvr_process_launch(
state: &mut WayVRClientState,
exec: String,
name: String,
env: Vec<String>,
target_display: packet_server::WvrDisplayHandle,
args: String,
userdata: HashMap<String, String>,
) {
handle_result(state.pretty_print,
WayVRClient::fn_wvr_process_launch(
state.wayvr_client.clone(),
state.serial_generator.increment_get(),
packet_client::WvrProcessLaunchParams {
env,
exec,
name,
target_display,
args,
userdata,
},
)
.await.context("failed to launch process"),
)
}
pub async fn wlx_haptics(
state: &mut WayVRClientState,
intensity: f32,
duration: f32,
frequency: f32,
) {
handle_empty_result(
WayVRClient::fn_wlx_haptics(
state.wayvr_client.clone(),
packet_client::WlxHapticsParams {
intensity,
duration,
frequency,
},
)
.await.context("failed to trigger haptics"),
)
}
pub async fn wlx_panel_modify(
state: &mut WayVRClientState,
overlay: String,
element: String,
command: packet_client::WlxModifyPanelCommand,
) {
handle_empty_result(
WayVRClient::fn_wlx_modify_panel(
state.wayvr_client.clone(),
packet_client::WlxModifyPanelParams {
overlay,
element,
command,
},
)
.await.context("failed to modify panel"),
)
}
pub async fn wlx_input_state(
state: &mut WayVRClientState,
) {
handle_result(state.pretty_print,
WayVRClient::fn_wlx_input_state(
state.wayvr_client.clone(),
state.serial_generator.increment_get(),
)
.await.context("failed to get input state"),
)
}

279
wayvrctl/src/main.rs Normal file
View File

@@ -0,0 +1,279 @@
use std::{collections::HashMap, process::{self, ExitCode}};
use anyhow::Context;
use clap::Parser;
use env_logger::Env;
use wayvr_ipc::{client::WayVRClient, ipc, packet_client, };
use crate::helper::{wlx_haptics, wlx_input_state, wlx_panel_modify, wvr_display_create, wvr_display_get, wvr_display_list, wvr_display_remove, wvr_display_set_visible, wvr_display_window_list, wvr_process_get, wvr_process_launch, wvr_process_list, wvr_process_terminate, wvr_window_set_visible, WayVRClientState};
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;
}
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)
.with_context(|| format!("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).with_context(|| format!("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::DisplayCreate { width, height, name, scale } => {
wvr_display_create(state, width, height, name, scale, packet_client::AttachTo::None).await;
}
Subcommands::DisplayList => {
wvr_display_list(state).await;
}
Subcommands::DisplayGet { handle } => {
let handle = serde_json::from_str(&handle).context("Invalid handle")?;
wvr_display_get(state, handle).await;
}
Subcommands::DisplayWindowList { handle } => {
let handle = serde_json::from_str(&handle).context("Invalid handle")?;
wvr_display_window_list(state, handle).await;
}
Subcommands::DisplayRemove { handle } => {
let handle = serde_json::from_str(&handle).context("Invalid handle")?;
wvr_display_remove(state, handle).await;
}
Subcommands::DisplaySetVisible { handle, visible_0_or_1 } => {
let handle = serde_json::from_str(&handle).context("Invalid handle")?;
wvr_display_set_visible(state, handle, visible_0_or_1 != 0).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, target_display, args } => {
let handle = serde_json::from_str(&target_display).context("Invalid target_display")?;
wvr_process_launch(state, exec, name, env, handle, args, HashMap::new()).await;
}
Subcommands::Haptics { intensity, duration, frequency } => {
wlx_haptics(state, intensity, duration, frequency).await;
}
Subcommands::PanelModify { overlay, element, command } => {
let command = match command {
SubcommandPanelModify::SetText { text } => packet_client::WlxModifyPanelCommand::SetText(text),
SubcommandPanelModify::SetColor { hex_color } => packet_client::WlxModifyPanelCommand::SetColor(hex_color),
SubcommandPanelModify::SetSprite { absolute_path } => packet_client::WlxModifyPanelCommand::SetSprite(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;
}
}
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,
/// Create a new WayVR display
DisplayCreate{
width: u16,
height: u16,
name: String,
#[arg(short, long)]
scale: Option<f32>,
//attach_to: packet_client::AttachTo,
},
/// List WayVR displays
DisplayList,
/// Retrieve information about a single WayVR display
DisplayGet {
/// A display handle JSON returned by DisplayList or DisplayCreate
handle: String,
},
/// List windows attached to a WayVR display
DisplayWindowList {
/// A display handle JSON returned by DisplayList or DisplayCreate
handle: String,
},
/// Delete a WayVR display
DisplayRemove {
/// A display handle JSON returned by DisplayList or DisplayCreate
handle: String,
},
/// Change the visibility of a WayVR display
DisplaySetVisible {
/// A display handle JSON returned by DisplayList or DisplayCreate
handle: String,
visible_0_or_1: u8,
},
// 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 {
exec: String,
name: String,
env: Vec<String>,
/// A display handle JSON returned by DisplayList or DisplayCreate
target_display: String,
args: String,
},
/// Trigger haptics on the user's controller
Haptics {
#[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,
},
/// 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,
}
}
#[derive(clap::Parser, Debug)]
enum SubcommandPanelModify {
/// Set the text of a <label> or <Button>
SetText {
/// Text that needs to be set
text: String,
},
/// Set the color of a <rectangle> or <label> or monochrome <sprite>
SetColor {
/// Color in HTML hex format (#rrggbb or #rrggbbaa)
hex_color: String,
},
/// Set the content of a sprite
SetSprite {
/// Absolute path to a svg, gif, png, jpeg or webp image.
absolute_path: String,
},
/// Set the visibility of a <div>, <rectangle>, <label> or <sprite>
SetVisible {
visible_0_or_1: u8,
},
/// Set the sticky state of a <Button>. Intended for buttons without `sticky="1"`.
SetStickyState {
sticky_state_0_or_1: u8,
}
}