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
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.

View File

@@ -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;
}
}

View File

@@ -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::warn!("pipe read error on {path}: {e:?}");
return None;
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())
}
});
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::{
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())
}) {
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));
}
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(&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.);

View File

@@ -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) = &params.on_custom_attrib {

View File

@@ -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)
}
}
}