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( layout: &mut Layout, attribs: &CustomAttribsInfoOwned, _app: &AppState, button: Rc, ) { 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 = 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> = 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::( 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, pid: Option, } struct ShellButtonState { button: Rc, exec: String, mut_state: RefCell, carry_over: RefCell>, } 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; } }