271 lines
9.4 KiB
Rust
271 lines
9.4 KiB
Rust
use std::{
|
|
cell::RefCell,
|
|
process::{Command, Stdio},
|
|
rc::Rc,
|
|
sync::{Arc, atomic::Ordering},
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use anyhow::Context;
|
|
use wgui::{
|
|
components::button::ComponentButton,
|
|
event::{self, EventCallback, EventListenerKind},
|
|
i18n::Translation,
|
|
layout::Layout,
|
|
parser::CustomAttribsInfoOwned,
|
|
widget::EventResult,
|
|
};
|
|
use wlx_common::overlays::ToastTopic;
|
|
|
|
use crate::{
|
|
RUNNING,
|
|
backend::task::{OverlayTask, PlayspaceTask, TaskType},
|
|
gui::panel::helper::PipeReaderThread,
|
|
overlays::{
|
|
mirror::{new_mirror, new_mirror_name},
|
|
toast::Toast,
|
|
},
|
|
state::AppState,
|
|
windowing::OverlaySelector,
|
|
};
|
|
|
|
#[cfg(feature = "wayvr")]
|
|
use crate::backend::wayvr::WayVRAction;
|
|
|
|
pub const BUTTON_EVENTS: [(&str, EventListenerKind); 2] = [
|
|
("_press", EventListenerKind::MousePress),
|
|
("_release", EventListenerKind::MouseRelease),
|
|
];
|
|
|
|
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 {
|
|
continue;
|
|
};
|
|
|
|
let mut args = action.split_whitespace();
|
|
let Some(command) = args.next() else {
|
|
continue;
|
|
};
|
|
|
|
let callback: EventCallback<AppState, S> = match command {
|
|
#[cfg(feature = "wayvr")]
|
|
"::DashToggle" => Box::new(move |_common, _data, app, _| {
|
|
app.tasks
|
|
.enqueue(TaskType::WayVR(WayVRAction::ToggleDashboard));
|
|
Ok(EventResult::Consumed)
|
|
}),
|
|
"::SetToggle" => {
|
|
let arg = args.next().unwrap_or_default();
|
|
let Ok(set_idx) = arg.parse() else {
|
|
log::error!("{command} has invalid argument: \"{arg}\"");
|
|
return;
|
|
};
|
|
Box::new(move |_common, _data, app, _| {
|
|
app.tasks
|
|
.enqueue(TaskType::Overlay(OverlayTask::ToggleSet(set_idx)));
|
|
Ok(EventResult::Consumed)
|
|
})
|
|
}
|
|
"::OverlayToggle" => {
|
|
let Some(arg): Option<Arc<str>> = args.next().map(Into::into) else {
|
|
log::error!("{command} has missing arguments");
|
|
return;
|
|
};
|
|
|
|
Box::new(move |_common, _data, app, _| {
|
|
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
|
|
OverlaySelector::Name(arg.clone()),
|
|
Box::new(move |app, owc| {
|
|
if owc.active_state.is_none() {
|
|
owc.activate(app);
|
|
} else {
|
|
owc.deactivate();
|
|
}
|
|
}),
|
|
)));
|
|
Ok(EventResult::Consumed)
|
|
})
|
|
}
|
|
"::EditToggle" => Box::new(move |_common, _data, app, _| {
|
|
app.tasks
|
|
.enqueue(TaskType::Overlay(OverlayTask::ToggleEditMode));
|
|
Ok(EventResult::Consumed)
|
|
}),
|
|
"::NewMirror" => Box::new(move |_common, _data, app, _| {
|
|
let name = new_mirror_name();
|
|
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Create(
|
|
OverlaySelector::Name(name.clone()),
|
|
Box::new(move |app| Some(new_mirror(name, &app.session))),
|
|
)));
|
|
Ok(EventResult::Consumed)
|
|
}),
|
|
"::CleanupMirrors" => Box::new(move |_common, _data, app, _| {
|
|
app.tasks
|
|
.enqueue(TaskType::Overlay(OverlayTask::CleanupMirrors));
|
|
Ok(EventResult::Consumed)
|
|
}),
|
|
"::PlayspaceReset" => Box::new(move |_common, _data, app, _| {
|
|
app.tasks.enqueue(TaskType::Playspace(PlayspaceTask::Reset));
|
|
Ok(EventResult::Consumed)
|
|
}),
|
|
"::PlayspaceRecenter" => Box::new(move |_common, _data, app, _| {
|
|
app.tasks
|
|
.enqueue(TaskType::Playspace(PlayspaceTask::Recenter));
|
|
Ok(EventResult::Consumed)
|
|
}),
|
|
"::PlayspaceFixFloor" => Box::new(move |_common, _data, app, _| {
|
|
for i in 0..5 {
|
|
Toast::new(
|
|
ToastTopic::System,
|
|
format!("Fixing floor in {}", 5 - i),
|
|
"Touch your controller to the floor!".into(),
|
|
)
|
|
.with_timeout(1.)
|
|
.with_sound(true)
|
|
.submit_at(app, Instant::now() + Duration::from_secs(i));
|
|
}
|
|
app.tasks.enqueue_at(
|
|
TaskType::Playspace(PlayspaceTask::FixFloor),
|
|
Instant::now() + Duration::from_secs(5),
|
|
);
|
|
Ok(EventResult::Consumed)
|
|
}),
|
|
"::Shutdown" => Box::new(move |_common, _data, _app, _| {
|
|
RUNNING.store(false, Ordering::Relaxed);
|
|
Ok(EventResult::Consumed)
|
|
}),
|
|
"::ShellExec" => {
|
|
let state = Rc::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(std::string::ToString::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,
|
|
};
|
|
|
|
let id = layout.add_event_listener(attribs.widget_id, *kind, callback);
|
|
log::debug!("Registered {action} on {:?} as {id:?}", attribs.widget_id);
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct ShellButtonMutableState {
|
|
reader: Option<PipeReaderThread>,
|
|
pid: Option<u32>,
|
|
}
|
|
|
|
struct ShellButtonState {
|
|
button: Rc<ComponentButton>,
|
|
exec: String,
|
|
mut_state: RefCell<ShellButtonMutableState>,
|
|
carry_over: RefCell<Option<String>>,
|
|
}
|
|
|
|
fn shell_on_action(state: &ShellButtonState) -> anyhow::Result<()> {
|
|
let mut mut_state = state.mut_state.borrow_mut();
|
|
|
|
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));
|
|
|
|
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;
|
|
}
|
|
}
|