From 4c66b37a2fd0c478d4354be8d79649164015b432 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Fri, 17 Feb 2023 13:06:13 +0100 Subject: [PATCH] enable conditional compilation for all backends To reduce binary size one can now enable only specific backends, e.g. wayland or x11 via cargo features Additionally adds stubs for libei and xdg-desktop-portal backends --- Cargo.toml | 25 ++- config.toml | 1 + src/backend.rs | 16 +- src/backend/consumer.rs | 14 ++ src/backend/consumer/libei.rs | 9 ++ .../consumer.rs => consumer/windows.rs} | 0 .../consumer.rs => consumer/wlroots.rs} | 143 +++++++++--------- .../{x11/consumer.rs => consumer/x11.rs} | 0 src/backend/consumer/xdg_desktop_portal.rs | 9 ++ src/backend/producer.rs | 6 + .../producer.rs => producer/wayland.rs} | 0 .../producer.rs => producer/windows.rs} | 0 .../{x11/producer.rs => producer/x11.rs} | 0 src/backend/wayland.rs | 2 - src/backend/windows.rs | 2 - src/backend/x11.rs | 2 - src/client.rs | 45 +++++- src/config.rs | 1 + src/consumer.rs | 70 +++++++++ src/lib.rs | 3 + src/main.rs | 107 +------------ src/producer.rs | 52 +++++++ 22 files changed, 310 insertions(+), 197 deletions(-) create mode 100644 src/backend/consumer.rs create mode 100644 src/backend/consumer/libei.rs rename src/backend/{windows/consumer.rs => consumer/windows.rs} (100%) rename src/backend/{wayland/consumer.rs => consumer/wlroots.rs} (73%) rename src/backend/{x11/consumer.rs => consumer/x11.rs} (100%) create mode 100644 src/backend/consumer/xdg_desktop_portal.rs create mode 100644 src/backend/producer.rs rename src/backend/{wayland/producer.rs => producer/wayland.rs} (100%) rename src/backend/{windows/producer.rs => producer/windows.rs} (100%) rename src/backend/{x11/producer.rs => producer/x11.rs} (100%) delete mode 100644 src/backend/wayland.rs delete mode 100644 src/backend/windows.rs delete mode 100644 src/backend/x11.rs create mode 100644 src/consumer.rs create mode 100644 src/producer.rs diff --git a/Cargo.toml b/Cargo.toml index dcb4631..d3aab63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,12 +15,25 @@ serde_derive = "1.0" threadpool = "1.8" [target.'cfg(unix)'.dependencies] -wayland-client = { version="0.30.0" } -wayland-protocols = { version="0.30.0", features=["client", "staging", "unstable"] } -wayland-protocols-wlr = { version="0.1.0", features=["client"] } -wayland-protocols-misc = { version="0.1.0", features=["client"] } -wayland-protocols-plasma = { version="0.1.0", features=["client"] } -x11 = { version = "2.21.0", features = ["xlib", "xtest"] } +wayland-client = { version="0.30.0", optional = true } +wayland-protocols = { version="0.30.0", features=["client", "staging", "unstable"], optional = true } +wayland-protocols-wlr = { version="0.1.0", features=["client"], optional = true } +wayland-protocols-misc = { version="0.1.0", features=["client"], optional = true } +wayland-protocols-plasma = { version="0.1.0", features=["client"], optional = true } +x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["winuser"] } + + +[features] +default = [ "wayland", "x11", "xdg_desktop_portal", "libei" ] +wayland = [ + "dep:wayland-client", + "dep:wayland-protocols", + "dep:wayland-protocols-wlr", + "dep:wayland-protocols-misc", + "dep:wayland-protocols-plasma" ] +x11 = [ "dep:x11" ] +xdg_desktop_portal = [] +libei = [] diff --git a/config.toml b/config.toml index 13be4f6..94b36cf 100644 --- a/config.toml +++ b/config.toml @@ -1,4 +1,5 @@ port = 42069 +backend = "wlroots" # [client.right] # host_name = "localhost" diff --git a/src/backend.rs b/src/backend.rs index c5d67de..934046a 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,14 +1,2 @@ -#[cfg(windows)] -pub mod windows; - -#[cfg(unix)] -pub mod wayland; - -#[cfg(unix)] -pub mod x11; - -#[derive(Clone, Copy, Debug)] -pub enum Backend { - X11, - WAYLAND, -} +pub mod consumer; +pub mod producer; diff --git a/src/backend/consumer.rs b/src/backend/consumer.rs new file mode 100644 index 0000000..7426052 --- /dev/null +++ b/src/backend/consumer.rs @@ -0,0 +1,14 @@ +#[cfg(windows)] +pub mod windows; + +#[cfg(feature="x11")] +pub mod x11; + +#[cfg(feature = "wayland")] +pub mod wlroots; + +#[cfg(feature = "xdg_desktop_portal")] +pub mod xdg_desktop_portal; + +#[cfg(feature = "libei")] +pub mod libei; diff --git a/src/backend/consumer/libei.rs b/src/backend/consumer/libei.rs new file mode 100644 index 0000000..2bf80fb --- /dev/null +++ b/src/backend/consumer/libei.rs @@ -0,0 +1,9 @@ +use std::sync::mpsc::Receiver; + +use crate::{event::Event, client::{ClientHandle, Client}}; + + + +pub(crate) fn run(_consume_rx: Receiver<(Event, ClientHandle)>, _clients: Vec) { + todo!() +} diff --git a/src/backend/windows/consumer.rs b/src/backend/consumer/windows.rs similarity index 100% rename from src/backend/windows/consumer.rs rename to src/backend/consumer/windows.rs diff --git a/src/backend/wayland/consumer.rs b/src/backend/consumer/wlroots.rs similarity index 73% rename from src/backend/wayland/consumer.rs rename to src/backend/consumer/wlroots.rs index 076f882..e162645 100644 --- a/src/backend/wayland/consumer.rs +++ b/src/backend/consumer/wlroots.rs @@ -35,13 +35,8 @@ use tempfile; use crate::event::{Event, KeyboardEvent, PointerEvent}; enum VirtualInputManager { - Wlroots { - vpm: VpManager, - vkm: VkManager, - }, - Kde { - fake_input: OrgKdeKwinFakeInput, - } + Wlroots { vpm: VpManager, vkm: VkManager }, + Kde { fake_input: OrgKdeKwinFakeInput }, } // App State, implements Dispatch event handlers @@ -72,16 +67,21 @@ impl App { let virtual_input_manager = match (vpm, vkm, fake_input) { (Ok(vpm), Ok(vkm), _) => VirtualInputManager::Wlroots { vpm, vkm }, (_, _, Ok(fake_input)) => { - fake_input.authenticate("lan-mouse".into(), "Allow remote clients to control this devices".into()); + fake_input.authenticate( + "lan-mouse".into(), + "Allow remote clients to control this devices".into(), + ); VirtualInputManager::Kde { fake_input } - }, + } (Err(e1), Err(e2), Err(e3)) => { eprintln!("zwlr_virtual_pointer_v1: {e1}"); eprintln!("zwp_virtual_keyboard_v1: {e2}"); eprintln!("org_kde_kwin_fake_input: {e3}"); panic!("neither wlroots nor kde input emulation protocol supported!") - }, - _ => { panic!() } + } + _ => { + panic!() + } }; let input_for_client: HashMap = HashMap::new(); @@ -152,28 +152,22 @@ impl App { buf.flush().unwrap(); keyboard.keymap(1, f.as_raw_fd(), data.len() as u32); - let vinput = VirtualInput::Wlroots{ pointer, keyboard }; + let vinput = VirtualInput::Wlroots { pointer, keyboard }; self.input_for_client.insert(client.handle, vinput); - - }, + } VirtualInputManager::Kde { fake_input } => { let fake_input = fake_input.clone(); let vinput = VirtualInput::Kde { fake_input }; self.input_for_client.insert(client.handle, vinput); - }, + } } } } enum VirtualInput { - Wlroots { - pointer: Vp, - keyboard: Vk, - }, - Kde { - fake_input: OrgKdeKwinFakeInput, - } + Wlroots { pointer: Vp, keyboard: Vk }, + Kde { fake_input: OrgKdeKwinFakeInput }, } impl VirtualInput { @@ -185,17 +179,18 @@ impl VirtualInput { time, relative_x, relative_y, - } => { - match self { - VirtualInput::Wlroots { pointer, keyboard:_ } => { - pointer.motion(time, relative_x, relative_y); - pointer.frame(); - }, - VirtualInput::Kde { fake_input } => { - fake_input.pointer_motion(relative_y, relative_y); - }, + } => match self { + VirtualInput::Wlroots { + pointer, + keyboard: _, + } => { + pointer.motion(time, relative_x, relative_y); + pointer.frame(); } - } + VirtualInput::Kde { fake_input } => { + fake_input.pointer_motion(relative_y, relative_y); + } + }, PointerEvent::Button { time, button, @@ -203,70 +198,80 @@ impl VirtualInput { } => { let state: ButtonState = state.try_into().unwrap(); match self { - VirtualInput::Wlroots { pointer, keyboard:_ } => { + VirtualInput::Wlroots { + pointer, + keyboard: _, + } => { pointer.button(time, button, state); pointer.frame(); - }, + } VirtualInput::Kde { fake_input } => { fake_input.button(button, state as u32); - }, + } } } PointerEvent::Axis { time, axis, value } => { let axis: Axis = (axis as u32).try_into().unwrap(); match self { - VirtualInput::Wlroots { pointer, keyboard:_ } => { + VirtualInput::Wlroots { + pointer, + keyboard: _, + } => { pointer.axis(time, axis, value); pointer.frame(); - }, + } VirtualInput::Kde { fake_input } => { fake_input.axis(axis as u32, value); - }, + } } } - PointerEvent::Frame {} => { - match self { - VirtualInput::Wlroots { pointer, keyboard:_ } => { - pointer.frame(); - }, - VirtualInput::Kde { fake_input:_ } => { }, + PointerEvent::Frame {} => match self { + VirtualInput::Wlroots { + pointer, + keyboard: _, + } => { + pointer.frame(); } - } + VirtualInput::Kde { fake_input: _ } => {} + }, }, Event::Keyboard(e) => match e { - KeyboardEvent::Key { time, key, state } => { - match self { - VirtualInput::Wlroots { pointer:_, keyboard } => { - keyboard.key(time, key, state as u32); - }, - VirtualInput::Kde { fake_input } => { - fake_input.keyboard_key(key, state as u32); - }, + KeyboardEvent::Key { time, key, state } => match self { + VirtualInput::Wlroots { + pointer: _, + keyboard, + } => { + keyboard.key(time, key, state as u32); } - } + VirtualInput::Kde { fake_input } => { + fake_input.keyboard_key(key, state as u32); + } + }, KeyboardEvent::Modifiers { mods_depressed, mods_latched, mods_locked, group, - } => { - match self { - VirtualInput::Wlroots { pointer:_, keyboard } => { - keyboard.modifiers(mods_depressed, mods_latched, mods_locked, group); - }, - VirtualInput::Kde { fake_input:_ } => { }, + } => match self { + VirtualInput::Wlroots { + pointer: _, + keyboard, + } => { + keyboard.modifiers(mods_depressed, mods_latched, mods_locked, group); } - } + VirtualInput::Kde { fake_input: _ } => {} + }, }, - Event::Release() => { - match self { - VirtualInput::Wlroots { pointer:_, keyboard } => { - keyboard.modifiers(77, 0, 0, 0); - keyboard.modifiers(0, 0, 0, 0); - }, - VirtualInput::Kde { fake_input:_ } => {}, + Event::Release() => match self { + VirtualInput::Wlroots { + pointer: _, + keyboard, + } => { + keyboard.modifiers(77, 0, 0, 0); + keyboard.modifiers(0, 0, 0, 0); } - } + VirtualInput::Kde { fake_input: _ } => {} + }, } Ok(()) } diff --git a/src/backend/x11/consumer.rs b/src/backend/consumer/x11.rs similarity index 100% rename from src/backend/x11/consumer.rs rename to src/backend/consumer/x11.rs diff --git a/src/backend/consumer/xdg_desktop_portal.rs b/src/backend/consumer/xdg_desktop_portal.rs new file mode 100644 index 0000000..2bf80fb --- /dev/null +++ b/src/backend/consumer/xdg_desktop_portal.rs @@ -0,0 +1,9 @@ +use std::sync::mpsc::Receiver; + +use crate::{event::Event, client::{ClientHandle, Client}}; + + + +pub(crate) fn run(_consume_rx: Receiver<(Event, ClientHandle)>, _clients: Vec) { + todo!() +} diff --git a/src/backend/producer.rs b/src/backend/producer.rs new file mode 100644 index 0000000..67a401b --- /dev/null +++ b/src/backend/producer.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "wayland")] +pub mod wayland; +#[cfg(windows)] +pub mod windows; +#[cfg(feature = "x11")] +pub mod x11; diff --git a/src/backend/wayland/producer.rs b/src/backend/producer/wayland.rs similarity index 100% rename from src/backend/wayland/producer.rs rename to src/backend/producer/wayland.rs diff --git a/src/backend/windows/producer.rs b/src/backend/producer/windows.rs similarity index 100% rename from src/backend/windows/producer.rs rename to src/backend/producer/windows.rs diff --git a/src/backend/x11/producer.rs b/src/backend/producer/x11.rs similarity index 100% rename from src/backend/x11/producer.rs rename to src/backend/producer/x11.rs diff --git a/src/backend/wayland.rs b/src/backend/wayland.rs deleted file mode 100644 index 934046a..0000000 --- a/src/backend/wayland.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod consumer; -pub mod producer; diff --git a/src/backend/windows.rs b/src/backend/windows.rs deleted file mode 100644 index 934046a..0000000 --- a/src/backend/windows.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod consumer; -pub mod producer; diff --git a/src/backend/x11.rs b/src/backend/x11.rs deleted file mode 100644 index 934046a..0000000 --- a/src/backend/x11.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod consumer; -pub mod producer; diff --git a/src/client.rs b/src/client.rs index 2865516..022a1a3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,5 +1,7 @@ use std::net::SocketAddr; +use crate::{config, dns}; + #[derive(Eq, Hash, PartialEq, Clone, Copy)] pub enum Position { Left, @@ -34,19 +36,56 @@ pub struct ClientManager { pub type ClientHandle = u32; impl ClientManager { + fn add_client(&mut self, client: &config::Client, pos: Position) { + let ip = match client.ip { + Some(ip) => ip, + None => match &client.host_name { + Some(host_name) => match dns::resolve(host_name) { + Ok(ip) => ip, + Err(e) => panic!("{}", e), + }, + None => panic!("neither ip nor hostname specified"), + }, + }; + let addr = SocketAddr::new(ip, client.port.unwrap_or(42069)); + self.register_client(addr, pos); + } + fn new_id(&mut self) -> ClientHandle { self.next_id += 1; self.next_id } - pub fn new() -> Self { - ClientManager { + pub fn new(config: &config::Config) -> Self { + + let mut client_manager = ClientManager { next_id: 0, clients: Vec::new(), + }; + + // add clients from config + for client in vec![ + &config.client.left, + &config.client.right, + &config.client.top, + &config.client.bottom, + ] { + if let Some(client) = client { + let pos = match client { + client if Some(client) == config.client.left.as_ref() => Position::Left, + client if Some(client) == config.client.right.as_ref() => Position::Right, + client if Some(client) == config.client.top.as_ref() => Position::Top, + client if Some(client) == config.client.bottom.as_ref() => Position::Bottom, + _ => panic!(), + }; + client_manager.add_client(client, pos); + } } + + client_manager } - pub fn add_client(&mut self, addr: SocketAddr, pos: Position) { + pub fn register_client(&mut self, addr: SocketAddr, pos: Position) { let handle = self.new_id(); let client = Client { addr, pos, handle }; self.clients.push(client); diff --git a/src/config.rs b/src/config.rs index 13f6f7d..29ad2fc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,6 +7,7 @@ use toml; pub struct Config { pub client: Clients, pub port: Option, + pub backend: Option, } #[derive(Serialize, Deserialize, Debug)] diff --git a/src/consumer.rs b/src/consumer.rs new file mode 100644 index 0000000..cfa5e40 --- /dev/null +++ b/src/consumer.rs @@ -0,0 +1,70 @@ +use std::{thread::{JoinHandle, self}, env, sync::mpsc::Receiver}; + +use crate::{backend::consumer, client::{Client, ClientHandle}, event::Event}; + +#[derive(Debug)] +enum Backend { + Wlroots, + X11, + RemoteDesktopPortal, + Libei, +} + +pub fn start(consume_rx: Receiver<(Event, ClientHandle)>, clients: Vec, backend: Option) -> JoinHandle<()> { + thread::Builder::new() + .name("event consumer".into()) + .spawn(move || { + #[cfg(windows)] + consumer::windows::run(consume_rx, clients); + + #[cfg(unix)] + let backend = match env::var("XDG_SESSION_TYPE") { + Ok(session_type) => match session_type.as_str() { + "x11" => Backend::X11, + "wayland" => { + match backend { + Some(backend) => match backend.as_str() { + "wlroots" => Backend::Wlroots, + "libei" => Backend::Libei, + "xdg_desktop_portal" => Backend::RemoteDesktopPortal, + backend => panic!("invalid backend: {}", backend) + } + // default to wlroots backend for now + _ => Backend::Wlroots, + } + } + _ => panic!("unknown XDG_SESSION_TYPE"), + }, + Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"), + }; + + #[cfg(unix)] + match backend { + Backend::Libei => { + #[cfg(not(feature = "libei"))] + panic!("feature libei not enabled"); + #[cfg(feature = "libei")] + consumer::libei::run(consume_rx, clients); + }, + Backend::RemoteDesktopPortal => { + #[cfg(not(feature = "xdg_desktop_portal"))] + panic!("feature xdg_desktop_portal not enabled"); + #[cfg(feature = "xdg_desktop_portal")] + consumer::xdg_desktop_portal::run(consume_rx, clients); + }, + Backend::Wlroots => { + #[cfg(not(feature = "wayland"))] + panic!("feature wayland not enabled"); + #[cfg(feature = "wayland")] + consumer::wlroots::run(consume_rx, clients); + }, + Backend::X11 => { + #[cfg(not(feature = "x11"))] + panic!("feature x11 not enabled"); + #[cfg(feature = "x11")] + consumer::x11::run(consume_rx, clients); + }, + } + }) + .unwrap() +} diff --git a/src/lib.rs b/src/lib.rs index 62b258e..cb0d557 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,7 @@ pub mod dns; pub mod event; pub mod request; +pub mod consumer; +pub mod producer; + pub mod backend; diff --git a/src/main.rs b/src/main.rs index 49ce4ae..04e8729 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,34 +1,11 @@ -use std::{net::SocketAddr, sync::mpsc, thread}; - -#[cfg(unix)] -use std::env; +use std::sync::mpsc; use lan_mouse::{ - client::{ClientManager, Position}, - config, dns, event, request, + client::ClientManager, + consumer, producer, + config, event, request, }; -#[cfg(windows)] -use lan_mouse::backend::windows; - -#[cfg(unix)] -use lan_mouse::backend::{wayland, x11, Backend}; - -fn add_client(client_manager: &mut ClientManager, client: &config::Client, pos: Position) { - let ip = match client.ip { - Some(ip) => ip, - None => match &client.host_name { - Some(host_name) => match dns::resolve(host_name) { - Ok(ip) => ip, - Err(e) => panic!("{}", e), - }, - None => panic!("neither ip nor hostname specified"), - }, - }; - let addr = SocketAddr::new(ip, client.port.unwrap_or(42069)); - client_manager.add_client(addr, pos); -} - pub fn main() { // parse config file let config = config::Config::new("config.toml").unwrap(); @@ -42,83 +19,15 @@ pub fn main() { // event channel for consuming events let (consume_tx, consume_rx) = mpsc::sync_channel(128); - let mut client_manager = ClientManager::new(); - - // add clients from config - for client in vec![ - &config.client.left, - &config.client.right, - &config.client.top, - &config.client.bottom, - ] { - if let Some(client) = client { - let pos = match client { - client if Some(client) == config.client.left.as_ref() => Position::Left, - client if Some(client) == config.client.right.as_ref() => Position::Right, - client if Some(client) == config.client.top.as_ref() => Position::Top, - client if Some(client) == config.client.bottom.as_ref() => Position::Bottom, - _ => panic!(), - }; - add_client(&mut client_manager, client, pos); - } - } + // create client manager + let mut client_manager = ClientManager::new(&config); // start receiving client connection requests let (request_server, request_thread) = request::Server::listen(port).unwrap(); - let clients = client_manager.get_clients(); - - #[cfg(unix)] - let backend = match env::var("XDG_SESSION_TYPE") { - Ok(session_type) => match session_type.as_str() { - "x11" => Backend::X11, - "wayland" => Backend::WAYLAND, - _ => panic!("unknown XDG_SESSION_TYPE"), - }, - Err(_) => panic!("could not detect session type"), - }; - - #[cfg(windows)] - println!("using backend: windows"); - - #[cfg(unix)] - println!( - "using backend: {}", - match backend { - Backend::X11 => "x11", - Backend::WAYLAND => "wayland", - } - ); - // start producing and consuming events - let event_producer = thread::Builder::new() - .name("event producer".into()) - .spawn(move || { - #[cfg(windows)] - windows::producer::run(produce_tx, request_server, clients); - - #[cfg(unix)] - match backend { - Backend::X11 => x11::producer::run(produce_tx, request_server, clients), - Backend::WAYLAND => wayland::producer::run(produce_tx, request_server, clients), - } - }) - .unwrap(); - - let clients = client_manager.get_clients(); - let event_consumer = thread::Builder::new() - .name("event consumer".into()) - .spawn(move || { - #[cfg(windows)] - windows::consumer::run(consume_rx, clients); - - #[cfg(unix)] - match backend { - Backend::X11 => x11::consumer::run(consume_rx, clients), - Backend::WAYLAND => wayland::consumer::run(consume_rx, clients), - } - }) - .unwrap(); + let event_producer = producer::start(produce_tx, client_manager.get_clients(), request_server); + let event_consumer = consumer::start(consume_rx, client_manager.get_clients(), config.backend); // start sending and receiving events let event_server = event::server::Server::new(port); diff --git a/src/producer.rs b/src/producer.rs new file mode 100644 index 0000000..a75cfbf --- /dev/null +++ b/src/producer.rs @@ -0,0 +1,52 @@ +#[cfg(unix)] +use std::env; +use std::{thread::{JoinHandle, self}, sync::mpsc::SyncSender}; + +use crate::{client::{Client, ClientHandle}, event::Event, request::Server}; + +use crate::backend::producer; + +enum Backend { + Wayland, + X11, +} + +pub fn start( + produce_tx: SyncSender<(Event, ClientHandle)>, + clients: Vec, + request_server: Server, +) -> JoinHandle<()> { + thread::Builder::new() + .name("event producer".into()) + .spawn(move || { + #[cfg(windows)] + producer::windows::run(produce_tx, request_server, clients); + + #[cfg(unix)] + let backend = match env::var("XDG_SESSION_TYPE") { + Ok(session_type) => match session_type.as_str() { + "x11" => Backend::X11, + "wayland" => Backend::Wayland, + _ => panic!("unknown XDG_SESSION_TYPE"), + }, + Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"), + }; + + #[cfg(unix)] + match backend { + Backend::X11 => { + #[cfg(not(feature = "x11"))] + panic!("feature x11 not enabled"); + #[cfg(feature = "x11")] + producer::x11::run(produce_tx, request_server, clients); + } + Backend::Wayland => { + #[cfg(not(feature = "wayland"))] + panic!("feature wayland not enabled"); + #[cfg(feature = "wayland")] + producer::wayland::run(produce_tx, request_server, clients); + } + } + }) + .unwrap() +}