custom labels & buttons
This commit is contained in:
@@ -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
|
||||
<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.
|
||||
|
||||
- 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
|
||||
<label _source="shell" _exec="$HOME/.local/bin/my-test-script.sh" [...] />
|
||||
@@ -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
|
||||
<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.
|
||||
|
||||
@@ -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<S: 'static>(
|
||||
layout: &mut Layout,
|
||||
attribs: &CustomAttribsInfoOwned,
|
||||
_app: &AppState,
|
||||
button: Rc<ComponentButton>,
|
||||
) {
|
||||
for (name, kind) in &BUTTON_EVENTS {
|
||||
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);
|
||||
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::<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
|
||||
_ => return,
|
||||
};
|
||||
@@ -147,64 +207,64 @@ pub(super) fn setup_custom_button<S: 'static>(
|
||||
log::debug!("Registered {action} on {:?} as {id:?}", attribs.widget_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ShellButtonMutableState {
|
||||
child: Option<Child>,
|
||||
reader: Option<BufReader<ChildStdout>>,
|
||||
reader: Option<PipeReaderThread>,
|
||||
pid: Option<u32>,
|
||||
}
|
||||
|
||||
struct ShellButtonState {
|
||||
button: Rc<ComponentButton>,
|
||||
exec: String,
|
||||
mut_state: RefCell<ShellButtonMutableState>,
|
||||
carry_over: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
// TODO
|
||||
#[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,
|
||||
) {
|
||||
fn shell_on_action(state: &ShellButtonState) -> anyhow::Result<()> {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
if let Some(mut child) = mut_state.child.take() {
|
||||
match child.try_wait() {
|
||||
// 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())
|
||||
}) {
|
||||
//TODO update label
|
||||
}
|
||||
mut_state.child = Some(child);
|
||||
}
|
||||
// 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())
|
||||
}) {
|
||||
//TODO update label
|
||||
}
|
||||
mut_state.child = None;
|
||||
}
|
||||
// exited with failure
|
||||
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.");
|
||||
}
|
||||
}
|
||||
if mut_state.reader.as_ref().is_some_and(|r| !r.is_finished())
|
||||
&& let Some(pid) = mut_state.pid.as_ref()
|
||||
{
|
||||
log::info!("ShellExec triggered while child is still running; sending SIGUSR1");
|
||||
let _ = Command::new("kill")
|
||||
.arg("-s")
|
||||
.arg("USR1")
|
||||
.arg(pid.to_string())
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let child = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&state.exec)
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to run shell script: '{}'", &state.exec))?;
|
||||
|
||||
mut_state.pid = Some(child.id());
|
||||
mut_state.reader = Some(PipeReaderThread::new_from_child(child));
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
fs,
|
||||
io::{BufRead, BufReader, Read},
|
||||
sync::LazyLock,
|
||||
process::Child,
|
||||
sync::{
|
||||
mpsc::{self, Receiver},
|
||||
Arc, LazyLock,
|
||||
},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
static ENV_VAR_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
@@ -19,31 +25,94 @@ pub(super) fn expand_env_vars(template: &str) -> String {
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
pub(super) fn read_label_from_pipe<R>(
|
||||
path: &str,
|
||||
reader: &mut BufReader<R>,
|
||||
carry_over: &mut Option<String>,
|
||||
) -> Option<String>
|
||||
where
|
||||
R: Read + Sized,
|
||||
{
|
||||
let mut prev = String::new();
|
||||
let mut cur = String::new();
|
||||
pub(super) struct PipeReaderThread {
|
||||
receiver: Receiver<String>,
|
||||
handle: JoinHandle<bool>,
|
||||
}
|
||||
|
||||
for r in reader.lines() {
|
||||
match r {
|
||||
Ok(line) => {
|
||||
prev = cur;
|
||||
cur = carry_over.take().map(|s| s + &line).unwrap_or(line);
|
||||
impl PipeReaderThread {
|
||||
pub fn new_from_child(mut c: Child) -> Self {
|
||||
const BUF_LEN: usize = 128;
|
||||
let (sender, receiver) = mpsc::sync_channel::<String>(4);
|
||||
|
||||
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) => {
|
||||
log::error!("Error reading pipe: {e:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
c.wait()
|
||||
.inspect_err(|e| log::error!("Failed to wait for child process: {e:?}"))
|
||||
.map_or(false, |c| c.success())
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("pipe read error on {path}: {e:?}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self { receiver, handle }
|
||||
}
|
||||
|
||||
carry_over.replace(cur);
|
||||
pub fn new_from_fifo(path: Arc<str>) -> Self {
|
||||
const BUF_LEN: usize = 128;
|
||||
let (sender, receiver) = mpsc::sync_channel::<String>(4);
|
||||
|
||||
if prev.is_empty() { None } else { Some(prev) }
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fs, io,
|
||||
fs,
|
||||
os::unix::fs::FileTypeExt,
|
||||
process::{Child, ChildStdout, Command, Stdio},
|
||||
process::{Command, Stdio},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use chrono::Local;
|
||||
use chrono_tz::Tz;
|
||||
use interprocess::os::unix::fifo_file::create_fifo;
|
||||
@@ -15,13 +17,13 @@ use wgui::{
|
||||
event::{self, EventCallback},
|
||||
i18n::Translation,
|
||||
layout::Layout,
|
||||
parser::{CustomAttribsInfoOwned, parse_color_hex},
|
||||
widget::{EventResult, label::WidgetLabel},
|
||||
parser::{parse_color_hex, CustomAttribsInfoOwned},
|
||||
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)]
|
||||
pub(super) fn setup_custom_label<S: 'static>(
|
||||
@@ -42,15 +44,14 @@ pub(super) fn setup_custom_label<S: 'static>(
|
||||
};
|
||||
let state = ShellLabelState {
|
||||
exec: exec.to_string(),
|
||||
mut_state: RefCell::new(ShellLabelMutableState {
|
||||
child: None,
|
||||
mut_state: RefCell::new(PipeLabelMutableState {
|
||||
reader: None,
|
||||
next_try: Instant::now(),
|
||||
}),
|
||||
carry_over: RefCell::new(None),
|
||||
};
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -60,15 +61,15 @@ pub(super) fn setup_custom_label<S: 'static>(
|
||||
return;
|
||||
};
|
||||
let state = FifoLabelState {
|
||||
path: expand_env_vars(path),
|
||||
path: expand_env_vars(path).into(),
|
||||
carry_over: RefCell::new(None),
|
||||
mut_state: RefCell::new(FifoLabelMutableState {
|
||||
mut_state: RefCell::new(PipeLabelMutableState {
|
||||
reader: None,
|
||||
next_try: Instant::now(),
|
||||
}),
|
||||
};
|
||||
Box::new(move |common, data, _, _| {
|
||||
pipe_on_tick(&state, common, data);
|
||||
fifo_on_tick(&state, common, data);
|
||||
Ok(EventResult::Pass)
|
||||
})
|
||||
}
|
||||
@@ -185,103 +186,64 @@ pub(super) fn setup_custom_label<S: 'static>(
|
||||
);
|
||||
}
|
||||
|
||||
struct ShellLabelMutableState {
|
||||
child: Option<Child>,
|
||||
reader: Option<io::BufReader<ChildStdout>>,
|
||||
struct PipeLabelMutableState {
|
||||
reader: Option<PipeReaderThread>,
|
||||
next_try: Instant,
|
||||
}
|
||||
|
||||
struct ShellLabelState {
|
||||
exec: String,
|
||||
mut_state: RefCell<ShellLabelMutableState>,
|
||||
mut_state: RefCell<PipeLabelMutableState>,
|
||||
carry_over: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::redundant_else)]
|
||||
fn shell_on_tick(
|
||||
state: &ShellLabelState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
) -> anyhow::Result<()> {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
if let Some(mut child) = mut_state.child.take() {
|
||||
match child.try_wait() {
|
||||
// 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();
|
||||
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;
|
||||
if let Some(reader) = mut_state.reader.as_mut() {
|
||||
if let Some(text) = reader.get_last_line() {
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
} else if mut_state.next_try > Instant::now() {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match Command::new("sh")
|
||||
let child = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&state.exec)
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
.with_context(|| format!("Failed to run shell script: '{}'", &state.exec))?;
|
||||
|
||||
struct FifoLabelMutableState {
|
||||
reader: Option<io::BufReader<fs::File>>,
|
||||
next_try: Instant,
|
||||
mut_state.reader = Some(PipeReaderThread::new_from_child(child));
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
struct FifoLabelState {
|
||||
path: String,
|
||||
mut_state: RefCell<FifoLabelMutableState>,
|
||||
path: Arc<str>,
|
||||
mut_state: RefCell<PipeLabelMutableState>,
|
||||
carry_over: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
impl FifoLabelState {
|
||||
fn try_remove_fifo(&self) -> anyhow::Result<()> {
|
||||
let meta = match fs::metadata(&self.path) {
|
||||
let meta = match fs::metadata(&*self.path) {
|
||||
Ok(meta) => meta,
|
||||
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);
|
||||
}
|
||||
return Ok(());
|
||||
@@ -292,7 +254,7 @@ impl FifoLabelState {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -308,16 +270,14 @@ impl Drop for FifoLabelState {
|
||||
}
|
||||
}
|
||||
|
||||
fn pipe_on_tick(
|
||||
fn fifo_on_tick(
|
||||
state: &FifoLabelState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
let reader = if let Some(f) = mut_state.reader.as_mut() {
|
||||
f
|
||||
} else {
|
||||
let Some(reader) = mut_state.reader.as_mut() else {
|
||||
if mut_state.next_try > Instant::now() {
|
||||
return;
|
||||
}
|
||||
@@ -328,29 +288,26 @@ fn pipe_on_tick(
|
||||
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);
|
||||
log::warn!("Failed to create FIFO: {e:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
mut_state.reader = fs::File::open(&state.path)
|
||||
.inspect_err(|e| {
|
||||
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()
|
||||
mut_state.reader = Some(PipeReaderThread::new_from_fifo(state.path.clone()));
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(text) =
|
||||
read_label_from_pipe(&state.path, reader, &mut state.carry_over.borrow_mut())
|
||||
{
|
||||
if let Some(text) = reader.get_last_line() {
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
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.);
|
||||
|
||||
@@ -5,6 +5,7 @@ use glam::{vec2, Affine2, Vec2};
|
||||
use label::setup_custom_label;
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::button::ComponentButton,
|
||||
drawing,
|
||||
event::{
|
||||
Event as WguiEvent, EventCallback, EventListenerID, EventListenerKind,
|
||||
@@ -13,9 +14,9 @@ use wgui::{
|
||||
},
|
||||
gfx::cmd::WGfxClearMode,
|
||||
layout::{Layout, LayoutParams, WidgetID},
|
||||
parser::{CustomAttribsInfoOwned, ParserState},
|
||||
parser::{CustomAttribsInfoOwned, Fetchable, ParserState},
|
||||
renderer_vk::context::Context as WguiContext,
|
||||
widget::{label::WidgetLabel, rectangle::WidgetRectangle, EventResult},
|
||||
widget::{label::WidgetLabel, EventResult},
|
||||
};
|
||||
use wlx_common::timestep::Timestep;
|
||||
|
||||
@@ -138,13 +139,10 @@ impl<S: 'static> GuiPanel<S> {
|
||||
.is_some()
|
||||
{
|
||||
setup_custom_label::<S>(&mut layout, elem, app);
|
||||
} else if layout
|
||||
.state
|
||||
.widgets
|
||||
.get_as::<WidgetRectangle>(elem.widget_id)
|
||||
.is_some()
|
||||
} else if let Ok(button) =
|
||||
parser_state.fetch_component_from_widget_id_as::<ComponentButton>(elem.widget_id)
|
||||
{
|
||||
setup_custom_button::<S>(&mut layout, elem, app);
|
||||
setup_custom_button::<S>(&mut layout, elem, app, button);
|
||||
}
|
||||
|
||||
if let Some(on_custom_attrib) = ¶ms.on_custom_attrib {
|
||||
|
||||
@@ -130,9 +130,7 @@ impl OscSender {
|
||||
let level = device.soc.unwrap_or(-1.0);
|
||||
let parameter = match device.role {
|
||||
TrackedDeviceRole::None => continue,
|
||||
TrackedDeviceRole::Hmd => {
|
||||
"hmd"
|
||||
}
|
||||
TrackedDeviceRole::Hmd => "hmd",
|
||||
TrackedDeviceRole::LeftHand => {
|
||||
controller_count += 1;
|
||||
controller_total_bat += level;
|
||||
@@ -183,14 +181,30 @@ impl OscSender {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_single_param(
|
||||
&mut self,
|
||||
parameter: String,
|
||||
values: Vec<OscType>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.send_message(parameter, values)?;
|
||||
pub fn parse_osc_value(s: &str) -> anyhow::Result<OscType> {
|
||||
let lower = s.to_lowercase();
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user