From 9c06198c12521ab5b6d3d5837e7e2bb81018579b Mon Sep 17 00:00:00 2001
From: galister <22305755+galister@users.noreply.github.com>
Date: Fri, 12 Dec 2025 20:30:44 +0900
Subject: [PATCH] custom labels & buttons
---
wlx-overlay-s/src/gui/README.md | 88 +++++++++++---
wlx-overlay-s/src/gui/panel/button.rs | 168 +++++++++++++++++---------
wlx-overlay-s/src/gui/panel/helper.rs | 115 ++++++++++++++----
wlx-overlay-s/src/gui/panel/label.rs | 141 ++++++++-------------
wlx-overlay-s/src/gui/panel/mod.rs | 14 +--
wlx-overlay-s/src/subsystem/osc.rs | 34 ++++--
6 files changed, 356 insertions(+), 204 deletions(-)
diff --git a/wlx-overlay-s/src/gui/README.md b/wlx-overlay-s/src/gui/README.md
index cde6cc1..5f9717a 100644
--- a/wlx-overlay-s/src/gui/README.md
+++ b/wlx-overlay-s/src/gui/README.md
@@ -1,12 +1,12 @@
# WayVR GUI Customization
-Place custom XML files under ~/.config/wayvr/gui
+When customizing the watch, keyboard, dashboard, etc; place custom XML files under ~/.config/wlxoverlay/theme/gui
## Custom timezones, 12 vs 24-hour clock
These are not done via the GUI system, but via the regular config.
-Create `~/.config/wayvr/conf.d/clock.yaml` as such:
+Create `~/.config/wlxoverlay/conf.d/clock.yaml` as such:
```yaml
timezones:
@@ -28,7 +28,9 @@ There is usually no need to specify your own local timezone in here; omitting `_
#### Clock label
-Clock labels are driven by the current time. Available display values are: `name` (timezone name), `time`, `date`, `dow`
+Clock labels are driven by the current time, adhering to the user's 12/24 hour setting as well as timezone settings.
+
+Available display values are: `name` (timezone name), `time`, `date`, `dow`
See the Custom Timezones section for more info on timezones. Skip `_timezone` to use local time.
@@ -40,10 +42,9 @@ See the Custom Timezones section for more info on timezones. Skip `_timezone` to
Fifo label creates a fifo on your system that other programs can pipe output into.
-- The label will look for the last line that has a trailing `\n` and display it as its text.
-- The pipe is only actively read while the HMD is active.
- - If the producer fills up the pipe buffer before the headset is activated, a SIGPIPE will be sent to the producer, which shall be handled gracefully.
-- If the pipe breaks for any reason, re-creation is attempted after 15 seconds.
+- The label will look for the last complete line to use as its text.
+- If the pipe breaks due to an IO error, re-creation is attempted after 15 seconds.
+- `_path` supports environment variables, but not `~`!
```xml
@@ -59,11 +60,11 @@ for i in {0..99}; do echo "i is $i" > $XDG_RUNTIME_DIR/my-test-label; sleep 1; d
This label executes a shell script using the `sh` shell.
- Write lines to the script's stdout to update the label text.
-- The label will look for the last line that has a trailing `\n` and display it as its text.
-- Long-running scripts are allowed, but the stdout buffer is only read from while the headset is active.
- - As a consequence, the buffer may fill up during very long periods of inactivity, hanging the script due to IO wait until the headset is activated.
-- If the script exits successfully (code 0), it will be re-ran on the next frame.
+- The label will look for the last complete line to use as its text.
+- Long-running scripts are allowed, but the label is only updated while the HMD is active.
+- If the script exits successfully (code 0), it will be re-ran on the next frame. Otherwise, it will be re-ran in 15s.
- Control the pacing from inside the script itself. For example, adding a sleep 5 will make the script execute at most once per 5 seconds.
+- `_exec` supports everything that `sh` supports!
```xml
@@ -94,17 +95,70 @@ Format: `ipd`
### Buttons
-Buttons consist of a label component and and one or more actions to handle press or release events.
-
-If a shell-type press/release event's script writes to stdout, the last line of stdout will be set as the label text.
-
-Long-running processes are allowed, but a new execution will not be triggered until the previous process has exited.
+Buttons consist of a label component and one or more actions to handle press and/or release events.
Note: As of WlxOverlay 25.10, we no longer support events based on laser color, as this was bad practice accessibility-wise.
Supported events:
```xml
-
+
```
+#### Supported button actions
+
+##### `::ShellExec [args ..]`
+
+This button action executes a shell script using the `sh` shell.
+
+- Long-running processes are allowed, but a new execution will not be triggered until the previous process has exited.
+- If triggered again while the previous process is still running, SIGUSR1 will be sent to that child process.
+
+```xml
+
+```
+
+###### Update the button's label from stdout
+
+```xml
+
+```
+
+- Write lines to the script's stdout to update the label text.
+- The label will look for the last complete line to use as its text.
+- Long-running scripts are allowed, but the label is only updated while the HMD is active.
+
+##### `::OscSend `
+
+Send an OSC message. The target port comes from the `osc_out_port` configuration setting.
+
+```xml
+
+```
+
+Available argument value types (case insensitive):
+- Bool: `true` or `false`
+- Nil: `nil`
+- Inf: `inf`
+- Int: `-1i32`, `1i32`, etc
+- Long: `-1i64`, `1i64`, etc
+- Float: `1f32`, `1.0f32`, etc
+- Double: `1f64`, `1.0f64`, etc
+
+##### `::ShutDown`
+
+Gracefully shuts down WlxOverlay-S. Useful when using an auto-restart script.
+
+##### `::PlayspaceReset`
+
+Resets the STAGE space to (0,0,0) with identity rotation.
+
+##### `::PlayspaceRecenter`
+
+Recenters the STAGE space position so that the HMD is in the center. Does not modify floor level.
+
+##### `::PlayspaceFixFloor`
+
+Adjusts the level of floor for STAGE and LOCAL_FLOOR spaces.
+
+The user is asked to place one controller on the floor.
diff --git a/wlx-overlay-s/src/gui/panel/button.rs b/wlx-overlay-s/src/gui/panel/button.rs
index a62bb74..93565ce 100644
--- a/wlx-overlay-s/src/gui/panel/button.rs
+++ b/wlx-overlay-s/src/gui/panel/button.rs
@@ -1,13 +1,16 @@
use std::{
cell::RefCell,
- io::BufReader,
- process::{Child, ChildStdout},
+ process::{Command, Stdio},
+ rc::Rc,
sync::{atomic::Ordering, Arc},
time::{Duration, Instant},
};
+use anyhow::Context;
use wgui::{
+ components::button::ComponentButton,
event::{self, EventCallback, EventListenerKind},
+ i18n::Translation,
layout::Layout,
parser::CustomAttribsInfoOwned,
widget::EventResult,
@@ -16,6 +19,7 @@ use wlx_common::overlays::ToastTopic;
use crate::{
backend::task::{OverlayTask, PlayspaceTask, TaskType},
+ gui::panel::helper::PipeReaderThread,
overlays::{
mirror::{new_mirror, new_mirror_name},
toast::Toast,
@@ -28,8 +32,6 @@ use crate::{
#[cfg(feature = "wayvr")]
use crate::backend::wayvr::WayVRAction;
-use super::helper::read_label_from_pipe;
-
pub const BUTTON_EVENTS: [(&str, EventListenerKind); 2] = [
("_press", EventListenerKind::MousePress),
("_release", EventListenerKind::MouseRelease),
@@ -39,6 +41,7 @@ pub(super) fn setup_custom_button(
layout: &mut Layout,
attribs: &CustomAttribsInfoOwned,
_app: &AppState,
+ button: Rc,
) {
for (name, kind) in &BUTTON_EVENTS {
let Some(action) = attribs.get_value(name) else {
@@ -137,8 +140,65 @@ pub(super) fn setup_custom_button(
RUNNING.store(false, Ordering::Relaxed);
Ok(EventResult::Consumed)
}),
- #[allow(clippy::match_same_arms)]
- "::OscSend" => return,
+ "::ShellExec" => {
+ let state = Arc::new(ShellButtonState {
+ button: button.clone(),
+ exec: args.fold(String::new(), |c, n| c + " " + n),
+ mut_state: RefCell::new(ShellButtonMutableState::default()),
+ carry_over: RefCell::new(None),
+ });
+
+ let piped = attribs.get_value("_update_label").is_some_and(|s| s == "1");
+
+ layout.add_event_listener::(
+ attribs.widget_id,
+ EventListenerKind::InternalStateChange,
+ Box::new({
+ let state = state.clone();
+ move |common, _data, _, _| {
+ shell_on_tick(&state, common, piped);
+ Ok(EventResult::Consumed)
+ }
+ }),
+ );
+
+ Box::new(move |_common, _data, _app, _| {
+ let _ = shell_on_action(&state).inspect_err(|e| log::error!("{e:?}"));
+ Ok(EventResult::Consumed)
+ })
+ }
+ #[cfg(feature = "osc")]
+ "::OscSend" => {
+ use crate::subsystem::osc::parse_osc_value;
+
+ let Some(address) = args.next().map(|s| s.to_string()) else {
+ log::error!("{command} has missing arguments");
+ return;
+ };
+
+ let mut osc_args = vec![];
+ for arg in args {
+ let Ok(osc_arg) = parse_osc_value(arg)
+ .inspect_err(|e| log::error!("Could not parse OSC value '{arg}': {e:?}"))
+ else {
+ return;
+ };
+ osc_args.push(osc_arg);
+ }
+
+ Box::new(move |_common, _data, app, _| {
+ let Some(sender) = app.osc_sender.as_mut() else {
+ log::error!("OscSend: sender is not available.");
+ return Ok(EventResult::Consumed);
+ };
+
+ let _ = sender
+ .send_message(address.clone(), osc_args.clone())
+ .inspect_err(|e| log::error!("OscSend: Could not send message: {e:?}"));
+
+ Ok(EventResult::Consumed)
+ })
+ }
// shell
_ => return,
};
@@ -147,64 +207,64 @@ pub(super) fn setup_custom_button(
log::debug!("Registered {action} on {:?} as {id:?}", attribs.widget_id);
}
}
+
+#[derive(Default)]
struct ShellButtonMutableState {
- child: Option,
- reader: Option>,
+ reader: Option,
+ pid: Option,
}
struct ShellButtonState {
+ button: Rc,
exec: String,
mut_state: RefCell,
carry_over: RefCell