mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-30 20:34:49 +01:00
Setup Lua API integration tests
This commit is contained in:
parent
f4a5328c2c
commit
6d83e34868
7 changed files with 342 additions and 61 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
@ -1768,6 +1768,7 @@ dependencies = [
|
||||||
"dircpy",
|
"dircpy",
|
||||||
"image",
|
"image",
|
||||||
"nix",
|
"nix",
|
||||||
|
"pinnacle",
|
||||||
"pinnacle-api-defs",
|
"pinnacle-api-defs",
|
||||||
"prost",
|
"prost",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -1777,6 +1778,7 @@ dependencies = [
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"temp-env",
|
"temp-env",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"test-log",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
@ -2405,6 +2407,27 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "test-log"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b319995299c65d522680decf80f2c108d85b861d81dfe340a10d16cee29d9e6"
|
||||||
|
dependencies = [
|
||||||
|
"test-log-macros",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "test-log-macros"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8f546451eaa38373f549093fe9fd05e7d2bade739e2ddf834b9968621d60107"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "textwrap"
|
name = "textwrap"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
|
|
54
Cargo.toml
54
Cargo.toml
|
@ -36,7 +36,7 @@ keywords = ["wayland", "compositor", "smithay", "lua"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Smithay
|
# Smithay
|
||||||
smithay = { git = "https://github.com/Smithay/smithay", rev = "418190e", default-features = false, features = ["desktop", "wayland_frontend"] }
|
# smithay is down there somewhere
|
||||||
smithay-drm-extras = { git = "https://github.com/Smithay/smithay", rev = "418190e" }
|
smithay-drm-extras = { git = "https://github.com/Smithay/smithay", rev = "418190e" }
|
||||||
# Tracing
|
# Tracing
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
|
@ -72,32 +72,44 @@ pinnacle-api-defs = { workspace = true }
|
||||||
dircpy = "0.3.16"
|
dircpy = "0.3.16"
|
||||||
chrono = "0.4.34"
|
chrono = "0.4.34"
|
||||||
|
|
||||||
|
[dependencies.smithay]
|
||||||
|
git = "https://github.com/Smithay/smithay"
|
||||||
|
rev = "418190e"
|
||||||
|
default-features = false
|
||||||
|
features = [
|
||||||
|
"desktop",
|
||||||
|
"wayland_frontend",
|
||||||
|
# udev
|
||||||
|
"backend_libinput",
|
||||||
|
"backend_udev",
|
||||||
|
"backend_drm",
|
||||||
|
"backend_gbm",
|
||||||
|
"backend_vulkan",
|
||||||
|
"backend_egl",
|
||||||
|
"backend_session_libseat",
|
||||||
|
"renderer_gl",
|
||||||
|
"renderer_multi",
|
||||||
|
# egl
|
||||||
|
"use_system_lib",
|
||||||
|
"backend_egl",
|
||||||
|
# winit
|
||||||
|
"backend_winit",
|
||||||
|
"backend_drm",
|
||||||
|
# xwayland
|
||||||
|
"xwayland",
|
||||||
|
"x11rb_event_source",
|
||||||
|
]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
xdg = { workspace = true }
|
xdg = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
temp-env = "0.3.6"
|
temp-env = "0.3.6"
|
||||||
tempfile = "3.10.1"
|
tempfile = "3.10.1"
|
||||||
|
test-log = { version = "0.2.15", default-features = false, features = ["trace"] }
|
||||||
|
pinnacle = { path = ".", features = ["testing"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [
|
testing = [
|
||||||
# udev
|
"smithay/renderer_test",
|
||||||
"smithay/backend_libinput",
|
|
||||||
"smithay/backend_udev",
|
|
||||||
"smithay/backend_drm",
|
|
||||||
"smithay/backend_gbm",
|
|
||||||
"smithay/backend_vulkan",
|
|
||||||
"smithay/backend_egl",
|
|
||||||
"smithay/backend_session_libseat",
|
|
||||||
"smithay/renderer_gl",
|
|
||||||
"smithay/renderer_multi",
|
|
||||||
# egl
|
|
||||||
"smithay/use_system_lib",
|
|
||||||
"smithay/backend_egl",
|
|
||||||
# winit
|
|
||||||
"smithay/backend_winit",
|
|
||||||
"smithay/backend_drm",
|
|
||||||
# xwayland
|
|
||||||
"smithay/xwayland",
|
|
||||||
"smithay/x11rb_event_source"
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -32,8 +32,12 @@ use crate::{
|
||||||
window::WindowElement,
|
window::WindowElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "testing")]
|
||||||
|
use self::dummy::Dummy;
|
||||||
use self::{udev::Udev, winit::Winit};
|
use self::{udev::Udev, winit::Winit};
|
||||||
|
|
||||||
|
#[cfg(feature = "testing")]
|
||||||
|
pub mod dummy;
|
||||||
pub mod udev;
|
pub mod udev;
|
||||||
pub mod winit;
|
pub mod winit;
|
||||||
|
|
||||||
|
@ -42,6 +46,8 @@ pub enum Backend {
|
||||||
Winit(Winit),
|
Winit(Winit),
|
||||||
/// The compositor is running in a tty
|
/// The compositor is running in a tty
|
||||||
Udev(Udev),
|
Udev(Udev),
|
||||||
|
#[cfg(feature = "testing")]
|
||||||
|
Dummy(Dummy),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend {
|
impl Backend {
|
||||||
|
@ -49,6 +55,8 @@ impl Backend {
|
||||||
match self {
|
match self {
|
||||||
Backend::Winit(winit) => winit.seat_name(),
|
Backend::Winit(winit) => winit.seat_name(),
|
||||||
Backend::Udev(udev) => udev.seat_name(),
|
Backend::Udev(udev) => udev.seat_name(),
|
||||||
|
#[cfg(feature = "testing")]
|
||||||
|
Backend::Dummy(dummy) => dummy.seat_name(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +64,8 @@ impl Backend {
|
||||||
match self {
|
match self {
|
||||||
Backend::Winit(winit) => winit.early_import(surface),
|
Backend::Winit(winit) => winit.early_import(surface),
|
||||||
Backend::Udev(udev) => udev.early_import(surface),
|
Backend::Udev(udev) => udev.early_import(surface),
|
||||||
|
#[cfg(feature = "testing")]
|
||||||
|
Backend::Dummy(dummy) => dummy.early_import(surface),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,6 +190,8 @@ impl DmabufHandler for State {
|
||||||
.expect("udev had no dmabuf state")
|
.expect("udev had no dmabuf state")
|
||||||
.0
|
.0
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "testing")]
|
||||||
|
Backend::Dummy(_) => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,6 +214,12 @@ impl DmabufHandler for State {
|
||||||
.and_then(|mut renderer| renderer.import_dmabuf(&dmabuf, None))
|
.and_then(|mut renderer| renderer.import_dmabuf(&dmabuf, None))
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(|_| ()),
|
.map_err(|_| ()),
|
||||||
|
#[cfg(feature = "testing")]
|
||||||
|
Backend::Dummy(dummy) => dummy
|
||||||
|
.renderer
|
||||||
|
.import_dmabuf(&dmabuf, None)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|_| ()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if res.is_ok() {
|
if res.is_ok() {
|
||||||
|
|
118
src/backend/dummy.rs
Normal file
118
src/backend/dummy.rs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
use smithay::backend::renderer::test::DummyRenderer;
|
||||||
|
use smithay::backend::renderer::ImportMemWl;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use smithay::{
|
||||||
|
output::{Output, Subpixel},
|
||||||
|
reexports::{calloop::EventLoop, wayland_server::Display},
|
||||||
|
utils::Transform,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
use super::Backend;
|
||||||
|
use super::BackendData;
|
||||||
|
|
||||||
|
pub struct Dummy {
|
||||||
|
pub renderer: DummyRenderer,
|
||||||
|
// pub dmabuf_state: (DmabufState, DmabufGlobal, Option<DmabufFeedback>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend {
|
||||||
|
fn dummy_mut(&mut self) -> &Dummy {
|
||||||
|
let Backend::Dummy(dummy) = self else { unreachable!() };
|
||||||
|
dummy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackendData for Dummy {
|
||||||
|
fn seat_name(&self) -> String {
|
||||||
|
"Dummy".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_buffers(&mut self, _output: &Output) {}
|
||||||
|
|
||||||
|
fn early_import(&mut self, _surface: &WlSurface) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_dummy(
|
||||||
|
no_config: bool,
|
||||||
|
config_dir: Option<PathBuf>,
|
||||||
|
) -> anyhow::Result<(State, EventLoop<'static, State>)> {
|
||||||
|
let event_loop: EventLoop<State> = EventLoop::try_new()?;
|
||||||
|
|
||||||
|
let display: Display<State> = Display::new()?;
|
||||||
|
let display_handle = display.handle();
|
||||||
|
|
||||||
|
let loop_handle = event_loop.handle();
|
||||||
|
|
||||||
|
let mode = smithay::output::Mode {
|
||||||
|
size: (1920, 1080).into(),
|
||||||
|
refresh: 60_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
let physical_properties = smithay::output::PhysicalProperties {
|
||||||
|
size: (0, 0).into(),
|
||||||
|
subpixel: Subpixel::Unknown,
|
||||||
|
make: "Pinnacle".to_string(),
|
||||||
|
model: "Winit Window".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = Output::new("Pinnacle Window".to_string(), physical_properties);
|
||||||
|
|
||||||
|
output.create_global::<State>(&display_handle);
|
||||||
|
|
||||||
|
output.change_current_state(
|
||||||
|
Some(mode),
|
||||||
|
Some(Transform::Flipped180),
|
||||||
|
None,
|
||||||
|
Some((0, 0).into()),
|
||||||
|
);
|
||||||
|
|
||||||
|
output.set_preferred(mode);
|
||||||
|
|
||||||
|
let renderer = DummyRenderer::new();
|
||||||
|
|
||||||
|
// let dmabuf_state = {
|
||||||
|
// let dmabuf_formats = renderer.dmabuf_formats().collect::<Vec<_>>();
|
||||||
|
// let mut dmabuf_state = DmabufState::new();
|
||||||
|
// let dmabuf_global = dmabuf_state.create_global::<State>(&display_handle, dmabuf_formats);
|
||||||
|
// (dmabuf_state, dmabuf_global, None)
|
||||||
|
// };
|
||||||
|
|
||||||
|
let backend = Dummy {
|
||||||
|
renderer,
|
||||||
|
// dmabuf_state,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut state = State::init(
|
||||||
|
super::Backend::Dummy(backend),
|
||||||
|
display,
|
||||||
|
event_loop.get_signal(),
|
||||||
|
loop_handle,
|
||||||
|
no_config,
|
||||||
|
config_dir,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
state.output_focus_stack.set_focus(output.clone());
|
||||||
|
|
||||||
|
let dummy = state.backend.dummy_mut();
|
||||||
|
|
||||||
|
state.shm_state.update_formats(dummy.renderer.shm_formats());
|
||||||
|
|
||||||
|
state.space.map_output(&output, (0, 0));
|
||||||
|
|
||||||
|
if let Err(err) = state.xwayland.start(
|
||||||
|
state.loop_handle.clone(),
|
||||||
|
None,
|
||||||
|
std::iter::empty::<(OsString, OsString)>(),
|
||||||
|
true,
|
||||||
|
|_| {},
|
||||||
|
) {
|
||||||
|
tracing::error!("Failed to start XWayland: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((state, event_loop))
|
||||||
|
}
|
|
@ -167,49 +167,46 @@ impl State {
|
||||||
///
|
///
|
||||||
/// Does nothing when called on the winit backend.
|
/// Does nothing when called on the winit backend.
|
||||||
pub fn switch_vt(&mut self, vt: i32) {
|
pub fn switch_vt(&mut self, vt: i32) {
|
||||||
match &mut self.backend {
|
if let Backend::Udev(udev) = &mut self.backend {
|
||||||
Backend::Winit(_) => (),
|
for backend in udev.backends.values_mut() {
|
||||||
Backend::Udev(udev) => {
|
for surface in backend.surfaces.values_mut() {
|
||||||
for backend in udev.backends.values_mut() {
|
// Clear the overlay planes on tty switch.
|
||||||
for surface in backend.surfaces.values_mut() {
|
//
|
||||||
// Clear the overlay planes on tty switch.
|
// On my machine, switching a tty would leave the topmost window on the
|
||||||
//
|
// screen. Smithay will render the topmost window on the overlay plane,
|
||||||
// On my machine, switching a tty would leave the topmost window on the
|
// so we clear it here.
|
||||||
// screen. Smithay will render the topmost window on the overlay plane,
|
let planes = surface.compositor.surface().planes().clone();
|
||||||
// so we clear it here.
|
tracing::debug!("Clearing overlay planes");
|
||||||
let planes = surface.compositor.surface().planes().clone();
|
for overlay_plane in planes.overlay {
|
||||||
tracing::debug!("Clearing overlay planes");
|
if let Err(err) = surface
|
||||||
for overlay_plane in planes.overlay {
|
.compositor
|
||||||
if let Err(err) = surface
|
.surface()
|
||||||
.compositor
|
.clear_plane(overlay_plane.handle)
|
||||||
.surface()
|
{
|
||||||
.clear_plane(overlay_plane.handle)
|
tracing::warn!("Failed to clear overlay planes: {err}");
|
||||||
{
|
|
||||||
tracing::warn!("Failed to clear overlay planes: {err}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the clear to commit before switching
|
|
||||||
self.schedule(
|
|
||||||
|state| {
|
|
||||||
let udev = state.backend.udev();
|
|
||||||
!udev
|
|
||||||
.backends
|
|
||||||
.values()
|
|
||||||
.flat_map(|backend| backend.surfaces.values())
|
|
||||||
.map(|surface| surface.compositor.surface())
|
|
||||||
.any(|drm_surf| drm_surf.commit_pending())
|
|
||||||
},
|
|
||||||
move |state| {
|
|
||||||
let udev = state.backend.udev_mut();
|
|
||||||
if let Err(err) = udev.session.change_vt(vt) {
|
|
||||||
tracing::error!("Failed to switch to vt {vt}: {err}");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for the clear to commit before switching
|
||||||
|
self.schedule(
|
||||||
|
|state| {
|
||||||
|
let udev = state.backend.udev();
|
||||||
|
!udev
|
||||||
|
.backends
|
||||||
|
.values()
|
||||||
|
.flat_map(|backend| backend.surfaces.values())
|
||||||
|
.map(|surface| surface.compositor.surface())
|
||||||
|
.any(|drm_surf| drm_surf.commit_pending())
|
||||||
|
},
|
||||||
|
move |state| {
|
||||||
|
let udev = state.backend.udev_mut();
|
||||||
|
if let Err(err) = udev.session.change_vt(vt) {
|
||||||
|
tracing::error!("Failed to switch to vt {vt}: {err}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -404,7 +404,7 @@ impl State {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_grpc_server(&mut self, socket_dir: &Path) -> anyhow::Result<()> {
|
pub fn start_grpc_server(&mut self, socket_dir: &Path) -> anyhow::Result<()> {
|
||||||
self.system_processes
|
self.system_processes
|
||||||
.refresh_processes_specifics(ProcessRefreshKind::new());
|
.refresh_processes_specifics(ProcessRefreshKind::new());
|
||||||
|
|
||||||
|
|
113
tests/lua_api.rs
Normal file
113
tests/lua_api.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use std::{
|
||||||
|
io::Write,
|
||||||
|
process::{Command, Stdio},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use pinnacle::{backend::dummy::setup_dummy, state::State};
|
||||||
|
use smithay::reexports::calloop::{
|
||||||
|
self,
|
||||||
|
channel::{Event, Sender},
|
||||||
|
};
|
||||||
|
|
||||||
|
use test_log::test;
|
||||||
|
|
||||||
|
fn run_lua(ident: &str, code: &str) {
|
||||||
|
let code = format!(r#"require("pinnacle").setup(function({ident}) {code} end)"#);
|
||||||
|
|
||||||
|
let mut child = Command::new("lua").stdin(Stdio::piped()).spawn().unwrap();
|
||||||
|
|
||||||
|
let mut stdin = child
|
||||||
|
.stdin
|
||||||
|
.take()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("failed to open child stdin"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
stdin.write_all(code.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
drop(stdin);
|
||||||
|
|
||||||
|
child.wait().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn assert(
|
||||||
|
sender: &Sender<Box<dyn FnOnce(&mut State) + Send>>,
|
||||||
|
assert: impl FnOnce(&mut State) + Send + 'static,
|
||||||
|
) {
|
||||||
|
sender.send(Box::new(assert)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sleep_secs(secs: u64) {
|
||||||
|
std::thread::sleep(Duration::from_secs(secs));
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! run_lua {
|
||||||
|
{ |$ident:ident| $($body:tt)* } => {
|
||||||
|
run_lua(stringify!($ident), stringify!($($body)*));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_lua_api(
|
||||||
|
test: impl FnOnce(Sender<Box<dyn FnOnce(&mut State) + Send>>) + Send + 'static,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let (mut state, mut event_loop) = setup_dummy(true, None)?;
|
||||||
|
|
||||||
|
let (sender, recv) = calloop::channel::channel::<Box<dyn FnOnce(&mut State) + Send>>();
|
||||||
|
|
||||||
|
event_loop
|
||||||
|
.handle()
|
||||||
|
.insert_source(recv, |event, _, state| match event {
|
||||||
|
Event::Msg(f) => f(state),
|
||||||
|
Event::Closed => panic!(),
|
||||||
|
})
|
||||||
|
.map_err(|_| anyhow::anyhow!("failed to insert source"))?;
|
||||||
|
|
||||||
|
let tempdir = tempfile::tempdir()?;
|
||||||
|
|
||||||
|
state.start_grpc_server(tempdir.path())?;
|
||||||
|
|
||||||
|
std::thread::spawn(move || test(sender));
|
||||||
|
|
||||||
|
event_loop.run(None, &mut state, |state| {
|
||||||
|
state.fixup_z_layering();
|
||||||
|
state.space.refresh();
|
||||||
|
state.popup_manager.cleanup();
|
||||||
|
|
||||||
|
state
|
||||||
|
.display_handle
|
||||||
|
.flush_clients()
|
||||||
|
.expect("failed to flush client buffers");
|
||||||
|
|
||||||
|
// TODO: couple these or something, this is really error-prone
|
||||||
|
assert_eq!(
|
||||||
|
state.windows.len(),
|
||||||
|
state.z_index_stack.len(),
|
||||||
|
"Length of `windows` and `z_index_stack` are different. \
|
||||||
|
If you see this, report it to the developer."
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
#[test]
|
||||||
|
async fn window_count_with_tag_is_correct() -> anyhow::Result<()> {
|
||||||
|
test_lua_api(|sender| {
|
||||||
|
run_lua! { |Pinnacle|
|
||||||
|
Pinnacle.tag.add(Pinnacle.output.get_all()[1], "1")
|
||||||
|
Pinnacle.process.spawn("foot")
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep_secs(1);
|
||||||
|
|
||||||
|
assert(&sender, |state| assert_eq!(state.windows.len(), 1));
|
||||||
|
|
||||||
|
sleep_secs(1);
|
||||||
|
|
||||||
|
run_lua! { |Pinnacle|
|
||||||
|
Pinnacle.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue