custom labels & buttons
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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()
|
||||||
mut_state.child = Some(child);
|
.unwrap()
|
||||||
}
|
.wait();
|
||||||
// exited successfully
|
return Ok(());
|
||||||
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 child = Command::new("sh")
|
||||||
}) {
|
.arg("-c")
|
||||||
//TODO update label
|
.arg(&state.exec)
|
||||||
}
|
.stdout(Stdio::piped())
|
||||||
mut_state.child = None;
|
.spawn()
|
||||||
}
|
.with_context(|| format!("Failed to run shell script: '{}'", &state.exec))?;
|
||||||
// exited with failure
|
|
||||||
Ok(Some(code)) => {
|
mut_state.pid = Some(child.id());
|
||||||
mut_state.child = None;
|
mut_state.reader = Some(PipeReaderThread::new_from_child(child));
|
||||||
log::warn!("Label process exited with code {code}");
|
|
||||||
}
|
return Ok(());
|
||||||
// lost
|
}
|
||||||
Err(_) => {
|
|
||||||
mut_state.child = None;
|
fn shell_on_tick(state: &ShellButtonState, common: &mut event::CallbackDataCommon, piped: bool) {
|
||||||
log::warn!("Label child process lost.");
|
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 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) => {
|
||||||
|
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::{
|
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
|
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||||
Ok(None) => {
|
label.set_text(common, Translation::from_raw_text(&text));
|
||||||
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())
|
|
||||||
}) {
|
if reader.is_finished() {
|
||||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
if !mut_state.reader.take().unwrap().is_success() {
|
||||||
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);
|
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() {
|
} else if mut_state.next_try > Instant::now() {
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
match Command::new("sh")
|
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.);
|
||||||
|
|||||||
@@ -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) = ¶ms.on_custom_attrib {
|
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 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user