custom labels & buttons

This commit is contained in:
galister
2025-12-12 20:30:44 +09:00
parent 97c11c6791
commit 9c06198c12
6 changed files with 356 additions and 204 deletions

View File

@@ -1,12 +1,12 @@
# WayVR GUI Customization # 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 ## Custom timezones, 12 vs 24-hour clock
These are not done via the GUI system, but via the regular config. 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 ```yaml
timezones: timezones:
@@ -28,7 +28,9 @@ There is usually no need to specify your own local timezone in here; omitting `_
#### Clock label #### 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. 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. 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 label will look for the last complete line to use as its text.
- The pipe is only actively read while the HMD is active. - If the pipe breaks due to an IO error, re-creation is attempted after 15 seconds.
- 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. - `_path` supports environment variables, but not `~`!
- If the pipe breaks for any reason, re-creation is attempted after 15 seconds.
```xml ```xml
<label _source="fifo" _path="$XDG_RUNTIME_DIR/my-test-label" [...] /> <label _source="fifo" _path="$XDG_RUNTIME_DIR/my-test-label" [...] />
@@ -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. This label executes a shell script using the `sh` shell.
- Write lines to the script's stdout to update the label text. - 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. - The label will look for the last complete line to use as its text.
- Long-running scripts are allowed, but the stdout buffer is only read from while the headset is active. - Long-running scripts are allowed, but the label is only updated while the HMD 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. Otherwise, it will be re-ran in 15s.
- If the script exits successfully (code 0), it will be re-ran on the next frame.
- 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. - 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 ```xml
<label _source="shell" _exec="$HOME/.local/bin/my-test-script.sh" [...] /> <label _source="shell" _exec="$HOME/.local/bin/my-test-script.sh" [...] />
@@ -94,17 +95,70 @@ Format: `ipd`
### Buttons ### Buttons
Buttons consist of a label component and and one or more actions to handle press or release events. Buttons consist of a label component and one or more actions to handle press and/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.
Note: As of WlxOverlay 25.10, we no longer support events based on laser color, as this was bad practice accessibility-wise. Note: As of WlxOverlay 25.10, we no longer support events based on laser color, as this was bad practice accessibility-wise.
Supported events: Supported events:
```xml ```xml
<button _press="" _release="" /> <button _press="..." _release="..." />
``` ```
#### Supported button actions
##### `::ShellExec <command> [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
<button _press="::ShellExec $HOME/myscript.sh test-argument" [...] />
```
###### Update the button's label from stdout
```xml
<button _press="::ShellExec $HOME/myscript.sh test-argument" _update_label="1" [...] />
```
- 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 <path> <args ..>`
Send an OSC message. The target port comes from the `osc_out_port` configuration setting.
```xml
<button _press="::OscSend /avatar/parameters/MyInt 1i32" [...] />
```
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.

View File

@@ -1,13 +1,16 @@
use std::{ use std::{
cell::RefCell, cell::RefCell,
io::BufReader, process::{Command, Stdio},
process::{Child, ChildStdout}, rc::Rc,
sync::{atomic::Ordering, Arc}, sync::{atomic::Ordering, Arc},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use anyhow::Context;
use wgui::{ use wgui::{
components::button::ComponentButton,
event::{self, EventCallback, EventListenerKind}, event::{self, EventCallback, EventListenerKind},
i18n::Translation,
layout::Layout, layout::Layout,
parser::CustomAttribsInfoOwned, parser::CustomAttribsInfoOwned,
widget::EventResult, widget::EventResult,
@@ -16,6 +19,7 @@ use wlx_common::overlays::ToastTopic;
use crate::{ use crate::{
backend::task::{OverlayTask, PlayspaceTask, TaskType}, backend::task::{OverlayTask, PlayspaceTask, TaskType},
gui::panel::helper::PipeReaderThread,
overlays::{ overlays::{
mirror::{new_mirror, new_mirror_name}, mirror::{new_mirror, new_mirror_name},
toast::Toast, toast::Toast,
@@ -28,8 +32,6 @@ use crate::{
#[cfg(feature = "wayvr")] #[cfg(feature = "wayvr")]
use crate::backend::wayvr::WayVRAction; use crate::backend::wayvr::WayVRAction;
use super::helper::read_label_from_pipe;
pub const BUTTON_EVENTS: [(&str, EventListenerKind); 2] = [ pub const BUTTON_EVENTS: [(&str, EventListenerKind); 2] = [
("_press", EventListenerKind::MousePress), ("_press", EventListenerKind::MousePress),
("_release", EventListenerKind::MouseRelease), ("_release", EventListenerKind::MouseRelease),
@@ -39,6 +41,7 @@ pub(super) fn setup_custom_button<S: 'static>(
layout: &mut Layout, layout: &mut Layout,
attribs: &CustomAttribsInfoOwned, attribs: &CustomAttribsInfoOwned,
_app: &AppState, _app: &AppState,
button: Rc<ComponentButton>,
) { ) {
for (name, kind) in &BUTTON_EVENTS { for (name, kind) in &BUTTON_EVENTS {
let Some(action) = attribs.get_value(name) else { let Some(action) = attribs.get_value(name) else {
@@ -137,8 +140,65 @@ pub(super) fn setup_custom_button<S: 'static>(
RUNNING.store(false, Ordering::Relaxed); RUNNING.store(false, Ordering::Relaxed);
Ok(EventResult::Consumed) Ok(EventResult::Consumed)
}), }),
#[allow(clippy::match_same_arms)] "::ShellExec" => {
"::OscSend" => return, 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::<AppState, S>(
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 // shell
_ => return, _ => return,
}; };
@@ -147,64 +207,64 @@ pub(super) fn setup_custom_button<S: 'static>(
log::debug!("Registered {action} on {:?} as {id:?}", attribs.widget_id); log::debug!("Registered {action} on {:?} as {id:?}", attribs.widget_id);
} }
} }
#[derive(Default)]
struct ShellButtonMutableState { struct ShellButtonMutableState {
child: Option<Child>, reader: Option<PipeReaderThread>,
reader: Option<BufReader<ChildStdout>>, pid: Option<u32>,
} }
struct ShellButtonState { struct ShellButtonState {
button: Rc<ComponentButton>,
exec: String, exec: String,
mut_state: RefCell<ShellButtonMutableState>, mut_state: RefCell<ShellButtonMutableState>,
carry_over: RefCell<Option<String>>, carry_over: RefCell<Option<String>>,
} }
// TODO fn shell_on_action(state: &ShellButtonState) -> anyhow::Result<()> {
#[allow(clippy::missing_const_for_fn)]
fn shell_on_action(
_state: &ShellButtonState,
_common: &mut event::CallbackDataCommon,
_data: &mut event::CallbackData,
) {
//let mut mut_state = state.mut_state.borrow_mut();
}
fn shell_on_tick(
state: &ShellButtonState,
_common: &mut event::CallbackDataCommon,
_data: &mut event::CallbackData,
) {
let mut mut_state = state.mut_state.borrow_mut(); let mut mut_state = state.mut_state.borrow_mut();
if let Some(mut child) = mut_state.child.take() { if mut_state.reader.as_ref().is_some_and(|r| !r.is_finished())
match child.try_wait() { && let Some(pid) = mut_state.pid.as_ref()
// not exited yet {
Ok(None) => { log::info!("ShellExec triggered while child is still running; sending SIGUSR1");
if let Some(_text) = mut_state.reader.as_mut().and_then(|r| { let _ = Command::new("kill")
read_label_from_pipe("child process", r, &mut state.carry_over.borrow_mut()) .arg("-s")
}) { .arg("USR1")
//TODO update label .arg(pid.to_string())
.spawn()
.unwrap()
.wait();
return Ok(());
} }
mut_state.child = Some(child);
} let child = Command::new("sh")
// exited successfully .arg("-c")
Ok(Some(code)) if code.success() => { .arg(&state.exec)
if let Some(_text) = mut_state.reader.as_mut().and_then(|r| { .stdout(Stdio::piped())
read_label_from_pipe("child process", r, &mut state.carry_over.borrow_mut()) .spawn()
}) { .with_context(|| format!("Failed to run shell script: '{}'", &state.exec))?;
//TODO update label
} mut_state.pid = Some(child.id());
mut_state.child = None; mut_state.reader = Some(PipeReaderThread::new_from_child(child));
}
// exited with failure return Ok(());
Ok(Some(code)) => {
mut_state.child = None;
log::warn!("Label process exited with code {code}");
}
// lost
Err(_) => {
mut_state.child = None;
log::warn!("Label child process lost.");
} }
fn shell_on_tick(state: &ShellButtonState, common: &mut event::CallbackDataCommon, piped: bool) {
let mut mut_state = state.mut_state.borrow_mut();
let Some(reader) = mut_state.reader.as_mut() else {
return;
};
if piped && let Some(text) = reader.get_last_line() {
state
.button
.set_text(common, Translation::from_raw_text(&text));
} }
if reader.is_finished() {
mut_state.reader = None;
} }
} }

View File

@@ -1,7 +1,13 @@
use regex::Regex; use regex::Regex;
use std::{ use std::{
fs,
io::{BufRead, BufReader, Read}, io::{BufRead, BufReader, Read},
sync::LazyLock, process::Child,
sync::{
mpsc::{self, Receiver},
Arc, LazyLock,
},
thread::JoinHandle,
}; };
static ENV_VAR_REGEX: LazyLock<Regex> = LazyLock::new(|| { static ENV_VAR_REGEX: LazyLock<Regex> = LazyLock::new(|| {
@@ -19,31 +25,94 @@ pub(super) fn expand_env_vars(template: &str) -> String {
.into_owned() .into_owned()
} }
pub(super) fn read_label_from_pipe<R>( pub(super) struct PipeReaderThread {
path: &str, receiver: Receiver<String>,
reader: &mut BufReader<R>, handle: JoinHandle<bool>,
carry_over: &mut Option<String>, }
) -> Option<String>
where
R: Read + Sized,
{
let mut prev = String::new();
let mut cur = String::new();
for r in reader.lines() { impl PipeReaderThread {
match r { pub fn new_from_child(mut c: Child) -> Self {
Ok(line) => { const BUF_LEN: usize = 128;
prev = cur; let (sender, receiver) = mpsc::sync_channel::<String>(4);
cur = carry_over.take().map(|s| s + &line).unwrap_or(line);
let handle = std::thread::spawn({
move || {
let stdout = c.stdout.take().unwrap();
let mut reader = BufReader::new(stdout).take(BUF_LEN as _);
loop {
let mut buf = String::with_capacity(BUF_LEN);
match reader.read_line(&mut buf) {
Ok(0) => {
// EOF reached
break;
}
Ok(_) => {
let _ = sender.try_send(buf);
} }
Err(e) => { Err(e) => {
log::warn!("pipe read error on {path}: {e:?}"); log::error!("Error reading pipe: {e:?}");
return None; break;
}
}
}
c.wait()
.inspect_err(|e| log::error!("Failed to wait for child process: {e:?}"))
.map_or(false, |c| c.success())
}
});
Self { receiver, handle }
}
pub fn new_from_fifo(path: Arc<str>) -> Self {
const BUF_LEN: usize = 128;
let (sender, receiver) = mpsc::sync_channel::<String>(4);
let handle = std::thread::spawn({
move || {
let Ok(mut reader) = fs::File::open(&*path)
.inspect_err(|e| {
log::warn!("Failed to open fifo: {e:?}");
})
.map(|r| BufReader::new(r).take(BUF_LEN as _))
else {
return false;
};
loop {
let mut buf = String::with_capacity(BUF_LEN);
match reader.read_line(&mut buf) {
Ok(0) => {
// EOF reached
break;
}
Ok(_) => {
let _ = sender.try_send(buf);
}
Err(e) => {
log::error!("Error reading fifo: {e:?}");
break;
} }
} }
} }
carry_over.replace(cur); true
}
if prev.is_empty() { None } else { Some(prev) } });
Self { receiver, handle }
}
pub fn get_last_line(&mut self) -> Option<String> {
self.receiver.try_iter().last()
}
pub fn is_finished(&self) -> bool {
self.handle.is_finished()
}
pub fn is_success(self) -> bool {
self.handle.join().unwrap_or(false)
}
} }

View File

@@ -1,12 +1,14 @@
use std::{ use std::{
cell::RefCell, cell::RefCell,
fs, io, fs,
os::unix::fs::FileTypeExt, os::unix::fs::FileTypeExt,
process::{Child, ChildStdout, Command, Stdio}, process::{Command, Stdio},
rc::Rc, rc::Rc,
sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use anyhow::Context;
use chrono::Local; use chrono::Local;
use chrono_tz::Tz; use chrono_tz::Tz;
use interprocess::os::unix::fifo_file::create_fifo; use interprocess::os::unix::fifo_file::create_fifo;
@@ -15,13 +17,13 @@ use wgui::{
event::{self, EventCallback}, event::{self, EventCallback},
i18n::Translation, i18n::Translation,
layout::Layout, layout::Layout,
parser::{CustomAttribsInfoOwned, parse_color_hex}, parser::{parse_color_hex, CustomAttribsInfoOwned},
widget::{EventResult, label::WidgetLabel}, widget::{label::WidgetLabel, EventResult},
}; };
use crate::state::AppState; use crate::{gui::panel::helper::PipeReaderThread, state::AppState};
use super::helper::{expand_env_vars, read_label_from_pipe}; use super::helper::expand_env_vars;
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub(super) fn setup_custom_label<S: 'static>( pub(super) fn setup_custom_label<S: 'static>(
@@ -42,15 +44,14 @@ pub(super) fn setup_custom_label<S: 'static>(
}; };
let state = ShellLabelState { let state = ShellLabelState {
exec: exec.to_string(), exec: exec.to_string(),
mut_state: RefCell::new(ShellLabelMutableState { mut_state: RefCell::new(PipeLabelMutableState {
child: None,
reader: None, reader: None,
next_try: Instant::now(), next_try: Instant::now(),
}), }),
carry_over: RefCell::new(None), carry_over: RefCell::new(None),
}; };
Box::new(move |common, data, _, _| { Box::new(move |common, data, _, _| {
shell_on_tick(&state, common, data); let _ = shell_on_tick(&state, common, data).inspect_err(|e| log::error!("{e:?}"));
Ok(EventResult::Pass) Ok(EventResult::Pass)
}) })
} }
@@ -60,15 +61,15 @@ pub(super) fn setup_custom_label<S: 'static>(
return; return;
}; };
let state = FifoLabelState { let state = FifoLabelState {
path: expand_env_vars(path), path: expand_env_vars(path).into(),
carry_over: RefCell::new(None), carry_over: RefCell::new(None),
mut_state: RefCell::new(FifoLabelMutableState { mut_state: RefCell::new(PipeLabelMutableState {
reader: None, reader: None,
next_try: Instant::now(), next_try: Instant::now(),
}), }),
}; };
Box::new(move |common, data, _, _| { Box::new(move |common, data, _, _| {
pipe_on_tick(&state, common, data); fifo_on_tick(&state, common, data);
Ok(EventResult::Pass) Ok(EventResult::Pass)
}) })
} }
@@ -185,103 +186,64 @@ pub(super) fn setup_custom_label<S: 'static>(
); );
} }
struct ShellLabelMutableState { struct PipeLabelMutableState {
child: Option<Child>, reader: Option<PipeReaderThread>,
reader: Option<io::BufReader<ChildStdout>>,
next_try: Instant, next_try: Instant,
} }
struct ShellLabelState { struct ShellLabelState {
exec: String, exec: String,
mut_state: RefCell<ShellLabelMutableState>, mut_state: RefCell<PipeLabelMutableState>,
carry_over: RefCell<Option<String>>, carry_over: RefCell<Option<String>>,
} }
#[allow(clippy::redundant_else)]
fn shell_on_tick( fn shell_on_tick(
state: &ShellLabelState, state: &ShellLabelState,
common: &mut event::CallbackDataCommon, common: &mut event::CallbackDataCommon,
data: &mut event::CallbackData, data: &mut event::CallbackData,
) { ) -> anyhow::Result<()> {
let mut mut_state = state.mut_state.borrow_mut(); let mut mut_state = state.mut_state.borrow_mut();
if let Some(mut child) = mut_state.child.take() { if let Some(reader) = mut_state.reader.as_mut() {
match child.try_wait() { if let Some(text) = reader.get_last_line() {
// not exited yet
Ok(None) => {
if let Some(text) = mut_state.reader.as_mut().and_then(|r| {
read_label_from_pipe("child process", r, &mut state.carry_over.borrow_mut())
}) {
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap(); let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
label.set_text(common, Translation::from_raw_text(&text)); label.set_text(common, Translation::from_raw_text(&text));
} }
mut_state.child = Some(child);
return;
}
// exited successfully
Ok(Some(code)) if code.success() => {
if let Some(text) = mut_state.reader.as_mut().and_then(|r| {
read_label_from_pipe("child process", r, &mut state.carry_over.borrow_mut())
}) {
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
label.set_text(common, Translation::from_raw_text(&text));
}
mut_state.child = None;
return;
}
// exited with failure
Ok(Some(code)) => {
mut_state.child = None;
mut_state.next_try = Instant::now() + Duration::from_secs(15);
log::warn!("Label process exited with code {code}");
return;
}
// lost
Err(_) => {
mut_state.child = None;
mut_state.next_try = Instant::now() + Duration::from_secs(15);
log::warn!("Label child process lost.");
return;
}
}
} else if mut_state.next_try > Instant::now() {
return;
}
match Command::new("sh") if reader.is_finished() {
if !mut_state.reader.take().unwrap().is_success() {
mut_state.next_try = Instant::now() + Duration::from_secs(15);
}
}
return Ok(());
} else if mut_state.next_try > Instant::now() {
return Ok(());
}
let child = Command::new("sh")
.arg("-c") .arg("-c")
.arg(&state.exec) .arg(&state.exec)
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.spawn() .spawn()
{ .with_context(|| format!("Failed to run shell script: '{}'", &state.exec))?;
Ok(mut child) => {
let stdout = child.stdout.take().unwrap();
mut_state.child = Some(child);
mut_state.reader = Some(io::BufReader::new(stdout));
}
Err(e) => {
log::warn!("Failed to run shell script '{}': {e:?}", &state.exec);
}
}
}
struct FifoLabelMutableState { mut_state.reader = Some(PipeReaderThread::new_from_child(child));
reader: Option<io::BufReader<fs::File>>,
next_try: Instant, return Ok(());
} }
struct FifoLabelState { struct FifoLabelState {
path: String, path: Arc<str>,
mut_state: RefCell<FifoLabelMutableState>, mut_state: RefCell<PipeLabelMutableState>,
carry_over: RefCell<Option<String>>, carry_over: RefCell<Option<String>>,
} }
impl FifoLabelState { impl FifoLabelState {
fn try_remove_fifo(&self) -> anyhow::Result<()> { fn try_remove_fifo(&self) -> anyhow::Result<()> {
let meta = match fs::metadata(&self.path) { let meta = match fs::metadata(&*self.path) {
Ok(meta) => meta, Ok(meta) => meta,
Err(e) => { Err(e) => {
if fs::exists(&self.path).unwrap_or(true) { if fs::exists(&*self.path).unwrap_or(true) {
anyhow::bail!("Could not stat existing file at {}: {e:?}", &self.path); anyhow::bail!("Could not stat existing file at {}: {e:?}", &self.path);
} }
return Ok(()); return Ok(());
@@ -292,7 +254,7 @@ impl FifoLabelState {
anyhow::bail!("Existing file at {} is not a FIFO", &self.path); anyhow::bail!("Existing file at {} is not a FIFO", &self.path);
} }
if let Err(e) = fs::remove_file(&self.path) { if let Err(e) = fs::remove_file(&*self.path) {
anyhow::bail!("Unable to remove existing FIFO at {}: {e:?}", &self.path); anyhow::bail!("Unable to remove existing FIFO at {}: {e:?}", &self.path);
} }
@@ -308,16 +270,14 @@ impl Drop for FifoLabelState {
} }
} }
fn pipe_on_tick( fn fifo_on_tick(
state: &FifoLabelState, state: &FifoLabelState,
common: &mut event::CallbackDataCommon, common: &mut event::CallbackDataCommon,
data: &mut event::CallbackData, data: &mut event::CallbackData,
) { ) {
let mut mut_state = state.mut_state.borrow_mut(); let mut mut_state = state.mut_state.borrow_mut();
let reader = if let Some(f) = mut_state.reader.as_mut() { let Some(reader) = mut_state.reader.as_mut() else {
f
} else {
if mut_state.next_try > Instant::now() { if mut_state.next_try > Instant::now() {
return; return;
} }
@@ -328,29 +288,26 @@ fn pipe_on_tick(
return; return;
} }
if let Err(e) = create_fifo(&state.path, 0o777) { if let Err(e) = create_fifo(&*state.path, 0o777) {
mut_state.next_try = Instant::now() + Duration::from_secs(15); mut_state.next_try = Instant::now() + Duration::from_secs(15);
log::warn!("Failed to create FIFO: {e:?}"); log::warn!("Failed to create FIFO: {e:?}");
return; return;
} }
mut_state.reader = fs::File::open(&state.path) mut_state.reader = Some(PipeReaderThread::new_from_fifo(state.path.clone()));
.inspect_err(|e| { return;
log::warn!("Failed to open FIFO: {e:?}");
mut_state.next_try = Instant::now() + Duration::from_secs(15);
})
.map(io::BufReader::new)
.ok();
mut_state.reader.as_mut().unwrap()
}; };
if let Some(text) = if let Some(text) = reader.get_last_line() {
read_label_from_pipe(&state.path, reader, &mut state.carry_over.borrow_mut())
{
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap(); let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
label.set_text(common, Translation::from_raw_text(&text)); label.set_text(common, Translation::from_raw_text(&text));
} }
if reader.is_finished() {
if !mut_state.reader.take().unwrap().is_success() {
mut_state.next_try = Instant::now() + Duration::from_secs(15);
}
}
} }
const BAT_LOW: drawing::Color = drawing::Color::new(0.69, 0.38, 0.38, 1.); const BAT_LOW: drawing::Color = drawing::Color::new(0.69, 0.38, 0.38, 1.);

View File

@@ -5,6 +5,7 @@ use glam::{vec2, Affine2, Vec2};
use label::setup_custom_label; use label::setup_custom_label;
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::button::ComponentButton,
drawing, drawing,
event::{ event::{
Event as WguiEvent, EventCallback, EventListenerID, EventListenerKind, Event as WguiEvent, EventCallback, EventListenerID, EventListenerKind,
@@ -13,9 +14,9 @@ use wgui::{
}, },
gfx::cmd::WGfxClearMode, gfx::cmd::WGfxClearMode,
layout::{Layout, LayoutParams, WidgetID}, layout::{Layout, LayoutParams, WidgetID},
parser::{CustomAttribsInfoOwned, ParserState}, parser::{CustomAttribsInfoOwned, Fetchable, ParserState},
renderer_vk::context::Context as WguiContext, renderer_vk::context::Context as WguiContext,
widget::{label::WidgetLabel, rectangle::WidgetRectangle, EventResult}, widget::{label::WidgetLabel, EventResult},
}; };
use wlx_common::timestep::Timestep; use wlx_common::timestep::Timestep;
@@ -138,13 +139,10 @@ impl<S: 'static> GuiPanel<S> {
.is_some() .is_some()
{ {
setup_custom_label::<S>(&mut layout, elem, app); setup_custom_label::<S>(&mut layout, elem, app);
} else if layout } else if let Ok(button) =
.state parser_state.fetch_component_from_widget_id_as::<ComponentButton>(elem.widget_id)
.widgets
.get_as::<WidgetRectangle>(elem.widget_id)
.is_some()
{ {
setup_custom_button::<S>(&mut layout, elem, app); setup_custom_button::<S>(&mut layout, elem, app, button);
} }
if let Some(on_custom_attrib) = &params.on_custom_attrib { if let Some(on_custom_attrib) = &params.on_custom_attrib {

View File

@@ -130,9 +130,7 @@ impl OscSender {
let level = device.soc.unwrap_or(-1.0); let level = device.soc.unwrap_or(-1.0);
let parameter = match device.role { let parameter = match device.role {
TrackedDeviceRole::None => continue, TrackedDeviceRole::None => continue,
TrackedDeviceRole::Hmd => { TrackedDeviceRole::Hmd => "hmd",
"hmd"
}
TrackedDeviceRole::LeftHand => { TrackedDeviceRole::LeftHand => {
controller_count += 1; controller_count += 1;
controller_total_bat += level; controller_total_bat += level;
@@ -183,14 +181,30 @@ impl OscSender {
Ok(()) Ok(())
} }
}
pub fn send_single_param( pub fn parse_osc_value(s: &str) -> anyhow::Result<OscType> {
&mut self, let lower = s.to_lowercase();
parameter: String,
values: Vec<OscType>,
) -> anyhow::Result<()> {
self.send_message(parameter, values)?;
Ok(()) match lower.as_str() {
"true" => Ok(OscType::Bool(true)),
"false" => Ok(OscType::Bool(false)),
"inf" => Ok(OscType::Inf),
"nil" => Ok(OscType::Nil),
_ => {
if lower.len() > 3 {
let (num, suffix) = lower.split_at(lower.len() - 3);
match suffix {
"f32" => return Ok(OscType::Float(num.parse::<f32>()?)),
"f64" => return Ok(OscType::Double(num.parse::<f64>()?)),
"i32" => return Ok(OscType::Int(num.parse::<i32>()?)),
"i64" => return Ok(OscType::Long(num.parse::<i64>()?)),
_ => {}
}
}
anyhow::bail!("Unknown OSC type literal: {}", s)
}
} }
} }