From 2c5c500786364129946ae5d24e4190bf104468d9 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 16 Jan 2024 18:46:54 -0600 Subject: [PATCH 01/26] Start rewriting Rust API --- api/rust_grpc/Cargo.toml | 8 ++++++++ api/rust_grpc/src/lib.rs | 14 ++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 api/rust_grpc/Cargo.toml create mode 100644 api/rust_grpc/src/lib.rs diff --git a/api/rust_grpc/Cargo.toml b/api/rust_grpc/Cargo.toml new file mode 100644 index 0000000..afeb07b --- /dev/null +++ b/api/rust_grpc/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rust_grpc" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/api/rust_grpc/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From 2bf8e25e215a3f29e70e16ac7e0b993bdcc36ac3 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Wed, 17 Jan 2024 21:55:34 -0600 Subject: [PATCH 02/26] Add a bunch of functions --- Cargo.toml | 1 + api/rust_grpc/.gitignore | 1 + api/rust_grpc/Cargo.toml | 14 +- api/rust_grpc/src/input.rs | 164 +++++++++++++++++++ api/rust_grpc/src/input/libinput.rs | 44 ++++++ api/rust_grpc/src/lib.rs | 50 ++++-- api/rust_grpc/src/output.rs | 144 +++++++++++++++++ api/rust_grpc/src/process.rs | 88 +++++++++++ api/rust_grpc/src/tag.rs | 84 ++++++++++ api/rust_grpc/src/util.rs | 7 + api/rust_grpc/src/window.rs | 235 ++++++++++++++++++++++++++++ 11 files changed, 817 insertions(+), 15 deletions(-) create mode 100644 api/rust_grpc/.gitignore create mode 100644 api/rust_grpc/src/input.rs create mode 100644 api/rust_grpc/src/input/libinput.rs create mode 100644 api/rust_grpc/src/output.rs create mode 100644 api/rust_grpc/src/process.rs create mode 100644 api/rust_grpc/src/tag.rs create mode 100644 api/rust_grpc/src/util.rs create mode 100644 api/rust_grpc/src/window.rs diff --git a/Cargo.toml b/Cargo.toml index c3054ca..5e4322a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,3 +65,4 @@ xwayland = ["smithay/xwayland", "x11rb", "smithay/x11rb_event_source", "xcursor" [workspace] members = ["pinnacle-api-defs"] +exclude = ["api/rust_grpc"] diff --git a/api/rust_grpc/.gitignore b/api/rust_grpc/.gitignore new file mode 100644 index 0000000..03314f7 --- /dev/null +++ b/api/rust_grpc/.gitignore @@ -0,0 +1 @@ +Cargo.lock diff --git a/api/rust_grpc/Cargo.toml b/api/rust_grpc/Cargo.toml index afeb07b..e4beb69 100644 --- a/api/rust_grpc/Cargo.toml +++ b/api/rust_grpc/Cargo.toml @@ -1,8 +1,14 @@ [package] -name = "rust_grpc" -version = "0.1.0" +name = "pinnacle-api" +version = "0.0.1" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] +pinnacle-api-defs = { path = "../../pinnacle-api-defs" } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] } +tokio-stream = { version = "0.1.14", features = ["net"] } +tonic = "0.10.2" +tower = { version = "0.4.13", features = ["util"] } +futures-lite = "2.2.0" +num_enum = "0.7.2" +xkbcommon = "0.7.0" diff --git a/api/rust_grpc/src/input.rs b/api/rust_grpc/src/input.rs new file mode 100644 index 0000000..613858b --- /dev/null +++ b/api/rust_grpc/src/input.rs @@ -0,0 +1,164 @@ +use futures_lite::future::block_on; +use num_enum::TryFromPrimitive; +use pinnacle_api_defs::pinnacle::input::{ + self, + v0alpha1::{ + input_service_client::InputServiceClient, SetKeybindRequest, SetMousebindRequest, + SetRepeatRateRequest, + }, +}; +use tokio_stream::StreamExt; +use tonic::transport::Channel; +use xkbcommon::xkb::Keysym; + +pub use pinnacle_api_defs::pinnacle::input::v0alpha1::SetXkbConfigRequest as XkbConfig; + +use self::libinput::LibinputSetting; +pub mod libinput; + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub enum MouseButton { + Left = 0x110, + Right = 0x111, + Middle = 0x112, + Side = 0x113, + Extra = 0x114, + Forward = 0x115, + Back = 0x116, +} + +#[repr(i32)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] +pub enum Mod { + Shift = 1, + Ctrl, + Alt, + Super, +} + +#[repr(i32)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] +pub enum MouseEdge { + Press = 1, + Release, +} + +pub struct Input { + client: InputServiceClient, +} + +impl Input { + pub(crate) fn new(client: InputServiceClient) -> Self { + Self { client } + } + + pub fn keybind( + &self, + mods: impl IntoIterator, + key: impl Key + Send + 'static, + mut action: impl FnMut() + 'static + Send, + ) { + let mut client = self.client.clone(); + + let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); + + tokio::spawn(async move { + let mut stream = client + .set_keybind(SetKeybindRequest { + modifiers, + key: Some(input::v0alpha1::set_keybind_request::Key::RawCode( + key.into_keysym().raw(), + )), + }) + .await + .unwrap() + .into_inner(); + + while let Some(Ok(_response)) = stream.next().await { + action(); + } + }); + } + + pub fn mousebind( + &self, + mods: impl IntoIterator, + button: MouseButton, + edge: MouseEdge, + mut action: impl FnMut() + 'static + Send, + ) { + let mut client = self.client.clone(); + + let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); + + tokio::spawn(async move { + let mut stream = client + .set_mousebind(SetMousebindRequest { + modifiers, + button: Some(button as u32), + edge: Some(edge as i32), + }) + .await + .unwrap() + .into_inner(); + + while let Some(Ok(_response)) = stream.next().await { + action(); + } + }); + } + + pub fn set_xkb_config(&self, xkb_config: XkbConfig) { + let mut client = self.client.clone(); + + block_on(client.set_xkb_config(xkb_config)).unwrap(); + } + + pub fn set_repeat_rate(&self, rate: i32, delay: i32) { + let mut client = self.client.clone(); + + block_on(client.set_repeat_rate(SetRepeatRateRequest { + rate: Some(rate), + delay: Some(delay), + })) + .unwrap(); + } + + pub fn set_libinput_setting(setting: LibinputSetting) { + todo!() + } +} + +pub trait Key { + fn into_keysym(self) -> Keysym; +} + +impl Key for Keysym { + fn into_keysym(self) -> Keysym { + self + } +} + +impl Key for char { + fn into_keysym(self) -> Keysym { + Keysym::from_char(self) + } +} + +impl Key for &str { + fn into_keysym(self) -> Keysym { + xkbcommon::xkb::keysym_from_name(self, xkbcommon::xkb::KEYSYM_NO_FLAGS) + } +} + +impl Key for String { + fn into_keysym(self) -> Keysym { + xkbcommon::xkb::keysym_from_name(&self, xkbcommon::xkb::KEYSYM_NO_FLAGS) + } +} + +impl Key for u32 { + fn into_keysym(self) -> Keysym { + Keysym::from(self) + } +} diff --git a/api/rust_grpc/src/input/libinput.rs b/api/rust_grpc/src/input/libinput.rs new file mode 100644 index 0000000..b0db1b5 --- /dev/null +++ b/api/rust_grpc/src/input/libinput.rs @@ -0,0 +1,44 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AccelProfile { + Flat = 1, + Adaptive, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ClickMethod { + ButtonAreas = 1, + Clickfinger, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ScrollMethod { + NoScroll = 1, + TwoFinger, + Edge, + OnButtonDown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TapButtonMap { + LeftRightMiddle, + LeftMiddleRight, +} + +pub enum LibinputSetting { + AccelProfile(AccelProfile), + AccelSpeed(f64), + CalibrationMatrix([f32; 6]), + ClickMethod(ClickMethod), + DisableWhileTyping(bool), + LeftHanded(bool), + MiddleEmulation(bool), + RotationAngle(u32), + ScrollButton(u32), + ScrollButtonLock(u32), + ScrollMethod(ScrollMethod), + NaturalScroll(bool), + TapButtonMap(TapButtonMap), + TapDrag(bool), + TapDragLock(bool), + Tap(bool), +} diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs index 7d12d9a..8b7da88 100644 --- a/api/rust_grpc/src/lib.rs +++ b/api/rust_grpc/src/lib.rs @@ -1,14 +1,42 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right +// #![warn(missing_docs)] + +use pinnacle_api_defs::pinnacle::{ + input::v0alpha1::input_service_client::InputServiceClient, + output::v0alpha1::output_service_client::OutputServiceClient, + process::v0alpha1::process_service_client::ProcessServiceClient, + tag::v0alpha1::tag_service_client::TagServiceClient, + v0alpha1::pinnacle_service_client::PinnacleServiceClient, + window::v0alpha1::window_service_client::WindowServiceClient, +}; +use tokio::net::UnixStream; +use tonic::transport::{Channel, Endpoint, Uri}; +use tower::service_fn; + +pub mod input; +pub mod output; +pub mod process; +pub mod tag; +pub mod util; +pub mod window; + +pub fn setup() -> Result<(), Box> { + let channel = futures_lite::future::block_on(connect())?; + + let pinnacle_client = PinnacleServiceClient::new(channel.clone()); + let window_client = WindowServiceClient::new(channel.clone()); + let input_client = InputServiceClient::new(channel.clone()); + let output_client = OutputServiceClient::new(channel.clone()); + let tag_client = TagServiceClient::new(channel.clone()); + let process_client = ProcessServiceClient::new(channel.clone()); + + Ok(()) } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } +async fn connect() -> Result> { + Endpoint::try_from("http://[::]:50051")? + .connect_with_connector(service_fn(|_: Uri| { + UnixStream::connect(std::env::var("PINNACLE_GRPC_SOCKET").unwrap()) + })) + .await + .map_err(|err| err.into()) } diff --git a/api/rust_grpc/src/output.rs b/api/rust_grpc/src/output.rs new file mode 100644 index 0000000..458f5b4 --- /dev/null +++ b/api/rust_grpc/src/output.rs @@ -0,0 +1,144 @@ +use futures_lite::future::block_on; +use pinnacle_api_defs::pinnacle::{ + output::{ + self, + v0alpha1::{ + output_service_client::OutputServiceClient, ConnectForAllRequest, SetLocationRequest, + }, + }, + tag::v0alpha1::tag_service_client::TagServiceClient, +}; +use tokio_stream::StreamExt; +use tonic::transport::Channel; + +use crate::tag::TagHandle; + +pub struct Output { + client: OutputServiceClient, + tag_client: TagServiceClient, +} + +impl Output { + pub(crate) fn new( + client: OutputServiceClient, + tag_client: TagServiceClient, + ) -> Self { + Self { client, tag_client } + } + + pub fn get_all(&self) -> impl Iterator { + let mut client = self.client.clone(); + let tag_client = self.tag_client.clone(); + block_on(client.get(output::v0alpha1::GetRequest {})) + .unwrap() + .into_inner() + .output_names + .into_iter() + .map(move |name| OutputHandle { + client: client.clone(), + tag_client: tag_client.clone(), + name, + }) + } + + pub fn get_focused(&self) -> Option { + self.get_all() + .find(|output| matches!(output.props().focused, Some(true))) + } + + pub fn connect_for_all(&self, mut for_all: impl FnMut(OutputHandle) + 'static + Send) { + for output in self.get_all() { + for_all(output); + } + + let mut client = self.client.clone(); + let tag_client = self.tag_client.clone(); + + tokio::spawn(async move { + let mut stream = client + .connect_for_all(ConnectForAllRequest {}) + .await + .unwrap() + .into_inner(); + + while let Some(Ok(response)) = stream.next().await { + let Some(output_name) = response.output_name else { + continue; + }; + + let output = OutputHandle { + client: client.clone(), + tag_client: tag_client.clone(), + name: output_name, + }; + + for_all(output); + } + }); + } +} + +pub struct OutputHandle { + client: OutputServiceClient, + tag_client: TagServiceClient, + name: String, +} + +impl OutputHandle { + pub fn set_location(&self, x: Option, y: Option) { + let mut client = self.client.clone(); + block_on(client.set_location(SetLocationRequest { + output_name: Some(self.name.clone()), + x, + y, + })) + .unwrap(); + } + + pub fn props(&self) -> OutputProperties { + let mut client = self.client.clone(); + let response = block_on( + client.get_properties(output::v0alpha1::GetPropertiesRequest { + output_name: Some(self.name.clone()), + }), + ) + .unwrap() + .into_inner(); + + OutputProperties { + make: response.make, + model: response.model, + x: response.x, + y: response.y, + pixel_width: response.pixel_width, + pixel_height: response.pixel_height, + refresh_rate: response.refresh_rate, + physical_width: response.physical_width, + physical_height: response.physical_height, + focused: response.focused, + tags: response + .tag_ids + .into_iter() + .map(|id| TagHandle { + client: self.tag_client.clone(), + id, + }) + .collect(), + } + } +} + +#[derive(Clone, Debug)] +pub struct OutputProperties { + pub make: Option, + pub model: Option, + pub x: Option, + pub y: Option, + pub pixel_width: Option, + pub pixel_height: Option, + pub refresh_rate: Option, + pub physical_width: Option, + pub physical_height: Option, + pub focused: Option, + pub tags: Vec, +} diff --git a/api/rust_grpc/src/process.rs b/api/rust_grpc/src/process.rs new file mode 100644 index 0000000..fcdaf7c --- /dev/null +++ b/api/rust_grpc/src/process.rs @@ -0,0 +1,88 @@ +use pinnacle_api_defs::pinnacle::process::v0alpha1::{ + process_service_client::ProcessServiceClient, SpawnRequest, +}; +use tokio_stream::StreamExt; +use tonic::transport::Channel; + +#[derive(Debug, Clone)] +pub struct Process { + client: ProcessServiceClient, +} + +pub struct SpawnCallbacks { + pub stdout: Option>, + pub stderr: Option>, + pub exit: Option, String) + 'static + Send>>, +} + +impl Process { + pub(crate) fn new(client: ProcessServiceClient) -> Self { + Self { client } + } + + pub fn spawn(&self, args: impl IntoIterator>) { + self.spawn_inner(args, false, None); + } + + pub fn spawn_with_callbacks( + &self, + args: impl IntoIterator>, + callbacks: SpawnCallbacks, + ) { + self.spawn_inner(args, false, Some(callbacks)); + } + + pub fn spawn_once(&self, args: impl IntoIterator>) { + self.spawn_inner(args, true, None); + } + + pub fn spawn_once_with_callbacks( + &self, + args: impl IntoIterator>, + callbacks: SpawnCallbacks, + ) { + self.spawn_inner(args, true, Some(callbacks)); + } + + fn spawn_inner( + &self, + args: impl IntoIterator>, + once: bool, + callbacks: Option, + ) { + let args = args + .into_iter() + .map(|arg| Into::::into(arg)) + .collect::>(); + + let request = SpawnRequest { + args, + once: Some(once), + has_callback: Some(callbacks.is_some()), + }; + + let mut client = self.client.clone(); + + tokio::spawn(async move { + let mut stream = client.spawn(request).await.unwrap().into_inner(); + let Some(mut callbacks) = callbacks else { return }; + while let Some(Ok(response)) = stream.next().await { + if let Some(line) = response.stdout { + if let Some(stdout) = callbacks.stdout.as_mut() { + stdout(line); + } + } + if let Some(line) = response.stderr { + if let Some(stderr) = callbacks.stderr.as_mut() { + stderr(line); + } + } + if let Some(exit_msg) = response.exit_message { + if let Some(exit) = callbacks.exit.as_mut() { + exit(response.exit_code, exit_msg); + } + } + } + }); + } +} diff --git a/api/rust_grpc/src/tag.rs b/api/rust_grpc/src/tag.rs new file mode 100644 index 0000000..4712054 --- /dev/null +++ b/api/rust_grpc/src/tag.rs @@ -0,0 +1,84 @@ +use futures_lite::future::block_on; +use num_enum::TryFromPrimitive; +use pinnacle_api_defs::pinnacle::tag::{ + self, + v0alpha1::{ + tag_service_client::TagServiceClient, RemoveRequest, SetActiveRequest, SetLayoutRequest, + }, +}; +use tonic::transport::Channel; + +#[derive(Clone, Debug)] +pub struct Tag { + client: TagServiceClient, +} + +impl Tag { + pub(crate) fn new(client: TagServiceClient) -> Self { + Self { client } + } +} + +#[derive(Debug, Clone)] +pub struct TagHandle { + pub(crate) client: TagServiceClient, + pub(crate) id: u32, +} + +#[repr(i32)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] +pub enum Layout { + MasterStack = 1, + Dwindle, + Spiral, + CornerTopLeft, + CornerTopRight, + CornerBottomLeft, + CornerBottomRight, +} + +impl TagHandle { + pub fn set_active(&self, set: bool) { + let mut client = self.client.clone(); + block_on(client.set_active(SetActiveRequest { + tag_id: Some(self.id), + set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Set(set)), + })) + .unwrap(); + } + + pub fn toggle_active(&self) { + let mut client = self.client.clone(); + block_on(client.set_active(SetActiveRequest { + tag_id: Some(self.id), + set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Toggle(())), + })) + .unwrap(); + } + + pub fn remove(mut self) { + block_on(self.client.remove(RemoveRequest { + tag_ids: vec![self.id], + })) + .unwrap(); + } + + pub fn set_layout(&self, layout: Layout) { + let mut client = self.client.clone(); + block_on(client.set_layout(SetLayoutRequest { + tag_id: Some(self.id), + layout: Some(layout as i32), + })) + .unwrap(); + } + + pub fn props(&self) -> TagProperties { + todo!() + } +} + +pub struct TagProperties { + pub active: Option, + pub name: Option, + pub output: (), +} diff --git a/api/rust_grpc/src/util.rs b/api/rust_grpc/src/util.rs new file mode 100644 index 0000000..828557b --- /dev/null +++ b/api/rust_grpc/src/util.rs @@ -0,0 +1,7 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Geometry { + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, +} diff --git a/api/rust_grpc/src/window.rs b/api/rust_grpc/src/window.rs new file mode 100644 index 0000000..2a2bac3 --- /dev/null +++ b/api/rust_grpc/src/window.rs @@ -0,0 +1,235 @@ +use futures_lite::future::block_on; +use num_enum::TryFromPrimitive; +use pinnacle_api_defs::pinnacle::{ + tag::v0alpha1::tag_service_client::TagServiceClient, + window::v0alpha1::{ + window_service_client::WindowServiceClient, MoveToTagRequest, SetTagRequest, + }, + window::{ + self, + v0alpha1::{ + GetRequest, MoveGrabRequest, ResizeGrabRequest, SetFloatingRequest, + SetFullscreenRequest, SetMaximizedRequest, + }, + }, +}; +use tonic::transport::Channel; + +use crate::{input::MouseButton, tag::TagHandle, util::Geometry}; + +#[derive(Debug, Clone)] +pub struct Window { + client: WindowServiceClient, + tag_client: TagServiceClient, +} + +impl Window { + pub(crate) fn new( + client: WindowServiceClient, + tag_client: TagServiceClient, + ) -> Self { + Self { client, tag_client } + } + + /// Get all windows. + pub fn get_all(&self) -> impl Iterator { + let mut client = self.client.clone(); + let tag_client = self.tag_client.clone(); + block_on(client.get(GetRequest {})) + .unwrap() + .into_inner() + .window_ids + .into_iter() + .map(move |id| WindowHandle { + client: client.clone(), + id, + tag_client: tag_client.clone(), + }) + } + + /// Get the currently focused window. + pub fn get_focused(&self) -> Option { + self.get_all() + .find(|window| matches!(window.props().focused, Some(true))) + } +} + +#[derive(Debug, Clone)] +pub struct WindowHandle { + pub(crate) client: WindowServiceClient, + pub(crate) id: u32, + pub(crate) tag_client: TagServiceClient, +} + +#[repr(i32)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] +pub enum FullscreenOrMaximized { + Neither = 1, + Fullscreen, + Maximized, +} + +#[derive(Debug, Clone)] +pub struct WindowProperties { + pub geometry: Option, + pub class: Option, + pub title: Option, + pub focused: Option, + pub floating: Option, + pub fullscreen_or_maximized: Option, + pub tags: Vec, +} + +impl WindowHandle { + pub fn set_fullscreen(&mut self, set: bool) { + let mut client = self.client.clone(); + block_on(client.set_fullscreen(SetFullscreenRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Set( + set, + )), + })) + .unwrap(); + } + + pub fn toggle_fullscreen(&mut self) { + let mut client = self.client.clone(); + block_on(client.set_fullscreen(SetFullscreenRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Toggle(())), + })) + .unwrap(); + } + + pub fn set_maximized(&mut self, set: bool) { + let mut client = self.client.clone(); + block_on(client.set_maximized(SetMaximizedRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Set( + set, + )), + })) + .unwrap(); + } + + pub fn toggle_maximized(&mut self) { + let mut client = self.client.clone(); + block_on(client.set_maximized(SetMaximizedRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Toggle(())), + })) + .unwrap(); + } + + pub fn set_floating(&mut self, set: bool) { + let mut client = self.client.clone(); + block_on(client.set_floating(SetFloatingRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Set( + set, + )), + })) + .unwrap(); + } + + pub fn toggle_floating(&mut self) { + let mut client = self.client.clone(); + block_on(client.set_floating(SetFloatingRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Toggle( + (), + )), + })) + .unwrap(); + } + + pub fn move_to_tag(&self, tag: &TagHandle) { + let mut client = self.client.clone(); + + block_on(client.move_to_tag(MoveToTagRequest { + window_id: Some(self.id), + tag_id: Some(tag.id), + })) + .unwrap(); + } + + pub fn set_tag(&self, tag: &TagHandle, set: bool) { + let mut client = self.client.clone(); + + block_on(client.set_tag(SetTagRequest { + window_id: Some(self.id), + tag_id: Some(tag.id), + set_or_toggle: Some(window::v0alpha1::set_tag_request::SetOrToggle::Set(set)), + })) + .unwrap(); + } + + pub fn toggle_tag(&self, tag: &TagHandle) { + let mut client = self.client.clone(); + + block_on(client.set_tag(SetTagRequest { + window_id: Some(self.id), + tag_id: Some(tag.id), + set_or_toggle: Some(window::v0alpha1::set_tag_request::SetOrToggle::Toggle(())), + })) + .unwrap(); + } + + pub fn begin_move(&self, button: MouseButton) { + let mut client = self.client.clone(); + block_on(client.move_grab(MoveGrabRequest { + button: Some(button as u32), + })) + .unwrap(); + } + + pub fn begin_resize(&self, button: MouseButton) { + let mut client = self.client.clone(); + block_on(client.resize_grab(ResizeGrabRequest { + button: Some(button as u32), + })) + .unwrap(); + } + + pub fn props(&self) -> WindowProperties { + let mut client = self.client.clone(); + let tag_client = self.tag_client.clone(); + let response = block_on( + client.get_properties(window::v0alpha1::GetPropertiesRequest { + window_id: Some(self.id), + }), + ) + .unwrap() + .into_inner(); + + let fullscreen_or_maximized = response + .fullscreen_or_maximized + .unwrap_or_default() + .try_into() + .ok(); + + let geometry = response.geometry.map(|geo| Geometry { + x: geo.x(), + y: geo.y(), + width: geo.width() as u32, + height: geo.height() as u32, + }); + + WindowProperties { + geometry, + class: response.class, + title: response.title, + focused: response.focused, + floating: response.floating, + fullscreen_or_maximized, + tags: response + .tag_ids + .into_iter() + .map(|id| TagHandle { + client: tag_client.clone(), + id, + }) + .collect(), + } + } +} From 2eea7bf71b1c5880b19c54193852bf9442010d8c Mon Sep 17 00:00:00 2001 From: Ottatop Date: Thu, 18 Jan 2024 23:11:25 -0600 Subject: [PATCH 03/26] Add a bunch more functions --- Cargo.toml | 2 +- api/rust_grpc/Cargo.toml | 6 +- api/rust_grpc/examples/default_config.rs | 18 +++ api/rust_grpc/pinnacle-api-macros/Cargo.toml | 14 +++ api/rust_grpc/pinnacle-api-macros/src/lib.rs | 31 +++++ api/rust_grpc/src/input.rs | 81 +++++++++---- api/rust_grpc/src/input/libinput.rs | 3 +- api/rust_grpc/src/lib.rs | 115 +++++++++++++++++-- api/rust_grpc/src/output.rs | 9 +- api/rust_grpc/src/pinnacle.rs | 21 ++++ api/rust_grpc/src/process.rs | 81 ++++++++----- api/rust_grpc/src/tag.rs | 4 +- api/rust_grpc/src/window.rs | 4 +- 13 files changed, 319 insertions(+), 70 deletions(-) create mode 100644 api/rust_grpc/examples/default_config.rs create mode 100644 api/rust_grpc/pinnacle-api-macros/Cargo.toml create mode 100644 api/rust_grpc/pinnacle-api-macros/src/lib.rs create mode 100644 api/rust_grpc/src/pinnacle.rs diff --git a/Cargo.toml b/Cargo.toml index 5e4322a..6d17cde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,4 +65,4 @@ xwayland = ["smithay/xwayland", "x11rb", "smithay/x11rb_event_source", "xcursor" [workspace] members = ["pinnacle-api-defs"] -exclude = ["api/rust_grpc"] +exclude = ["api/rust_grpc", "api/rust"] diff --git a/api/rust_grpc/Cargo.toml b/api/rust_grpc/Cargo.toml index e4beb69..9f301d6 100644 --- a/api/rust_grpc/Cargo.toml +++ b/api/rust_grpc/Cargo.toml @@ -5,10 +5,14 @@ edition = "2021" [dependencies] pinnacle-api-defs = { path = "../../pinnacle-api-defs" } +pinnacle-api-macros = { path = "./pinnacle-api-macros" } tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] } tokio-stream = { version = "0.1.14", features = ["net"] } tonic = "0.10.2" tower = { version = "0.4.13", features = ["util"] } -futures-lite = "2.2.0" +futures = "0.3.30" num_enum = "0.7.2" xkbcommon = "0.7.0" + +[workspace] +members = ["pinnacle-api-macros"] diff --git a/api/rust_grpc/examples/default_config.rs b/api/rust_grpc/examples/default_config.rs new file mode 100644 index 0000000..90c80f4 --- /dev/null +++ b/api/rust_grpc/examples/default_config.rs @@ -0,0 +1,18 @@ +use pinnacle_api::{input::Mod, ApiModules}; + +#[pinnacle_api::config(modules)] +#[tokio::main] +async fn main() { + let ApiModules { + pinnacle, + process, + window, + input, + output, + tag, + } = modules; + + input.keybind([Mod::Shift], 'a', || { + process.spawn(["alacritty"]); + }); +} diff --git a/api/rust_grpc/pinnacle-api-macros/Cargo.toml b/api/rust_grpc/pinnacle-api-macros/Cargo.toml new file mode 100644 index 0000000..9f4cb66 --- /dev/null +++ b/api/rust_grpc/pinnacle-api-macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pinnacle-api-macros" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +quote = "1.0.35" +syn = { version = "2.0.48", features = ["full"] } +proc-macro2 = "1.0.76" + +[lib] +proc-macro = true diff --git a/api/rust_grpc/pinnacle-api-macros/src/lib.rs b/api/rust_grpc/pinnacle-api-macros/src/lib.rs new file mode 100644 index 0000000..76bbc89 --- /dev/null +++ b/api/rust_grpc/pinnacle-api-macros/src/lib.rs @@ -0,0 +1,31 @@ +use quote::quote; +use syn::parse_macro_input; + +#[proc_macro_attribute] +pub fn config( + args: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let mut item = parse_macro_input!(item as syn::ItemFn); + let module_ident = parse_macro_input!(args as syn::Ident); + + let vis = item.vis; + + item.sig.inputs = syn::punctuated::Punctuated::new(); + let sig = item.sig; + + let attrs = item.attrs; + let stmts = item.block.stmts; + + quote! { + #(#attrs)* + #vis #sig { + let (#module_ident, __fut_receiver) = ::pinnacle_api::create_modules().unwrap(); + + #(#stmts)* + + ::pinnacle_api::listen(__fut_receiver); + } + } + .into() +} diff --git a/api/rust_grpc/src/input.rs b/api/rust_grpc/src/input.rs index 613858b..f500856 100644 --- a/api/rust_grpc/src/input.rs +++ b/api/rust_grpc/src/input.rs @@ -1,10 +1,11 @@ -use futures_lite::future::block_on; +use futures::executor::block_on; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::input::{ self, v0alpha1::{ - input_service_client::InputServiceClient, SetKeybindRequest, SetMousebindRequest, - SetRepeatRateRequest, + input_service_client::InputServiceClient, + set_libinput_setting_request::{CalibrationMatrix, Setting}, + SetKeybindRequest, SetLibinputSettingRequest, SetMousebindRequest, SetRepeatRateRequest, }, }; use tokio_stream::StreamExt; @@ -13,6 +14,8 @@ use xkbcommon::xkb::Keysym; pub use pinnacle_api_defs::pinnacle::input::v0alpha1::SetXkbConfigRequest as XkbConfig; +use crate::FutSender; + use self::libinput::LibinputSetting; pub mod libinput; @@ -43,41 +46,45 @@ pub enum MouseEdge { Release, } +#[derive(Debug, Clone)] pub struct Input { client: InputServiceClient, + fut_sender: FutSender, } impl Input { - pub(crate) fn new(client: InputServiceClient) -> Self { - Self { client } + pub fn new(client: InputServiceClient, fut_sender: FutSender) -> Self { + Self { client, fut_sender } } pub fn keybind( &self, mods: impl IntoIterator, key: impl Key + Send + 'static, - mut action: impl FnMut() + 'static + Send, + mut action: impl FnMut() + Send + 'static, ) { let mut client = self.client.clone(); let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); - tokio::spawn(async move { - let mut stream = client - .set_keybind(SetKeybindRequest { - modifiers, - key: Some(input::v0alpha1::set_keybind_request::Key::RawCode( - key.into_keysym().raw(), - )), - }) - .await - .unwrap() - .into_inner(); + self.fut_sender + .send(Box::pin(async move { + let mut stream = client + .set_keybind(SetKeybindRequest { + modifiers, + key: Some(input::v0alpha1::set_keybind_request::Key::RawCode( + key.into_keysym().raw(), + )), + }) + .await + .unwrap() + .into_inner(); - while let Some(Ok(_response)) = stream.next().await { - action(); - } - }); + while let Some(Ok(_response)) = stream.next().await { + action(); + } + })) + .unwrap(); } pub fn mousebind( @@ -124,8 +131,36 @@ impl Input { .unwrap(); } - pub fn set_libinput_setting(setting: LibinputSetting) { - todo!() + pub fn set_libinput_setting(&self, setting: LibinputSetting) { + let mut client = self.client.clone(); + + let setting = match setting { + LibinputSetting::AccelProfile(profile) => Setting::AccelProfile(profile as i32), + LibinputSetting::AccelSpeed(speed) => Setting::AccelSpeed(speed), + LibinputSetting::CalibrationMatrix(matrix) => { + Setting::CalibrationMatrix(CalibrationMatrix { + matrix: matrix.to_vec(), + }) + } + LibinputSetting::ClickMethod(method) => Setting::ClickMethod(method as i32), + LibinputSetting::DisableWhileTyping(disable) => Setting::DisableWhileTyping(disable), + LibinputSetting::LeftHanded(enable) => Setting::LeftHanded(enable), + LibinputSetting::MiddleEmulation(enable) => Setting::MiddleEmulation(enable), + LibinputSetting::RotationAngle(angle) => Setting::RotationAngle(angle), + LibinputSetting::ScrollButton(button) => Setting::RotationAngle(button), + LibinputSetting::ScrollButtonLock(enable) => Setting::ScrollButtonLock(enable), + LibinputSetting::ScrollMethod(method) => Setting::ScrollMethod(method as i32), + LibinputSetting::NaturalScroll(enable) => Setting::NaturalScroll(enable), + LibinputSetting::TapButtonMap(map) => Setting::TapButtonMap(map as i32), + LibinputSetting::TapDrag(enable) => Setting::TapDrag(enable), + LibinputSetting::TapDragLock(enable) => Setting::TapDragLock(enable), + LibinputSetting::Tap(enable) => Setting::Tap(enable), + }; + + block_on(client.set_libinput_setting(SetLibinputSettingRequest { + setting: Some(setting), + })) + .unwrap(); } } diff --git a/api/rust_grpc/src/input/libinput.rs b/api/rust_grpc/src/input/libinput.rs index b0db1b5..55ae198 100644 --- a/api/rust_grpc/src/input/libinput.rs +++ b/api/rust_grpc/src/input/libinput.rs @@ -24,6 +24,7 @@ pub enum TapButtonMap { LeftMiddleRight, } +#[derive(Debug, Clone, Copy, PartialEq)] pub enum LibinputSetting { AccelProfile(AccelProfile), AccelSpeed(f64), @@ -34,7 +35,7 @@ pub enum LibinputSetting { MiddleEmulation(bool), RotationAngle(u32), ScrollButton(u32), - ScrollButtonLock(u32), + ScrollButtonLock(bool), ScrollMethod(ScrollMethod), NaturalScroll(bool), TapButtonMap(TapButtonMap), diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs index 8b7da88..4d05446 100644 --- a/api/rust_grpc/src/lib.rs +++ b/api/rust_grpc/src/lib.rs @@ -1,5 +1,11 @@ // #![warn(missing_docs)] +use std::sync::OnceLock; + +use futures::{executor::block_on, future::BoxFuture, stream::FuturesUnordered, StreamExt}; +use input::Input; +use output::Output; +use pinnacle::Pinnacle; use pinnacle_api_defs::pinnacle::{ input::v0alpha1::input_service_client::InputServiceClient, output::v0alpha1::output_service_client::OutputServiceClient, @@ -8,19 +14,37 @@ use pinnacle_api_defs::pinnacle::{ v0alpha1::pinnacle_service_client::PinnacleServiceClient, window::v0alpha1::window_service_client::WindowServiceClient, }; -use tokio::net::UnixStream; +use process::Process; +use tag::Tag; +use tokio::{net::UnixStream, sync::mpsc::UnboundedSender}; +use tokio_stream::wrappers::UnboundedReceiverStream; use tonic::transport::{Channel, Endpoint, Uri}; use tower::service_fn; +use window::Window; pub mod input; pub mod output; +pub mod pinnacle; pub mod process; pub mod tag; pub mod util; pub mod window; -pub fn setup() -> Result<(), Box> { - let channel = futures_lite::future::block_on(connect())?; +pub use pinnacle_api_macros::config; + +static PINNACLE: OnceLock = OnceLock::new(); +static PROCESS: OnceLock = OnceLock::new(); +static WINDOW: OnceLock = OnceLock::new(); +static INPUT: OnceLock = OnceLock::new(); +static OUTPUT: OnceLock = OnceLock::new(); +static TAG: OnceLock = OnceLock::new(); + +pub(crate) type FutSender = UnboundedSender>; + +pub fn create_modules( +) -> Result<(ApiModules, UnboundedReceiverStream>), Box> +{ + let channel = connect()?; let pinnacle_client = PinnacleServiceClient::new(channel.clone()); let window_client = WindowServiceClient::new(channel.clone()); @@ -29,14 +53,83 @@ pub fn setup() -> Result<(), Box> { let tag_client = TagServiceClient::new(channel.clone()); let process_client = ProcessServiceClient::new(channel.clone()); - Ok(()) + let (fut_sender, fut_receiver) = tokio::sync::mpsc::unbounded_channel::>(); + + let fut_receiver = UnboundedReceiverStream::new(fut_receiver); + + let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(pinnacle_client)); + let process = PROCESS.get_or_init(|| Process::new(process_client, fut_sender.clone())); + let window = WINDOW.get_or_init(|| Window::new(window_client, tag_client.clone())); + let input = INPUT.get_or_init(|| Input::new(input_client, fut_sender.clone())); + let output = OUTPUT.get_or_init(|| Output::new(output_client, tag_client.clone())); + let tag = TAG.get_or_init(|| Tag::new(tag_client)); + + let modules = ApiModules { + pinnacle, + process, + window, + input, + output, + tag, + }; + + Ok((modules, fut_receiver)) } -async fn connect() -> Result> { - Endpoint::try_from("http://[::]:50051")? - .connect_with_connector(service_fn(|_: Uri| { - UnixStream::connect(std::env::var("PINNACLE_GRPC_SOCKET").unwrap()) - })) - .await - .map_err(|err| err.into()) +pub fn listen( + fut_receiver: UnboundedReceiverStream>, + // api_modules: ApiModules<'a>, +) { + let mut future_set = FuturesUnordered::< + BoxFuture<( + Option>, + Option>>, + )>, + >::new(); + + future_set.push(Box::pin(async move { + let (fut, stream) = fut_receiver.into_future().await; + (fut, Some(stream)) + })); + + block_on(async move { + while let Some((fut, stream)) = future_set.next().await { + if let Some(fut) = fut { + future_set.push(Box::pin(async move { + fut.await; + (None, None) + })); + } + if let Some(stream) = stream { + future_set.push(Box::pin(async move { + let (fut, stream) = stream.into_future().await; + (fut, Some(stream)) + })) + } + } + }); +} + +// #[derive(Debug, Clone)] +pub struct ApiModules { + pub pinnacle: &'static Pinnacle, + pub process: &'static Process, + pub window: &'static Window, + pub input: &'static Input, + pub output: &'static Output, + pub tag: &'static Tag, +} + +pub fn connect() -> Result> { + block_on(async { + Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket + .connect_with_connector(service_fn(|_: Uri| { + UnixStream::connect( + std::env::var("PINNACLE_GRPC_SOCKET") + .expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"), + ) + })) + .await + }) + .map_err(|err| err.into()) } diff --git a/api/rust_grpc/src/output.rs b/api/rust_grpc/src/output.rs index 458f5b4..4fdadc1 100644 --- a/api/rust_grpc/src/output.rs +++ b/api/rust_grpc/src/output.rs @@ -1,4 +1,4 @@ -use futures_lite::future::block_on; +use futures::executor::block_on; use pinnacle_api_defs::pinnacle::{ output::{ self, @@ -13,13 +13,14 @@ use tonic::transport::Channel; use crate::tag::TagHandle; +#[derive(Debug, Clone)] pub struct Output { client: OutputServiceClient, tag_client: TagServiceClient, } impl Output { - pub(crate) fn new( + pub fn new( client: OutputServiceClient, tag_client: TagServiceClient, ) -> Self { @@ -95,6 +96,10 @@ impl OutputHandle { .unwrap(); } + pub fn set_loc_adj_to(&self, other: &OutputHandle) { + todo!() + } + pub fn props(&self) -> OutputProperties { let mut client = self.client.clone(); let response = block_on( diff --git a/api/rust_grpc/src/pinnacle.rs b/api/rust_grpc/src/pinnacle.rs new file mode 100644 index 0000000..d4deef3 --- /dev/null +++ b/api/rust_grpc/src/pinnacle.rs @@ -0,0 +1,21 @@ +use futures::executor::block_on; +use pinnacle_api_defs::pinnacle::v0alpha1::{ + pinnacle_service_client::PinnacleServiceClient, QuitRequest, +}; +use tonic::transport::Channel; + +#[derive(Debug, Clone)] +pub struct Pinnacle { + client: PinnacleServiceClient, +} + +impl Pinnacle { + pub fn new(client: PinnacleServiceClient) -> Self { + Self { client } + } + + pub fn quit(&self) { + let mut client = self.client.clone(); + block_on(client.quit(QuitRequest {})).unwrap(); + } +} diff --git a/api/rust_grpc/src/process.rs b/api/rust_grpc/src/process.rs index fcdaf7c..382b0bf 100644 --- a/api/rust_grpc/src/process.rs +++ b/api/rust_grpc/src/process.rs @@ -4,20 +4,23 @@ use pinnacle_api_defs::pinnacle::process::v0alpha1::{ use tokio_stream::StreamExt; use tonic::transport::Channel; +use crate::FutSender; + #[derive(Debug, Clone)] pub struct Process { client: ProcessServiceClient, + fut_sender: FutSender, } pub struct SpawnCallbacks { - pub stdout: Option>, - pub stderr: Option>, - pub exit: Option, String) + 'static + Send>>, + pub stdout: Option>, + pub stderr: Option>, + pub exit: Option, String) + Send>>, } impl Process { - pub(crate) fn new(client: ProcessServiceClient) -> Self { - Self { client } + pub fn new(client: ProcessServiceClient, fut_sender: FutSender) -> Process { + Self { client, fut_sender } } pub fn spawn(&self, args: impl IntoIterator>) { @@ -50,6 +53,8 @@ impl Process { once: bool, callbacks: Option, ) { + let mut client = self.client.clone(); + let args = args .into_iter() .map(|arg| Into::::into(arg)) @@ -61,28 +66,50 @@ impl Process { has_callback: Some(callbacks.is_some()), }; - let mut client = self.client.clone(); + self.fut_sender + .send(Box::pin(async move { + let mut stream = client.spawn(request).await.unwrap().into_inner(); + let Some(mut callbacks) = callbacks else { return }; + while let Some(Ok(response)) = stream.next().await { + if let Some(line) = response.stdout { + if let Some(stdout) = callbacks.stdout.as_mut() { + stdout(line); + } + } + if let Some(line) = response.stderr { + if let Some(stderr) = callbacks.stderr.as_mut() { + stderr(line); + } + } + if let Some(exit_msg) = response.exit_message { + if let Some(exit) = callbacks.exit.as_mut() { + exit(response.exit_code, exit_msg); + } + } + } + })) + .unwrap(); - tokio::spawn(async move { - let mut stream = client.spawn(request).await.unwrap().into_inner(); - let Some(mut callbacks) = callbacks else { return }; - while let Some(Ok(response)) = stream.next().await { - if let Some(line) = response.stdout { - if let Some(stdout) = callbacks.stdout.as_mut() { - stdout(line); - } - } - if let Some(line) = response.stderr { - if let Some(stderr) = callbacks.stderr.as_mut() { - stderr(line); - } - } - if let Some(exit_msg) = response.exit_message { - if let Some(exit) = callbacks.exit.as_mut() { - exit(response.exit_code, exit_msg); - } - } - } - }); + // tokio::spawn(async move { + // let mut stream = client.spawn(request).await.unwrap().into_inner(); + // let Some(mut callbacks) = callbacks else { return }; + // while let Some(Ok(response)) = stream.next().await { + // if let Some(line) = response.stdout { + // if let Some(stdout) = callbacks.stdout.as_mut() { + // stdout(line); + // } + // } + // if let Some(line) = response.stderr { + // if let Some(stderr) = callbacks.stderr.as_mut() { + // stderr(line); + // } + // } + // if let Some(exit_msg) = response.exit_message { + // if let Some(exit) = callbacks.exit.as_mut() { + // exit(response.exit_code, exit_msg); + // } + // } + // } + // }); } } diff --git a/api/rust_grpc/src/tag.rs b/api/rust_grpc/src/tag.rs index 4712054..0f7ea00 100644 --- a/api/rust_grpc/src/tag.rs +++ b/api/rust_grpc/src/tag.rs @@ -1,4 +1,4 @@ -use futures_lite::future::block_on; +use futures::executor::block_on; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::tag::{ self, @@ -14,7 +14,7 @@ pub struct Tag { } impl Tag { - pub(crate) fn new(client: TagServiceClient) -> Self { + pub fn new(client: TagServiceClient) -> Self { Self { client } } } diff --git a/api/rust_grpc/src/window.rs b/api/rust_grpc/src/window.rs index 2a2bac3..5c2bd5f 100644 --- a/api/rust_grpc/src/window.rs +++ b/api/rust_grpc/src/window.rs @@ -1,4 +1,4 @@ -use futures_lite::future::block_on; +use futures::executor::block_on; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::{ tag::v0alpha1::tag_service_client::TagServiceClient, @@ -24,7 +24,7 @@ pub struct Window { } impl Window { - pub(crate) fn new( + pub fn new( client: WindowServiceClient, tag_client: TagServiceClient, ) -> Self { From ac56abf549b2ce68f174f64d0720d047b77f0a23 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Fri, 19 Jan 2024 19:37:00 -0600 Subject: [PATCH 04/26] Losing my mind --- api/rust_grpc/Cargo.toml | 5 +- api/rust_grpc/examples/default_config.rs | 18 --- api/rust_grpc/examples/default_config/main.rs | 105 +++++++++++++++ .../examples/default_config/metaconfig.toml | 46 +++++++ api/rust_grpc/pinnacle-api-macros/src/lib.rs | 2 +- api/rust_grpc/src/input.rs | 105 +++++++++------ api/rust_grpc/src/lib.rs | 104 +++++++-------- api/rust_grpc/src/output.rs | 86 +++++++----- api/rust_grpc/src/pinnacle.rs | 14 +- api/rust_grpc/src/process.rs | 83 +++++------- api/rust_grpc/src/tag.rs | 125 ++++++++++++++++-- api/rust_grpc/src/window.rs | 86 +++++++----- 12 files changed, 526 insertions(+), 253 deletions(-) delete mode 100644 api/rust_grpc/examples/default_config.rs create mode 100644 api/rust_grpc/examples/default_config/main.rs create mode 100644 api/rust_grpc/examples/default_config/metaconfig.toml diff --git a/api/rust_grpc/Cargo.toml b/api/rust_grpc/Cargo.toml index 9f301d6..3e873f4 100644 --- a/api/rust_grpc/Cargo.toml +++ b/api/rust_grpc/Cargo.toml @@ -7,10 +7,13 @@ edition = "2021" pinnacle-api-defs = { path = "../../pinnacle-api-defs" } pinnacle-api-macros = { path = "./pinnacle-api-macros" } tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] } -tokio-stream = { version = "0.1.14", features = ["net"] } +# tokio-stream = { version = "0.1.14", features = ["net"] } +async-net = "2.0.0" +async-compat = "0.2.3" tonic = "0.10.2" tower = { version = "0.4.13", features = ["util"] } futures = "0.3.30" +# futures-lite = "2.2.0" num_enum = "0.7.2" xkbcommon = "0.7.0" diff --git a/api/rust_grpc/examples/default_config.rs b/api/rust_grpc/examples/default_config.rs deleted file mode 100644 index 90c80f4..0000000 --- a/api/rust_grpc/examples/default_config.rs +++ /dev/null @@ -1,18 +0,0 @@ -use pinnacle_api::{input::Mod, ApiModules}; - -#[pinnacle_api::config(modules)] -#[tokio::main] -async fn main() { - let ApiModules { - pinnacle, - process, - window, - input, - output, - tag, - } = modules; - - input.keybind([Mod::Shift], 'a', || { - process.spawn(["alacritty"]); - }); -} diff --git a/api/rust_grpc/examples/default_config/main.rs b/api/rust_grpc/examples/default_config/main.rs new file mode 100644 index 0000000..e242964 --- /dev/null +++ b/api/rust_grpc/examples/default_config/main.rs @@ -0,0 +1,105 @@ +use pinnacle_api::{ + input::{Mod, MouseButton, MouseEdge}, + ApiModules, +}; +use xkbcommon::xkb::keysyms; + +#[pinnacle_api::config(modules)] +#[tokio::main] +async fn main() { + let ApiModules { + pinnacle, + process, + window, + input, + output, + tag, + } = modules; + + let mod_key = Mod::Ctrl; + + input.mousebind([mod_key], MouseButton::Left, MouseEdge::Press, || { + window.begin_move(MouseButton::Left); + }); + + input.mousebind([mod_key], MouseButton::Right, MouseEdge::Press, || { + window.begin_resize(MouseButton::Right); + }); + + // Keybinds + + input.keybind([mod_key, Mod::Alt], 'q', || { + pinnacle.quit(); + }); + + input.keybind([mod_key, Mod::Alt], 'c', || { + if let Some(window) = window.get_focused() { + window.close(); + } + }); + + println!("BEFORE KEYBIND"); + input.keybind([mod_key], keysyms::KEY_Return, || { + process.spawn(["alacritty"]); + }); + println!("AFTER"); + + input.keybind([mod_key, Mod::Alt], keysyms::KEY_space, || { + if let Some(window) = window.get_focused() { + window.toggle_floating(); + } + }); + + input.keybind([mod_key], 'f', || { + if let Some(window) = window.get_focused() { + window.toggle_fullscreen(); + } + }); + + input.keybind([mod_key], 'm', || { + if let Some(window) = window.get_focused() { + window.toggle_maximized(); + } + }); + + // Tags + + let tag_names = ["1", "2", "3", "4", "5"]; + + output.connect_for_all(move |op| { + let mut tags = tag.add(&op, tag_names); + tags.next().unwrap().set_active(true); + }); + + process.spawn_once(["alacritty"]); + + for tag_name in tag_names { + input.keybind([mod_key], tag_name, move || { + if let Some(tg) = tag.get(tag_name, None) { + tg.switch_to(); + } + }); + + input.keybind([mod_key, Mod::Shift], tag_name, move || { + if let Some(tg) = tag.get(tag_name, None) { + tg.toggle_active(); + } + }); + + input.keybind([mod_key, Mod::Alt], tag_name, move || { + if let Some(tg) = tag.get(tag_name, None) { + if let Some(win) = window.get_focused() { + win.move_to_tag(&tg); + } + } + }); + + input.keybind([mod_key, Mod::Shift, Mod::Alt], tag_name, move || { + if let Some(tg) = tag.get(tag_name, None) { + if let Some(win) = window.get_focused() { + win.toggle_tag(&tg); + } + } + }); + } +} diff --git a/api/rust_grpc/examples/default_config/metaconfig.toml b/api/rust_grpc/examples/default_config/metaconfig.toml new file mode 100644 index 0000000..414a25a --- /dev/null +++ b/api/rust_grpc/examples/default_config/metaconfig.toml @@ -0,0 +1,46 @@ +# This metaconfig.toml file dictates what config Pinnacle will run. +# +# When running Pinnacle, the compositor will look in the following directories for a metaconfig.toml file, +# in order from top to bottom: +# $PINNACLE_CONFIG_DIR +# $XDG_CONFIG_HOME/pinnacle/ +# ~/.config/pinnacle/ +# +# When Pinnacle finds a metaconfig.toml file, it will execute the command provided to `command`. +# To use a Rust config, this should be changed to something like ["cargo", "run"]. +# +# Because configuration is done using an external process, if it ever crashes, you lose all of your keybinds. +# The compositor will load the default config if that happens, but in the event that you don't have +# the necessary dependencies for it to run, you may get softlocked. +# In order prevent you from getting stuck in the compositor, you must define keybinds to reload your config +# and kill Pinnacle. +# +# More details on each setting can be found below. + +# The command Pinnacle will run on startup and when you reload your config. +# Paths are relative to the directory the metaconfig.toml file is in. +# This must be an array. +command = ["cargo", "run", "--example", "default_config"] + +### Keybinds ### +# Each keybind takes in a table with two fields: `modifiers` and `key`. +# - `modifiers` can be one of "Ctrl", "Alt", "Shift", or "Super". +# - `key` can be a string of any lowercase letter, number, +# "numN" where N is a number for numpad keys, or "esc"/"escape". +# Support for any xkbcommon key is planned for a future update. + +# The keybind that will reload your config. +reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } +# The keybind that will kill Pinnacle. +kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } + +### Socket directory ### +# Pinnacle will open a Unix socket at `$XDG_RUNTIME_DIR` by default, falling back to `/tmp` if it doesn't exist. +# If you want/need to change this, use the `socket_dir` setting set to the directory of your choosing. +# +# socket_dir = "/your/dir/here/" + +### Environment Variables ### +# If you need to spawn your config with any environment variables, list them here. +[envs] +# key = "value" diff --git a/api/rust_grpc/pinnacle-api-macros/src/lib.rs b/api/rust_grpc/pinnacle-api-macros/src/lib.rs index 76bbc89..943a730 100644 --- a/api/rust_grpc/pinnacle-api-macros/src/lib.rs +++ b/api/rust_grpc/pinnacle-api-macros/src/lib.rs @@ -20,7 +20,7 @@ pub fn config( quote! { #(#attrs)* #vis #sig { - let (#module_ident, __fut_receiver) = ::pinnacle_api::create_modules().unwrap(); + let (#module_ident, __fut_receiver) = ::pinnacle_api::connect().unwrap(); #(#stmts)* diff --git a/api/rust_grpc/src/input.rs b/api/rust_grpc/src/input.rs index f500856..1176a18 100644 --- a/api/rust_grpc/src/input.rs +++ b/api/rust_grpc/src/input.rs @@ -1,4 +1,4 @@ -use futures::executor::block_on; +use futures::{channel::mpsc::UnboundedSender, future::BoxFuture, FutureExt, StreamExt}; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::input::{ self, @@ -8,13 +8,12 @@ use pinnacle_api_defs::pinnacle::input::{ SetKeybindRequest, SetLibinputSettingRequest, SetMousebindRequest, SetRepeatRateRequest, }, }; -use tokio_stream::StreamExt; use tonic::transport::Channel; use xkbcommon::xkb::Keysym; pub use pinnacle_api_defs::pinnacle::input::v0alpha1::SetXkbConfigRequest as XkbConfig; -use crate::FutSender; +use crate::block_on; use self::libinput::LibinputSetting; pub mod libinput; @@ -48,13 +47,21 @@ pub enum MouseEdge { #[derive(Debug, Clone)] pub struct Input { - client: InputServiceClient, - fut_sender: FutSender, + // client: InputServiceClient, + channel: Channel, + fut_sender: UnboundedSender>, } impl Input { - pub fn new(client: InputServiceClient, fut_sender: FutSender) -> Self { - Self { client, fut_sender } + pub fn new(channel: Channel, fut_sender: UnboundedSender>) -> Self { + Self { + channel, + fut_sender, + } + } + + fn create_input_client(&self) -> InputServiceClient { + InputServiceClient::new(self.channel.clone()) } pub fn keybind( @@ -63,27 +70,38 @@ impl Input { key: impl Key + Send + 'static, mut action: impl FnMut() + Send + 'static, ) { - let mut client = self.client.clone(); + let mut client = self.create_input_client(); let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); - self.fut_sender - .send(Box::pin(async move { - let mut stream = client - .set_keybind(SetKeybindRequest { - modifiers, - key: Some(input::v0alpha1::set_keybind_request::Key::RawCode( - key.into_keysym().raw(), - )), - }) - .await - .unwrap() - .into_inner(); + println!("BEFORE TOKIO SPAWN"); - while let Some(Ok(_response)) = stream.next().await { - action(); + self.fut_sender + .unbounded_send( + async move { + println!("TOP OF TOKIO SPAWN"); + + let mut stream = client + .set_keybind(SetKeybindRequest { + modifiers, + key: Some(input::v0alpha1::set_keybind_request::Key::RawCode( + key.into_keysym().raw(), + )), + }) + .await + .unwrap() + .into_inner(); + + println!("AFTER SET_KEYBIND AWAIT"); + + while let Some(Ok(_response)) = stream.next().await { + println!("START OF STREAM LOOP"); + action(); + println!("ACTION PERFORMED"); + } } - })) + .boxed(), + ) .unwrap(); } @@ -94,35 +112,40 @@ impl Input { edge: MouseEdge, mut action: impl FnMut() + 'static + Send, ) { - let mut client = self.client.clone(); + let mut client = self.create_input_client(); let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); - tokio::spawn(async move { - let mut stream = client - .set_mousebind(SetMousebindRequest { - modifiers, - button: Some(button as u32), - edge: Some(edge as i32), - }) - .await - .unwrap() - .into_inner(); + self.fut_sender + .unbounded_send( + async move { + let mut stream = client + .set_mousebind(SetMousebindRequest { + modifiers, + button: Some(button as u32), + edge: Some(edge as i32), + }) + .await + .unwrap() + .into_inner(); - while let Some(Ok(_response)) = stream.next().await { - action(); - } - }); + while let Some(Ok(_response)) = stream.next().await { + action(); + } + } + .boxed(), + ) + .unwrap(); } pub fn set_xkb_config(&self, xkb_config: XkbConfig) { - let mut client = self.client.clone(); + let mut client = self.create_input_client(); block_on(client.set_xkb_config(xkb_config)).unwrap(); } pub fn set_repeat_rate(&self, rate: i32, delay: i32) { - let mut client = self.client.clone(); + let mut client = self.create_input_client(); block_on(client.set_repeat_rate(SetRepeatRateRequest { rate: Some(rate), @@ -132,7 +155,7 @@ impl Input { } pub fn set_libinput_setting(&self, setting: LibinputSetting) { - let mut client = self.client.clone(); + let mut client = self.create_input_client(); let setting = match setting { LibinputSetting::AccelProfile(profile) => Setting::AccelProfile(profile as i32), diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs index 4d05446..8e7d6c3 100644 --- a/api/rust_grpc/src/lib.rs +++ b/api/rust_grpc/src/lib.rs @@ -2,23 +2,16 @@ use std::sync::OnceLock; -use futures::{executor::block_on, future::BoxFuture, stream::FuturesUnordered, StreamExt}; +use futures::{ + channel::mpsc::UnboundedReceiver, future::BoxFuture, stream::FuturesUnordered, Future, + StreamExt, +}; use input::Input; use output::Output; use pinnacle::Pinnacle; -use pinnacle_api_defs::pinnacle::{ - input::v0alpha1::input_service_client::InputServiceClient, - output::v0alpha1::output_service_client::OutputServiceClient, - process::v0alpha1::process_service_client::ProcessServiceClient, - tag::v0alpha1::tag_service_client::TagServiceClient, - v0alpha1::pinnacle_service_client::PinnacleServiceClient, - window::v0alpha1::window_service_client::WindowServiceClient, -}; use process::Process; use tag::Tag; -use tokio::{net::UnixStream, sync::mpsc::UnboundedSender}; -use tokio_stream::wrappers::UnboundedReceiverStream; -use tonic::transport::{Channel, Endpoint, Uri}; +use tonic::transport::{Endpoint, Uri}; use tower::service_fn; use window::Window; @@ -31,6 +24,7 @@ pub mod util; pub mod window; pub use pinnacle_api_macros::config; +pub use xkbcommon; static PINNACLE: OnceLock = OnceLock::new(); static PROCESS: OnceLock = OnceLock::new(); @@ -39,30 +33,44 @@ static INPUT: OnceLock = OnceLock::new(); static OUTPUT: OnceLock = OnceLock::new(); static TAG: OnceLock = OnceLock::new(); -pub(crate) type FutSender = UnboundedSender>; +#[derive(Debug, Clone)] +pub struct ApiModules { + pub pinnacle: &'static Pinnacle, + pub process: &'static Process, + pub window: &'static Window, + pub input: &'static Input, + pub output: &'static Output, + pub tag: &'static Tag, +} -pub fn create_modules( -) -> Result<(ApiModules, UnboundedReceiverStream>), Box> -{ - let channel = connect()?; +pub fn connect( +) -> Result<(ApiModules, UnboundedReceiver>), Box> { + println!("BEFORE CONNECT"); + let channel = block_on(async { + Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket + .connect_with_connector(service_fn(|_: Uri| { + println!("BEFORE UnixStream CONNECT"); + tokio::net::UnixStream::connect( + std::env::var("PINNACLE_GRPC_SOCKET") + .expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"), + ) + // .map(|stream| stream.map(|stream| stream.compat())) + })) + .await + })?; - let pinnacle_client = PinnacleServiceClient::new(channel.clone()); - let window_client = WindowServiceClient::new(channel.clone()); - let input_client = InputServiceClient::new(channel.clone()); - let output_client = OutputServiceClient::new(channel.clone()); - let tag_client = TagServiceClient::new(channel.clone()); - let process_client = ProcessServiceClient::new(channel.clone()); + println!("AFTER CONNECT"); - let (fut_sender, fut_receiver) = tokio::sync::mpsc::unbounded_channel::>(); + let (fut_sender, fut_recv) = futures::channel::mpsc::unbounded::>(); - let fut_receiver = UnboundedReceiverStream::new(fut_receiver); + let output = Output::new(channel.clone(), fut_sender.clone()); - let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(pinnacle_client)); - let process = PROCESS.get_or_init(|| Process::new(process_client, fut_sender.clone())); - let window = WINDOW.get_or_init(|| Window::new(window_client, tag_client.clone())); - let input = INPUT.get_or_init(|| Input::new(input_client, fut_sender.clone())); - let output = OUTPUT.get_or_init(|| Output::new(output_client, tag_client.clone())); - let tag = TAG.get_or_init(|| Tag::new(tag_client)); + let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(channel.clone())); + let process = PROCESS.get_or_init(|| Process::new(channel.clone(), fut_sender.clone())); + let window = WINDOW.get_or_init(|| Window::new(channel.clone())); + let input = INPUT.get_or_init(|| Input::new(channel.clone(), fut_sender.clone())); + let tag = TAG.get_or_init(|| Tag::new(channel.clone(), fut_sender.clone())); + let output = OUTPUT.get_or_init(|| output); let modules = ApiModules { pinnacle, @@ -73,22 +81,20 @@ pub fn create_modules( tag, }; - Ok((modules, fut_receiver)) + Ok((modules, fut_recv)) } -pub fn listen( - fut_receiver: UnboundedReceiverStream>, - // api_modules: ApiModules<'a>, +pub fn listen(fut_recv: UnboundedReceiver>, // api_modules: ApiModules<'a>, ) { let mut future_set = FuturesUnordered::< BoxFuture<( Option>, - Option>>, + Option>>, )>, >::new(); future_set.push(Box::pin(async move { - let (fut, stream) = fut_receiver.into_future().await; + let (fut, stream) = fut_recv.into_future().await; (fut, Some(stream)) })); @@ -110,26 +116,6 @@ pub fn listen( }); } -// #[derive(Debug, Clone)] -pub struct ApiModules { - pub pinnacle: &'static Pinnacle, - pub process: &'static Process, - pub window: &'static Window, - pub input: &'static Input, - pub output: &'static Output, - pub tag: &'static Tag, -} - -pub fn connect() -> Result> { - block_on(async { - Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket - .connect_with_connector(service_fn(|_: Uri| { - UnixStream::connect( - std::env::var("PINNACLE_GRPC_SOCKET") - .expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"), - ) - })) - .await - }) - .map_err(|err| err.into()) +pub(crate) fn block_on(fut: F) -> F::Output { + tokio::task::block_in_place(|| futures::executor::block_on(fut)) } diff --git a/api/rust_grpc/src/output.rs b/api/rust_grpc/src/output.rs index 4fdadc1..0d1088f 100644 --- a/api/rust_grpc/src/output.rs +++ b/api/rust_grpc/src/output.rs @@ -1,4 +1,4 @@ -use futures::executor::block_on; +use futures::{channel::mpsc::UnboundedSender, future::BoxFuture, FutureExt, StreamExt}; use pinnacle_api_defs::pinnacle::{ output::{ self, @@ -8,28 +8,37 @@ use pinnacle_api_defs::pinnacle::{ }, tag::v0alpha1::tag_service_client::TagServiceClient, }; -use tokio_stream::StreamExt; use tonic::transport::Channel; -use crate::tag::TagHandle; +use crate::{block_on, tag::TagHandle}; #[derive(Debug, Clone)] pub struct Output { - client: OutputServiceClient, - tag_client: TagServiceClient, + // client: OutputServiceClient, + // tag_client: TagServiceClient, + channel: Channel, + fut_sender: UnboundedSender>, } impl Output { - pub fn new( - client: OutputServiceClient, - tag_client: TagServiceClient, - ) -> Self { - Self { client, tag_client } + pub fn new(channel: Channel, fut_sender: UnboundedSender>) -> Self { + Self { + channel, + fut_sender, + } + } + + fn create_output_client(&self) -> OutputServiceClient { + OutputServiceClient::new(self.channel.clone()) + } + + fn create_tag_client(&self) -> TagServiceClient { + TagServiceClient::new(self.channel.clone()) } pub fn get_all(&self) -> impl Iterator { - let mut client = self.client.clone(); - let tag_client = self.tag_client.clone(); + let mut client = self.create_output_client(); + let tag_client = self.create_tag_client(); block_on(client.get(output::v0alpha1::GetRequest {})) .unwrap() .into_inner() @@ -52,37 +61,43 @@ impl Output { for_all(output); } - let mut client = self.client.clone(); - let tag_client = self.tag_client.clone(); + let mut client = self.create_output_client(); + let tag_client = self.create_tag_client(); - tokio::spawn(async move { - let mut stream = client - .connect_for_all(ConnectForAllRequest {}) - .await - .unwrap() - .into_inner(); + self.fut_sender + .unbounded_send( + async move { + let mut stream = client + .connect_for_all(ConnectForAllRequest {}) + .await + .unwrap() + .into_inner(); - while let Some(Ok(response)) = stream.next().await { - let Some(output_name) = response.output_name else { - continue; - }; + while let Some(Ok(response)) = stream.next().await { + let Some(output_name) = response.output_name else { + continue; + }; - let output = OutputHandle { - client: client.clone(), - tag_client: tag_client.clone(), - name: output_name, - }; + let output = OutputHandle { + client: client.clone(), + tag_client: tag_client.clone(), + name: output_name, + }; - for_all(output); - } - }); + for_all(output); + } + } + .boxed(), + ) + .unwrap(); } } +#[derive(Clone, Debug)] pub struct OutputHandle { - client: OutputServiceClient, - tag_client: TagServiceClient, - name: String, + pub(crate) client: OutputServiceClient, + pub(crate) tag_client: TagServiceClient, + pub(crate) name: String, } impl OutputHandle { @@ -126,6 +141,7 @@ impl OutputHandle { .into_iter() .map(|id| TagHandle { client: self.tag_client.clone(), + output_client: self.client.clone(), id, }) .collect(), diff --git a/api/rust_grpc/src/pinnacle.rs b/api/rust_grpc/src/pinnacle.rs index d4deef3..b86972d 100644 --- a/api/rust_grpc/src/pinnacle.rs +++ b/api/rust_grpc/src/pinnacle.rs @@ -1,4 +1,4 @@ -use futures::executor::block_on; +use crate::block_on; use pinnacle_api_defs::pinnacle::v0alpha1::{ pinnacle_service_client::PinnacleServiceClient, QuitRequest, }; @@ -6,16 +6,20 @@ use tonic::transport::Channel; #[derive(Debug, Clone)] pub struct Pinnacle { - client: PinnacleServiceClient, + channel: Channel, } impl Pinnacle { - pub fn new(client: PinnacleServiceClient) -> Self { - Self { client } + pub fn new(channel: Channel) -> Self { + Self { channel } + } + + fn create_pinnacle_client(&self) -> PinnacleServiceClient { + PinnacleServiceClient::new(self.channel.clone()) } pub fn quit(&self) { - let mut client = self.client.clone(); + let mut client = self.create_pinnacle_client(); block_on(client.quit(QuitRequest {})).unwrap(); } } diff --git a/api/rust_grpc/src/process.rs b/api/rust_grpc/src/process.rs index 382b0bf..aafe117 100644 --- a/api/rust_grpc/src/process.rs +++ b/api/rust_grpc/src/process.rs @@ -1,15 +1,13 @@ +use futures::{channel::mpsc::UnboundedSender, future::BoxFuture, FutureExt, StreamExt}; use pinnacle_api_defs::pinnacle::process::v0alpha1::{ process_service_client::ProcessServiceClient, SpawnRequest, }; -use tokio_stream::StreamExt; use tonic::transport::Channel; -use crate::FutSender; - #[derive(Debug, Clone)] pub struct Process { - client: ProcessServiceClient, - fut_sender: FutSender, + channel: Channel, + fut_sender: UnboundedSender>, } pub struct SpawnCallbacks { @@ -19,8 +17,15 @@ pub struct SpawnCallbacks { } impl Process { - pub fn new(client: ProcessServiceClient, fut_sender: FutSender) -> Process { - Self { client, fut_sender } + pub fn new(channel: Channel, fut_sender: UnboundedSender>) -> Process { + Self { + channel, + fut_sender, + } + } + + fn create_process_client(&self) -> ProcessServiceClient { + ProcessServiceClient::new(self.channel.clone()) } pub fn spawn(&self, args: impl IntoIterator>) { @@ -53,12 +58,9 @@ impl Process { once: bool, callbacks: Option, ) { - let mut client = self.client.clone(); + let mut client = self.create_process_client(); - let args = args - .into_iter() - .map(|arg| Into::::into(arg)) - .collect::>(); + let args = args.into_iter().map(Into::into).collect::>(); let request = SpawnRequest { args, @@ -67,49 +69,30 @@ impl Process { }; self.fut_sender - .send(Box::pin(async move { - let mut stream = client.spawn(request).await.unwrap().into_inner(); - let Some(mut callbacks) = callbacks else { return }; - while let Some(Ok(response)) = stream.next().await { - if let Some(line) = response.stdout { - if let Some(stdout) = callbacks.stdout.as_mut() { - stdout(line); + .unbounded_send( + async move { + let mut stream = client.spawn(request).await.unwrap().into_inner(); + let Some(mut callbacks) = callbacks else { return }; + while let Some(Ok(response)) = stream.next().await { + if let Some(line) = response.stdout { + if let Some(stdout) = callbacks.stdout.as_mut() { + stdout(line); + } } - } - if let Some(line) = response.stderr { - if let Some(stderr) = callbacks.stderr.as_mut() { - stderr(line); + if let Some(line) = response.stderr { + if let Some(stderr) = callbacks.stderr.as_mut() { + stderr(line); + } } - } - if let Some(exit_msg) = response.exit_message { - if let Some(exit) = callbacks.exit.as_mut() { - exit(response.exit_code, exit_msg); + if let Some(exit_msg) = response.exit_message { + if let Some(exit) = callbacks.exit.as_mut() { + exit(response.exit_code, exit_msg); + } } } } - })) + .boxed(), + ) .unwrap(); - - // tokio::spawn(async move { - // let mut stream = client.spawn(request).await.unwrap().into_inner(); - // let Some(mut callbacks) = callbacks else { return }; - // while let Some(Ok(response)) = stream.next().await { - // if let Some(line) = response.stdout { - // if let Some(stdout) = callbacks.stdout.as_mut() { - // stdout(line); - // } - // } - // if let Some(line) = response.stderr { - // if let Some(stderr) = callbacks.stderr.as_mut() { - // stderr(line); - // } - // } - // if let Some(exit_msg) = response.exit_message { - // if let Some(exit) = callbacks.exit.as_mut() { - // exit(response.exit_code, exit_msg); - // } - // } - // } - // }); } } diff --git a/api/rust_grpc/src/tag.rs b/api/rust_grpc/src/tag.rs index 0f7ea00..655c67b 100644 --- a/api/rust_grpc/src/tag.rs +++ b/api/rust_grpc/src/tag.rs @@ -1,27 +1,107 @@ -use futures::executor::block_on; +use futures::{channel::mpsc::UnboundedSender, future::BoxFuture}; use num_enum::TryFromPrimitive; -use pinnacle_api_defs::pinnacle::tag::{ - self, - v0alpha1::{ - tag_service_client::TagServiceClient, RemoveRequest, SetActiveRequest, SetLayoutRequest, +use pinnacle_api_defs::pinnacle::{ + output::v0alpha1::output_service_client::OutputServiceClient, + tag::{ + self, + v0alpha1::{ + tag_service_client::TagServiceClient, AddRequest, RemoveRequest, SetActiveRequest, + SetLayoutRequest, SwitchToRequest, + }, }, }; use tonic::transport::Channel; +use crate::{ + block_on, + output::{Output, OutputHandle}, +}; + #[derive(Clone, Debug)] pub struct Tag { - client: TagServiceClient, + channel: Channel, + fut_sender: UnboundedSender>, } impl Tag { - pub fn new(client: TagServiceClient) -> Self { - Self { client } + pub fn new(channel: Channel, fut_sender: UnboundedSender>) -> Self { + Self { + channel, + fut_sender, + } + } + + pub fn create_tag_client(&self) -> TagServiceClient { + TagServiceClient::new(self.channel.clone()) + } + + pub fn create_output_client(&self) -> OutputServiceClient { + OutputServiceClient::new(self.channel.clone()) + } + + pub fn add( + &self, + output: &OutputHandle, + tag_names: impl IntoIterator>, + ) -> impl Iterator { + let mut client = self.create_tag_client(); + let output_client = self.create_output_client(); + + let tag_names = tag_names.into_iter().map(Into::into).collect(); + + let response = block_on(client.add(AddRequest { + output_name: Some(output.name.clone()), + tag_names, + })) + .unwrap() + .into_inner(); + + response.tag_ids.into_iter().map(move |id| TagHandle { + client: client.clone(), + output_client: output_client.clone(), + id, + }) + } + + pub fn get_all(&self) -> impl Iterator { + let mut client = self.create_tag_client(); + let output_client = self.create_output_client(); + + let response = block_on(client.get(tag::v0alpha1::GetRequest {})) + .unwrap() + .into_inner(); + + response.tag_ids.into_iter().map(move |id| TagHandle { + client: client.clone(), + output_client: output_client.clone(), + id, + }) + } + + pub fn get(&self, name: impl Into, output: Option<&OutputHandle>) -> Option { + let name = name.into(); + let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); + + self.get_all().find(|tag| { + let props = tag.props(); + + let same_tag_name = props.name.as_ref() == Some(&name); + let same_output = props.output.is_some_and(|op| { + Some(op.name) + == output + .map(|o| o.name.clone()) + .or_else(|| output_module.get_focused().map(|o| o.name)) + }); + + same_tag_name && same_output + }) } } #[derive(Debug, Clone)] pub struct TagHandle { pub(crate) client: TagServiceClient, + pub(crate) output_client: OutputServiceClient, pub(crate) id: u32, } @@ -72,13 +152,38 @@ impl TagHandle { .unwrap(); } + pub fn switch_to(&self) { + let mut client = self.client.clone(); + block_on(client.switch_to(SwitchToRequest { + tag_id: Some(self.id), + })) + .unwrap(); + } + pub fn props(&self) -> TagProperties { - todo!() + let mut client = self.client.clone(); + let output_client = self.output_client.clone(); + + let response = block_on(client.get_properties(tag::v0alpha1::GetPropertiesRequest { + tag_id: Some(self.id), + })) + .unwrap() + .into_inner(); + + TagProperties { + active: response.active, + name: response.name, + output: response.output_name.map(|name| OutputHandle { + client: output_client, + tag_client: client, + name, + }), + } } } pub struct TagProperties { pub active: Option, pub name: Option, - pub output: (), + pub output: Option, } diff --git a/api/rust_grpc/src/window.rs b/api/rust_grpc/src/window.rs index 5c2bd5f..f9ef759 100644 --- a/api/rust_grpc/src/window.rs +++ b/api/rust_grpc/src/window.rs @@ -1,9 +1,10 @@ -use futures::executor::block_on; +use crate::block_on; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::{ + output::v0alpha1::output_service_client::OutputServiceClient, tag::v0alpha1::tag_service_client::TagServiceClient, window::v0alpha1::{ - window_service_client::WindowServiceClient, MoveToTagRequest, SetTagRequest, + window_service_client::WindowServiceClient, CloseRequest, MoveToTagRequest, SetTagRequest, }, window::{ self, @@ -19,22 +20,47 @@ use crate::{input::MouseButton, tag::TagHandle, util::Geometry}; #[derive(Debug, Clone)] pub struct Window { - client: WindowServiceClient, - tag_client: TagServiceClient, + channel: Channel, } impl Window { - pub fn new( - client: WindowServiceClient, - tag_client: TagServiceClient, - ) -> Self { - Self { client, tag_client } + pub fn new(channel: Channel) -> Self { + Self { channel } + } + + pub fn create_window_client(&self) -> WindowServiceClient { + WindowServiceClient::new(self.channel.clone()) + } + + pub fn create_tag_client(&self) -> TagServiceClient { + TagServiceClient::new(self.channel.clone()) + } + + pub fn create_output_client(&self) -> OutputServiceClient { + OutputServiceClient::new(self.channel.clone()) + } + + pub fn begin_move(&self, button: MouseButton) { + let mut client = self.create_window_client(); + block_on(client.move_grab(MoveGrabRequest { + button: Some(button as u32), + })) + .unwrap(); + } + + pub fn begin_resize(&self, button: MouseButton) { + let mut client = self.create_window_client(); + block_on(client.resize_grab(ResizeGrabRequest { + button: Some(button as u32), + })) + .unwrap(); } /// Get all windows. pub fn get_all(&self) -> impl Iterator { - let mut client = self.client.clone(); - let tag_client = self.tag_client.clone(); + let mut client = self.create_window_client(); + let tag_client = self.create_tag_client(); + let output_client = self.create_output_client(); block_on(client.get(GetRequest {})) .unwrap() .into_inner() @@ -44,6 +70,7 @@ impl Window { client: client.clone(), id, tag_client: tag_client.clone(), + output_client: output_client.clone(), }) } @@ -59,6 +86,7 @@ pub struct WindowHandle { pub(crate) client: WindowServiceClient, pub(crate) id: u32, pub(crate) tag_client: TagServiceClient, + pub(crate) output_client: OutputServiceClient, } #[repr(i32)] @@ -81,7 +109,14 @@ pub struct WindowProperties { } impl WindowHandle { - pub fn set_fullscreen(&mut self, set: bool) { + pub fn close(mut self) { + block_on(self.client.close(CloseRequest { + window_id: Some(self.id), + })) + .unwrap(); + } + + pub fn set_fullscreen(&self, set: bool) { let mut client = self.client.clone(); block_on(client.set_fullscreen(SetFullscreenRequest { window_id: Some(self.id), @@ -92,7 +127,7 @@ impl WindowHandle { .unwrap(); } - pub fn toggle_fullscreen(&mut self) { + pub fn toggle_fullscreen(&self) { let mut client = self.client.clone(); block_on(client.set_fullscreen(SetFullscreenRequest { window_id: Some(self.id), @@ -101,7 +136,7 @@ impl WindowHandle { .unwrap(); } - pub fn set_maximized(&mut self, set: bool) { + pub fn set_maximized(&self, set: bool) { let mut client = self.client.clone(); block_on(client.set_maximized(SetMaximizedRequest { window_id: Some(self.id), @@ -112,7 +147,7 @@ impl WindowHandle { .unwrap(); } - pub fn toggle_maximized(&mut self) { + pub fn toggle_maximized(&self) { let mut client = self.client.clone(); block_on(client.set_maximized(SetMaximizedRequest { window_id: Some(self.id), @@ -121,7 +156,7 @@ impl WindowHandle { .unwrap(); } - pub fn set_floating(&mut self, set: bool) { + pub fn set_floating(&self, set: bool) { let mut client = self.client.clone(); block_on(client.set_floating(SetFloatingRequest { window_id: Some(self.id), @@ -132,7 +167,7 @@ impl WindowHandle { .unwrap(); } - pub fn toggle_floating(&mut self) { + pub fn toggle_floating(&self) { let mut client = self.client.clone(); block_on(client.set_floating(SetFloatingRequest { window_id: Some(self.id), @@ -175,22 +210,6 @@ impl WindowHandle { .unwrap(); } - pub fn begin_move(&self, button: MouseButton) { - let mut client = self.client.clone(); - block_on(client.move_grab(MoveGrabRequest { - button: Some(button as u32), - })) - .unwrap(); - } - - pub fn begin_resize(&self, button: MouseButton) { - let mut client = self.client.clone(); - block_on(client.resize_grab(ResizeGrabRequest { - button: Some(button as u32), - })) - .unwrap(); - } - pub fn props(&self) -> WindowProperties { let mut client = self.client.clone(); let tag_client = self.tag_client.clone(); @@ -227,6 +246,7 @@ impl WindowHandle { .into_iter() .map(|id| TagHandle { client: tag_client.clone(), + output_client: self.output_client.clone(), id, }) .collect(), From 80edacd6e2893cae4f6c61adc4e786e6ef125b8c Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sat, 20 Jan 2024 12:37:22 -0600 Subject: [PATCH 05/26] Remove nested block_on --- api/rust_grpc/pinnacle-api-macros/src/lib.rs | 2 +- api/rust_grpc/src/lib.rs | 34 ++++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/api/rust_grpc/pinnacle-api-macros/src/lib.rs b/api/rust_grpc/pinnacle-api-macros/src/lib.rs index 943a730..ab741b1 100644 --- a/api/rust_grpc/pinnacle-api-macros/src/lib.rs +++ b/api/rust_grpc/pinnacle-api-macros/src/lib.rs @@ -24,7 +24,7 @@ pub fn config( #(#stmts)* - ::pinnacle_api::listen(__fut_receiver); + ::pinnacle_api::listen(__fut_receiver).await; } } .into() diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs index 8e7d6c3..b390ea2 100644 --- a/api/rust_grpc/src/lib.rs +++ b/api/rust_grpc/src/lib.rs @@ -84,7 +84,8 @@ pub fn connect( Ok((modules, fut_recv)) } -pub fn listen(fut_recv: UnboundedReceiver>, // api_modules: ApiModules<'a>, +pub async fn listen( + fut_recv: UnboundedReceiver>, // api_modules: ApiModules<'a>, ) { let mut future_set = FuturesUnordered::< BoxFuture<( @@ -98,24 +99,23 @@ pub fn listen(fut_recv: UnboundedReceiver>, // api_modules: ApiMod (fut, Some(stream)) })); - block_on(async move { - while let Some((fut, stream)) = future_set.next().await { - if let Some(fut) = fut { - future_set.push(Box::pin(async move { - fut.await; - (None, None) - })); - } - if let Some(stream) = stream { - future_set.push(Box::pin(async move { - let (fut, stream) = stream.into_future().await; - (fut, Some(stream)) - })) - } + while let Some((fut, stream)) = future_set.next().await { + if let Some(fut) = fut { + future_set.push(Box::pin(async move { + fut.await; + (None, None) + })); } - }); + if let Some(stream) = stream { + future_set.push(Box::pin(async move { + let (fut, stream) = stream.into_future().await; + (fut, Some(stream)) + })) + } + } } pub(crate) fn block_on(fut: F) -> F::Output { - tokio::task::block_in_place(|| futures::executor::block_on(fut)) + futures::executor::block_on(fut) + // tokio::task::block_in_place(|| futures::executor::block_on(fut)) } From 8a469fb5258124a53ad3ac796d190ec4cd7e15a8 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sat, 20 Jan 2024 15:31:45 -0600 Subject: [PATCH 06/26] Make things work --- api/rust_grpc/examples/default_config/main.rs | 2 -- api/rust_grpc/pinnacle-api-macros/src/lib.rs | 2 +- api/rust_grpc/src/input.rs | 14 ++------ api/rust_grpc/src/lib.rs | 33 ++++++------------- api/rust_grpc/src/output.rs | 6 ++-- api/rust_grpc/src/pinnacle.rs | 2 +- api/rust_grpc/src/tag.rs | 7 ++-- api/rust_grpc/src/window.rs | 2 +- 8 files changed, 22 insertions(+), 46 deletions(-) diff --git a/api/rust_grpc/examples/default_config/main.rs b/api/rust_grpc/examples/default_config/main.rs index e242964..191962a 100644 --- a/api/rust_grpc/examples/default_config/main.rs +++ b/api/rust_grpc/examples/default_config/main.rs @@ -38,11 +38,9 @@ async fn main() { } }); - println!("BEFORE KEYBIND"); input.keybind([mod_key], keysyms::KEY_Return, || { process.spawn(["alacritty"]); }); - println!("AFTER"); input.keybind([mod_key, Mod::Alt], keysyms::KEY_space, || { if let Some(window) = window.get_focused() { diff --git a/api/rust_grpc/pinnacle-api-macros/src/lib.rs b/api/rust_grpc/pinnacle-api-macros/src/lib.rs index ab741b1..7249ef6 100644 --- a/api/rust_grpc/pinnacle-api-macros/src/lib.rs +++ b/api/rust_grpc/pinnacle-api-macros/src/lib.rs @@ -20,7 +20,7 @@ pub fn config( quote! { #(#attrs)* #vis #sig { - let (#module_ident, __fut_receiver) = ::pinnacle_api::connect().unwrap(); + let (#module_ident, __fut_receiver) = ::pinnacle_api::connect().await.unwrap(); #(#stmts)* diff --git a/api/rust_grpc/src/input.rs b/api/rust_grpc/src/input.rs index 1176a18..8044459 100644 --- a/api/rust_grpc/src/input.rs +++ b/api/rust_grpc/src/input.rs @@ -1,4 +1,6 @@ -use futures::{channel::mpsc::UnboundedSender, future::BoxFuture, FutureExt, StreamExt}; +use futures::{ + channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, +}; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::input::{ self, @@ -13,8 +15,6 @@ use xkbcommon::xkb::Keysym; pub use pinnacle_api_defs::pinnacle::input::v0alpha1::SetXkbConfigRequest as XkbConfig; -use crate::block_on; - use self::libinput::LibinputSetting; pub mod libinput; @@ -74,13 +74,9 @@ impl Input { let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); - println!("BEFORE TOKIO SPAWN"); - self.fut_sender .unbounded_send( async move { - println!("TOP OF TOKIO SPAWN"); - let mut stream = client .set_keybind(SetKeybindRequest { modifiers, @@ -92,12 +88,8 @@ impl Input { .unwrap() .into_inner(); - println!("AFTER SET_KEYBIND AWAIT"); - while let Some(Ok(_response)) = stream.next().await { - println!("START OF STREAM LOOP"); action(); - println!("ACTION PERFORMED"); } } .boxed(), diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs index b390ea2..508ee89 100644 --- a/api/rust_grpc/src/lib.rs +++ b/api/rust_grpc/src/lib.rs @@ -3,8 +3,7 @@ use std::sync::OnceLock; use futures::{ - channel::mpsc::UnboundedReceiver, future::BoxFuture, stream::FuturesUnordered, Future, - StreamExt, + channel::mpsc::UnboundedReceiver, future::BoxFuture, stream::FuturesUnordered, StreamExt, }; use input::Input; use output::Output; @@ -43,23 +42,16 @@ pub struct ApiModules { pub tag: &'static Tag, } -pub fn connect( +pub async fn connect( ) -> Result<(ApiModules, UnboundedReceiver>), Box> { - println!("BEFORE CONNECT"); - let channel = block_on(async { - Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket - .connect_with_connector(service_fn(|_: Uri| { - println!("BEFORE UnixStream CONNECT"); - tokio::net::UnixStream::connect( - std::env::var("PINNACLE_GRPC_SOCKET") - .expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"), - ) - // .map(|stream| stream.map(|stream| stream.compat())) - })) - .await - })?; - - println!("AFTER CONNECT"); + let channel = Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket + .connect_with_connector(service_fn(|_: Uri| { + tokio::net::UnixStream::connect( + std::env::var("PINNACLE_GRPC_SOCKET") + .expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"), + ) + })) + .await?; let (fut_sender, fut_recv) = futures::channel::mpsc::unbounded::>(); @@ -114,8 +106,3 @@ pub async fn listen( } } } - -pub(crate) fn block_on(fut: F) -> F::Output { - futures::executor::block_on(fut) - // tokio::task::block_in_place(|| futures::executor::block_on(fut)) -} diff --git a/api/rust_grpc/src/output.rs b/api/rust_grpc/src/output.rs index 0d1088f..094ecf7 100644 --- a/api/rust_grpc/src/output.rs +++ b/api/rust_grpc/src/output.rs @@ -1,4 +1,6 @@ -use futures::{channel::mpsc::UnboundedSender, future::BoxFuture, FutureExt, StreamExt}; +use futures::{ + channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, +}; use pinnacle_api_defs::pinnacle::{ output::{ self, @@ -10,7 +12,7 @@ use pinnacle_api_defs::pinnacle::{ }; use tonic::transport::Channel; -use crate::{block_on, tag::TagHandle}; +use crate::tag::TagHandle; #[derive(Debug, Clone)] pub struct Output { diff --git a/api/rust_grpc/src/pinnacle.rs b/api/rust_grpc/src/pinnacle.rs index b86972d..f6b69ab 100644 --- a/api/rust_grpc/src/pinnacle.rs +++ b/api/rust_grpc/src/pinnacle.rs @@ -1,4 +1,4 @@ -use crate::block_on; +use futures::executor::block_on; use pinnacle_api_defs::pinnacle::v0alpha1::{ pinnacle_service_client::PinnacleServiceClient, QuitRequest, }; diff --git a/api/rust_grpc/src/tag.rs b/api/rust_grpc/src/tag.rs index 655c67b..7fe8ef6 100644 --- a/api/rust_grpc/src/tag.rs +++ b/api/rust_grpc/src/tag.rs @@ -1,4 +1,4 @@ -use futures::{channel::mpsc::UnboundedSender, future::BoxFuture}; +use futures::{channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture}; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::{ output::v0alpha1::output_service_client::OutputServiceClient, @@ -12,10 +12,7 @@ use pinnacle_api_defs::pinnacle::{ }; use tonic::transport::Channel; -use crate::{ - block_on, - output::{Output, OutputHandle}, -}; +use crate::output::{Output, OutputHandle}; #[derive(Clone, Debug)] pub struct Tag { diff --git a/api/rust_grpc/src/window.rs b/api/rust_grpc/src/window.rs index f9ef759..43bc6de 100644 --- a/api/rust_grpc/src/window.rs +++ b/api/rust_grpc/src/window.rs @@ -1,4 +1,4 @@ -use crate::block_on; +use futures::executor::block_on; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::{ output::v0alpha1::output_service_client::OutputServiceClient, From e08c653ab4168f7a0dca09ef85deae8fd3c1cd7f Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sat, 20 Jan 2024 16:30:52 -0600 Subject: [PATCH 07/26] Add convenience methods --- api/rust_grpc/src/output.rs | 127 ++++++++++++++++++++++++++++++++++-- api/rust_grpc/src/tag.rs | 12 ++++ api/rust_grpc/src/window.rs | 28 ++++++++ 3 files changed, 163 insertions(+), 4 deletions(-) diff --git a/api/rust_grpc/src/output.rs b/api/rust_grpc/src/output.rs index 094ecf7..9d54c8f 100644 --- a/api/rust_grpc/src/output.rs +++ b/api/rust_grpc/src/output.rs @@ -16,8 +16,6 @@ use crate::tag::TagHandle; #[derive(Debug, Clone)] pub struct Output { - // client: OutputServiceClient, - // tag_client: TagServiceClient, channel: Channel, fut_sender: UnboundedSender>, } @@ -102,6 +100,22 @@ pub struct OutputHandle { pub(crate) name: String, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Alignment { + TopAlignLeft, + TopAlignCenter, + TopAlignRight, + BottomAlignLeft, + BottomAlignCenter, + BottomAlignRight, + LeftAlignTop, + LeftAlignCenter, + LeftAlignBottom, + RightAlignTop, + RightAlignCenter, + RightAlignBottom, +} + impl OutputHandle { pub fn set_location(&self, x: Option, y: Option) { let mut client = self.client.clone(); @@ -113,8 +127,67 @@ impl OutputHandle { .unwrap(); } - pub fn set_loc_adj_to(&self, other: &OutputHandle) { - todo!() + pub fn set_loc_adj_to(&self, other: &OutputHandle, alignment: Alignment) { + let self_props = self.props(); + let other_props = other.props(); + + let attempt_set_loc = || -> Option<()> { + let other_x = other_props.x?; + let other_y = other_props.y?; + let other_width = other_props.pixel_width? as i32; + let other_height = other_props.pixel_height? as i32; + + let self_width = self_props.pixel_width? as i32; + let self_height = self_props.pixel_height? as i32; + + use Alignment::*; + + let x: i32; + let y: i32; + + if let TopAlignLeft | TopAlignCenter | TopAlignRight | BottomAlignLeft + | BottomAlignCenter | BottomAlignRight = alignment + { + if let TopAlignLeft | TopAlignCenter | TopAlignRight = alignment { + y = other_y - self_height; + } else { + // bottom + y = other_y + other_height; + } + + match alignment { + TopAlignLeft | BottomAlignLeft => x = other_x, + TopAlignCenter | BottomAlignCenter => { + x = other_x + (other_width - self_width) / 2; + } + TopAlignRight | BottomAlignRight => x = other_x + (other_width - self_width), + _ => unreachable!(), + } + } else { + if let LeftAlignTop | LeftAlignCenter | LeftAlignBottom = alignment { + x = other_x - self_width; + } else { + x = other_x + other_width; + } + + match alignment { + LeftAlignTop | RightAlignTop => y = other_y, + LeftAlignCenter | RightAlignCenter => { + y = other_y + (other_height - self_height) / 2; + } + LeftAlignBottom | RightAlignBottom => { + y = other_y + (other_height - self_height); + } + _ => unreachable!(), + } + } + + self.set_location(Some(x), Some(y)); + + Some(()) + }; + + attempt_set_loc(); } pub fn props(&self) -> OutputProperties { @@ -149,6 +222,52 @@ impl OutputHandle { .collect(), } } + + // TODO: make a macro for the following or something + + pub fn make(&self) -> Option { + self.props().make + } + + pub fn model(&self) -> Option { + self.props().model + } + + pub fn x(&self) -> Option { + self.props().x + } + + pub fn y(&self) -> Option { + self.props().y + } + + pub fn pixel_width(&self) -> Option { + self.props().pixel_width + } + + pub fn pixel_height(&self) -> Option { + self.props().pixel_height + } + + pub fn refresh_rate(&self) -> Option { + self.props().refresh_rate + } + + pub fn physical_width(&self) -> Option { + self.props().physical_width + } + + pub fn physical_height(&self) -> Option { + self.props().physical_height + } + + pub fn focused(&self) -> Option { + self.props().focused + } + + pub fn tags(&self) -> Vec { + self.props().tags + } } #[derive(Clone, Debug)] diff --git a/api/rust_grpc/src/tag.rs b/api/rust_grpc/src/tag.rs index 7fe8ef6..22f6678 100644 --- a/api/rust_grpc/src/tag.rs +++ b/api/rust_grpc/src/tag.rs @@ -177,6 +177,18 @@ impl TagHandle { }), } } + + pub fn active(&self) -> Option { + self.props().active + } + + pub fn name(&self) -> Option { + self.props().name + } + + pub fn output(&self) -> Option { + self.props().output + } } pub struct TagProperties { diff --git a/api/rust_grpc/src/window.rs b/api/rust_grpc/src/window.rs index 43bc6de..f5b0208 100644 --- a/api/rust_grpc/src/window.rs +++ b/api/rust_grpc/src/window.rs @@ -252,4 +252,32 @@ impl WindowHandle { .collect(), } } + + pub fn geometry(&self) -> Option { + self.props().geometry + } + + pub fn class(&self) -> Option { + self.props().class + } + + pub fn title(&self) -> Option { + self.props().title + } + + pub fn focused(&self) -> Option { + self.props().focused + } + + pub fn floating(&self) -> Option { + self.props().floating + } + + pub fn fullscreen_or_maximized(&self) -> Option { + self.props().fullscreen_or_maximized + } + + pub fn tags(&self) -> Vec { + self.props().tags + } } From 575201e3164d3bc62be06a09bb78976f4b2d8aad Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sat, 20 Jan 2024 17:37:54 -0600 Subject: [PATCH 08/26] Add input docs --- api/rust_grpc/pinnacle-api-macros/src/lib.rs | 5 +- api/rust_grpc/src/input.rs | 162 ++++++++++++++++++- api/rust_grpc/src/lib.rs | 32 +++- 3 files changed, 190 insertions(+), 9 deletions(-) diff --git a/api/rust_grpc/pinnacle-api-macros/src/lib.rs b/api/rust_grpc/pinnacle-api-macros/src/lib.rs index 7249ef6..57e8ce5 100644 --- a/api/rust_grpc/pinnacle-api-macros/src/lib.rs +++ b/api/rust_grpc/pinnacle-api-macros/src/lib.rs @@ -6,14 +6,11 @@ pub fn config( args: proc_macro::TokenStream, item: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - let mut item = parse_macro_input!(item as syn::ItemFn); + let item = parse_macro_input!(item as syn::ItemFn); let module_ident = parse_macro_input!(args as syn::Ident); let vis = item.vis; - - item.sig.inputs = syn::punctuated::Punctuated::new(); let sig = item.sig; - let attrs = item.attrs; let stmts = item.block.stmts; diff --git a/api/rust_grpc/src/input.rs b/api/rust_grpc/src/input.rs index 8044459..96fb5e3 100644 --- a/api/rust_grpc/src/input.rs +++ b/api/rust_grpc/src/input.rs @@ -8,43 +8,81 @@ use pinnacle_api_defs::pinnacle::input::{ input_service_client::InputServiceClient, set_libinput_setting_request::{CalibrationMatrix, Setting}, SetKeybindRequest, SetLibinputSettingRequest, SetMousebindRequest, SetRepeatRateRequest, + SetXkbConfigRequest, }, }; use tonic::transport::Channel; use xkbcommon::xkb::Keysym; -pub use pinnacle_api_defs::pinnacle::input::v0alpha1::SetXkbConfigRequest as XkbConfig; - use self::libinput::LibinputSetting; + +/// Types for Libinput pub mod libinput; +/// A mouse button. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum MouseButton { + /// The left mouse button Left = 0x110, + /// The right mouse button Right = 0x111, + /// The middle mouse button Middle = 0x112, + /// The side mouse button Side = 0x113, + /// The extra mouse button Extra = 0x114, + /// The forward mouse button Forward = 0x115, + /// The backward mouse button Back = 0x116, } +/// Keyboard modifiers. #[repr(i32)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] pub enum Mod { + /// The shift key Shift = 1, + /// The ctrl key Ctrl, + /// The alt key Alt, + /// The super key, aka meta, win, mod4 Super, } +/// Press or release. #[repr(i32)] #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] pub enum MouseEdge { + /// Perform actions on button press Press = 1, + /// Perform actions on button release Release, } +/// A struct that lets you define xkeyboard config options. +/// +/// See `xkeyboard-config(7)` for more information. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +pub struct XkbConfig { + /// Files of rules to be used for keyboard mapping composition + pub rules: Option<&'static str>, + /// Name of the model of your keyboard type + pub model: Option<&'static str>, + /// Layout(s) you intend to use + pub layout: Option<&'static str>, + /// Variant(s) of the layout you intend to use + pub variant: Option<&'static str>, + /// Extra xkb configuration options + pub options: Option<&'static str>, +} + +/// The `Input` struct. +/// +/// This struct contains methods that allow you to set key- and mousebinds, +/// change xkeyboard and libinput settings, and change the keyboard's repeat rate. #[derive(Debug, Clone)] pub struct Input { // client: InputServiceClient, @@ -53,7 +91,10 @@ pub struct Input { } impl Input { - pub fn new(channel: Channel, fut_sender: UnboundedSender>) -> Self { + pub(crate) fn new( + channel: Channel, + fut_sender: UnboundedSender>, + ) -> Self { Self { channel, fut_sender, @@ -64,6 +105,33 @@ impl Input { InputServiceClient::new(self.channel.clone()) } + /// Set a keybind. + /// + /// If called with an already set keybind, it gets replaced. + /// + /// You must supply: + /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger. + /// - `key`: The key that needs to be pressed. This can be anything that implements the [Key] trait: + /// - `char` + /// - `&str` and `String`: These must be the xkeyboard key name without `XKB_` + /// - `u32` + /// - A [`keysym`][Keysym] from the [`xkbcommon`] re-export. + /// - `action`: A closure that will be run when the keybind is triggered. + /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate + /// something, consider using channels or [`Box::leak`]. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::Mod; + /// + /// // Set `Super + Shift + c` to close the focused window + /// input.keybind([Mod::Super, Mod::Shift], 'c', || { + /// if let Some(win) = window.get_focused() { + /// win.close(); + /// } + /// }); + /// ``` pub fn keybind( &self, mods: impl IntoIterator, @@ -97,6 +165,28 @@ impl Input { .unwrap(); } + /// Set a mousebind. + /// + /// If called with an already set mousebind, it gets replaced. + /// + /// You must supply: + /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger. + /// - `button`: A [`MouseButton`]. + /// - `edge`: A [`MouseEdge`]. This allows you to trigger the bind on either mouse press or release. + /// - `action`: A closure that will be run when the mousebind is triggered. + /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate + /// something, consider using channels or [`Box::leak`]. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; + /// + /// // Set `Super + left click` to start moving a window + /// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || { + /// window.begin_move(MouseButton::Press); + /// }); + /// ``` pub fn mousebind( &self, mods: impl IntoIterator, @@ -130,12 +220,50 @@ impl Input { .unwrap(); } + /// Set the xkeyboard config. + /// + /// This allows you to set several xkeyboard options like `layout` and `rules`. + /// + /// See `xkeyboard-config(7)` for more information. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::XkbConfig; + /// + /// input.set_xkb_config(Xkbconfig { + /// layout: Some("us,fr,ge"), + /// options: Some("ctrl:swapcaps,caps:shift"), + /// ..Default::default() + /// }); + /// ``` pub fn set_xkb_config(&self, xkb_config: XkbConfig) { let mut client = self.create_input_client(); - block_on(client.set_xkb_config(xkb_config)).unwrap(); + block_on(client.set_xkb_config(SetXkbConfigRequest { + rules: xkb_config.rules.map(String::from), + variant: xkb_config.variant.map(String::from), + layout: xkb_config.layout.map(String::from), + model: xkb_config.model.map(String::from), + options: xkb_config.options.map(String::from), + })) + .unwrap(); } + /// Set the keyboard's repeat rate. + /// + /// This allows you to set the time between holding down a key and it repeating + /// as well as the time between each repeat. + /// + /// Units are in milliseconds. + /// + /// # Examples + /// + /// ``` + /// // Set keyboard to repeat after holding down for half a second, + /// // and repeat once every 25ms (40 times a second) + /// input.set_repeat_rate(25, 500); + /// ``` pub fn set_repeat_rate(&self, rate: i32, delay: i32) { let mut client = self.create_input_client(); @@ -146,6 +274,30 @@ impl Input { .unwrap(); } + /// Set a libinput setting. + /// + /// From [freedesktop.org][https://www.freedesktop.org/wiki/Software/libinput/]: + /// > libinput is a library to handle input devices in Wayland compositors + /// + /// As such, this method allows you to set various settings related to input devices. + /// This includes things like pointer acceleration and natural scrolling. + /// + /// See [`LibinputSetting`] for all the settings you can change. + /// + /// Note: currently Pinnacle applies anything set here to *every* device, regardless of what it + /// actually is. This will be fixed in the future. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::libinput::*; + /// + /// // Set pointer acceleration to flat + /// input.set_libinput_setting(LibinputSetting::AccelProfile(AccelProfile::Flat)); + /// + /// // Enable natural scrolling (reverses scroll direction; usually used with trackpads) + /// input.set_libinput_setting(LibinputSetting::NaturalScroll(true)); + /// ``` pub fn set_libinput_setting(&self, setting: LibinputSetting) { let mut client = self.create_input_client(); @@ -179,7 +331,9 @@ impl Input { } } +/// A trait that designates anything that can be converted into a [`Keysym`]. pub trait Key { + /// Convert this into a [`Keysym`]. fn into_keysym(self) -> Keysym; } diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs index 508ee89..99383c2 100644 --- a/api/rust_grpc/src/lib.rs +++ b/api/rust_grpc/src/lib.rs @@ -1,4 +1,15 @@ -// #![warn(missing_docs)] +#![warn(missing_docs)] + +//! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s +//! configuration API. +//! +//! This library allows to to interface with the Pinnacle compositor and configure various aspects +//! like input and the tag system. +//! +//! # Configuration +//! +//! To create your own Rust config, create a Cargo project in `~/.config/pinnacle`. +//! TODO: use std::sync::OnceLock; @@ -14,12 +25,19 @@ use tonic::transport::{Endpoint, Uri}; use tower::service_fn; use window::Window; +/// Input management pub mod input; +/// Output management pub mod output; +/// Main compositor options pub mod pinnacle; +/// Process management pub mod process; +/// Tag management pub mod tag; +/// Utilities pub mod util; +/// Window management pub mod window; pub use pinnacle_api_macros::config; @@ -32,6 +50,7 @@ static INPUT: OnceLock = OnceLock::new(); static OUTPUT: OnceLock = OnceLock::new(); static TAG: OnceLock = OnceLock::new(); +/// A struct containing static references to all of the configuration structs. #[derive(Debug, Clone)] pub struct ApiModules { pub pinnacle: &'static Pinnacle, @@ -42,6 +61,10 @@ pub struct ApiModules { pub tag: &'static Tag, } +/// Connects to Pinnacle and builds the configuration structs. +/// +/// This function is inserted at the top of your config through the [`config`] macro. +/// You should use that macro instead of this function directly. pub async fn connect( ) -> Result<(ApiModules, UnboundedReceiver>), Box> { let channel = Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket @@ -76,6 +99,13 @@ pub async fn connect( Ok((modules, fut_recv)) } +/// Listen to Pinnacle for incoming messages. +/// +/// This will run all futures returned by configuration methods that take in callbacks in order to +/// call them. +/// +/// This function is inserted at the end of your config through the [`config`] macro. +/// You should use the macro instead of this function directly. pub async fn listen( fut_recv: UnboundedReceiver>, // api_modules: ApiModules<'a>, ) { From af1c3148f1fa2df8b522aa2e81d267b394a65603 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sat, 20 Jan 2024 20:13:38 -0600 Subject: [PATCH 09/26] Add tag docs --- api/rust_grpc/src/input.rs | 24 +++- api/rust_grpc/src/lib.rs | 1 - api/rust_grpc/src/tag.rs | 229 +++++++++++++++++++++++++++++++++++-- 3 files changed, 237 insertions(+), 17 deletions(-) diff --git a/api/rust_grpc/src/input.rs b/api/rust_grpc/src/input.rs index 96fb5e3..1c70c6d 100644 --- a/api/rust_grpc/src/input.rs +++ b/api/rust_grpc/src/input.rs @@ -1,3 +1,9 @@ +//! Input management. +//! +//! This module provides [`Input`], a struct that gives you several different +//! methods for setting key- and mousebinds, changing xkeyboard settings, and more. +//! View the struct's documentation for more information. + use futures::{ channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, }; @@ -85,7 +91,6 @@ pub struct XkbConfig { /// change xkeyboard and libinput settings, and change the keyboard's repeat rate. #[derive(Debug, Clone)] pub struct Input { - // client: InputServiceClient, channel: Channel, fut_sender: UnboundedSender>, } @@ -113,8 +118,10 @@ impl Input { /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger. /// - `key`: The key that needs to be pressed. This can be anything that implements the [Key] trait: /// - `char` - /// - `&str` and `String`: These must be the xkeyboard key name without `XKB_` - /// - `u32` + /// - `&str` and `String`: This is any name from + /// [xkbcommon-keysyms.h](https://xkbcommon.org/doc/current/xkbcommon-keysyms_8h.html) + /// without the `XKB_KEY_` prefix. + /// - `u32`: The numerical key code from the website above. /// - A [`keysym`][Keysym] from the [`xkbcommon`] re-export. /// - `action`: A closure that will be run when the keybind is triggered. /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate @@ -131,6 +138,15 @@ impl Input { /// win.close(); /// } /// }); + /// + /// // With a string key + /// input.keybind([], "BackSpace", || { /* ... */ }); + /// + /// // With a numeric key + /// input.keybind([], 65, || { /* ... */ }); // 65 = 'A' + /// + /// // With a `Keysym` + /// input.keybind([], pinnacle_api::xkbcommon::xkb::Keysym::Return, || { /* ... */ }); /// ``` pub fn keybind( &self, @@ -276,7 +292,7 @@ impl Input { /// Set a libinput setting. /// - /// From [freedesktop.org][https://www.freedesktop.org/wiki/Software/libinput/]: + /// From [freedesktop.org](https://www.freedesktop.org/wiki/Software/libinput/): /// > libinput is a library to handle input devices in Wayland compositors /// /// As such, this method allows you to set various settings related to input devices. diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs index 99383c2..5503520 100644 --- a/api/rust_grpc/src/lib.rs +++ b/api/rust_grpc/src/lib.rs @@ -33,7 +33,6 @@ pub mod output; pub mod pinnacle; /// Process management pub mod process; -/// Tag management pub mod tag; /// Utilities pub mod util; diff --git a/api/rust_grpc/src/tag.rs b/api/rust_grpc/src/tag.rs index 22f6678..e8b04a0 100644 --- a/api/rust_grpc/src/tag.rs +++ b/api/rust_grpc/src/tag.rs @@ -1,3 +1,30 @@ +//! Tag management. +//! +//! This module allows you to interact with Pinnacle's tag system. +//! +//! # The Tag System +//! Many Wayland compositors use workspaces for window management. +//! Each window is assigned to a workspace and will only show up if that workspace is being +//! viewed. This is a find way to manage windows, but it's not that powerful. +//! +//! Instead, Pinnacle works with a tag system similar to window managers like [dwm](https://dwm.suckless.org/) +//! and, the window manager Pinnacle takes inspiration from, [awesome](https://awesomewm.org/). +//! +//! In a tag system, there are no workspaces. Instead, each window can be tagged with zero or more +//! tags, and zero or more tags can be displayed on a monitor at once. This allows you to, for +//! example, bring in your browsers on the same screen as your IDE by toggling the "Browser" tag. +//! +//! Workspaces can be emulated by only displaying one tag at a time. Combining this feature with +//! the ability to tag windows with multiple tags allows you to have one window show up on multiple +//! different "workspaces". As you can see, this system is much more powerful than workspaces +//! alone. +//! +//! # Configuration +//! `tag` contains the [`Tag`] struct, which allows you to add new tags +//! and get handles to already defined ones. +//! +//! These [`TagHandle`]s allow you to manipulate individual tags and get their properties. + use futures::{channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture}; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::{ @@ -14,6 +41,7 @@ use tonic::transport::Channel; use crate::output::{Output, OutputHandle}; +/// A struct that allows you to add and remove tags and get [`TagHandle`]s. #[derive(Clone, Debug)] pub struct Tag { channel: Channel, @@ -21,21 +49,37 @@ pub struct Tag { } impl Tag { - pub fn new(channel: Channel, fut_sender: UnboundedSender>) -> Self { + pub(crate) fn new( + channel: Channel, + fut_sender: UnboundedSender>, + ) -> Self { Self { channel, fut_sender, } } - pub fn create_tag_client(&self) -> TagServiceClient { + fn create_tag_client(&self) -> TagServiceClient { TagServiceClient::new(self.channel.clone()) } - pub fn create_output_client(&self) -> OutputServiceClient { + fn create_output_client(&self) -> OutputServiceClient { OutputServiceClient::new(self.channel.clone()) } + /// Add tags to the specified output. + /// + /// This will add tags with the given names to `output` and return [`TagHandle`]s to all of + /// them. + /// + /// # Examples + /// + /// ``` + /// // Add tags 1-5 to the focused output + /// if let Some(op) = output.get_focused() { + /// let tags = tag.add(&op, ["1", "2", "3", "4", "5"]); + /// } + /// ``` pub fn add( &self, output: &OutputHandle, @@ -60,6 +104,13 @@ impl Tag { }) } + /// Get handles to all tags across all outputs. + /// + /// # Examples + /// + /// ``` + /// let all_tags = tag.get_all(); + /// ``` pub fn get_all(&self) -> impl Iterator { let mut client = self.create_tag_client(); let output_client = self.create_output_client(); @@ -75,8 +126,28 @@ impl Tag { }) } - pub fn get(&self, name: impl Into, output: Option<&OutputHandle>) -> Option { + /// Get a handle to the first tag with the given name on `output`. + /// + /// If `output` is `None`, the focused output will be used. + /// + /// # Examples + /// + /// ``` + /// // Get tag "1" on output "HDMI-1" + /// if let Some(op) = output.get_by_name("HDMI-1") { + /// let tg = tag.get("1", &op); + /// } + /// + /// // Get tag "Thing" on the focused output + /// let tg = tag.get("Thing", None); + /// ``` + pub fn get<'a>( + &self, + name: impl Into, + output: impl Into>, + ) -> Option { let name = name.into(); + let output: Option<&OutputHandle> = output.into(); let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); self.get_all().find(|tag| { @@ -93,8 +164,28 @@ impl Tag { same_tag_name && same_output }) } + + /// Remove the given tags from their outputs. + /// + /// # Examples + /// + /// ``` + /// let tags = tag.add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]); + /// + /// tag.remove(tags); // "DP-1" no longer has any tags + /// ``` + pub fn remove(&self, tags: impl IntoIterator) { + let tag_ids = tags.into_iter().map(|handle| handle.id).collect::>(); + + let mut client = self.create_tag_client(); + + block_on(client.remove(RemoveRequest { tag_ids })).unwrap(); + } } +/// A handle to a tag. +/// +/// This handle allows you to do things like switch to tags and get their properties. #[derive(Debug, Clone)] pub struct TagHandle { pub(crate) client: TagServiceClient, @@ -102,19 +193,67 @@ pub struct TagHandle { pub(crate) id: u32, } +/// Various static layouts. #[repr(i32)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] pub enum Layout { + /// One master window on the left with all other windows stacked to the right MasterStack = 1, + /// Windows split in half towards the bottom right corner Dwindle, + /// Windows split in half in a spiral Spiral, + /// One main corner window in the top left with a column of windows on the right and a row on the bottom CornerTopLeft, + /// One main corner window in the top right with a column of windows on the left and a row on the bottom CornerTopRight, + /// One main corner window in the bottom left with a column of windows on the right and a row on the top. CornerBottomLeft, + /// One main corner window in the bottom right with a column of windows on the left and a row on the top. CornerBottomRight, } impl TagHandle { + /// Activate this tag and deactivate all other ones on the same output. + /// + /// This essentially emulates what a traditional workspace is. + /// + /// # Examples + /// + /// ``` + /// // Assume the focused output has the following inactive tags and windows: + /// // "1": Alacritty + /// // "2": Firefox, Discord + /// // "3": Steam + /// tag.get("2")?.switch_to(); // Displays Firefox and Discord + /// tag.get("3")?.switch_to(); // Displays Steam + /// ``` + pub fn switch_to(&self) { + let mut client = self.client.clone(); + block_on(client.switch_to(SwitchToRequest { + tag_id: Some(self.id), + })) + .unwrap(); + } + + /// Set this tag to active or not. + /// + /// While active, windows with this tag will be displayed. + /// + /// While inactive, windows with this tag will not be displayed unless they have other active + /// tags. + /// + /// # Examples + /// + /// ``` + /// // Assume the focused output has the following inactive tags and windows: + /// // "1": Alacritty + /// // "2": Firefox, Discord + /// // "3": Steam + /// tag.get("2")?.set_active(true); // Displays Firefox and Discord + /// tag.get("3")?.set_active(true); // Displays Firefox, Discord, and Steam + /// tag.get("2")?.set_active(false); // Displays Steam + /// ``` pub fn set_active(&self, set: bool) { let mut client = self.client.clone(); block_on(client.set_active(SetActiveRequest { @@ -124,6 +263,25 @@ impl TagHandle { .unwrap(); } + /// Toggle this tag between active and inactive. + /// + /// While active, windows with this tag will be displayed. + /// + /// While inactive, windows with this tag will not be displayed unless they have other active + /// tags. + /// + /// # Examples + /// + /// ``` + /// // Assume the focused output has the following inactive tags and windows: + /// // "1": Alacritty + /// // "2": Firefox, Discord + /// // "3": Steam + /// tag.get("2")?.toggle(); // Displays Firefox and Discord + /// tag.get("3")?.toggle(); // Displays Firefox, Discord, and Steam + /// tag.get("3")?.toggle(); // Displays Firefox, Discord + /// tag.get("2")?.toggle(); // Displays nothing + /// ``` pub fn toggle_active(&self) { let mut client = self.client.clone(); block_on(client.set_active(SetActiveRequest { @@ -133,6 +291,19 @@ impl TagHandle { .unwrap(); } + /// Remove this tag from its output. + /// + /// # Examples + /// + /// ``` + /// let tags = tag + /// .add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]) + /// .collect::>; + /// + /// tags[1].remove(); + /// tags[3].remove(); + /// // "DP-1" now only has tags "1" and "Buckle" + /// ``` pub fn remove(mut self) { block_on(self.client.remove(RemoveRequest { tag_ids: vec![self.id], @@ -140,6 +311,22 @@ impl TagHandle { .unwrap(); } + /// Set this tag's layout. + /// + /// Layouting only applies to tiled windows (windows that are not floating, maximized, or + /// fullscreen). If multiple tags are active on an output, the first active tag's layout will + /// determine the layout strategy. + /// + /// See [`Layout`] for the different static layouts Pinnacle currently has to offer. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::tag::Layout; + /// + /// // Set the layout of tag "1" on the focused output to "corner top left". + /// tag.get("1", None)?.set_layout(Layout::CornerTopLeft); + /// ``` pub fn set_layout(&self, layout: Layout) { let mut client = self.client.clone(); block_on(client.set_layout(SetLayoutRequest { @@ -149,14 +336,19 @@ impl TagHandle { .unwrap(); } - pub fn switch_to(&self) { - let mut client = self.client.clone(); - block_on(client.switch_to(SwitchToRequest { - tag_id: Some(self.id), - })) - .unwrap(); - } - + /// Get all properties of this tag. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::tag::TagProperties; + /// + /// let TagProperties { + /// active, + /// name, + /// output, + /// } = tag.get("1", None)?.props(); + /// ``` pub fn props(&self) -> TagProperties { let mut client = self.client.clone(); let output_client = self.output_client.clone(); @@ -178,21 +370,34 @@ impl TagHandle { } } + /// Get this tag's active status. + /// + /// Shorthand for `self.props().active`. pub fn active(&self) -> Option { self.props().active } + /// Get this tag's name. + /// + /// Shorthand for `self.props().name`. pub fn name(&self) -> Option { self.props().name } + /// Get a handle to the output this tag is on. + /// + /// Shorthand for `self.props().output`. pub fn output(&self) -> Option { self.props().output } } +/// Properties of a tag. pub struct TagProperties { + /// Whether the tag is active or not pub active: Option, + /// The name of the tag pub name: Option, + /// The output the tag is on pub output: Option, } From b262f11f4d1c2953b3a4194bb12a7d39bd43a3d5 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sat, 20 Jan 2024 21:27:22 -0600 Subject: [PATCH 10/26] Add layout cycler --- api/rust_grpc/examples/default_config/main.rs | 28 ++++- api/rust_grpc/src/tag.rs | 100 ++++++++++++++++++ 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/api/rust_grpc/examples/default_config/main.rs b/api/rust_grpc/examples/default_config/main.rs index 191962a..b2c5a42 100644 --- a/api/rust_grpc/examples/default_config/main.rs +++ b/api/rust_grpc/examples/default_config/main.rs @@ -1,8 +1,9 @@ use pinnacle_api::{ input::{Mod, MouseButton, MouseEdge}, + tag::{Layout, LayoutCycler}, ApiModules, }; -use xkbcommon::xkb::keysyms; +use xkbcommon::xkb::Keysym; #[pinnacle_api::config(modules)] #[tokio::main] @@ -38,11 +39,11 @@ async fn main() { } }); - input.keybind([mod_key], keysyms::KEY_Return, || { + input.keybind([mod_key], Keysym::Return, || { process.spawn(["alacritty"]); }); - input.keybind([mod_key, Mod::Alt], keysyms::KEY_space, || { + input.keybind([mod_key, Mod::Alt], Keysym::space, || { if let Some(window) = window.get_focused() { window.toggle_floating(); } @@ -71,6 +72,27 @@ async fn main() { process.spawn_once(["alacritty"]); + let LayoutCycler { + prev: layout_prev, + next: layout_next, + } = tag.new_layout_cycler([ + Layout::MasterStack, + Layout::Dwindle, + Layout::Spiral, + Layout::CornerTopLeft, + Layout::CornerTopRight, + Layout::CornerBottomLeft, + Layout::CornerBottomRight, + ]); + + input.keybind([mod_key], Keysym::space, move || { + layout_next(None); + }); + + input.keybind([mod_key, Mod::Shift], Keysym::space, move || { + layout_prev(None); + }); + for tag_name in tag_names { input.keybind([mod_key], tag_name, move || { if let Some(tg) = tag.get(tag_name, None) { diff --git a/api/rust_grpc/src/tag.rs b/api/rust_grpc/src/tag.rs index e8b04a0..870c4b5 100644 --- a/api/rust_grpc/src/tag.rs +++ b/api/rust_grpc/src/tag.rs @@ -25,6 +25,11 @@ //! //! These [`TagHandle`]s allow you to manipulate individual tags and get their properties. +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + use futures::{channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture}; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::{ @@ -181,6 +186,101 @@ impl Tag { block_on(client.remove(RemoveRequest { tag_ids })).unwrap(); } + + /// Create a [`LayoutCycler`] to cycle layouts on outputs. + /// + /// This will create a `LayoutCycler` with two functions: one to cycle forward the layout for + /// the first active tag on the specified output, and one to cycle backward. + /// + /// If you do not specify an output for `LayoutCycler` functions, it will default to the + /// focused output. + /// + /// # Examples + /// + /// ``` + /// + /// ``` + pub fn new_layout_cycler(&self, layouts: impl IntoIterator) -> LayoutCycler { + let indices = Arc::new(Mutex::new(HashMap::::new())); + let indices_clone = indices.clone(); + + let layouts = layouts.into_iter().collect::>(); + let layouts_clone = layouts.clone(); + let len = layouts.len(); + + let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); + let output_module_clone = output_module.clone(); + + let next = move |output: Option<&OutputHandle>| { + let Some(output) = output.cloned().or_else(|| output_module.get_focused()) else { + return; + }; + + let Some(first_tag) = output + .props() + .tags + .into_iter() + .find(|tag| tag.active() == Some(true)) + else { + return; + }; + + let mut indices = indices.lock().expect("layout next mutex lock failed"); + let index = indices.entry(first_tag.id).or_insert(0); + + if *index + 1 >= len { + *index = 0; + } else { + *index += 1; + } + + first_tag.set_layout(layouts[*index]); + }; + + let prev = move |output: Option<&OutputHandle>| { + let Some(output) = output + .cloned() + .or_else(|| output_module_clone.get_focused()) + else { + return; + }; + + let Some(first_tag) = output + .props() + .tags + .into_iter() + .find(|tag| tag.active() == Some(true)) + else { + return; + }; + + let mut indices = indices_clone.lock().expect("layout next mutex lock failed"); + let index = indices.entry(first_tag.id).or_insert(0); + + if index.checked_sub(1).is_none() { + *index = len - 1; + } else { + *index -= 1; + } + + first_tag.set_layout(layouts_clone[*index]); + }; + + LayoutCycler { + prev: Box::new(prev), + next: Box::new(next), + } + } +} + +/// A layout cycler that keeps track of tags and their layouts and provides functions to cycle +/// layouts on them. +#[allow(clippy::type_complexity)] +pub struct LayoutCycler { + /// Cycle to the next layout on the given output, or the focused output if `None`. + pub prev: Box) + Send + Sync + 'static>, + /// Cycle to the previous layout on the given output, or the focused output if `None`. + pub next: Box) + Send + Sync + 'static>, } /// A handle to a tag. From b55c9e78133442a4328bdf5a4fd02a410e085827 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sat, 20 Jan 2024 22:14:49 -0600 Subject: [PATCH 11/26] Add output docs --- api/lua/pinnacle/output.lua | 2 +- api/rust_grpc/src/lib.rs | 6 - api/rust_grpc/src/output.rs | 237 +++++++++++++++++++++++++++++++++++- api/rust_grpc/src/tag.rs | 25 ++++ 4 files changed, 258 insertions(+), 12 deletions(-) diff --git a/api/lua/pinnacle/output.lua b/api/lua/pinnacle/output.lua index 8e5415c..3139c9d 100644 --- a/api/lua/pinnacle/output.lua +++ b/api/lua/pinnacle/output.lua @@ -191,7 +191,7 @@ end --- -- ┌─────┤ │ --- -- │DP-1 │HDMI-1 │ --- -- └─────┴───────┘ ---- -- Notice that x = 0 aligns with the top of "DP-1", and the top of "HDMI-1" is at x = -360. +--- -- Notice that y = 0 aligns with the top of "DP-1", and the top of "HDMI-1" is at y = -360. ---``` --- ---@param loc { x: integer?, y: integer? } diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs index 5503520..18de691 100644 --- a/api/rust_grpc/src/lib.rs +++ b/api/rust_grpc/src/lib.rs @@ -25,18 +25,12 @@ use tonic::transport::{Endpoint, Uri}; use tower::service_fn; use window::Window; -/// Input management pub mod input; -/// Output management pub mod output; -/// Main compositor options pub mod pinnacle; -/// Process management pub mod process; pub mod tag; -/// Utilities pub mod util; -/// Window management pub mod window; pub use pinnacle_api_macros::config; diff --git a/api/rust_grpc/src/output.rs b/api/rust_grpc/src/output.rs index 9d54c8f..a794db0 100644 --- a/api/rust_grpc/src/output.rs +++ b/api/rust_grpc/src/output.rs @@ -1,3 +1,10 @@ +//! Output management. +//! +//! An output is Pinnacle's terminology for a monitor. +//! +//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different +//! connected monitors and set them up. + use futures::{ channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, }; @@ -14,6 +21,9 @@ use tonic::transport::Channel; use crate::tag::TagHandle; +/// A struct that allows you to get handles to connected outputs and set them up. +/// +/// See [`OutputHandle`] for more information. #[derive(Debug, Clone)] pub struct Output { channel: Channel, @@ -21,7 +31,10 @@ pub struct Output { } impl Output { - pub fn new(channel: Channel, fut_sender: UnboundedSender>) -> Self { + pub(crate) fn new( + channel: Channel, + fut_sender: UnboundedSender>, + ) -> Self { Self { channel, fut_sender, @@ -36,6 +49,13 @@ impl Output { TagServiceClient::new(self.channel.clone()) } + /// Get a handle to all connected outputs. + /// + /// # Examples + /// + /// ``` + /// let outputs = output.get_all(); + /// ``` pub fn get_all(&self) -> impl Iterator { let mut client = self.create_output_client(); let tag_client = self.create_tag_client(); @@ -51,12 +71,55 @@ impl Output { }) } + /// Get a handle to the output with the given name. + /// + /// By "name", we mean the name of the connector the output is connected to. + /// + /// # Examples + /// + /// ``` + /// let op = output.get_by_name("eDP-1")?; + /// let op2 = output.get_by_name("HDMI-2")?; + /// ``` + pub fn get_by_name(&self, name: impl Into) -> Option { + let name: String = name.into(); + self.get_all().find(|output| output.name == name) + } + + /// Get a handle to the focused output. + /// + /// This is currently implemented as the one that has had the most recent pointer movement. + /// + /// # Examples + /// + /// ``` + /// let op = output.get_focused()?; + /// ``` pub fn get_focused(&self) -> Option { self.get_all() .find(|output| matches!(output.props().focused, Some(true))) } - pub fn connect_for_all(&self, mut for_all: impl FnMut(OutputHandle) + 'static + Send) { + /// Connect a closure to be run on all current and future outputs. + /// + /// When called, `connect_for_all` will do two things: + /// 1. Immediately run `for_all` with all currently connected outputs. + /// 2. Create a future that will call `for_all` with any newly connected outputs. + /// + /// Note that `for_all` will *not* run with outputs that have been unplugged and replugged. + /// This is to prevent duplicate setup. Instead, the compositor keeps track of any tags and + /// state the output had when unplugged and restores them on replug. + /// + /// # Examples + /// + /// ``` + /// // Add tags 1-3 to all outputs and set tag "1" to active + /// output.connect_for_all(|op| { + /// let tags = tag.add(&op, ["1", "2", "3"]); + /// tags.next().unwrap().set_active(true); + /// }); + /// ``` + pub fn connect_for_all(&self, mut for_all: impl FnMut(OutputHandle) + Send + 'static) { for output in self.get_all() { for_all(output); } @@ -93,6 +156,9 @@ impl Output { } } +/// A handle to an output. +/// +/// This allows you to manipulate outputs and get their properties. #[derive(Clone, Debug)] pub struct OutputHandle { pub(crate) client: OutputServiceClient, @@ -100,37 +166,120 @@ pub struct OutputHandle { pub(crate) name: String, } +/// The alignment to use for [`OutputHandle::set_loc_adj_to`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Alignment { + /// Set above, align left borders TopAlignLeft, + /// Set above, align centers TopAlignCenter, + /// Set above, align right borders TopAlignRight, + /// Set below, align left borders BottomAlignLeft, + /// Set below, align centers BottomAlignCenter, + /// Set below, align right borders BottomAlignRight, + /// Set to left, align top borders LeftAlignTop, + /// Set to left, align centers LeftAlignCenter, + /// Set to left, align bottom borders LeftAlignBottom, + /// Set to right, align top borders RightAlignTop, + /// Set to right, align centers RightAlignCenter, + /// Set to right, align bottom borders RightAlignBottom, } impl OutputHandle { - pub fn set_location(&self, x: Option, y: Option) { + /// Set the location of this output in the global space. + /// + /// On startup, Pinnacle will lay out all connected outputs starting at (0, 0) + /// and going to the right, with their top borders aligned. + /// + /// This method allows you to move outputs where necessary. + /// + /// Note: If you leave space between two outputs when setting their locations, + /// the pointer will not be able to move between them. + /// + /// # Examples + /// + /// ``` + /// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions: + /// // - "DP-1": ┌─────┐ + /// // │ │1920x1080 + /// // └─────┘ + /// // - "HDMI-1": ┌───────┐ + /// // │ 2560x │ + /// // │ 1440 │ + /// // └───────┘ + /// + /// output.get_by_name("DP-1")?.set_location(0, 0); + /// output.get_by_name("HDMI-1")?.set_location(1920, -360); + /// + /// // Results in: + /// // x=0 ┌───────┐y=-360 + /// // y=0┌─────┤ │ + /// // │DP-1 │HDMI-1 │ + /// // └─────┴───────┘ + /// // ^x=1920 + /// ``` + pub fn set_location(&self, x: impl Into>, y: impl Into>) { let mut client = self.client.clone(); block_on(client.set_location(SetLocationRequest { output_name: Some(self.name.clone()), - x, - y, + x: x.into(), + y: y.into(), })) .unwrap(); } + /// Set this output adjacent to another one. + /// + /// This is a helper method over [`OutputHandle::set_location`] to make laying out outputs + /// easier. + /// + /// `alignment` is an [`Alignment`] of how you want this output to be placed. + /// For example, [`TopAlignLeft`][Alignment::TopAlignLeft] will place this output + /// above `other` and align the left borders. + /// Similarly, [`RightAlignCenter`][Alignment::RightAlignCenter] will place this output + /// to the right of `other` and align their centers. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::output::Alignment; + /// + /// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions: + /// // - "DP-1": ┌─────┐ + /// // │ │1920x1080 + /// // └─────┘ + /// // - "HDMI-1": ┌───────┐ + /// // │ 2560x │ + /// // │ 1440 │ + /// // └───────┘ + /// + /// output.get_by_name("DP-1")?.set_loc_adj_to(output.get_by_name("HDMI-1")?, Alignment::BottomAlignRight); + /// + /// // Results in: + /// // ┌───────┐ + /// // │ │ + /// // │HDMI-1 │ + /// // └──┬────┤ + /// // │DP-1│ + /// // └────┘ + /// // Notice that "DP-1" now has the coordinates (2280, 1440) because "DP-1" is getting moved, not "HDMI-1". + /// // "HDMI-1" was placed at (1920, 0) during the compositor's initial output layout. + /// ``` pub fn set_loc_adj_to(&self, other: &OutputHandle, alignment: Alignment) { let self_props = self.props(); let other_props = other.props(); + // poor man's try {} let attempt_set_loc = || -> Option<()> { let other_x = other_props.x?; let other_y = other_props.y?; @@ -190,6 +339,25 @@ impl OutputHandle { attempt_set_loc(); } + /// Get all properties of this output. + /// + /// # Examples + /// + /// ``` + /// let OutputProperties { + /// make, + /// model, + /// x, + /// y, + /// pixel_width, + /// pixel_height, + /// refresh_rate, + /// physical_width, + /// physical_height, + /// focused, + /// tags, + /// } = output.get_focused()?.props(); + /// ``` pub fn props(&self) -> OutputProperties { let mut client = self.client.clone(); let response = block_on( @@ -225,62 +393,121 @@ impl OutputHandle { // TODO: make a macro for the following or something + /// Get this output's make. + /// + /// Shorthand for `self.props().make`. pub fn make(&self) -> Option { self.props().make } + /// Get this output's model. + /// + /// Shorthand for `self.props().make`. pub fn model(&self) -> Option { self.props().model } + /// Get this output's x position in the global space. + /// + /// Shorthand for `self.props().x`. pub fn x(&self) -> Option { self.props().x } + /// Get this output's y position in the global space. + /// + /// Shorthand for `self.props().y`. pub fn y(&self) -> Option { self.props().y } + /// Get this output's screen width in pixels. + /// + /// Shorthand for `self.props().pixel_width`. pub fn pixel_width(&self) -> Option { self.props().pixel_width } + /// Get this output's screen height in pixels. + /// + /// Shorthand for `self.props().pixel_height`. pub fn pixel_height(&self) -> Option { self.props().pixel_height } + /// Get this output's refresh rate in millihertz. + /// + /// For example, 144Hz will be returned as 144000. + /// + /// Shorthand for `self.props().refresh_rate`. pub fn refresh_rate(&self) -> Option { self.props().refresh_rate } + /// Get this output's physical width in millimeters. + /// + /// Shorthand for `self.props().physical_width`. pub fn physical_width(&self) -> Option { self.props().physical_width } + /// Get this output's physical height in millimeters. + /// + /// Shorthand for `self.props().physical_height`. pub fn physical_height(&self) -> Option { self.props().physical_height } + /// Get whether this output is focused or not. + /// + /// This is currently implemented as the output with the most recent pointer motion. + /// + /// Shorthand for `self.props().focused`. pub fn focused(&self) -> Option { self.props().focused } + /// Get the tags this output has. + /// + /// Shorthand for `self.props().tags` pub fn tags(&self) -> Vec { self.props().tags } + + /// Get this output's unique name (the name of its connector). + pub fn name(&self) -> &str { + &self.name + } } +/// The properties of an output. #[derive(Clone, Debug)] pub struct OutputProperties { + /// The make of the output pub make: Option, + /// The model of the output + /// + /// This is something like "27GL83A" or whatever crap monitor manufacturers name their monitors + /// these days. pub model: Option, + /// The x position of the output in the global space pub x: Option, + /// The y position of the output in the global space pub y: Option, + /// The output's screen width in pixels pub pixel_width: Option, + /// The output's screen height in pixels pub pixel_height: Option, + /// The output's refresh rate in millihertz pub refresh_rate: Option, + /// The output's physical width in millimeters pub physical_width: Option, + /// The output's physical height in millimeters pub physical_height: Option, + /// Whether this output is focused or not + /// + /// This is currently implemented as the output with the most recent pointer motion. pub focused: Option, + /// The tags this output has pub tags: Vec, } diff --git a/api/rust_grpc/src/tag.rs b/api/rust_grpc/src/tag.rs index 870c4b5..256fdd3 100644 --- a/api/rust_grpc/src/tag.rs +++ b/api/rust_grpc/src/tag.rs @@ -198,7 +198,32 @@ impl Tag { /// # Examples /// /// ``` + /// use pinnacle_api::tag::{Layout, LayoutCycler}; + /// use pinnacle_api::xkbcommon::xkb::Keysym; + /// use pinnacle_api::input::Mod; /// + /// // Create a layout cycler that cycles through the listed layouts + /// let LayoutCycler { + /// prev: layout_prev, + /// next: layout_next, + /// } = tag.new_layout_cycler([ + /// Layout::MasterStack, + /// Layout::Dwindle, + /// Layout::Spiral, + /// Layout::CornerTopLeft, + /// Layout::CornerTopRight, + /// Layout::CornerBottomLeft, + /// Layout::CornerBottomRight, + /// ]); + /// + /// // Cycle layouts forward on the focused output + /// layout_next(None); + /// + /// // Cycle layouts backward on the focused output + /// layout_prev(None); + /// + /// // Cycle layouts forward on "eDP-1" + /// layout_next(output.get_by_name("eDP-1")?); /// ``` pub fn new_layout_cycler(&self, layouts: impl IntoIterator) -> LayoutCycler { let indices = Arc::new(Mutex::new(HashMap::::new())); From 119251223098106622d0ea38e254a65bff34b99a Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sat, 20 Jan 2024 22:48:44 -0600 Subject: [PATCH 12/26] Add window docs --- api/rust_grpc/src/output.rs | 2 + api/rust_grpc/src/window.rs | 232 +++++++++++++++++++++++++++++++++++- 2 files changed, 230 insertions(+), 4 deletions(-) diff --git a/api/rust_grpc/src/output.rs b/api/rust_grpc/src/output.rs index a794db0..6ef6d9b 100644 --- a/api/rust_grpc/src/output.rs +++ b/api/rust_grpc/src/output.rs @@ -344,6 +344,8 @@ impl OutputHandle { /// # Examples /// /// ``` + /// use pinnacle_api::output::OutputProperties; + /// /// let OutputProperties { /// make, /// model, diff --git a/api/rust_grpc/src/window.rs b/api/rust_grpc/src/window.rs index f5b0208..0c7f10b 100644 --- a/api/rust_grpc/src/window.rs +++ b/api/rust_grpc/src/window.rs @@ -1,3 +1,11 @@ +//! Window management. +//! +//! This module provides [`Window`], which allows you to get [`WindowHandle`]s and move and resize +//! windows using the mouse. +//! +//! [`WindowHandle`]s allow you to do things like resize and move windows, toggle them between +//! floating and tiled, close them, and more. + use futures::executor::block_on; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::{ @@ -18,28 +26,48 @@ use tonic::transport::Channel; use crate::{input::MouseButton, tag::TagHandle, util::Geometry}; +/// A struct containing methods that get [`WindowHandle`]s and move windows with the mouse. +/// +/// See [`WindowHandle`] for more information. #[derive(Debug, Clone)] pub struct Window { channel: Channel, } impl Window { - pub fn new(channel: Channel) -> Self { + pub(crate) fn new(channel: Channel) -> Self { Self { channel } } - pub fn create_window_client(&self) -> WindowServiceClient { + fn create_window_client(&self) -> WindowServiceClient { WindowServiceClient::new(self.channel.clone()) } - pub fn create_tag_client(&self) -> TagServiceClient { + fn create_tag_client(&self) -> TagServiceClient { TagServiceClient::new(self.channel.clone()) } - pub fn create_output_client(&self) -> OutputServiceClient { + fn create_output_client(&self) -> OutputServiceClient { OutputServiceClient::new(self.channel.clone()) } + /// Start moving the window with the mouse. + /// + /// This will begin moving the window under the pointer using the specified [`MouseButton`]. + /// The button must be held down at the time this method is called for the move to start. + /// + /// This is intended to be used with [`Input::keybind`][pinnacle_api::input::Keybind]. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; + /// + /// // Set `Super + left click` to begin moving a window + /// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || { + /// window.begin_move(MouseButton::Left); + /// }); + /// ``` pub fn begin_move(&self, button: MouseButton) { let mut client = self.create_window_client(); block_on(client.move_grab(MoveGrabRequest { @@ -48,6 +76,23 @@ impl Window { .unwrap(); } + /// Start resizing the window with the mouse. + /// + /// This will begin resizing the window under the pointer using the specified [`MouseButton`]. + /// The button must be held down at the time this method is called for the resize to start. + /// + /// This is intended to be used with [`Input::keybind`][pinnacle_api::input::Keybind]. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; + /// + /// // Set `Super + right click` to begin moving a window + /// input.mousebind([Mod::Super], MouseButton::Right, MouseEdge::Press, || { + /// window.begin_resize(MouseButton::Right); + /// }); + /// ``` pub fn begin_resize(&self, button: MouseButton) { let mut client = self.create_window_client(); block_on(client.resize_grab(ResizeGrabRequest { @@ -57,6 +102,12 @@ impl Window { } /// Get all windows. + /// + /// # Examples + /// + /// ``` + /// let windows = window.get_all(); + /// ``` pub fn get_all(&self) -> impl Iterator { let mut client = self.create_window_client(); let tag_client = self.create_tag_client(); @@ -75,12 +126,21 @@ impl Window { } /// Get the currently focused window. + /// + /// # Examples + /// + /// ``` + /// let focused_window = window.get_focused()?; + /// ``` pub fn get_focused(&self) -> Option { self.get_all() .find(|window| matches!(window.props().focused, Some(true))) } } +/// A handle to a window. +/// +/// This allows you to manipulate the window and get its properties. #[derive(Debug, Clone)] pub struct WindowHandle { pub(crate) client: WindowServiceClient, @@ -89,26 +149,51 @@ pub struct WindowHandle { pub(crate) output_client: OutputServiceClient, } +/// Whether a window is fullscreen, maximized, or neither. #[repr(i32)] #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] pub enum FullscreenOrMaximized { + /// The window is neither fullscreen nor maximized Neither = 1, + /// The window is fullscreen Fullscreen, + /// The window is maximized Maximized, } +/// Properties of a window. #[derive(Debug, Clone)] pub struct WindowProperties { + /// The location and size of the window pub geometry: Option, + /// The window's class pub class: Option, + /// The window's title pub title: Option, + /// Whether the window is focused or not pub focused: Option, + /// Whether the window is floating or not + /// + /// Note that a window can still be floating even if it's fullscreen or maximized; those two + /// state will just override the floating state. pub floating: Option, + /// Whether the window is fullscreen, maximized, or neither pub fullscreen_or_maximized: Option, + /// All the tags on the window pub tags: Vec, } impl WindowHandle { + /// Send a close request to this window. + /// + /// If the window is unresponsive, it may not close. + /// + /// # Examples + /// + /// ``` + /// // Close the focused window + /// window.get_focused()?.close() + /// ``` pub fn close(mut self) { block_on(self.client.close(CloseRequest { window_id: Some(self.id), @@ -116,6 +201,16 @@ impl WindowHandle { .unwrap(); } + /// Set this window to fullscreen or not. + /// + /// If it is maximized, setting it to fullscreen will remove the maximized state. + /// + /// # Examples + /// + /// ``` + /// // Set the focused window to fullscreen. + /// window.get_focused()?.set_fullscreen(true); + /// ``` pub fn set_fullscreen(&self, set: bool) { let mut client = self.client.clone(); block_on(client.set_fullscreen(SetFullscreenRequest { @@ -127,6 +222,16 @@ impl WindowHandle { .unwrap(); } + /// Toggle this window between fullscreen and not. + /// + /// If it is maximized, toggling it to fullscreen will remove the maximized state. + /// + /// # Examples + /// + /// ``` + /// // Toggle the focused window to and from fullscreen. + /// window.get_focused()?.toggle_fullscreen(); + /// ``` pub fn toggle_fullscreen(&self) { let mut client = self.client.clone(); block_on(client.set_fullscreen(SetFullscreenRequest { @@ -136,6 +241,16 @@ impl WindowHandle { .unwrap(); } + /// Set this window to maximized or not. + /// + /// If it is fullscreen, setting it to maximized will remove the fullscreen state. + /// + /// # Examples + /// + /// ``` + /// // Set the focused window to maximized. + /// window.get_focused()?.set_maximized(true); + /// ``` pub fn set_maximized(&self, set: bool) { let mut client = self.client.clone(); block_on(client.set_maximized(SetMaximizedRequest { @@ -147,6 +262,16 @@ impl WindowHandle { .unwrap(); } + /// Toggle this window between maximized and not. + /// + /// If it is fullscreen, setting it to maximized will remove the fullscreen state. + /// + /// # Examples + /// + /// ``` + /// // Toggle the focused window to and from maximized. + /// window.get_focused()?.toggle_maximized(); + /// ``` pub fn toggle_maximized(&self) { let mut client = self.client.clone(); block_on(client.set_maximized(SetMaximizedRequest { @@ -156,6 +281,19 @@ impl WindowHandle { .unwrap(); } + /// Set this window to floating or not. + /// + /// Floating windows will not be tiled and can be moved around and resized freely. + /// + /// Note that fullscreen and maximized windows can still be floating; those two states will + /// just override the floating state. + /// + /// # Examples + /// + /// ``` + /// // Set the focused window to floating. + /// window.get_focused()?.set_floating(true); + /// ``` pub fn set_floating(&self, set: bool) { let mut client = self.client.clone(); block_on(client.set_floating(SetFloatingRequest { @@ -167,6 +305,19 @@ impl WindowHandle { .unwrap(); } + /// Toggle this window to and from floating. + /// + /// Floating windows will not be tiled and can be moved around and resized freely. + /// + /// Note that fullscreen and maximized windows can still be floating; those two states will + /// just override the floating state. + /// + /// # Examples + /// + /// ``` + /// // Toggle the focused window to and from floating. + /// window.get_focused()?.toggle_floating(); + /// ``` pub fn toggle_floating(&self) { let mut client = self.client.clone(); block_on(client.set_floating(SetFloatingRequest { @@ -178,6 +329,17 @@ impl WindowHandle { .unwrap(); } + /// Move this window to the given `tag`. + /// + /// This will remove all tags from this window then tag it with `tag`, essentially moving the + /// window to that tag. + /// + /// # Examples + /// + /// ``` + /// // Move the focused window to tag "Code" on the focused output + /// window.get_focused()?.move_to_tag(&tag.get("Code", None)?); + /// ``` pub fn move_to_tag(&self, tag: &TagHandle) { let mut client = self.client.clone(); @@ -188,6 +350,17 @@ impl WindowHandle { .unwrap(); } + /// Set or unset a tag on this window. + /// + /// # Examples + /// + /// ``` + /// let focused = window.get_focused()?; + /// let tg = tag.get("Potato", None)?; + /// + /// focused.set_tag(&tg, true); // `focused` now has tag "Potato" + /// focused.set_tag(&tg, false); // `focused` no longer has tag "Potato" + /// ``` pub fn set_tag(&self, tag: &TagHandle, set: bool) { let mut client = self.client.clone(); @@ -199,6 +372,19 @@ impl WindowHandle { .unwrap(); } + /// Toggle a tag on this window. + /// + /// # Examples + /// + /// ``` + /// let focused = window.get_focused()?; + /// let tg = tag.get("Potato", None)?; + /// + /// // Assume `focused` does not have tag `tg` + /// + /// focused.toggle_tag(&tg); // `focused` now has tag "Potato" + /// focused.toggle_tag(&tg); // `focused` no longer has tag "Potato" + /// ``` pub fn toggle_tag(&self, tag: &TagHandle) { let mut client = self.client.clone(); @@ -210,6 +396,23 @@ impl WindowHandle { .unwrap(); } + /// Get all properties of this window. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::WindowProperties; + /// + /// let WindowProperties { + /// geometry, + /// class, + /// title, + /// focused, + /// floating, + /// fullscreen_or_maximized, + /// tags, + /// } = window.get_focused()?.props(); + /// ``` pub fn props(&self) -> WindowProperties { let mut client = self.client.clone(); let tag_client = self.tag_client.clone(); @@ -253,30 +456,51 @@ impl WindowHandle { } } + /// Get this window's location and size. + /// + /// Shorthand for `self.props().geometry`. pub fn geometry(&self) -> Option { self.props().geometry } + /// Get this window's class. + /// + /// Shorthand for `self.props().class`. pub fn class(&self) -> Option { self.props().class } + /// Get this window's title. + /// + /// Shorthand for `self.props().title`. pub fn title(&self) -> Option { self.props().title } + /// Get whether or not this window is focused. + /// + /// Shorthand for `self.props().focused`. pub fn focused(&self) -> Option { self.props().focused } + /// Get whether or not this window is floating. + /// + /// Shorthand for `self.props().floating`. pub fn floating(&self) -> Option { self.props().floating } + /// Get whether this window is fullscreen, maximized, or neither. + /// + /// Shorthand for `self.props().fullscreen_or_maximized`. pub fn fullscreen_or_maximized(&self) -> Option { self.props().fullscreen_or_maximized } + /// Get all the tags on this window. + /// + /// Shorthand for `self.props().tags`. pub fn tags(&self) -> Vec { self.props().tags } From 82ef55024c9c7d3f90fa418401c0230c9741d410 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sat, 20 Jan 2024 23:05:50 -0600 Subject: [PATCH 13/26] Add process docs, set_env --- api/lua/pinnacle/process.lua | 12 +++++ api/rust_grpc/src/process.rs | 86 ++++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/api/lua/pinnacle/process.lua b/api/lua/pinnacle/process.lua index 28c70f2..da9d195 100644 --- a/api/lua/pinnacle/process.lua +++ b/api/lua/pinnacle/process.lua @@ -108,6 +108,18 @@ function Process:spawn_once(args, callbacks) spawn_inner(self.config_client, args, callbacks, true) end +---Set an environment variable for the compositor. +---This will cause any future spawned processes to have this environment variable. +--- +---@param key string The environment variable key +---@param value string The environment variable value +function Process:set_env(key, value) + self.config_client:unary_request(build_grpc_request_params("SetEnv", { + key = key, + value = value, + })) +end + function process.new(config_client) ---@type Process local self = { config_client = config_client } diff --git a/api/rust_grpc/src/process.rs b/api/rust_grpc/src/process.rs index aafe117..121f2ec 100644 --- a/api/rust_grpc/src/process.rs +++ b/api/rust_grpc/src/process.rs @@ -1,23 +1,40 @@ -use futures::{channel::mpsc::UnboundedSender, future::BoxFuture, FutureExt, StreamExt}; +//! Process management. +//! +//! This module provides [`Process`], which allows you to spawn processes and set environment +//! variables. + +use futures::{ + channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, +}; use pinnacle_api_defs::pinnacle::process::v0alpha1::{ - process_service_client::ProcessServiceClient, SpawnRequest, + process_service_client::ProcessServiceClient, SetEnvRequest, SpawnRequest, }; use tonic::transport::Channel; +/// A struct containing methods to spawn processes with optional callbacks and set environment +/// variables. #[derive(Debug, Clone)] pub struct Process { channel: Channel, fut_sender: UnboundedSender>, } +/// Optional callbacks to be run when a spawned process prints to stdout or stderr or exits. pub struct SpawnCallbacks { + /// A callback that will be run when a process prints to stdout with a line pub stdout: Option>, + /// A callback that will be run when a process prints to stderr with a line pub stderr: Option>, + /// A callback that will be run when a process exits with a status code and message + #[allow(clippy::type_complexity)] pub exit: Option, String) + Send>>, } impl Process { - pub fn new(channel: Channel, fut_sender: UnboundedSender>) -> Process { + pub(crate) fn new( + channel: Channel, + fut_sender: UnboundedSender>, + ) -> Process { Self { channel, fut_sender, @@ -28,10 +45,41 @@ impl Process { ProcessServiceClient::new(self.channel.clone()) } + /// Spawn a process. + /// + /// Note that windows spawned *before* tags are added will not be displayed. + /// This will be changed in the future to be more like Awesome, where windows with no tags are + /// displayed on every tag instead. + /// + /// # Examples + /// + /// ``` + /// process.spawn(["alacritty"]); + /// process.spawn(["bash", "-c", "swaybg -i ~/path_to_wallpaper"]); + /// ``` pub fn spawn(&self, args: impl IntoIterator>) { self.spawn_inner(args, false, None); } + /// Spawn a process with callbacks for its stdout, stderr, and exit information. + /// + /// See [`SpawnCallbacks`] for the passed in struct. + /// + /// Note that windows spawned *before* tags are added will not be displayed. + /// This will be changed in the future to be more like Awesome, where windows with no tags are + /// displayed on every tag instead. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::process::SpawnCallbacks; + /// + /// process.spawn_with_callbacks(["alacritty"], SpawnCallbacks { + /// stdout: Some(Box::new(|line| println!("stdout: {line}"))), + /// stderr: Some(Box::new(|line| println!("stderr: {line}"))), + /// stdout: Some(Box::new(|code, msg| println!("exit code: {code:?}, exit_msg: {msg}"))), + /// }); + /// ``` pub fn spawn_with_callbacks( &self, args: impl IntoIterator>, @@ -40,10 +88,21 @@ impl Process { self.spawn_inner(args, false, Some(callbacks)); } + /// Spawn a process only if it isn't already running. + /// + /// This is useful for startup programs. + /// + /// See [`Process::spawn`] for details. pub fn spawn_once(&self, args: impl IntoIterator>) { self.spawn_inner(args, true, None); } + /// Spawn a process only if it isn't already running with optional callbacks for its stdout, + /// stderr, and exit information. + /// + /// This is useful for startup programs. + /// + /// See [`Process::spawn_with_callbacks`] for details. pub fn spawn_once_with_callbacks( &self, args: impl IntoIterator>, @@ -95,4 +154,25 @@ impl Process { ) .unwrap(); } + + /// Set an environment variable for the compositor. + /// This will cause any future spawned processes to have this environment variable. + /// + /// # Examples + /// + /// ``` + /// process.set_env("ENV", "a value lalala"); + /// ``` + pub fn set_env(&self, key: impl Into, value: impl Into) { + let key = key.into(); + let value = value.into(); + + let mut client = self.create_process_client(); + + block_on(client.set_env(SetEnvRequest { + key: Some(key), + value: Some(value), + })) + .unwrap(); + } } From ac5b5c939f838e7602c0d7c65dbc8cdefc2eca0a Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sat, 20 Jan 2024 23:19:01 -0600 Subject: [PATCH 14/26] Add the rest of the docs --- api/rust_grpc/src/input.rs | 1 - api/rust_grpc/src/input/libinput.rs | 40 +++++++++++++++++++++++++++++ api/rust_grpc/src/lib.rs | 6 +++++ api/rust_grpc/src/pinnacle.rs | 15 ++++++++++- api/rust_grpc/src/util.rs | 7 +++++ 5 files changed, 67 insertions(+), 2 deletions(-) diff --git a/api/rust_grpc/src/input.rs b/api/rust_grpc/src/input.rs index 1c70c6d..5127c0a 100644 --- a/api/rust_grpc/src/input.rs +++ b/api/rust_grpc/src/input.rs @@ -22,7 +22,6 @@ use xkbcommon::xkb::Keysym; use self::libinput::LibinputSetting; -/// Types for Libinput pub mod libinput; /// A mouse button. diff --git a/api/rust_grpc/src/input/libinput.rs b/api/rust_grpc/src/input/libinput.rs index 55ae198..5e24df7 100644 --- a/api/rust_grpc/src/input/libinput.rs +++ b/api/rust_grpc/src/input/libinput.rs @@ -1,45 +1,85 @@ +//! Types for libinput configuration. + +/// Pointer acceleration profile #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AccelProfile { + /// A flat acceleration profile. + /// + /// Pointer motion is accelerated by a constant (device-specific) factor, depending on the current speed. Flat = 1, + /// An adaptive acceleration profile. + /// + /// Pointer acceleration depends on the input speed. This is the default profile for most devices. Adaptive, } +/// The click method defines when to generate software-emulated buttons, usually on a device +/// that does not have a specific physical button available. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ClickMethod { + /// Use software-button areas to generate button events. ButtonAreas = 1, + /// The number of fingers decides which button press to generate. Clickfinger, } +/// The scroll method of a device selects when to generate scroll axis events instead of pointer motion events. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ScrollMethod { + /// Never send scroll events instead of pointer motion events. + /// + /// This has no effect on events generated by scroll wheels. NoScroll = 1, + /// Send scroll events when two fingers are logically down on the device. TwoFinger, + /// Send scroll events when a finger moves along the bottom or right edge of a device. Edge, + /// Send scroll events when a button is down and the device moves along a scroll-capable axis. OnButtonDown, } +/// Map 1/2/3 finger tips to buttons. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TapButtonMap { + /// 1/2/3 finger tap maps to left/right/middle LeftRightMiddle, + /// 1/2/3 finger tap maps to left/middle/right LeftMiddleRight, } +/// Possible settings for libinput. #[derive(Debug, Clone, Copy, PartialEq)] pub enum LibinputSetting { + /// Set the pointer acceleration profile AccelProfile(AccelProfile), + /// Set pointer acceleration speed AccelSpeed(f64), + /// Set the calibration matrix CalibrationMatrix([f32; 6]), + /// Set the [`ClickMethod`] ClickMethod(ClickMethod), + /// Set whether the device gets disabled while typing DisableWhileTyping(bool), + /// Set left handed mode LeftHanded(bool), + /// Allow or disallow middle mouse button emulation MiddleEmulation(bool), + /// Set the rotation angle RotationAngle(u32), + /// Set the scroll button ScrollButton(u32), + /// Set whether the scroll button should be a drag or toggle ScrollButtonLock(bool), + /// Set the [`ScrollMethod`] ScrollMethod(ScrollMethod), + /// Enable or disable natural scrolling NaturalScroll(bool), + /// Set the [`TapButtonMap`] TapButtonMap(TapButtonMap), + /// Enable or disable tap-to-drag TapDrag(bool), + /// Enable or disable a timeout where lifting a finger off the device will not stop dragging TapDragLock(bool), + /// Enable or disable tap-to-click Tap(bool), } diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs index 18de691..49006e3 100644 --- a/api/rust_grpc/src/lib.rs +++ b/api/rust_grpc/src/lib.rs @@ -46,11 +46,17 @@ static TAG: OnceLock = OnceLock::new(); /// A struct containing static references to all of the configuration structs. #[derive(Debug, Clone)] pub struct ApiModules { + /// The [`Pinnacle`] struct pub pinnacle: &'static Pinnacle, + /// The [`Process`] struct pub process: &'static Process, + /// The [`Window`] struct pub window: &'static Window, + /// The [`Input`] struct pub input: &'static Input, + /// The [`Output`] struct pub output: &'static Output, + /// The [`Tag`] struct pub tag: &'static Tag, } diff --git a/api/rust_grpc/src/pinnacle.rs b/api/rust_grpc/src/pinnacle.rs index f6b69ab..90a7c24 100644 --- a/api/rust_grpc/src/pinnacle.rs +++ b/api/rust_grpc/src/pinnacle.rs @@ -1,16 +1,21 @@ +//! Compositor management. +//! +//! This module provides [`Pinnacle`], which allows you to quit the compositor. + use futures::executor::block_on; use pinnacle_api_defs::pinnacle::v0alpha1::{ pinnacle_service_client::PinnacleServiceClient, QuitRequest, }; use tonic::transport::Channel; +/// A struct that allows you to quit the compositor. #[derive(Debug, Clone)] pub struct Pinnacle { channel: Channel, } impl Pinnacle { - pub fn new(channel: Channel) -> Self { + pub(crate) fn new(channel: Channel) -> Self { Self { channel } } @@ -18,6 +23,14 @@ impl Pinnacle { PinnacleServiceClient::new(self.channel.clone()) } + /// Quit Pinnacle. + /// + /// # Examples + /// + /// ``` + /// // Quits Pinnacle. What else were you expecting? + /// pinnacle.quit(); + /// ``` pub fn quit(&self) { let mut client = self.create_pinnacle_client(); block_on(client.quit(QuitRequest {})).unwrap(); diff --git a/api/rust_grpc/src/util.rs b/api/rust_grpc/src/util.rs index 828557b..ca7d9de 100644 --- a/api/rust_grpc/src/util.rs +++ b/api/rust_grpc/src/util.rs @@ -1,7 +1,14 @@ +//! Utility types. + +/// The size and location of something. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Geometry { + /// The x position pub x: i32, + /// The y position pub y: i32, + /// The width pub width: u32, + /// The height pub height: u32, } From c742807a34e7f2cfc5d5ea7f5dfb015961a9cb70 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sun, 21 Jan 2024 21:01:41 -0600 Subject: [PATCH 15/26] Add window rules And a lot of docs --- api/rust_grpc/src/window.rs | 27 +- api/rust_grpc/src/window/rules.rs | 521 ++++++++++++++++++++++++++++++ 2 files changed, 545 insertions(+), 3 deletions(-) create mode 100644 api/rust_grpc/src/window/rules.rs diff --git a/api/rust_grpc/src/window.rs b/api/rust_grpc/src/window.rs index 0c7f10b..b2e2c3b 100644 --- a/api/rust_grpc/src/window.rs +++ b/api/rust_grpc/src/window.rs @@ -12,7 +12,8 @@ use pinnacle_api_defs::pinnacle::{ output::v0alpha1::output_service_client::OutputServiceClient, tag::v0alpha1::tag_service_client::TagServiceClient, window::v0alpha1::{ - window_service_client::WindowServiceClient, CloseRequest, MoveToTagRequest, SetTagRequest, + window_service_client::WindowServiceClient, AddWindowRuleRequest, CloseRequest, + MoveToTagRequest, SetTagRequest, }, window::{ self, @@ -26,6 +27,10 @@ use tonic::transport::Channel; use crate::{input::MouseButton, tag::TagHandle, util::Geometry}; +use self::rules::{WindowRule, WindowRuleCondition}; + +pub mod rules; + /// A struct containing methods that get [`WindowHandle`]s and move windows with the mouse. /// /// See [`WindowHandle`] for more information. @@ -56,7 +61,7 @@ impl Window { /// This will begin moving the window under the pointer using the specified [`MouseButton`]. /// The button must be held down at the time this method is called for the move to start. /// - /// This is intended to be used with [`Input::keybind`][pinnacle_api::input::Keybind]. + /// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind]. /// /// # Examples /// @@ -81,7 +86,7 @@ impl Window { /// This will begin resizing the window under the pointer using the specified [`MouseButton`]. /// The button must be held down at the time this method is called for the resize to start. /// - /// This is intended to be used with [`Input::keybind`][pinnacle_api::input::Keybind]. + /// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind]. /// /// # Examples /// @@ -136,6 +141,22 @@ impl Window { self.get_all() .find(|window| matches!(window.props().focused, Some(true))) } + + /// Add a window rule. + /// + /// A window rule is a set of criteria that a window must open with. + /// For it to apply, a [`WindowRuleCondition`] must evaluate to true for the window in question. + /// + /// TODO: + pub fn add_window_rule(&self, cond: WindowRuleCondition, rule: WindowRule) { + let mut client = self.create_window_client(); + + block_on(client.add_window_rule(AddWindowRuleRequest { + cond: Some(cond.0), + rule: Some(rule.0), + })) + .unwrap(); + } } /// A handle to a window. diff --git a/api/rust_grpc/src/window/rules.rs b/api/rust_grpc/src/window/rules.rs new file mode 100644 index 0000000..86e5a0a --- /dev/null +++ b/api/rust_grpc/src/window/rules.rs @@ -0,0 +1,521 @@ +//! Types for window rules. +//! +//! A window rule is a way to set the properties of a window on open. +//! +//! They are comprised of two parts: the [condition][WindowRuleCondition] and the actual [rule][WindowRule]. +//! +//! # [`WindowRuleCondition`]s +//! `WindowRuleCondition`s are conditions that the window needs to open with in order to apply a +//! rule. For example, you may want to set a window to maximized if it has the class "steam", or +//! you might want to open all Firefox instances on tag "3". +//! +//! To do this, you must build a `WindowRuleCondition` to tell the compositor when to apply any +//! rules. +//! +//! ## Building `WindowRuleCondition`s +//! A condition is created through [`WindowRuleCondition::new`]: +//! ``` +//! let cond = WindowRuleCondition::new(); +//! ``` +//! +//! In order to understand conditions, you must understand the concept of "any" and "all". +//! +//! **"Any"** +//! +//! "Any" conditions only need one of their constituent items to be true for the whole condition to +//! evaluate to true. Think of it as one big `if a || b || c || d || ... {}` block. +//! +//! **"All"** +//! +//! "All" conditions need *all* of their constituent items to be true for the condition to evaluate +//! to true. This is like a big `if a && b && c && d && ... {}` block. +//! +//! Note that any items in a top level `WindowRuleCondition` fall under "all", so all those items +//! must be true. +//! +//! With that out of the way, we can get started building conditions. +//! +//! ### `WindowRuleCondition::classes` +//! With [`WindowRuleCondition::classes`], you can specify what classes a window needs to have for +//! a rule to apply. +//! +//! The following will apply to windows with the class "firefox": +//! ``` +//! let cond = WindowRuleCondition::new().classes(["firefox"]); +//! ``` +//! +//! Note that you pass in some `impl IntoIterator>`. This means you can +//! pass in more than one class here: +//! ``` +//! let failing_cond = WindowRuleCondition::new().classes(["firefox", "steam"]); +//! ``` +//! *HOWEVER*: this will not work. Recall that top level conditions are implicitly "all". This +//! means the above would require windows to have *both classes*, which is impossible. Thus, the +//! condition above will never be true. +//! +//! ### `WindowRuleCondition::titles` +//! Like `classes`, you can use `titles` to specify that the window needs to open with a specific +//! title for the condition to apply. +//! +//! ``` +//! let cond = WindowRuleCondition::new().titles(["Steam"]); +//! ``` +//! +//! Like `classes`, passing in multiple titles at the top level will cause the condition to always +//! fail. +//! +//! ### `WindowRuleCondition::tags` +//! You can specify that the window needs to open on the given tags in order to apply a rule. +//! +//! ``` +//! let cond = WindowRuleCondition::new().tags([&tag.get("3", output.get_by_name("HDMI-1")?)?]); +//! ``` +//! +//! Here, if you have tag "3" active on "HDMI-1" and spawn a window on that output, this condition +//! will apply. +//! +//! Unlike `classes` and `titles`, you can specify multiple tags at the top level: +//! +//! ``` +//! let op = output.get_by_name("HDMI-1")?; +//! let tag1 = tag.get("1", &op)?; +//! let tag2 = tag.get("2", &op)?; +//! +//! let cond = WindowRuleCondition::new().tags([&tag1, &tag2]); +//! ``` +//! +//! Now, you must have both tags "1" and "2" active and spawn a window for the condition to apply. +//! +//! ### `WindowRuleCondition::any` +//! Now we can get to ways to compose more complex conditions. +//! +//! `WindowRuleCondition::any` takes in conditions and will evaluate to true if *anything* in those +//! conditions are true. +//! +//! ``` +//! let cond = WindowRuleCondition::new() +//! .any([ +//! WindowRuleCondition::new().classes(["Alacritty"]), +//! WindowRuleCondition::new().tags([&tag.get("2", None)?]), +//! ]); +//! ``` +//! +//! This condition will apply if the window is *either* "Alacritty" *or* opens on tag "2". +//! +//! ### `WindowRuleCondition::all` +//! With `WindowRuleCondition::all`, *all* specified conditions must be true for the condition to +//! be true. +//! +//! ``` +//! let cond = WindowRuleCondition::new() +//! .all([ +//! WindowRuleCondition::new().classes(["Alacritty"]), +//! WindowRuleCondition::new().tags([&tag.get("2", None)?]), +//! ]); +//! ``` +//! +//! This condition applies if the window has the class "Alacritty" *and* opens on tag "2". +//! +//! You can write the above a bit shorter, as top level conditions are already "all": +//! +//! ``` +//! let cond = WindowRuleCondition::new() +//! .classes(["Alacritty"]) +//! .tags([&tag.get("2", None)?]); +//! ``` +//! +//! ## Complex condition composition +//! You can arbitrarily nest `any` and `all` to achieve desired logic. +//! +//! ``` +//! let op = output.get_by_name("HDMI-1")?; +//! let tag1 = tag.get("1", &op)?; +//! let tag2 = tag.get("2", &op)?; +//! +//! let complex_cond = WindowRuleCondition::new() +//! .any([ +//! WindowRuleCondition::new().all([ +//! WindowRuleCondition::new() +//! .classes("Alacritty") +//! .tags([&tag1, &tag2]) +//! ]), +//! WindowRuleCondition::new().all([ +//! WindowRuleCondition::new().any([ +//! WindowRuleCondition::new().titles(["nvim", "emacs", "nano"]), +//! ]), +//! WindowRuleCondition::new().any([ +//! WindowRuleCondition::new().tags([&tag1, &tag2]), +//! ]), +//! ]) +//! ]) +//! ``` +//! +//! The above is true if either of the following are true: +//! - The window has class "Alacritty" and opens on both tags "1" and "2", or +//! - The window's class is either "nvim", "emacs", or "nano" *and* it opens on either tag "1" or +//! "2". +//! +//! # [`WindowRule`]s +//! `WindowRuleCondition`s are half of a window rule. The other half is the [`WindowRule`] itself. +//! +//! A `WindowRule` is what will apply to a window if a condition is true. +//! +//! ## Building `WindowRule`s +//! +//! Create a new window rule with [`WindowRule::new`]: +//! +//! ``` +//! let rule = WindowRule::new(); +//! ``` +//! +//! There are several rules you can set currently. +//! +//! ### [`WindowRule::output`] +//! This will cause the window to open on the specified output. +//! +//! ### [`WindowRule::tags`] +//! This will cause the window to open with the given tags. +//! +//! ### [`WindowRule::floating`] +//! This will cause the window to open either floating or tiled. +//! +//! ### [`WindowRule::fullscreen_or_maximized`] +//! This will cause the window to open either fullscreen, maximized, or neither. +//! +//! ### [`WindowRule::x`] +//! This will cause the window to open at the given x-coordinate. +//! +//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by +//! layouting. +//! +//! ### [`WindowRule::y`] +//! This will cause the window to open at the given y-coordinate. +//! +//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by +//! layouting. +//! +//! ### [`WindowRule::width`] +//! This will cause the window to open with the given width in pixels. +//! +//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by +//! layouting. +//! +//! ### [`WindowRule::height`] +//! This will cause the window to open with the given height in pixels. +//! +//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by +//! layouting. + +use pinnacle_api_defs::pinnacle::window; + +use crate::{output::OutputHandle, tag::TagHandle}; + +use super::FullscreenOrMaximized; + +/// A condition for a [`WindowRule`] to apply to a window. +/// +/// `WindowRuleCondition`s are built using the builder pattern. +#[derive(Default, Debug, Clone)] +pub struct WindowRuleCondition(pub(super) window::v0alpha1::WindowRuleCondition); + +impl WindowRuleCondition { + /// Create a new, empty `WindowRuleCondition`. + pub fn new() -> Self { + Default::default() + } + + /// This condition requires that at least one provided condition is true. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// // `cond` will be true if the window opens with *either* class "Alacritty" or "firefox" + /// // *or* with title "Steam" + /// let cond = WindowRuleCondition::new() + /// .any([ + /// WindowRuleCondition::new().classes(["Alacritty", "firefox"]), + /// WindowRuleCondition::new().titles(["Steam"]). + /// ]); + /// ``` + pub fn any(mut self, conds: impl IntoIterator) -> Self { + self.0.any = conds.into_iter().map(|cond| cond.0).collect(); + self + } + + /// This condition requires that all provided conditions are true. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// // `cond` will be true if the window opens with class "Alacritty" *and* on tag "1" + /// let cond = WindowRuleCondition::new() + /// .any([ + /// WindowRuleCondition::new().tags([tag.get("1", None)?]), + /// WindowRuleCondition::new().titles(["Alacritty"]). + /// ]); + /// ``` + pub fn all(mut self, conds: impl IntoIterator) -> Self { + self.0.all = conds.into_iter().map(|cond| cond.0).collect(); + self + } + + /// This condition requires that the window's class matches. + /// + /// When used in a top level condition or inside of [`WindowRuleCondition::all`], + /// *all* classes must match (this is impossible). + /// + /// When used in [`WindowRuleCondition::any`], at least one of the + /// provided classes must match. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// // `cond` will be true if the window opens with class "Alacritty" + /// let cond = WindowRuleCondition::new().classes(["Alacritty"]); + /// + /// // Top level conditions need all items to be true, + /// // so the following will never be true as windows can't have two classes at once + /// let always_false = WindowRuleCondition::new().classes(["Alacritty", "firefox"]); + /// + /// // To make the above work, use [`WindowRuleCondition::any`]. + /// // The following will be true if the window is "Alacritty" or "firefox" + /// let any_class = WindowRuleCondition::new() + /// .any([ WindowRuleCondition::new().classes(["Alacritty", "firefox"]) ]); + /// ``` + pub fn classes(mut self, classes: impl IntoIterator>) -> Self { + self.0.classes = classes.into_iter().map(Into::into).collect(); + self + } + + /// This condition requires that the window's title matches. + /// + /// When used in a top level condition or inside of [`WindowRuleCondition::all`], + /// *all* titles must match (this is impossible). + /// + /// When used in [`WindowRuleCondition::any`], at least one of the + /// provided titles must match. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// // `cond` will be true if the window opens with title "vim" + /// let cond = WindowRuleCondition::new().titles(["vim"]); + /// + /// // Top level conditions need all items to be true, + /// // so the following will never be true as windows can't have two titles at once + /// let always_false = WindowRuleCondition::new().titles(["vim", "emacs"]); + /// + /// // To make the above work, use [`WindowRuleCondition::any`]. + /// // The following will be true if the window has the title "vim" or "emacs" + /// let any_title = WindowRuleCondition::new() + /// .any([WindowRuleCondition::new().titles(["vim", "emacs"])]); + /// ``` + pub fn titles(mut self, titles: impl IntoIterator>) -> Self { + self.0.titles = titles.into_iter().map(Into::into).collect(); + self + } + + /// This condition requires that the window's is opened on the given tags. + /// + /// When used in a top level condition or inside of [`WindowRuleCondition::all`], + /// the window must open on *all* given tags. + /// + /// When used in [`WindowRuleCondition::any`], the window must open on at least + /// one of the given tags. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// let tag1 = tag.get("1", None)?; + /// let tag2 = tag.get("2", None)?; + /// + /// // `cond` will be true if the window opens with tag "1" + /// let cond = WindowRuleCondition::new().tags([&tag1]); + /// + /// // Top level conditions need all items to be true, + /// // so the following will be true if the window opens with both tags "1" and "2" + /// let all_tags = WindowRuleCondition::new().tags([&tag1, &tag2]); + /// + /// // This does the same as the above + /// let all_tags = WindowRuleCondition::new() + /// .all([WindowRuleCondition::new().tags([&tag1, &tag2])]); + /// + /// // The following will be true if the window opens with *either* tag "1" or "2" + /// let any_tag = WindowRuleCondition::new() + /// .any([WindowRuleCondition::new().tags([&tag1, &tag2])]); + /// ``` + pub fn tags<'a>(mut self, tags: impl IntoIterator) -> Self { + self.0.tags = tags.into_iter().map(|tag| tag.id).collect(); + self + } +} + +/// A window rule. +/// +/// This is what will be applied to a window if it meets a [`WindowRuleCondition`]. +/// +/// `WindowRule`s are built using the builder pattern. +#[derive(Clone, Debug, Default)] +pub struct WindowRule(pub(super) window::v0alpha1::WindowRule); + +impl WindowRule { + /// Create a new, empty window rule. + pub fn new() -> Self { + Default::default() + } + + /// This rule will force windows to open on the provided `output`. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open on "HDMI-1" + /// let rule = WindowRule::new().output(output.get_by_name("HDMI-1")?); + /// ``` + pub fn output(mut self, output: &OutputHandle) -> Self { + self.0.output = Some(output.name.clone()); + self + } + + /// This rule will force windows to open with the provided `tags`. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// let op = output.get_by_name("HDMI-1")?; + /// let tag1 = tag.get("1", &op)?; + /// let tag2 = tag.get("2", &op)?; + /// + /// // Force the window to open with tags "1" and "2" + /// let rule = WindowRule::new().tags([&tag1, &tag2]); + /// ``` + pub fn tags<'a>(mut self, tags: impl IntoIterator) -> Self { + self.0.tags = tags.into_iter().map(|tag| tag.id).collect(); + self + } + + /// This rule will force windows to open either floating or not. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open floating + /// let rule = WindowRule::new().floating(true); + /// + /// // Force the window to open tiled + /// let rule = WindowRule::new().floating(false); + /// ``` + pub fn floating(mut self, floating: bool) -> Self { + self.0.floating = Some(floating); + self + } + + /// This rule will force windows to open either fullscreen, maximized, or neither. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// use pinnacle_api::window::FullscreenOrMaximized; + /// + /// // Force the window to open fullscreen + /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Fullscreen); + /// + /// // Force the window to open maximized + /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Maximized); + /// + /// // Force the window to open not fullscreen nor maximized + /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Neither); + /// ``` + pub fn fullscreen_or_maximized( + mut self, + fullscreen_or_maximized: FullscreenOrMaximized, + ) -> Self { + self.0.fullscreen_or_maximized = Some(fullscreen_or_maximized as i32); + self + } + + /// This rule will force windows to open at a specific x-coordinate. + /// + /// This will only actually be visible if the window is also floating. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open at x = 480 + /// let rule = WindowRule::new().x(480); + /// ``` + pub fn x(mut self, x: i32) -> Self { + self.0.x = Some(x); + self + } + + /// This rule will force windows to open at a specific y-coordinate. + /// + /// This will only actually be visible if the window is also floating. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open at y = 240 + /// let rule = WindowRule::new().y(240); + /// ``` + pub fn y(mut self, y: i32) -> Self { + self.0.y = Some(y); + self + } + + /// This rule will force windows to open with a specific width. + /// + /// This will only actually be visible if the window is also floating. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open with a width of 500 pixels + /// let rule = WindowRule::new().width(500); + /// ``` + pub fn width(mut self, width: u32) -> Self { + self.0.width = Some(width as i32); + self + } + + /// This rule will force windows to open with a specific height. + /// + /// This will only actually be visible if the window is also floating. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open with a height of 250 pixels + /// let rule = WindowRule::new().height(250); + /// ``` + pub fn height(mut self, height: u32) -> Self { + self.0.height = Some(height as i32); + self + } +} From a8ffecab2f0503fc79ebe85b228ba211a29a2517 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sun, 21 Jan 2024 22:34:17 -0600 Subject: [PATCH 16/26] Improve config macro --- api/rust_grpc/examples/default_config/main.rs | 1 - api/rust_grpc/pinnacle-api-macros/Cargo.toml | 2 +- api/rust_grpc/pinnacle-api-macros/src/lib.rs | 132 +++++++++++++++++- api/rust_grpc/src/lib.rs | 1 + api/rust_grpc/src/window.rs | 2 + 5 files changed, 133 insertions(+), 5 deletions(-) diff --git a/api/rust_grpc/examples/default_config/main.rs b/api/rust_grpc/examples/default_config/main.rs index b2c5a42..0945672 100644 --- a/api/rust_grpc/examples/default_config/main.rs +++ b/api/rust_grpc/examples/default_config/main.rs @@ -6,7 +6,6 @@ use pinnacle_api::{ use xkbcommon::xkb::Keysym; #[pinnacle_api::config(modules)] -#[tokio::main] async fn main() { let ApiModules { pinnacle, diff --git a/api/rust_grpc/pinnacle-api-macros/Cargo.toml b/api/rust_grpc/pinnacle-api-macros/Cargo.toml index 9f4cb66..ba39c76 100644 --- a/api/rust_grpc/pinnacle-api-macros/Cargo.toml +++ b/api/rust_grpc/pinnacle-api-macros/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] quote = "1.0.35" -syn = { version = "2.0.48", features = ["full"] } +syn = { version = "2.0.48", features = ["full", "parsing"] } proc-macro2 = "1.0.76" [lib] diff --git a/api/rust_grpc/pinnacle-api-macros/src/lib.rs b/api/rust_grpc/pinnacle-api-macros/src/lib.rs index 57e8ce5..33f5ec2 100644 --- a/api/rust_grpc/pinnacle-api-macros/src/lib.rs +++ b/api/rust_grpc/pinnacle-api-macros/src/lib.rs @@ -1,21 +1,119 @@ -use quote::quote; -use syn::parse_macro_input; +use proc_macro2::{Ident, Span}; +use quote::{quote, quote_spanned}; +use syn::{ + parse::Parse, parse_macro_input, punctuated::Punctuated, spanned::Spanned, Expr, ExprLit, Lit, + MetaNameValue, Path, Token, +}; +/// Transform the annotated function into one used to configure the Pinnacle compositor. +/// +/// This will cause the function to connect to Pinnacle's gRPC server, run your configuration code, +/// then await all necessary futures needed to call callbacks. +/// +/// If your config contains anything that has a callback, this function will not return unless an +/// error occurs. +/// +/// # Usage +/// The function must be marked `async`, as this macro will insert the `#[tokio::main]` macro below +/// it. +/// +/// It takes in an ident, with which Pinnacle's `ApiModules` struct will be bound to. +/// +/// ``` +/// #[pinnacle_api::config(modules)] +/// async fn main() { +/// // `modules` is now accessible in the function body +/// let ApiModules { .. } = modules; +/// } +/// ``` +/// +/// `pinnacle_api` annotates the function with a bare `#[tokio::main]` attribute. +/// If you would like to configure Tokio's options, additionally pass in +/// `internal_tokio = false` to this macro and annotate the function +/// with your own `tokio::main` attribute. +/// +/// `pinnacle_api` provides a re-export of `tokio` that may prove useful. If you need other Tokio +/// features, you may need to bring them in with your own Cargo.toml. +/// +/// Note: the `tokio::main` attribute must be inserted *below* the `pinnacle_api::config` +/// attribute, as attributes are expanded from top to bottom. +/// +/// ``` +/// #[pinnacle_api::config(modules, internal_tokio = false)] +/// #[pinnacle_api::tokio::main(worker_threads = 8)] +/// async fn main() {} +/// ``` #[proc_macro_attribute] pub fn config( args: proc_macro::TokenStream, item: proc_macro::TokenStream, ) -> proc_macro::TokenStream { let item = parse_macro_input!(item as syn::ItemFn); - let module_ident = parse_macro_input!(args as syn::Ident); + let macro_input = parse_macro_input!(args as MacroInput); let vis = item.vis; let sig = item.sig; + + if sig.asyncness.is_none() { + return quote_spanned! {sig.fn_token.span()=> + compile_error!("This function must be marked `async` to run a Pinnacle config"); + } + .into(); + } + let attrs = item.attrs; + let stmts = item.block.stmts; + let module_ident = macro_input.ident; + + let options = macro_input.options; + + let mut has_internal_tokio = false; + + let mut internal_tokio = true; + + if let Some(options) = options { + for name_value in options.iter() { + if name_value.path.get_ident() == Some(&Ident::new("internal_tokio", Span::call_site())) + { + if has_internal_tokio { + return quote_spanned! {name_value.path.span()=> + compile_error!("`internal_tokio` defined twice, remove this one"); + } + .into(); + } + + has_internal_tokio = true; + if let Expr::Lit(lit) = &name_value.value { + if let Lit::Bool(bool) = &lit.lit { + internal_tokio = bool.value; + continue; + } + } + + return quote_spanned! {name_value.value.span()=> + compile_error!("expected `true` or `false`"); + } + .into(); + } else { + return quote_spanned! {name_value.path.span()=> + compile_error!("expected valid option (currently only `internal_tokio`)"); + } + .into(); + } + } + } + + let tokio_attr = internal_tokio.then(|| { + quote! { + #[::pinnacle_api::tokio::main] + } + }); + quote! { #(#attrs)* + #tokio_attr #vis #sig { let (#module_ident, __fut_receiver) = ::pinnacle_api::connect().await.unwrap(); @@ -26,3 +124,31 @@ pub fn config( } .into() } + +struct MacroInput { + ident: syn::Ident, + options: Option>, +} + +impl Parse for MacroInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let ident = input.parse()?; + + let comma = input.parse::(); + + let mut options = None; + + if comma.is_ok() { + options = Some(input.parse_terminated(MetaNameValue::parse, Token![,])?); + } + + if !input.is_empty() { + return Err(syn::Error::new( + input.span(), + "expected `,` followed by options", + )); + } + + Ok(MacroInput { ident, options }) + } +} diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs index 49006e3..e680089 100644 --- a/api/rust_grpc/src/lib.rs +++ b/api/rust_grpc/src/lib.rs @@ -34,6 +34,7 @@ pub mod util; pub mod window; pub use pinnacle_api_macros::config; +pub use tokio; pub use xkbcommon; static PINNACLE: OnceLock = OnceLock::new(); diff --git a/api/rust_grpc/src/window.rs b/api/rust_grpc/src/window.rs index b2e2c3b..c120ce8 100644 --- a/api/rust_grpc/src/window.rs +++ b/api/rust_grpc/src/window.rs @@ -5,6 +5,8 @@ //! //! [`WindowHandle`]s allow you to do things like resize and move windows, toggle them between //! floating and tiled, close them, and more. +//! +//! This module also allows you to set window rules; see the [rules] module for more information. use futures::executor::block_on; use num_enum::TryFromPrimitive; From 111cc76fa5326b10677efd5c5dac1b011f4eebc8 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sun, 21 Jan 2024 22:54:32 -0600 Subject: [PATCH 17/26] Add more docs --- api/rust_grpc/examples/default_config/main.rs | 2 +- api/rust_grpc/pinnacle-api-macros/src/lib.rs | 4 +- api/rust_grpc/src/lib.rs | 66 ++++++++++++++++++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/api/rust_grpc/examples/default_config/main.rs b/api/rust_grpc/examples/default_config/main.rs index 0945672..c77627a 100644 --- a/api/rust_grpc/examples/default_config/main.rs +++ b/api/rust_grpc/examples/default_config/main.rs @@ -1,9 +1,9 @@ +use pinnacle_api::xkbcommon::xkb::Keysym; use pinnacle_api::{ input::{Mod, MouseButton, MouseEdge}, tag::{Layout, LayoutCycler}, ApiModules, }; -use xkbcommon::xkb::Keysym; #[pinnacle_api::config(modules)] async fn main() { diff --git a/api/rust_grpc/pinnacle-api-macros/src/lib.rs b/api/rust_grpc/pinnacle-api-macros/src/lib.rs index 33f5ec2..36631d6 100644 --- a/api/rust_grpc/pinnacle-api-macros/src/lib.rs +++ b/api/rust_grpc/pinnacle-api-macros/src/lib.rs @@ -1,8 +1,8 @@ use proc_macro2::{Ident, Span}; use quote::{quote, quote_spanned}; use syn::{ - parse::Parse, parse_macro_input, punctuated::Punctuated, spanned::Spanned, Expr, ExprLit, Lit, - MetaNameValue, Path, Token, + parse::Parse, parse_macro_input, punctuated::Punctuated, spanned::Spanned, Expr, Lit, + MetaNameValue, Token, }; /// Transform the annotated function into one used to configure the Pinnacle compositor. diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs index e680089..92f5b1c 100644 --- a/api/rust_grpc/src/lib.rs +++ b/api/rust_grpc/src/lib.rs @@ -8,8 +8,72 @@ //! //! # Configuration //! +//! ## 1. Create a cargo project //! To create your own Rust config, create a Cargo project in `~/.config/pinnacle`. -//! TODO: +//! +//! ## 2. Create `metaconfig.toml` +//! Then, create a file named `metaconfig.toml`. This is the file Pinnacle will use to determine +//! what to run, kill and reload-config keybinds, an optional socket directory, and any environment +//! variables to give the config client. +//! +//! In `metaconfig.toml`, put the following: +//! ```toml +//! # `command` will tell Pinnacle to run `cargo run` in your config directory. +//! # You can add stuff like "--release" here if you want to. +//! command = ["cargo", "run"] +//! +//! # You must define a keybind to reload your config if it crashes, otherwise you'll get stuck if +//! # the Lua config doesn't kick in properly. +//! reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } +//! +//! # Similarly, you must define a keybind to kill Pinnacle. +//! kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } +//! +//! # You can specify an optional socket directory if you need to place the socket Pinnacle will +//! # use for configuration in a different place. +//! # socket_dir = "your/dir/here" +//! +//! # If you need to set any environment variables for the config process, you can do so here if +//! # you don't want to do it in the config itself. +//! [envs] +//! # key = "value" +//! ``` +//! +//! ## 3. Set up dependencies +//! In your `Cargo.toml`, add a dependency to `pinnacle-api`: +//! +//! ```toml +//! # Cargo.toml +//! +//! [dependencies] +//! pinnacle-api = { git = "https://github.com/pinnacle-comp/pinnacle" } +//! ``` +//! +//! ## 4. Set up the main function +//! In `main.rs`, change `fn main()` to `async fn main()` and annotate it with the +//! [`pinnacle_api::config`][`crate::config`] macro. Pass in the identifier you want to bind the +//! config modules to: +//! +//! ``` +//! use pinnacle_api::ApiModules; +//! +//! #[pinnacle_api::config(modules)] +//! async fn main() { +//! // `modules` is now available in the function body. +//! // You can deconstruct `ApiModules` to get all the config structs. +//! let ApiModules { +//! pinnacle, +//! process, +//! window, +//! input, +//! output, +//! tag, +//! } = modules; +//! } +//! ``` +//! +//! ## 5. Begin crafting your config! +//! You can peruse the documentation for things to configure. use std::sync::OnceLock; From 9acd0e5ce34b58282c42cd2b8d968bcaa1897219 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sun, 21 Jan 2024 23:07:10 -0600 Subject: [PATCH 18/26] Add finishing touches --- api/rust_grpc/Cargo.toml | 10 +++++++--- api/rust_grpc/src/lib.rs | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/api/rust_grpc/Cargo.toml b/api/rust_grpc/Cargo.toml index 3e873f4..0d0caf2 100644 --- a/api/rust_grpc/Cargo.toml +++ b/api/rust_grpc/Cargo.toml @@ -1,19 +1,23 @@ [package] name = "pinnacle-api" -version = "0.0.1" +version = "0.0.2" edition = "2021" +authors = ["Ottatop "] +description = "The Rust implementation of the Pinnacle compositor's configuration API" +license = "MPL-2.0" +repository = "https://github.com/pinnacle-comp/pinnacle" +keywords = ["compositor", "pinnacle", "api", "config"] +categories = ["api-bindings", "config"] [dependencies] pinnacle-api-defs = { path = "../../pinnacle-api-defs" } pinnacle-api-macros = { path = "./pinnacle-api-macros" } tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] } -# tokio-stream = { version = "0.1.14", features = ["net"] } async-net = "2.0.0" async-compat = "0.2.3" tonic = "0.10.2" tower = { version = "0.4.13", features = ["util"] } futures = "0.3.30" -# futures-lite = "2.2.0" num_enum = "0.7.2" xkbcommon = "0.7.0" diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs index 92f5b1c..e573209 100644 --- a/api/rust_grpc/src/lib.rs +++ b/api/rust_grpc/src/lib.rs @@ -109,7 +109,7 @@ static OUTPUT: OnceLock = OnceLock::new(); static TAG: OnceLock = OnceLock::new(); /// A struct containing static references to all of the configuration structs. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct ApiModules { /// The [`Pinnacle`] struct pub pinnacle: &'static Pinnacle, From 0b88ad298b639105ca98ed44aa426bc3d922c686 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sun, 21 Jan 2024 23:42:48 -0600 Subject: [PATCH 19/26] Completely rip out the old msgpack stuff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Did this break anything? ¯\_(ツ)_/¯ --- Cargo.lock | 30 - Cargo.toml | 2 - src/api.rs | 2030 +++++++++++++++++++++++++++++++++++++---- src/api/handlers.rs | 824 ----------------- src/api/msg.rs | 335 ------- src/api/protocol.rs | 1887 -------------------------------------- src/backend/udev.rs | 36 +- src/config.rs | 84 +- src/input.rs | 138 +-- src/input/libinput.rs | 104 +-- src/main.rs | 2 +- src/state.rs | 33 +- 12 files changed, 1911 insertions(+), 3594 deletions(-) delete mode 100644 src/api/handlers.rs delete mode 100644 src/api/msg.rs delete mode 100644 src/api/protocol.rs diff --git a/Cargo.lock b/Cargo.lock index 8109427..cde96be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1489,12 +1489,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "paste" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" - [[package]] name = "percent-encoding" version = "2.3.1" @@ -1556,8 +1550,6 @@ dependencies = [ "pinnacle-api-defs", "prost", "prost-types", - "rmp", - "rmp-serde", "serde", "shellexpand", "smithay", @@ -1868,28 +1860,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" -[[package]] -name = "rmp" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" -dependencies = [ - "byteorder", - "num-traits", - "paste", -] - -[[package]] -name = "rmp-serde" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" -dependencies = [ - "byteorder", - "rmp", - "serde", -] - [[package]] name = "rustc-demangle" version = "0.1.23" diff --git a/Cargo.toml b/Cargo.toml index 6d17cde..bba02d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,6 @@ thiserror = "1" xcursor = { version = "0.3", optional = true } image = { version = "0.24", default-features = false, optional = true } serde = { version = "1.0", features = ["derive"] } -rmp = { version = "0.8.12" } -rmp-serde = { version = "1.1.2" } x11rb = { version = "0.13", default-features = false, features = ["composite"], optional = true } shellexpand = "3.1.0" toml = "0.8" diff --git a/src/api.rs b/src/api.rs index adb5d96..ba6d2fe 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,227 +1,1887 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +use std::{ffi::OsString, num::NonZeroU32, pin::Pin, process::Stdio}; -pub mod handlers; -pub mod msg; -pub mod protocol; +use pinnacle_api_defs::pinnacle::{ + input::v0alpha1::{ + set_libinput_setting_request::{AccelProfile, ClickMethod, ScrollMethod, TapButtonMap}, + set_mousebind_request::MouseEdge, + SetKeybindRequest, SetKeybindResponse, SetLibinputSettingRequest, SetMousebindRequest, + SetMousebindResponse, SetRepeatRateRequest, SetXkbConfigRequest, + }, + output::v0alpha1::{ConnectForAllRequest, ConnectForAllResponse, SetLocationRequest}, + process::v0alpha1::{SetEnvRequest, SpawnRequest, SpawnResponse}, + tag::v0alpha1::{ + AddRequest, AddResponse, RemoveRequest, SetActiveRequest, SetLayoutRequest, SwitchToRequest, + }, + v0alpha1::{Geometry, QuitRequest}, + window::v0alpha1::{ + AddWindowRuleRequest, CloseRequest, FullscreenOrMaximized, MoveGrabRequest, + MoveToTagRequest, ResizeGrabRequest, SetFloatingRequest, SetFullscreenRequest, + SetGeometryRequest, SetMaximizedRequest, SetTagRequest, WindowRule, WindowRuleCondition, + }, +}; +use smithay::{ + desktop::space::SpaceElement, + input::keyboard::XkbConfig, + reexports::{calloop, input as libinput, wayland_protocols::xdg::shell::server}, + utils::{Point, Rectangle, SERIAL_COUNTER}, + wayland::{compositor, shell::xdg::XdgToplevelSurfaceData}, +}; +use sysinfo::ProcessRefreshKind; +use tokio::io::AsyncBufReadExt; +use tokio_stream::Stream; +use tonic::{Request, Response, Status}; -use std::{ - io::{self, Read, Write}, - os::unix::net::{UnixListener, UnixStream}, - path::Path, - sync::{Arc, Mutex}, +use crate::{ + config::ConnectorSavedState, + focus::FocusTarget, + input::ModifierMask, + output::OutputName, + state::{State, WithState}, + tag::{Tag, TagId}, + window::{window_state::WindowId, WindowElement}, }; -use self::msg::{Msg, OutgoingMsg}; -use anyhow::Context; -use calloop::RegistrationToken; -use smithay::reexports::calloop::{ - self, channel::Sender, generic::Generic, EventSource, Interest, Mode, PostAction, -}; +type ResponseStream = Pin> + Send>>; +pub type StateFnSender = calloop::channel::Sender>; -pub const SOCKET_NAME: &str = "pinnacle_socket"; +pub struct PinnacleService { + pub sender: StateFnSender, +} -/// Handle a config process. -/// -/// `stream` is the incoming stream where messages will be received, -/// and `sender` sends decoded messages to the main state's handler. -fn handle_client( - mut stream: UnixStream, - sender: Sender, -) -> Result<(), Box> { - loop { - let mut len_marker_bytes = [0u8; 4]; - if let Err(err) = stream.read_exact(&mut len_marker_bytes) { - if err.kind() == io::ErrorKind::UnexpectedEof { - tracing::warn!("stream closed: {}", err); - stream.shutdown(std::net::Shutdown::Both)?; - break Ok(()); - } - }; +#[tonic::async_trait] +impl pinnacle_api_defs::pinnacle::v0alpha1::pinnacle_service_server::PinnacleService + for PinnacleService +{ + async fn quit(&self, _request: Request) -> Result, Status> { + tracing::trace!("PinnacleService.quit"); + let f = Box::new(|state: &mut State| { + state.shutdown(); + }); + // Expect is ok here, if it panics then the state was dropped beforehand + self.sender.send(f).expect("failed to send f"); - let len_marker = u32::from_ne_bytes(len_marker_bytes); - let mut msg_bytes = vec![0u8; len_marker as usize]; - - if let Err(err) = stream.read_exact(msg_bytes.as_mut_slice()) { - if err.kind() == io::ErrorKind::UnexpectedEof { - tracing::warn!("stream closed: {}", err); - stream.shutdown(std::net::Shutdown::Both)?; - break Ok(()); - } - }; - let msg: Msg = rmp_serde::from_slice(msg_bytes.as_slice())?; // TODO: handle error - - sender.send(msg)?; + Ok(Response::new(())) } } -/// A socket source for an event loop that will listen for config processes. -pub struct PinnacleSocketSource { - /// The socket listener - socket: Generic, - /// The sender that will send messages from clients to the main event loop. - sender: Sender, +pub struct InputService { + pub sender: StateFnSender, } -impl PinnacleSocketSource { - /// Create a loop source that listens for connections to the provided `socket_dir`. - /// This will also set PINNACLE_SOCKET for use in API implementations. - pub fn new( - sender: Sender, - socket_dir: &Path, - multiple_instances: bool, - ) -> anyhow::Result { - tracing::debug!("Creating socket source for dir {socket_dir:?}"); +#[tonic::async_trait] +impl pinnacle_api_defs::pinnacle::input::v0alpha1::input_service_server::InputService + for InputService +{ + type SetKeybindStream = ResponseStream; + type SetMousebindStream = ResponseStream; - // Test if you are running multiple instances of Pinnacle - // let multiple_instances = system.processes_by_exact_name("pinnacle").count() > 1; + async fn set_keybind( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); - // If you are, append a suffix to the socket name - let socket_name = if multiple_instances { - let mut suffix: u8 = 1; - while let Ok(true) = socket_dir - .join(format!("{SOCKET_NAME}_{suffix}")) - .try_exists() - { - suffix += 1; + tracing::debug!(request = ?request); + + // TODO: impl From<&[Modifier]> for ModifierMask + let modifiers = request + .modifiers() + .fold(ModifierMask::empty(), |acc, modifier| match modifier { + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Unspecified => acc, + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Shift => { + acc | ModifierMask::SHIFT + } + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Ctrl => { + acc | ModifierMask::CTRL + } + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Alt => { + acc | ModifierMask::ALT + } + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Super => { + acc | ModifierMask::SUPER + } + }); + let key = request + .key + .ok_or_else(|| Status::invalid_argument("no key specified"))?; + + use pinnacle_api_defs::pinnacle::input::v0alpha1::set_keybind_request::Key; + let keysym = match key { + Key::RawCode(num) => { + tracing::info!("set keybind: {:?}, raw {}", modifiers, num); + xkbcommon::xkb::Keysym::new(num) } - format!("{SOCKET_NAME}_{suffix}") - } else { - SOCKET_NAME.to_string() - }; - - let socket_path = socket_dir.join(socket_name); - - // If there are multiple instances, don't touch other sockets - if multiple_instances { - if let Ok(exists) = socket_path.try_exists() { - if exists { - std::fs::remove_file(&socket_path) - .context(format!("Failed to remove old socket at {socket_path:?}",))?; + Key::XkbName(s) => { + if s.chars().count() == 1 { + let Some(ch) = s.chars().next() else { unreachable!() }; + let keysym = xkbcommon::xkb::Keysym::from_char(ch); + tracing::info!("set keybind: {:?}, {:?}", modifiers, keysym); + keysym + } else { + let keysym = + xkbcommon::xkb::keysym_from_name(&s, xkbcommon::xkb::KEYSYM_NO_FLAGS); + tracing::info!("set keybind: {:?}, {:?}", modifiers, keysym); + keysym } } - } else { - // If there aren't, remove them all - for file in std::fs::read_dir(socket_dir)? - .filter_map(|entry| entry.ok()) - .filter(|entry| entry.file_name().to_string_lossy().starts_with(SOCKET_NAME)) - { - tracing::debug!("Removing socket at {:?}", file.path()); - std::fs::remove_file(file.path()) - .context(format!("Failed to remove old socket at {:?}", file.path()))?; + }; + + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + + self.sender + .send(Box::new(move |state| { + state + .input_state + .keybinds + .insert((modifiers, keysym), sender); + })) + .map_err(|_| Status::internal("internal state was not running"))?; + + let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); + + Ok(Response::new(Box::pin(receiver_stream))) + } + + async fn set_mousebind( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + tracing::debug!(request = ?request); + + let modifiers = request + .modifiers() + .fold(ModifierMask::empty(), |acc, modifier| match modifier { + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Unspecified => acc, + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Shift => { + acc | ModifierMask::SHIFT + } + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Ctrl => { + acc | ModifierMask::CTRL + } + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Alt => { + acc | ModifierMask::ALT + } + pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Super => { + acc | ModifierMask::SUPER + } + }); + let button = request + .button + .ok_or_else(|| Status::invalid_argument("no key specified"))?; + + let edge = request.edge(); + + if let MouseEdge::Unspecified = edge { + return Err(Status::invalid_argument("press or release not specified")); + } + + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + + self.sender + .send(Box::new(move |state| { + state + .input_state + .mousebinds + .insert((modifiers, button, edge), sender); + })) + .map_err(|_| Status::internal("internal state was not running"))?; + + let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); + + Ok(Response::new(Box::pin(receiver_stream))) + } + + async fn set_xkb_config( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let f = Box::new(move |state: &mut State| { + let new_config = XkbConfig { + rules: request.rules(), + variant: request.variant(), + model: request.model(), + layout: request.layout(), + options: request.options.clone(), + }; + if let Some(kb) = state.seat.get_keyboard() { + if let Err(err) = kb.set_xkb_config(state, new_config) { + tracing::error!("Failed to set xkbconfig: {err}"); + } } - } + }); - let listener = UnixListener::bind(&socket_path) - .with_context(|| format!("Failed to bind to socket at {socket_path:?}"))?; - tracing::info!("Bound to socket at {socket_path:?}"); + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; - listener - .set_nonblocking(true) - .context("Failed to set socket to nonblocking")?; + Ok(Response::new(())) + } - let socket = Generic::new(listener, Interest::READ, Mode::Level); + async fn set_repeat_rate( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); - std::env::set_var("PINNACLE_SOCKET", socket_path); + let rate = request + .rate + .ok_or_else(|| Status::invalid_argument("no rate specified"))?; + let delay = request + .delay + .ok_or_else(|| Status::invalid_argument("no rate specified"))?; - Ok(Self { socket, sender }) + let f = Box::new(move |state: &mut State| { + if let Some(kb) = state.seat.get_keyboard() { + kb.change_repeat_info(rate, delay); + } + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_libinput_setting( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let setting = request + .setting + .ok_or_else(|| Status::invalid_argument("no setting specified"))?; + + let discriminant = std::mem::discriminant(&setting); + + use pinnacle_api_defs::pinnacle::input::v0alpha1::set_libinput_setting_request::Setting; + let apply_setting: Box = match setting { + Setting::AccelProfile(profile) => { + let profile = AccelProfile::try_from(profile).unwrap_or(AccelProfile::Unspecified); + + match profile { + AccelProfile::Unspecified => { + return Err(Status::invalid_argument("unspecified accel profile")); + } + AccelProfile::Flat => Box::new(|device| { + let _ = device.config_accel_set_profile(libinput::AccelProfile::Flat); + }), + AccelProfile::Adaptive => Box::new(|device| { + let _ = device.config_accel_set_profile(libinput::AccelProfile::Adaptive); + }), + } + } + Setting::AccelSpeed(speed) => Box::new(move |device| { + let _ = device.config_accel_set_speed(speed); + }), + Setting::CalibrationMatrix(matrix) => { + let matrix = <[f32; 6]>::try_from(matrix.matrix).map_err(|vec| { + Status::invalid_argument(format!( + "matrix requires exactly 6 floats but {} were specified", + vec.len() + )) + })?; + + Box::new(move |device| { + let _ = device.config_calibration_set_matrix(matrix); + }) + } + Setting::ClickMethod(method) => { + let method = ClickMethod::try_from(method).unwrap_or(ClickMethod::Unspecified); + + match method { + ClickMethod::Unspecified => { + return Err(Status::invalid_argument("unspecified click method")) + } + ClickMethod::ButtonAreas => Box::new(|device| { + let _ = device.config_click_set_method(libinput::ClickMethod::ButtonAreas); + }), + ClickMethod::ClickFinger => Box::new(|device| { + let _ = device.config_click_set_method(libinput::ClickMethod::Clickfinger); + }), + } + } + Setting::DisableWhileTyping(disable) => Box::new(move |device| { + let _ = device.config_dwt_set_enabled(disable); + }), + Setting::LeftHanded(enable) => Box::new(move |device| { + let _ = device.config_left_handed_set(enable); + }), + Setting::MiddleEmulation(enable) => Box::new(move |device| { + let _ = device.config_middle_emulation_set_enabled(enable); + }), + Setting::RotationAngle(angle) => Box::new(move |device| { + let _ = device.config_rotation_set_angle(angle % 360); + }), + Setting::ScrollButton(button) => Box::new(move |device| { + let _ = device.config_scroll_set_button(button); + }), + Setting::ScrollButtonLock(enable) => Box::new(move |device| { + let _ = device.config_scroll_set_button_lock(match enable { + true => libinput::ScrollButtonLockState::Enabled, + false => libinput::ScrollButtonLockState::Disabled, + }); + }), + Setting::ScrollMethod(method) => { + let method = ScrollMethod::try_from(method).unwrap_or(ScrollMethod::Unspecified); + + match method { + ScrollMethod::Unspecified => { + return Err(Status::invalid_argument("unspecified scroll method")); + } + ScrollMethod::NoScroll => Box::new(|device| { + let _ = device.config_scroll_set_method(libinput::ScrollMethod::NoScroll); + }), + ScrollMethod::TwoFinger => Box::new(|device| { + let _ = device.config_scroll_set_method(libinput::ScrollMethod::TwoFinger); + }), + ScrollMethod::Edge => Box::new(|device| { + let _ = device.config_scroll_set_method(libinput::ScrollMethod::Edge); + }), + ScrollMethod::OnButtonDown => Box::new(|device| { + let _ = + device.config_scroll_set_method(libinput::ScrollMethod::OnButtonDown); + }), + } + } + Setting::NaturalScroll(enable) => Box::new(move |device| { + let _ = device.config_scroll_set_natural_scroll_enabled(enable); + }), + Setting::TapButtonMap(map) => { + let map = TapButtonMap::try_from(map).unwrap_or(TapButtonMap::Unspecified); + + match map { + TapButtonMap::Unspecified => { + return Err(Status::invalid_argument("unspecified tap button map")); + } + TapButtonMap::LeftRightMiddle => Box::new(|device| { + let _ = device + .config_tap_set_button_map(libinput::TapButtonMap::LeftRightMiddle); + }), + TapButtonMap::LeftMiddleRight => Box::new(|device| { + let _ = device + .config_tap_set_button_map(libinput::TapButtonMap::LeftMiddleRight); + }), + } + } + Setting::TapDrag(enable) => Box::new(move |device| { + let _ = device.config_tap_set_drag_enabled(enable); + }), + Setting::TapDragLock(enable) => Box::new(move |device| { + let _ = device.config_tap_set_drag_lock_enabled(enable); + }), + Setting::Tap(enable) => Box::new(move |device| { + let _ = device.config_tap_set_enabled(enable); + }), + }; + + let f = Box::new(move |state: &mut State| { + for device in state.input_state.libinput_devices.iter_mut() { + apply_setting(device); + } + + state + .input_state + .libinput_settings + .insert(discriminant, apply_setting); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) } } -/// Send a message to a client. -pub fn send_to_client( - stream: &mut UnixStream, - msg: &OutgoingMsg, -) -> Result<(), rmp_serde::encode::Error> { - tracing::trace!("Sending {msg:?}"); - - let msg = rmp_serde::to_vec_named(msg)?; - let msg_len = msg.len() as u32; - let bytes = msg_len.to_ne_bytes(); - - if let Err(err) = stream.write_all(&bytes) { - if err.kind() == io::ErrorKind::BrokenPipe { - // TODO: notify user that config daemon is ded - return Ok(()); // TODO: - } - } - - if let Err(err) = stream.write_all(msg.as_slice()) { - if err.kind() == io::ErrorKind::BrokenPipe { - // TODO: something - return Ok(()); // TODO: - } - } - - Ok(()) +pub struct ProcessService { + pub sender: StateFnSender, } -impl EventSource for PinnacleSocketSource { - type Event = UnixStream; +#[tonic::async_trait] +impl pinnacle_api_defs::pinnacle::process::v0alpha1::process_service_server::ProcessService + for ProcessService +{ + type SpawnStream = ResponseStream; - type Metadata = (); + async fn spawn( + &self, + request: Request, + ) -> Result, Status> { + tracing::debug!("ProcessService.spawn"); + let request = request.into_inner(); - type Ret = (); + let once = request.once(); + let has_callback = request.has_callback(); + let mut command = request.args.into_iter(); + let arg0 = command + .next() + .ok_or_else(|| Status::invalid_argument("no args specified"))?; - type Error = io::Error; + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); - fn process_events( - &mut self, - readiness: calloop::Readiness, - token: calloop::Token, - mut callback: F, - ) -> Result - where - F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, - { - self.socket - .process_events(readiness, token, |_readiness, listener| { - while let Ok((stream, _sock_addr)) = listener.accept() { - let sender = self.sender.clone(); - let callback_stream = stream.try_clone()?; + let f = Box::new(move |state: &mut State| { + if once { + state + .system_processes + .refresh_processes_specifics(ProcessRefreshKind::new()); - callback(callback_stream, &mut ()); + let compositor_pid = std::process::id(); + let already_running = + state + .system_processes + .processes_by_exact_name(&arg0) + .any(|proc| { + proc.parent() + .is_some_and(|parent_pid| parent_pid.as_u32() == compositor_pid) + }); - // Handle the client in another thread as to not block the main one. - // - // No idea if this is even needed or if it's premature optimization. - std::thread::spawn(move || { - if let Err(err) = handle_client(stream, sender) { - tracing::error!("handle_client errored: {err}"); + if already_running { + return; + } + } + + let Ok(mut child) = tokio::process::Command::new(OsString::from(arg0.clone())) + .envs( + [("WAYLAND_DISPLAY", state.socket_name.clone())] + .into_iter() + .chain(state.xdisplay.map(|xdisp| ("DISPLAY", format!(":{xdisp}")))), + ) + .stdin(match has_callback { + true => Stdio::piped(), + false => Stdio::null(), + }) + .stdout(match has_callback { + true => Stdio::piped(), + false => Stdio::null(), + }) + .stderr(match has_callback { + true => Stdio::piped(), + false => Stdio::null(), + }) + .args(command) + .spawn() + else { + tracing::warn!("Tried to run {arg0}, but it doesn't exist",); + return; + }; + + if !has_callback { + return; + } + + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + + if let Some(stdout) = stdout { + let sender = sender.clone(); + + let mut reader = tokio::io::BufReader::new(stdout).lines(); + + tokio::spawn(async move { + while let Ok(Some(line)) = reader.next_line().await { + let response: Result<_, Status> = Ok(SpawnResponse { + stdout: Some(line), + ..Default::default() + }); + + // TODO: handle error + match sender.send(response) { + Ok(_) => (), + Err(err) => { + tracing::error!(err = ?err); + break; + } + } + } + }); + } + + if let Some(stderr) = stderr { + let sender = sender.clone(); + + let mut reader = tokio::io::BufReader::new(stderr).lines(); + + tokio::spawn(async move { + while let Ok(Some(line)) = reader.next_line().await { + let response: Result<_, Status> = Ok(SpawnResponse { + stderr: Some(line), + ..Default::default() + }); + + // TODO: handle error + match sender.send(response) { + Ok(_) => (), + Err(err) => { + tracing::error!(err = ?err); + break; + } + } + } + }); + } + + tokio::spawn(async move { + match child.wait().await { + Ok(exit_status) => { + let response = Ok(SpawnResponse { + exit_code: exit_status.code(), + exit_message: Some(exit_status.to_string()), + ..Default::default() + }); + // TODO: handle error + let _ = sender.send(response); + } + Err(err) => tracing::warn!("child wait() err: {err}"), + } + }); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); + + Ok(Response::new(Box::pin(receiver_stream))) + } + + async fn set_env(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let key = request + .key + .ok_or_else(|| Status::invalid_argument("no key specified"))?; + let value = request + .value + .ok_or_else(|| Status::invalid_argument("no value specified"))?; + + if key.is_empty() { + return Err(Status::invalid_argument("key was empty")); + } + + if key.contains(['\0', '=']) { + return Err(Status::invalid_argument("key contained NUL or =")); + } + + if value.contains('\0') { + return Err(Status::invalid_argument("value contained NUL")); + } + + std::env::set_var(key, value); + + Ok(Response::new(())) + } +} + +pub struct TagService { + pub sender: StateFnSender, +} + +#[tonic::async_trait] +impl pinnacle_api_defs::pinnacle::tag::v0alpha1::tag_service_server::TagService for TagService { + async fn set_active(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let tag_id = TagId::Some( + request + .tag_id + .ok_or_else(|| Status::invalid_argument("no tag specified"))?, + ); + + let set_or_toggle = match request.set_or_toggle { + Some( + pinnacle_api_defs::pinnacle::tag::v0alpha1::set_active_request::SetOrToggle::Set( + set, + ), + ) => Some(set), + Some( + pinnacle_api_defs::pinnacle::tag::v0alpha1::set_active_request::SetOrToggle::Toggle( + _, + ), + ) => None, + None => return Err(Status::invalid_argument("unspecified set or toggle")), + }; + + let f = Box::new(move |state: &mut State| { + let Some(tag) = tag_id.tag(state) else { + return; + }; + match set_or_toggle { + Some(set) => tag.set_active(set), + None => tag.set_active(!tag.active()), + } + + let Some(output) = tag.output(state) else { + return; + }; + + state.update_windows(&output); + state.update_focus(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn switch_to(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let tag_id = TagId::Some( + request + .tag_id + .ok_or_else(|| Status::invalid_argument("no tag specified"))?, + ); + + let f = Box::new(move |state: &mut State| { + let Some(tag) = tag_id.tag(state) else { return }; + let Some(output) = tag.output(state) else { return }; + + output.with_state(|state| { + for op_tag in state.tags.iter_mut() { + op_tag.set_active(false); + } + tag.set_active(true); + }); + + state.update_windows(&output); + state.update_focus(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn add(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let output_name = OutputName( + request + .output_name + .ok_or_else(|| Status::invalid_argument("no output specified"))?, + ); + + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::(); + + let f = Box::new(move |state: &mut State| { + let new_tags = request + .tag_names + .into_iter() + .map(Tag::new) + .collect::>(); + + let tag_ids = new_tags + .iter() + .map(|tag| tag.id()) + .map(|id| match id { + TagId::None => unreachable!(), + TagId::Some(id) => id, + }) + .collect::>(); + + let _ = sender.send(AddResponse { tag_ids }); + + if let Some(saved_state) = state.config.connector_saved_states.get_mut(&output_name) { + let mut tags = saved_state.tags.clone(); + tags.extend(new_tags.clone()); + saved_state.tags = tags; + } else { + state.config.connector_saved_states.insert( + output_name.clone(), + crate::config::ConnectorSavedState { + tags: new_tags.clone(), + ..Default::default() + }, + ); + } + + let Some(output) = state + .space + .outputs() + .find(|output| output.name() == output_name.0) + else { + return; + }; + + output.with_state(|state| { + state.tags.extend(new_tags.clone()); + tracing::debug!("tags added, are now {:?}", state.tags); + }); + + for tag in new_tags { + for window in state.windows.iter() { + window.with_state(|state| { + for win_tag in state.tags.iter_mut() { + if win_tag.id() == tag.id() { + *win_tag = tag.clone(); + } } }); } + } + }); - Ok(PostAction::Continue) + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } + + // TODO: test + async fn remove(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let tag_ids = request.tag_ids.into_iter().map(TagId::Some); + + let f = Box::new(move |state: &mut State| { + let tags_to_remove = tag_ids.flat_map(|id| id.tag(state)).collect::>(); + + for output in state.space.outputs().cloned().collect::>() { + // TODO: seriously, convert state.tags into a hashset + output.with_state(|state| { + for tag_to_remove in tags_to_remove.iter() { + state.tags.retain(|tag| tag != tag_to_remove); + } + }); + + state.update_windows(&output); + state.schedule_render(&output); + } + + for conn_saved_state in state.config.connector_saved_states.values_mut() { + for tag_to_remove in tags_to_remove.iter() { + conn_saved_state.tags.retain(|tag| tag != tag_to_remove); + } + } + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_layout(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let tag_id = TagId::Some( + request + .tag_id + .ok_or_else(|| Status::invalid_argument("no tag specified"))?, + ); + + use pinnacle_api_defs::pinnacle::tag::v0alpha1::set_layout_request::Layout; + + // TODO: from impl + let layout = match request.layout() { + Layout::Unspecified => return Err(Status::invalid_argument("unspecified layout")), + Layout::MasterStack => crate::layout::Layout::MasterStack, + Layout::Dwindle => crate::layout::Layout::Dwindle, + Layout::Spiral => crate::layout::Layout::Spiral, + Layout::CornerTopLeft => crate::layout::Layout::CornerTopLeft, + Layout::CornerTopRight => crate::layout::Layout::CornerTopRight, + Layout::CornerBottomLeft => crate::layout::Layout::CornerBottomLeft, + Layout::CornerBottomRight => crate::layout::Layout::CornerBottomRight, + }; + + let f = Box::new(move |state: &mut State| { + let Some(tag) = tag_id.tag(state) else { return }; + + tag.set_layout(layout); + + let Some(output) = tag.output(state) else { return }; + + state.update_windows(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn get( + &self, + _request: Request, + ) -> Result, Status> { + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< + pinnacle_api_defs::pinnacle::tag::v0alpha1::GetResponse, + >(); + + let f = Box::new(move |state: &mut State| { + let tag_ids = state + .space + .outputs() + .flat_map(|op| op.with_state(|state| state.tags.clone())) + .map(|tag| tag.id()) + .map(|id| match id { + TagId::None => unreachable!(), + TagId::Some(id) => id, + }) + .collect::>(); + + let _ = + sender.send(pinnacle_api_defs::pinnacle::tag::v0alpha1::GetResponse { tag_ids }); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } + + async fn get_properties( + &self, + request: Request, + ) -> Result, Status> + { + let request = request.into_inner(); + + let tag_id = TagId::Some( + request + .tag_id + .ok_or_else(|| Status::invalid_argument("no tag specified"))?, + ); + + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< + pinnacle_api_defs::pinnacle::tag::v0alpha1::GetPropertiesResponse, + >(); + + let f = Box::new(move |state: &mut State| { + let tag = tag_id.tag(state); + + let output_name = tag + .as_ref() + .and_then(|tag| tag.output(state)) + .map(|output| output.name()); + let active = tag.as_ref().map(|tag| tag.active()); + let name = tag.as_ref().map(|tag| tag.name()); + + let _ = sender.send( + pinnacle_api_defs::pinnacle::tag::v0alpha1::GetPropertiesResponse { + active, + name, + output_name, + }, + ); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } +} + +pub struct OutputService { + pub sender: StateFnSender, +} + +#[tonic::async_trait] +impl pinnacle_api_defs::pinnacle::output::v0alpha1::output_service_server::OutputService + for OutputService +{ + type ConnectForAllStream = ResponseStream; + + async fn set_location( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let output_name = OutputName( + request + .output_name + .ok_or_else(|| Status::invalid_argument("no output specified"))?, + ); + + let x = request.x; + let y = request.y; + + let f = Box::new(move |state: &mut State| { + if let Some(saved_state) = state.config.connector_saved_states.get_mut(&output_name) { + if let Some(x) = x { + saved_state.loc.x = x; + } + if let Some(y) = y { + saved_state.loc.y = y; + } + } else { + state.config.connector_saved_states.insert( + output_name.clone(), + ConnectorSavedState { + loc: (x.unwrap_or_default(), y.unwrap_or_default()).into(), + ..Default::default() + }, + ); + } + + let Some(output) = output_name.output(state) else { + return; + }; + let mut loc = output.current_location(); + if let Some(x) = x { + loc.x = x; + } + if let Some(y) = y { + loc.y = y; + } + output.change_current_state(None, None, None, Some(loc)); + state.space.map_output(&output, loc); + tracing::debug!("Mapping output {} to {loc:?}", output.name()); + state.update_windows(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + // TODO: remove this and integrate it into a signal/event system + async fn connect_for_all( + &self, + _request: Request, + ) -> Result, Status> { + tracing::trace!("OutputService.connect_for_all"); + let (sender, receiver) = + tokio::sync::mpsc::unbounded_channel::>(); + + let f = Box::new(move |state: &mut State| { + // for output in state.space.outputs() { + // let _ = sender.send(Ok(ConnectForAllResponse { + // output_name: Some(output.name()), + // })); + // tracing::debug!(name = output.name(), "sent connect_for_all"); + // } + + state.config.output_callback_senders.push(sender); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); + + Ok(Response::new(Box::pin(receiver_stream))) + } + + async fn get( + &self, + _request: Request, + ) -> Result, Status> { + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< + pinnacle_api_defs::pinnacle::output::v0alpha1::GetResponse, + >(); + + let f = Box::new(move |state: &mut State| { + let output_names = state + .space + .outputs() + .map(|output| output.name()) + .collect::>(); + + let _ = sender + .send(pinnacle_api_defs::pinnacle::output::v0alpha1::GetResponse { output_names }); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } + + async fn get_properties( + &self, + request: Request, + ) -> Result< + Response, + Status, + > { + let request = request.into_inner(); + + let output_name = OutputName( + request + .output_name + .ok_or_else(|| Status::invalid_argument("no output specified"))?, + ); + + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< + pinnacle_api_defs::pinnacle::output::v0alpha1::GetPropertiesResponse, + >(); + + let f = Box::new(move |state: &mut State| { + let output = output_name.output(state); + + let pixel_width = output + .as_ref() + .and_then(|output| output.current_mode().map(|mode| mode.size.w as u32)); + + let pixel_height = output + .as_ref() + .and_then(|output| output.current_mode().map(|mode| mode.size.h as u32)); + + let refresh_rate = output + .as_ref() + .and_then(|output| output.current_mode().map(|mode| mode.refresh as u32)); + + let model = output + .as_ref() + .map(|output| output.physical_properties().model); + + let physical_width = output + .as_ref() + .map(|output| output.physical_properties().size.w as u32); + + let physical_height = output + .as_ref() + .map(|output| output.physical_properties().size.h as u32); + + let make = output + .as_ref() + .map(|output| output.physical_properties().make); + + let x = output.as_ref().map(|output| output.current_location().x); + + let y = output.as_ref().map(|output| output.current_location().y); + + let focused = state + .focus_state + .focused_output + .as_ref() + .and_then(|foc_op| output.as_ref().map(|op| op == foc_op)); + + let tag_ids = output + .as_ref() + .map(|output| { + output.with_state(|state| { + state + .tags + .iter() + .map(|tag| match tag.id() { + TagId::None => unreachable!(), + TagId::Some(id) => id, + }) + .collect::>() + }) + }) + .unwrap_or_default(); + + let _ = sender.send( + pinnacle_api_defs::pinnacle::output::v0alpha1::GetPropertiesResponse { + make, + model, + x, + y, + pixel_width, + pixel_height, + refresh_rate, + physical_width, + physical_height, + focused, + tag_ids, + }, + ); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } +} + +pub struct WindowService { + pub sender: StateFnSender, +} + +#[tonic::async_trait] +impl pinnacle_api_defs::pinnacle::window::v0alpha1::window_service_server::WindowService + for WindowService +{ + async fn close(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { return }; + + match window { + WindowElement::Wayland(window) => window.toplevel().send_close(), + WindowElement::X11(surface) => surface.close().expect("failed to close x11 win"), + WindowElement::X11OverrideRedirect(_) => { + tracing::warn!("tried to close override redirect window"); + } + _ => unreachable!(), + } + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_geometry( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let geometry = request.geometry.unwrap_or_default(); + let x = geometry.x; + let y = geometry.y; + let width = geometry.width; + let height = geometry.height; + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { return }; + + // TODO: with no x or y, defaults unmapped windows to 0, 0 + let mut window_loc = state + .space + .element_location(&window) + .unwrap_or((x.unwrap_or_default(), y.unwrap_or_default()).into()); + window_loc.x = x.unwrap_or(window_loc.x); + window_loc.y = y.unwrap_or(window_loc.y); + + let mut window_size = window.geometry().size; + window_size.w = width.unwrap_or(window_size.w); + window_size.h = height.unwrap_or(window_size.h); + + let rect = Rectangle::from_loc_and_size(window_loc, window_size); + // window.change_geometry(rect); + window.with_state(|state| { + use crate::window::window_state::FloatingOrTiled; + state.floating_or_tiled = match state.floating_or_tiled { + FloatingOrTiled::Floating(_) => FloatingOrTiled::Floating(rect), + FloatingOrTiled::Tiled(_) => FloatingOrTiled::Tiled(Some(rect)), + } + }); + + for output in state.space.outputs_for_element(&window) { + state.update_windows(&output); + state.schedule_render(&output); + } + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_fullscreen( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let set_or_toggle = match request.set_or_toggle { + Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_fullscreen_request::SetOrToggle::Set(set)) => { + Some(set) + } + Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_fullscreen_request::SetOrToggle::Toggle(_)) => { + None + } + None => return Err(Status::invalid_argument("unspecified set or toggle")), + }; + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { + return; + }; + match set_or_toggle { + Some(set) => { + let is_fullscreen = + window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()); + if set != is_fullscreen { + window.toggle_fullscreen(); + } + } + None => window.toggle_fullscreen(), + } + + let Some(output) = window.output(state) else { + return; + }; + + state.update_windows(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_maximized( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let set_or_toggle = match request.set_or_toggle { + Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_maximized_request::SetOrToggle::Set(set)) => { + Some(set) + } + Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_maximized_request::SetOrToggle::Toggle(_)) => None, + None => return Err(Status::invalid_argument("unspecified set or toggle")), + }; + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { + return; + }; + match set_or_toggle { + Some(set) => { + let is_maximized = + window.with_state(|state| state.fullscreen_or_maximized.is_maximized()); + if set != is_maximized { + window.toggle_maximized(); + } + } + None => window.toggle_maximized(), + } + + let Some(output) = window.output(state) else { + return; + }; + + state.update_windows(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_floating( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let set_or_toggle = match request.set_or_toggle { + Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_floating_request::SetOrToggle::Set(set)) => { + Some(set) + } + Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_floating_request::SetOrToggle::Toggle(_)) => None, + None => return Err(Status::invalid_argument("unspecified set or toggle")), + }; + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { + return; + }; + match set_or_toggle { + Some(set) => { + let is_floating = + window.with_state(|state| state.floating_or_tiled.is_floating()); + if set != is_floating { + window.toggle_floating(); + } + } + None => window.toggle_floating(), + } + + let Some(output) = window.output(state) else { + return; + }; + + state.update_windows(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn move_to_tag( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let tag_id = TagId::Some( + request + .tag_id + .ok_or_else(|| Status::invalid_argument("no tag specified"))?, + ); + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { return }; + let Some(tag) = tag_id.tag(state) else { return }; + window.with_state(|state| { + state.tags = vec![tag.clone()]; + }); + let Some(output) = tag.output(state) else { return }; + state.update_windows(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn set_tag(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let tag_id = TagId::Some( + request + .tag_id + .ok_or_else(|| Status::invalid_argument("no tag specified"))?, + ); + + let set_or_toggle = match request.set_or_toggle { + Some( + pinnacle_api_defs::pinnacle::window::v0alpha1::set_tag_request::SetOrToggle::Set( + set, + ), + ) => Some(set), + Some( + pinnacle_api_defs::pinnacle::window::v0alpha1::set_tag_request::SetOrToggle::Toggle( + _, + ), + ) => None, + None => return Err(Status::invalid_argument("unspecified set or toggle")), + }; + + let f = Box::new(move |state: &mut State| { + let Some(window) = window_id.window(state) else { return }; + let Some(tag) = tag_id.tag(state) else { return }; + + // TODO: turn state.tags into a hashset + match set_or_toggle { + Some(set) => { + if set { + window.with_state(|state| { + state.tags.retain(|tg| tg != &tag); + state.tags.push(tag.clone()); + }) + } else { + window.with_state(|state| { + state.tags.retain(|tg| tg != &tag); + }) + } + } + None => window.with_state(|state| { + if !state.tags.contains(&tag) { + state.tags.push(tag.clone()); + } else { + state.tags.retain(|tg| tg != &tag); + } + }), + } + + let Some(output) = tag.output(state) else { return }; + state.update_windows(&output); + state.schedule_render(&output); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn move_grab(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + + let button = request + .button + .ok_or_else(|| Status::invalid_argument("no button specified"))?; + + let f = Box::new(move |state: &mut State| { + let Some((FocusTarget::Window(window), _)) = + state.focus_target_under(state.pointer_location) + else { + return; + }; + let Some(wl_surf) = window.wl_surface() else { return }; + let seat = state.seat.clone(); + + // We use the server one and not the client because windows like Steam don't provide + // GrabStartData, so we need to create it ourselves. + crate::grab::move_grab::move_request_server( + state, + &wl_surf, + &seat, + SERIAL_COUNTER.next_serial(), + button, + ); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn resize_grab( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let button = request + .button + .ok_or_else(|| Status::invalid_argument("no button specified"))?; + + let f = Box::new(move |state: &mut State| { + let pointer_loc = state.pointer_location; + let Some((FocusTarget::Window(window), window_loc)) = + state.focus_target_under(pointer_loc) + else { + return; + }; + let Some(wl_surf) = window.wl_surface() else { return }; + + let window_geometry = window.geometry(); + let window_x = window_loc.x as f64; + let window_y = window_loc.y as f64; + let window_width = window_geometry.size.w as f64; + let window_height = window_geometry.size.h as f64; + let half_width = window_x + window_width / 2.0; + let half_height = window_y + window_height / 2.0; + let full_width = window_x + window_width; + let full_height = window_y + window_height; + + let edges = match pointer_loc { + Point { x, y, .. } + if (window_x..=half_width).contains(&x) + && (window_y..=half_height).contains(&y) => + { + server::xdg_toplevel::ResizeEdge::TopLeft + } + Point { x, y, .. } + if (half_width..=full_width).contains(&x) + && (window_y..=half_height).contains(&y) => + { + server::xdg_toplevel::ResizeEdge::TopRight + } + Point { x, y, .. } + if (window_x..=half_width).contains(&x) + && (half_height..=full_height).contains(&y) => + { + server::xdg_toplevel::ResizeEdge::BottomLeft + } + Point { x, y, .. } + if (half_width..=full_width).contains(&x) + && (half_height..=full_height).contains(&y) => + { + server::xdg_toplevel::ResizeEdge::BottomRight + } + _ => server::xdg_toplevel::ResizeEdge::None, + }; + + crate::grab::resize_grab::resize_request_server( + state, + &wl_surf, + &state.seat.clone(), + SERIAL_COUNTER.next_serial(), + edges.into(), + button, + ); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } + + async fn get( + &self, + _request: Request, + ) -> Result, Status> { + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< + pinnacle_api_defs::pinnacle::window::v0alpha1::GetResponse, + >(); + + let f = Box::new(move |state: &mut State| { + let window_ids = state + .windows + .iter() + .map(|win| { + win.with_state(|state| match state.id { + WindowId::None => unreachable!(), + WindowId::Some(id) => id, + }) + }) + .collect::>(); + + let _ = sender + .send(pinnacle_api_defs::pinnacle::window::v0alpha1::GetResponse { window_ids }); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } + + async fn get_properties( + &self, + request: Request, + ) -> Result< + Response, + Status, + > { + let request = request.into_inner(); + + let window_id = WindowId::Some( + request + .window_id + .ok_or_else(|| Status::invalid_argument("no window specified"))?, + ); + + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< + pinnacle_api_defs::pinnacle::window::v0alpha1::GetPropertiesResponse, + >(); + + let f = Box::new(move |state: &mut State| { + let window = window_id.window(state); + + let width = window.as_ref().map(|win| win.geometry().size.w); + + let height = window.as_ref().map(|win| win.geometry().size.h); + + let x = window + .as_ref() + .and_then(|win| state.space.element_location(win)) + .map(|loc| loc.x); + + let y = window + .as_ref() + .and_then(|win| state.space.element_location(win)) + .map(|loc| loc.y); + + let geometry = if width.is_none() && height.is_none() && x.is_none() && y.is_none() { + None + } else { + Some(Geometry { + x, + y, + width, + height, + }) + }; + + let (class, title) = window.as_ref().map_or((None, None), |win| match &win { + WindowElement::Wayland(_) => { + if let Some(wl_surf) = win.wl_surface() { + compositor::with_states(&wl_surf, |states| { + let lock = states + .data_map + .get::() + .expect("XdgToplevelSurfaceData wasn't in surface's data map") + .lock() + .expect("failed to acquire lock"); + (lock.app_id.clone(), lock.title.clone()) + }) + } else { + (None, None) + } + } + WindowElement::X11(surface) | WindowElement::X11OverrideRedirect(surface) => { + (Some(surface.class()), Some(surface.title())) + } + _ => unreachable!(), + }); + + let focused = window.as_ref().and_then(|win| { + let output = win.output(state)?; + state.focused_window(&output).map(|foc_win| win == &foc_win) + }); + + let floating = window + .as_ref() + .map(|win| win.with_state(|state| state.floating_or_tiled.is_floating())); + + let fullscreen_or_maximized = window + .as_ref() + .map(|win| win.with_state(|state| state.fullscreen_or_maximized)) + .map(|fs_or_max| match fs_or_max { + // TODO: from impl + crate::window::window_state::FullscreenOrMaximized::Neither => { + FullscreenOrMaximized::Neither + } + crate::window::window_state::FullscreenOrMaximized::Fullscreen => { + FullscreenOrMaximized::Fullscreen + } + crate::window::window_state::FullscreenOrMaximized::Maximized => { + FullscreenOrMaximized::Maximized + } + } as i32); + + let tag_ids = window + .as_ref() + .map(|win| { + win.with_state(|state| { + state + .tags + .iter() + .map(|tag| match tag.id() { + TagId::Some(id) => id, + TagId::None => unreachable!(), + }) + .collect::>() + }) + }) + .unwrap_or_default(); + + let _ = sender.send( + pinnacle_api_defs::pinnacle::window::v0alpha1::GetPropertiesResponse { + geometry, + class, + title, + focused, + floating, + fullscreen_or_maximized, + tag_ids, + }, + ); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + let response = receiver + .recv() + .await + .ok_or_else(|| Status::internal("internal state was not running"))?; + + Ok(Response::new(response)) + } + + async fn add_window_rule( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let cond = request + .cond + .ok_or_else(|| Status::invalid_argument("no condition specified"))? + .into(); + + let rule = request + .rule + .ok_or_else(|| Status::invalid_argument("no rule specified"))? + .into(); + + let f = Box::new(move |state: &mut State| { + state.config.window_rules.push((cond, rule)); + }); + + self.sender + .send(f) + .map_err(|_| Status::internal("internal state was not running"))?; + + Ok(Response::new(())) + } +} + +impl From for crate::window::rules::WindowRuleCondition { + fn from(cond: WindowRuleCondition) -> Self { + let cond_any = match cond.any.is_empty() { + true => None, + false => Some( + cond.any + .into_iter() + .map(crate::window::rules::WindowRuleCondition::from) + .collect::>(), + ), + }; + + let cond_all = match cond.all.is_empty() { + true => None, + false => Some( + cond.all + .into_iter() + .map(crate::window::rules::WindowRuleCondition::from) + .collect::>(), + ), + }; + + let class = match cond.classes.is_empty() { + true => None, + false => Some(cond.classes), + }; + + let title = match cond.titles.is_empty() { + true => None, + false => Some(cond.titles), + }; + + let tag = match cond.tags.is_empty() { + true => None, + false => Some(cond.tags.into_iter().map(TagId::Some).collect::>()), + }; + + crate::window::rules::WindowRuleCondition { + cond_any, + cond_all, + class, + title, + tag, + } + } +} + +impl From for crate::window::rules::WindowRule { + fn from(rule: WindowRule) -> Self { + let fullscreen_or_maximized = match rule.fullscreen_or_maximized() { + FullscreenOrMaximized::Unspecified => None, + FullscreenOrMaximized::Neither => { + Some(crate::window::window_state::FullscreenOrMaximized::Neither) + } + FullscreenOrMaximized::Fullscreen => { + Some(crate::window::window_state::FullscreenOrMaximized::Fullscreen) + } + FullscreenOrMaximized::Maximized => { + Some(crate::window::window_state::FullscreenOrMaximized::Maximized) + } + }; + let output = rule.output.map(OutputName); + let tags = match rule.tags.is_empty() { + true => None, + false => Some(rule.tags.into_iter().map(TagId::Some).collect::>()), + }; + let floating_or_tiled = rule.floating.map(|floating| match floating { + true => crate::window::rules::FloatingOrTiled::Floating, + false => crate::window::rules::FloatingOrTiled::Tiled, + }); + let size = rule.width.and_then(|w| { + rule.height.and_then(|h| { + Some(( + NonZeroU32::try_from(w as u32).ok()?, + NonZeroU32::try_from(h as u32).ok()?, + )) }) - } + }); + let location = rule.x.and_then(|x| rule.y.map(|y| (x, y))); - fn register( - &mut self, - poll: &mut calloop::Poll, - token_factory: &mut calloop::TokenFactory, - ) -> calloop::Result<()> { - self.socket.register(poll, token_factory) - } - - fn reregister( - &mut self, - poll: &mut calloop::Poll, - token_factory: &mut calloop::TokenFactory, - ) -> calloop::Result<()> { - self.socket.reregister(poll, token_factory) - } - - fn unregister(&mut self, poll: &mut calloop::Poll) -> calloop::Result<()> { - self.socket.unregister(poll) + crate::window::rules::WindowRule { + output, + tags, + floating_or_tiled, + fullscreen_or_maximized, + size, + location, + } } } - -pub struct ApiState { - // TODO: this may not need to be in an arc mutex because of the move to async - /// The stream API messages are being sent through. - pub stream: Option>>, - /// A token used to remove the socket source from the event loop on config restart. - pub socket_token: Option, - /// The sending channel used to send API messages received from the socket source to a handler. - pub tx_channel: Sender, -} diff --git a/src/api/handlers.rs b/src/api/handlers.rs deleted file mode 100644 index 0373f82..0000000 --- a/src/api/handlers.rs +++ /dev/null @@ -1,824 +0,0 @@ -use std::{ffi::OsString, process::Stdio}; - -use smithay::{ - desktop::space::SpaceElement, - input::keyboard::XkbConfig, - reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge, - utils::{Point, Rectangle, SERIAL_COUNTER}, - wayland::{compositor, shell::xdg::XdgToplevelSurfaceData}, -}; -use sysinfo::ProcessRefreshKind; -use tokio::io::AsyncBufReadExt; - -use crate::{ - api::msg::{ - Args, CallbackId, KeyIntOrString, Msg, OutgoingMsg, Request, RequestId, RequestResponse, - }, - config::ConnectorSavedState, - focus::FocusTarget, - tag::Tag, - window::WindowElement, -}; - -use crate::state::{State, WithState}; - -impl State { - /// Handle a client message. - pub fn handle_msg(&mut self, msg: Msg) { - tracing::trace!("Got {msg:?}"); - - match msg { - Msg::SetKeybind { - key, - modifiers, - callback_id, - } => { - let key = match key { - KeyIntOrString::Int(num) => { - tracing::info!("set keybind: {:?}, raw {}", modifiers, num); - num - } - KeyIntOrString::String(s) => { - if s.chars().count() == 1 { - let Some(ch) = s.chars().next() else { unreachable!() }; - let raw = xkbcommon::xkb::Keysym::from_char(ch).raw(); - tracing::info!("set keybind: {:?}, {:?} (raw {})", modifiers, ch, raw); - raw - } else { - let raw = xkbcommon::xkb::keysym_from_name( - &s, - xkbcommon::xkb::KEYSYM_NO_FLAGS, - ) - .raw(); - tracing::info!("set keybind: {:?}, {:?}", modifiers, raw); - raw - } - } - }; - - self.input_state - .keybinds - .insert((modifiers.into(), key.into()), callback_id); - } - Msg::SetMousebind { - modifiers, - button, - edge, - callback_id, - } => { - // TODO: maybe validate/parse valid codes? - self.input_state - .mousebinds - .insert((modifiers.into(), button, edge), callback_id); - } - Msg::CloseWindow { window_id } => { - if let Some(window) = window_id.window(self) { - match window { - WindowElement::Wayland(window) => window.toplevel().send_close(), - WindowElement::X11(surface) => { - surface.close().expect("failed to close x11 win"); - } - WindowElement::X11OverrideRedirect(_) => (), - _ => unreachable!(), - } - } - } - - Msg::Spawn { - command, - callback_id, - } => { - self.handle_spawn(command, callback_id); - } - Msg::SpawnOnce { - command, - callback_id, - } => { - self.system_processes - .refresh_processes_specifics(ProcessRefreshKind::new()); - - let Some(arg0) = command.first() else { - tracing::warn!("No command specified for `SpawnOnce`"); - return; - }; - - let compositor_pid = std::process::id(); - let already_running = - self.system_processes - .processes_by_exact_name(arg0) - .any(|proc| { - proc.parent() - .is_some_and(|parent_pid| parent_pid.as_u32() == compositor_pid) - }); - - if !already_running { - self.handle_spawn(command, callback_id); - } - } - Msg::SetEnv { key, value } => std::env::set_var(key, value), - - Msg::SetWindowSize { - window_id, - width, - height, - } => { - let Some(window) = window_id.window(self) else { return }; - - // TODO: tiled vs floating - // FIXME: this will map unmapped windows at 0,0 - let window_loc = self - .space - .element_location(&window) - .unwrap_or((0, 0).into()); - let mut window_size = window.geometry().size; - if let Some(width) = width { - window_size.w = width; - } - if let Some(height) = height { - window_size.h = height; - } - use crate::window::window_state::FloatingOrTiled; - - let rect = Rectangle::from_loc_and_size(window_loc, window_size); - window.change_geometry(rect); - window.with_state(|state| { - state.floating_or_tiled = match state.floating_or_tiled { - FloatingOrTiled::Floating(_) => FloatingOrTiled::Floating(rect), - FloatingOrTiled::Tiled(_) => FloatingOrTiled::Tiled(Some(rect)), - } - }); - - for output in self.space.outputs_for_element(&window) { - self.update_windows(&output); - self.schedule_render(&output); - } - } - Msg::MoveWindowToTag { window_id, tag_id } => { - let Some(window) = window_id.window(self) else { return }; - let Some(tag) = tag_id.tag(self) else { return }; - window.with_state(|state| { - state.tags = vec![tag.clone()]; - }); - let Some(output) = tag.output(self) else { return }; - self.update_windows(&output); - self.schedule_render(&output); - } - Msg::ToggleTagOnWindow { window_id, tag_id } => { - let Some(window) = window_id.window(self) else { return }; - let Some(tag) = tag_id.tag(self) else { return }; - - window.with_state(|state| { - if state.tags.contains(&tag) { - state.tags.retain(|tg| tg != &tag); - } else { - state.tags.push(tag.clone()); - } - }); - - let Some(output) = tag.output(self) else { return }; - self.update_windows(&output); - self.schedule_render(&output); - } - Msg::ToggleFloating { window_id } => { - let Some(window) = window_id.window(self) else { return }; - window.toggle_floating(); - - let Some(output) = window.output(self) else { return }; - self.update_windows(&output); - self.schedule_render(&output); - } - Msg::ToggleFullscreen { window_id } => { - let Some(window) = window_id.window(self) else { return }; - window.toggle_fullscreen(); - - let Some(output) = window.output(self) else { return }; - self.update_windows(&output); - self.schedule_render(&output); - } - Msg::ToggleMaximized { window_id } => { - let Some(window) = window_id.window(self) else { return }; - window.toggle_maximized(); - - let Some(output) = window.output(self) else { return }; - self.update_windows(&output); - self.schedule_render(&output); - } - Msg::AddWindowRule { cond, rule } => { - self.config.window_rules.push((cond, rule)); - } - Msg::WindowMoveGrab { button } => { - let Some((FocusTarget::Window(window), _)) = - self.focus_target_under(self.pointer_location) - else { - return; - }; - let Some(wl_surf) = window.wl_surface() else { return }; - let seat = self.seat.clone(); - - // We use the server one and not the client because windows like Steam don't provide - // GrabStartData, so we need to create it ourselves. - crate::grab::move_grab::move_request_server( - self, - &wl_surf, - &seat, - SERIAL_COUNTER.next_serial(), - button, - ); - } - Msg::WindowResizeGrab { button } => { - // TODO: in the future, there may be movable layer surfaces - let pointer_loc = self.pointer_location; - let Some((FocusTarget::Window(window), window_loc)) = - self.focus_target_under(pointer_loc) - else { - return; - }; - let Some(wl_surf) = window.wl_surface() else { return }; - - let window_geometry = window.geometry(); - let window_x = window_loc.x as f64; - let window_y = window_loc.y as f64; - let window_width = window_geometry.size.w as f64; - let window_height = window_geometry.size.h as f64; - let half_width = window_x + window_width / 2.0; - let half_height = window_y + window_height / 2.0; - let full_width = window_x + window_width; - let full_height = window_y + window_height; - - let edges = match pointer_loc { - Point { x, y, .. } - if (window_x..=half_width).contains(&x) - && (window_y..=half_height).contains(&y) => - { - ResizeEdge::TopLeft - } - Point { x, y, .. } - if (half_width..=full_width).contains(&x) - && (window_y..=half_height).contains(&y) => - { - ResizeEdge::TopRight - } - Point { x, y, .. } - if (window_x..=half_width).contains(&x) - && (half_height..=full_height).contains(&y) => - { - ResizeEdge::BottomLeft - } - Point { x, y, .. } - if (half_width..=full_width).contains(&x) - && (half_height..=full_height).contains(&y) => - { - ResizeEdge::BottomRight - } - _ => ResizeEdge::None, - }; - - crate::grab::resize_grab::resize_request_server( - self, - &wl_surf, - &self.seat.clone(), - SERIAL_COUNTER.next_serial(), - edges.into(), - button, - ); - } - - // Tags ---------------------------------------- - Msg::ToggleTag { tag_id } => { - tracing::debug!("ToggleTag"); - if let Some(tag) = tag_id.tag(self) { - tag.set_active(!tag.active()); - if let Some(output) = tag.output(self) { - self.update_windows(&output); - self.update_focus(&output); - self.schedule_render(&output); - } - } - } - Msg::SwitchToTag { tag_id } => { - let Some(tag) = tag_id.tag(self) else { return }; - let Some(output) = tag.output(self) else { return }; - output.with_state(|state| { - for op_tag in state.tags.iter_mut() { - op_tag.set_active(false); - } - tag.set_active(true); - }); - self.update_windows(&output); - self.update_focus(&output); - self.schedule_render(&output); - } - Msg::AddTags { - output_name, - tag_names, - } => { - let new_tags = tag_names.into_iter().map(Tag::new).collect::>(); - if let Some(saved_state) = self.config.connector_saved_states.get_mut(&output_name) - { - let mut tags = saved_state.tags.clone(); - tags.extend(new_tags.clone()); - saved_state.tags = tags; - } else { - self.config.connector_saved_states.insert( - output_name.clone(), - ConnectorSavedState { - tags: new_tags.clone(), - ..Default::default() - }, - ); - } - - if let Some(output) = self - .space - .outputs() - .find(|output| output.name() == output_name.0) - { - output.with_state(|state| { - state.tags.extend(new_tags.clone()); - tracing::debug!("tags added, are now {:?}", state.tags); - }); - - // replace tags that windows have that are the same id - // (this should only happen on config reload) - for tag in new_tags { - for window in self.windows.iter() { - window.with_state(|state| { - for win_tag in state.tags.iter_mut() { - if win_tag.id() == tag.id() { - *win_tag = tag.clone(); - } - } - }); - } - } - } - } - Msg::RemoveTags { tag_ids } => { - let tags = tag_ids - .into_iter() - .filter_map(|tag_id| tag_id.tag(self)) - .collect::>(); - - for tag in tags { - for saved_state in self.config.connector_saved_states.values_mut() { - saved_state.tags.retain(|tg| tg != &tag); - } - let Some(output) = tag.output(self) else { continue }; - output.with_state(|state| { - state.tags.retain(|tg| tg != &tag); - }); - } - } - Msg::SetLayout { tag_id, layout } => { - let Some(tag) = tag_id.tag(self) else { return }; - tag.set_layout(layout); - let Some(output) = tag.output(self) else { return }; - self.update_windows(&output); - - self.schedule_render(&output); - } - - Msg::ConnectForAllOutputs { callback_id } => { - let stream = self - .api_state - .stream - .as_ref() - .expect("stream doesn't exist"); - - for output in self.space.outputs() { - crate::api::send_to_client( - &mut stream.lock().expect("couldn't lock stream"), - &OutgoingMsg::CallCallback { - callback_id, - args: Some(Args::ConnectForAllOutputs { - output_name: output.name(), - }), - }, - ) - .expect("Send to client failed"); - } - - self.config.output_callback_ids.push(callback_id); - } - Msg::SetOutputLocation { output_name, x, y } => { - if let Some(saved_state) = self.config.connector_saved_states.get_mut(&output_name) - { - if let Some(x) = x { - saved_state.loc.x = x; - } - if let Some(y) = y { - saved_state.loc.y = y; - } - } else { - self.config.connector_saved_states.insert( - output_name.clone(), - ConnectorSavedState { - loc: (x.unwrap_or_default(), y.unwrap_or_default()).into(), - ..Default::default() - }, - ); - } - - let Some(output) = output_name.output(self) else { return }; - let mut loc = output.current_location(); - if let Some(x) = x { - loc.x = x; - } - if let Some(y) = y { - loc.y = y; - } - output.change_current_state(None, None, None, Some(loc)); - self.space.map_output(&output, loc); - tracing::debug!("Mapping output {} to {loc:?}", output.name()); - self.update_windows(&output); - } - - Msg::Quit => { - tracing::info!("Quitting Pinnacle"); - self.shutdown(); - } - - Msg::SetXkbConfig { - rules, - variant, - layout, - model, - options, - } => { - let new_config = XkbConfig { - rules: &rules.unwrap_or_default(), - model: &model.unwrap_or_default(), - layout: &layout.unwrap_or_default(), - variant: &variant.unwrap_or_default(), - options, - }; - if let Some(kb) = self.seat.get_keyboard() { - if let Err(err) = kb.set_xkb_config(self, new_config) { - tracing::error!("Failed to set xkbconfig: {err}"); - } - } - } - - Msg::SetLibinputSetting(setting) => { - for device in self.input_state.libinput_devices.iter_mut() { - // We're just gonna indiscriminately apply everything and ignore errors - setting.apply_to_device(device); - } - - self.input_state.libinput_settings.push(setting); - } - - Msg::Request { - request_id, - request, - } => { - self.handle_request(request_id, request); - } - } - } - - /// Handle a client request. - fn handle_request(&mut self, request_id: RequestId, request: Request) { - let stream = self - .api_state - .stream - .clone() // clone due to use of self below - .expect("Stream doesn't exist"); - let mut stream = stream.lock().expect("Couldn't lock stream"); - - match request { - Request::GetWindows => { - let window_ids = self - .windows - .iter() - .map(|win| win.with_state(|state| state.id)) - .collect::>(); - - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id, - response: RequestResponse::Windows { window_ids }, - }, - ) - .expect("Couldn't send to client"); - } - Request::GetWindowProps { window_id } => { - let window = window_id.window(self); - - let size = window - .as_ref() - .map(|win| (win.geometry().size.w, win.geometry().size.h)); - - let loc = window - .as_ref() - .and_then(|win| self.space.element_location(win)) - .map(|loc| (loc.x, loc.y)); - - let (class, title) = window.as_ref().map_or((None, None), |win| match &win { - WindowElement::Wayland(_) => { - if let Some(wl_surf) = win.wl_surface() { - compositor::with_states(&wl_surf, |states| { - let lock = states - .data_map - .get::() - .expect("XdgToplevelSurfaceData wasn't in surface's data map") - .lock() - .expect("failed to acquire lock"); - (lock.app_id.clone(), lock.title.clone()) - }) - } else { - (None, None) - } - } - WindowElement::X11(surface) | WindowElement::X11OverrideRedirect(surface) => { - (Some(surface.class()), Some(surface.title())) - } - _ => unreachable!(), - }); - - let focused = window.as_ref().and_then(|win| { - let output = win.output(self)?; - self.focused_window(&output).map(|foc_win| win == &foc_win) - }); - - let floating = window - .as_ref() - .map(|win| win.with_state(|state| state.floating_or_tiled.is_floating())); - - let fullscreen_or_maximized = window - .as_ref() - .map(|win| win.with_state(|state| state.fullscreen_or_maximized)); - - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id, - response: RequestResponse::WindowProps { - size, - loc, - class, - title, - focused, - floating, - fullscreen_or_maximized, - }, - }, - ) - .expect("failed to send to client"); - } - Request::GetOutputs => { - let output_names = self - .space - .outputs() - .map(|output| output.name()) - .collect::>(); - - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id, - response: RequestResponse::Outputs { output_names }, - }, - ) - .expect("failed to send to client"); - } - Request::GetOutputProps { output_name } => { - let output = self - .space - .outputs() - .find(|output| output.name() == output_name); - - let res = output.as_ref().and_then(|output| { - output.current_mode().map(|mode| (mode.size.w, mode.size.h)) - }); - - let refresh_rate = output - .as_ref() - .and_then(|output| output.current_mode().map(|mode| mode.refresh)); - - let model = output - .as_ref() - .map(|output| output.physical_properties().model); - - let physical_size = output.as_ref().map(|output| { - ( - output.physical_properties().size.w, - output.physical_properties().size.h, - ) - }); - - let make = output - .as_ref() - .map(|output| output.physical_properties().make); - - let loc = output - .as_ref() - .map(|output| (output.current_location().x, output.current_location().y)); - - let focused = self - .focus_state - .focused_output - .as_ref() - .and_then(|foc_op| output.map(|op| op == foc_op)); - - let tag_ids = output.as_ref().map(|output| { - output.with_state(|state| { - state.tags.iter().map(|tag| tag.id()).collect::>() - }) - }); - - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id, - response: RequestResponse::OutputProps { - make, - model, - loc, - res, - refresh_rate, - physical_size, - focused, - tag_ids, - }, - }, - ) - .expect("failed to send to client"); - } - Request::GetTags => { - let tag_ids = self - .space - .outputs() - .flat_map(|op| op.with_state(|state| state.tags.clone())) - .map(|tag| tag.id()) - .collect::>(); - - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id, - response: RequestResponse::Tags { tag_ids }, - }, - ) - .expect("failed to send to client"); - } - Request::GetTagProps { tag_id } => { - let tag = tag_id.tag(self); - - let output_name = tag - .as_ref() - .and_then(|tag| tag.output(self)) - .map(|output| output.name()); - - let active = tag.as_ref().map(|tag| tag.active()); - let name = tag.as_ref().map(|tag| tag.name()); - - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id, - response: RequestResponse::TagProps { - active, - name, - output_name, - }, - }, - ) - .expect("failed to send to client"); - } - } - } - - // Welcome to indentation hell - /// Handle a received spawn command by spawning the command and hooking up any callbacks. - pub fn handle_spawn(&self, command: Vec, callback_id: Option) { - let mut command = command.into_iter(); - let Some(program) = command.next() else { - // TODO: notify that command was nothing - tracing::warn!("got an empty command"); - return; - }; - - let program = OsString::from(program); - let Ok(mut child) = tokio::process::Command::new(&program) - .envs( - [("WAYLAND_DISPLAY", self.socket_name.clone())] - .into_iter() - .chain(self.xdisplay.map(|xdisp| ("DISPLAY", format!(":{xdisp}")))), - ) - .stdin(if callback_id.is_some() { - Stdio::piped() - } else { - // piping to null because foot won't open without a callback_id - // otherwise - Stdio::null() - }) - .stdout(if callback_id.is_some() { - Stdio::piped() - } else { - Stdio::null() - }) - .stderr(if callback_id.is_some() { - Stdio::piped() - } else { - Stdio::null() - }) - .args(command) - .spawn() - else { - // TODO: notify user that program doesn't exist - tracing::warn!( - "Tried to run {}, but it doesn't exist", - program.to_string_lossy() - ); - return; - }; - - if let Some(callback_id) = callback_id { - let stdout = child.stdout.take(); - let stderr = child.stderr.take(); - - let stream_out = self.api_state.stream.clone().expect("Stream doesn't exist"); - let stream_err = stream_out.clone(); - let stream_exit = stream_out.clone(); - - if let Some(stdout) = stdout { - let future = async move { - let mut reader = tokio::io::BufReader::new(stdout).lines(); - while let Ok(Some(line)) = reader.next_line().await { - let msg = OutgoingMsg::CallCallback { - callback_id, - args: Some(Args::Spawn { - stdout: Some(line), - stderr: None, - exit_code: None, - exit_msg: None, - }), - }; - - crate::api::send_to_client( - &mut stream_out.lock().expect("Couldn't lock stream"), - &msg, - ) - .expect("Send to client failed"); // TODO: notify instead of crash - } - }; - - tokio::spawn(future); - } - - if let Some(stderr) = stderr { - let future = async move { - let mut reader = tokio::io::BufReader::new(stderr).lines(); - while let Ok(Some(line)) = reader.next_line().await { - let msg = OutgoingMsg::CallCallback { - callback_id, - args: Some(Args::Spawn { - stdout: None, - stderr: Some(line), - exit_code: None, - exit_msg: None, - }), - }; - - crate::api::send_to_client( - &mut stream_err.lock().expect("Couldn't lock stream"), - &msg, - ) - .expect("Send to client failed"); // TODO: notify instead of crash - } - }; - - tokio::spawn(future); - } - - let future = async move { - match child.wait().await { - Ok(exit_status) => { - let msg = OutgoingMsg::CallCallback { - callback_id, - args: Some(Args::Spawn { - stdout: None, - stderr: None, - exit_code: exit_status.code(), - exit_msg: Some(exit_status.to_string()), - }), - }; - - crate::api::send_to_client( - &mut stream_exit.lock().expect("Couldn't lock stream"), - &msg, - ) - .expect("Send to client failed"); // TODO: notify instead of crash - } - Err(err) => { - tracing::warn!("child wait() err: {err}"); - } - } - }; - - tokio::spawn(future); - } - } -} diff --git a/src/api/msg.rs b/src/api/msg.rs deleted file mode 100644 index 588177b..0000000 --- a/src/api/msg.rs +++ /dev/null @@ -1,335 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -// The MessagePack format for these is a one-element map where the element's key is the enum name and its -// value is a map of the enum's values - -use smithay::input::keyboard::ModifiersState; - -use crate::{ - input::libinput::LibinputSetting, - layout::Layout, - output::OutputName, - tag::TagId, - window::{ - rules::{WindowRule, WindowRuleCondition}, - window_state::{FullscreenOrMaximized, WindowId}, - }, -}; - -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Copy)] -pub struct CallbackId(pub u32); - -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] -pub enum KeyIntOrString { - Int(u32), - String(String), -} - -#[derive(Debug, Hash, serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq)] -pub enum MouseEdge { - Press, - Release, -} - -#[derive(Debug, serde::Deserialize)] -pub enum Msg { - // Input - SetKeybind { - key: KeyIntOrString, - modifiers: Vec, - callback_id: CallbackId, - }, - SetMousebind { - modifiers: Vec, - button: u32, - edge: MouseEdge, - callback_id: CallbackId, - }, - - // Window management - CloseWindow { - window_id: WindowId, - }, - SetWindowSize { - window_id: WindowId, - #[serde(default)] - width: Option, - #[serde(default)] - height: Option, - }, - MoveWindowToTag { - window_id: WindowId, - tag_id: TagId, - }, - ToggleTagOnWindow { - window_id: WindowId, - tag_id: TagId, - }, - ToggleFloating { - window_id: WindowId, - }, - ToggleFullscreen { - window_id: WindowId, - }, - ToggleMaximized { - window_id: WindowId, - }, - AddWindowRule { - cond: WindowRuleCondition, - rule: WindowRule, - }, - WindowMoveGrab { - button: u32, - }, - WindowResizeGrab { - button: u32, - }, - - // Tag management - ToggleTag { - tag_id: TagId, - }, - SwitchToTag { - tag_id: TagId, - }, - AddTags { - /// The name of the output you want these tags on. - output_name: OutputName, - tag_names: Vec, - }, - RemoveTags { - /// The name of the output you want these tags removed from. - tag_ids: Vec, - }, - SetLayout { - tag_id: TagId, - layout: Layout, - }, - - // Output management - ConnectForAllOutputs { - callback_id: CallbackId, - }, - SetOutputLocation { - output_name: OutputName, - #[serde(default)] - x: Option, - #[serde(default)] - y: Option, - }, - - // Process management - /// Spawn a program with an optional callback. - Spawn { - command: Vec, - #[serde(default)] - callback_id: Option, - }, - SpawnOnce { - command: Vec, - #[serde(default)] - callback_id: Option, - }, - SetEnv { - key: String, - value: String, - }, - - // Pinnacle management - /// Quit the compositor. - Quit, - - // Input management - SetXkbConfig { - #[serde(default)] - rules: Option, - #[serde(default)] - variant: Option, - #[serde(default)] - layout: Option, - #[serde(default)] - model: Option, - #[serde(default)] - options: Option, - }, - - SetLibinputSetting(LibinputSetting), - - Request { - request_id: RequestId, - request: Request, - }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct RequestId(u32); - -#[allow(clippy::enum_variant_names)] -#[derive(Debug, serde::Serialize, serde::Deserialize)] -/// Messages that require a server response, usually to provide some data. -pub enum Request { - // Windows - GetWindows, - GetWindowProps { window_id: WindowId }, - // Outputs - GetOutputs, - GetOutputProps { output_name: String }, - // Tags - GetTags, - GetTagProps { tag_id: TagId }, -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)] -pub enum Modifier { - Shift = 0b0000_0001, - Ctrl = 0b0000_0010, - Alt = 0b0000_0100, - Super = 0b0000_1000, -} - -/// A bitmask of [`Modifier`]s for the purpose of hashing. -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -pub struct ModifierMask(u8); - -impl From> for ModifierMask { - fn from(value: Vec) -> Self { - let value = value.into_iter(); - let mut mask: u8 = 0b0000_0000; - for modifier in value { - mask |= modifier as u8; - } - Self(mask) - } -} - -impl From<&[Modifier]> for ModifierMask { - fn from(value: &[Modifier]) -> Self { - let value = value.iter(); - let mut mask: u8 = 0b0000_0000; - for modifier in value { - mask |= *modifier as u8; - } - Self(mask) - } -} - -impl From for ModifierMask { - fn from(state: ModifiersState) -> Self { - let mut mask: u8 = 0b0000_0000; - if state.shift { - mask |= Modifier::Shift as u8; - } - if state.ctrl { - mask |= Modifier::Ctrl as u8; - } - if state.alt { - mask |= Modifier::Alt as u8; - } - if state.logo { - mask |= Modifier::Super as u8; - } - Self(mask) - } -} - -impl ModifierMask { - #[allow(dead_code)] - pub fn values(self) -> Vec { - let mut res = Vec::::new(); - if self.0 & Modifier::Shift as u8 == Modifier::Shift as u8 { - res.push(Modifier::Shift); - } - if self.0 & Modifier::Ctrl as u8 == Modifier::Ctrl as u8 { - res.push(Modifier::Ctrl); - } - if self.0 & Modifier::Alt as u8 == Modifier::Alt as u8 { - res.push(Modifier::Alt); - } - if self.0 & Modifier::Super as u8 == Modifier::Super as u8 { - res.push(Modifier::Super); - } - res - } -} - -/// Messages sent from the server to the client. -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub enum OutgoingMsg { - CallCallback { - callback_id: CallbackId, - #[serde(default)] - args: Option, - }, - RequestResponse { - request_id: RequestId, - response: RequestResponse, - }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub enum Args { - /// Send a message with lines from the spawned process. - Spawn { - #[serde(default)] - stdout: Option, - #[serde(default)] - stderr: Option, - #[serde(default)] - exit_code: Option, - #[serde(default)] - exit_msg: Option, - }, - ConnectForAllOutputs { - output_name: String, - }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub enum RequestResponse { - Window { - window_id: Option, - }, - Windows { - window_ids: Vec, - }, - WindowProps { - size: Option<(i32, i32)>, - loc: Option<(i32, i32)>, - class: Option, - title: Option, - focused: Option, - floating: Option, - fullscreen_or_maximized: Option, - }, - Output { - output_name: Option, - }, - Outputs { - output_names: Vec, - }, - OutputProps { - /// The make of the output. - make: Option, - /// The model of the output. - model: Option, - /// The location of the output in the space. - loc: Option<(i32, i32)>, - /// The resolution of the output. - res: Option<(i32, i32)>, - /// The refresh rate of the output. - refresh_rate: Option, - /// The size of the output, in millimeters. - physical_size: Option<(i32, i32)>, - /// Whether the output is focused or not. - focused: Option, - tag_ids: Option>, - }, - Tags { - tag_ids: Vec, - }, - TagProps { - active: Option, - name: Option, - output_name: Option, - }, -} diff --git a/src/api/protocol.rs b/src/api/protocol.rs deleted file mode 100644 index 23b0fee..0000000 --- a/src/api/protocol.rs +++ /dev/null @@ -1,1887 +0,0 @@ -use std::{ffi::OsString, num::NonZeroU32, pin::Pin, process::Stdio}; - -use pinnacle_api_defs::pinnacle::{ - input::v0alpha1::{ - set_libinput_setting_request::{AccelProfile, ClickMethod, ScrollMethod, TapButtonMap}, - set_mousebind_request::MouseEdge, - SetKeybindRequest, SetKeybindResponse, SetLibinputSettingRequest, SetMousebindRequest, - SetMousebindResponse, SetRepeatRateRequest, SetXkbConfigRequest, - }, - output::v0alpha1::{ConnectForAllRequest, ConnectForAllResponse, SetLocationRequest}, - process::v0alpha1::{SetEnvRequest, SpawnRequest, SpawnResponse}, - tag::v0alpha1::{ - AddRequest, AddResponse, RemoveRequest, SetActiveRequest, SetLayoutRequest, SwitchToRequest, - }, - v0alpha1::{Geometry, QuitRequest}, - window::v0alpha1::{ - AddWindowRuleRequest, CloseRequest, FullscreenOrMaximized, MoveGrabRequest, - MoveToTagRequest, ResizeGrabRequest, SetFloatingRequest, SetFullscreenRequest, - SetGeometryRequest, SetMaximizedRequest, SetTagRequest, WindowRule, WindowRuleCondition, - }, -}; -use smithay::{ - desktop::space::SpaceElement, - input::keyboard::XkbConfig, - reexports::{calloop, input as libinput, wayland_protocols::xdg::shell::server}, - utils::{Point, Rectangle, SERIAL_COUNTER}, - wayland::{compositor, shell::xdg::XdgToplevelSurfaceData}, -}; -use sysinfo::ProcessRefreshKind; -use tokio::io::AsyncBufReadExt; -use tokio_stream::Stream; -use tonic::{Request, Response, Status}; - -use crate::{ - config::ConnectorSavedState, - focus::FocusTarget, - input::ModifierMask, - output::OutputName, - state::{State, WithState}, - tag::{Tag, TagId}, - window::{window_state::WindowId, WindowElement}, -}; - -type ResponseStream = Pin> + Send>>; -pub type StateFnSender = calloop::channel::Sender>; - -pub struct PinnacleService { - pub sender: StateFnSender, -} - -#[tonic::async_trait] -impl pinnacle_api_defs::pinnacle::v0alpha1::pinnacle_service_server::PinnacleService - for PinnacleService -{ - async fn quit(&self, _request: Request) -> Result, Status> { - tracing::trace!("PinnacleService.quit"); - let f = Box::new(|state: &mut State| { - state.shutdown(); - }); - // Expect is ok here, if it panics then the state was dropped beforehand - self.sender.send(f).expect("failed to send f"); - - Ok(Response::new(())) - } -} - -pub struct InputService { - pub sender: StateFnSender, -} - -#[tonic::async_trait] -impl pinnacle_api_defs::pinnacle::input::v0alpha1::input_service_server::InputService - for InputService -{ - type SetKeybindStream = ResponseStream; - type SetMousebindStream = ResponseStream; - - async fn set_keybind( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - tracing::debug!(request = ?request); - - // TODO: impl From<&[Modifier]> for ModifierMask - let modifiers = request - .modifiers() - .fold(ModifierMask::empty(), |acc, modifier| match modifier { - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Unspecified => acc, - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Shift => { - acc | ModifierMask::SHIFT - } - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Ctrl => { - acc | ModifierMask::CTRL - } - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Alt => { - acc | ModifierMask::ALT - } - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Super => { - acc | ModifierMask::SUPER - } - }); - let key = request - .key - .ok_or_else(|| Status::invalid_argument("no key specified"))?; - - use pinnacle_api_defs::pinnacle::input::v0alpha1::set_keybind_request::Key; - let keysym = match key { - Key::RawCode(num) => { - tracing::info!("set keybind: {:?}, raw {}", modifiers, num); - xkbcommon::xkb::Keysym::new(num) - } - Key::XkbName(s) => { - if s.chars().count() == 1 { - let Some(ch) = s.chars().next() else { unreachable!() }; - let keysym = xkbcommon::xkb::Keysym::from_char(ch); - tracing::info!("set keybind: {:?}, {:?}", modifiers, keysym); - keysym - } else { - let keysym = - xkbcommon::xkb::keysym_from_name(&s, xkbcommon::xkb::KEYSYM_NO_FLAGS); - tracing::info!("set keybind: {:?}, {:?}", modifiers, keysym); - keysym - } - } - }; - - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); - - self.sender - .send(Box::new(move |state| { - state - .input_state - .grpc_keybinds - .insert((modifiers, keysym), sender); - })) - .map_err(|_| Status::internal("internal state was not running"))?; - - let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); - - Ok(Response::new(Box::pin(receiver_stream))) - } - - async fn set_mousebind( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - tracing::debug!(request = ?request); - - let modifiers = request - .modifiers() - .fold(ModifierMask::empty(), |acc, modifier| match modifier { - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Unspecified => acc, - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Shift => { - acc | ModifierMask::SHIFT - } - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Ctrl => { - acc | ModifierMask::CTRL - } - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Alt => { - acc | ModifierMask::ALT - } - pinnacle_api_defs::pinnacle::input::v0alpha1::Modifier::Super => { - acc | ModifierMask::SUPER - } - }); - let button = request - .button - .ok_or_else(|| Status::invalid_argument("no key specified"))?; - - let edge = request.edge(); - - if let MouseEdge::Unspecified = edge { - return Err(Status::invalid_argument("press or release not specified")); - } - - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); - - self.sender - .send(Box::new(move |state| { - state - .input_state - .grpc_mousebinds - .insert((modifiers, button, edge), sender); - })) - .map_err(|_| Status::internal("internal state was not running"))?; - - let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); - - Ok(Response::new(Box::pin(receiver_stream))) - } - - async fn set_xkb_config( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let f = Box::new(move |state: &mut State| { - let new_config = XkbConfig { - rules: request.rules(), - variant: request.variant(), - model: request.model(), - layout: request.layout(), - options: request.options.clone(), - }; - if let Some(kb) = state.seat.get_keyboard() { - if let Err(err) = kb.set_xkb_config(state, new_config) { - tracing::error!("Failed to set xkbconfig: {err}"); - } - } - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_repeat_rate( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let rate = request - .rate - .ok_or_else(|| Status::invalid_argument("no rate specified"))?; - let delay = request - .delay - .ok_or_else(|| Status::invalid_argument("no rate specified"))?; - - let f = Box::new(move |state: &mut State| { - if let Some(kb) = state.seat.get_keyboard() { - kb.change_repeat_info(rate, delay); - } - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_libinput_setting( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let setting = request - .setting - .ok_or_else(|| Status::invalid_argument("no setting specified"))?; - - let discriminant = std::mem::discriminant(&setting); - - use pinnacle_api_defs::pinnacle::input::v0alpha1::set_libinput_setting_request::Setting; - let apply_setting: Box = match setting { - Setting::AccelProfile(profile) => { - let profile = AccelProfile::try_from(profile).unwrap_or(AccelProfile::Unspecified); - - match profile { - AccelProfile::Unspecified => { - return Err(Status::invalid_argument("unspecified accel profile")); - } - AccelProfile::Flat => Box::new(|device| { - let _ = device.config_accel_set_profile(libinput::AccelProfile::Flat); - }), - AccelProfile::Adaptive => Box::new(|device| { - let _ = device.config_accel_set_profile(libinput::AccelProfile::Adaptive); - }), - } - } - Setting::AccelSpeed(speed) => Box::new(move |device| { - let _ = device.config_accel_set_speed(speed); - }), - Setting::CalibrationMatrix(matrix) => { - let matrix = <[f32; 6]>::try_from(matrix.matrix).map_err(|vec| { - Status::invalid_argument(format!( - "matrix requires exactly 6 floats but {} were specified", - vec.len() - )) - })?; - - Box::new(move |device| { - let _ = device.config_calibration_set_matrix(matrix); - }) - } - Setting::ClickMethod(method) => { - let method = ClickMethod::try_from(method).unwrap_or(ClickMethod::Unspecified); - - match method { - ClickMethod::Unspecified => { - return Err(Status::invalid_argument("unspecified click method")) - } - ClickMethod::ButtonAreas => Box::new(|device| { - let _ = device.config_click_set_method(libinput::ClickMethod::ButtonAreas); - }), - ClickMethod::ClickFinger => Box::new(|device| { - let _ = device.config_click_set_method(libinput::ClickMethod::Clickfinger); - }), - } - } - Setting::DisableWhileTyping(disable) => Box::new(move |device| { - let _ = device.config_dwt_set_enabled(disable); - }), - Setting::LeftHanded(enable) => Box::new(move |device| { - let _ = device.config_left_handed_set(enable); - }), - Setting::MiddleEmulation(enable) => Box::new(move |device| { - let _ = device.config_middle_emulation_set_enabled(enable); - }), - Setting::RotationAngle(angle) => Box::new(move |device| { - let _ = device.config_rotation_set_angle(angle % 360); - }), - Setting::ScrollButton(button) => Box::new(move |device| { - let _ = device.config_scroll_set_button(button); - }), - Setting::ScrollButtonLock(enable) => Box::new(move |device| { - let _ = device.config_scroll_set_button_lock(match enable { - true => libinput::ScrollButtonLockState::Enabled, - false => libinput::ScrollButtonLockState::Disabled, - }); - }), - Setting::ScrollMethod(method) => { - let method = ScrollMethod::try_from(method).unwrap_or(ScrollMethod::Unspecified); - - match method { - ScrollMethod::Unspecified => { - return Err(Status::invalid_argument("unspecified scroll method")); - } - ScrollMethod::NoScroll => Box::new(|device| { - let _ = device.config_scroll_set_method(libinput::ScrollMethod::NoScroll); - }), - ScrollMethod::TwoFinger => Box::new(|device| { - let _ = device.config_scroll_set_method(libinput::ScrollMethod::TwoFinger); - }), - ScrollMethod::Edge => Box::new(|device| { - let _ = device.config_scroll_set_method(libinput::ScrollMethod::Edge); - }), - ScrollMethod::OnButtonDown => Box::new(|device| { - let _ = - device.config_scroll_set_method(libinput::ScrollMethod::OnButtonDown); - }), - } - } - Setting::NaturalScroll(enable) => Box::new(move |device| { - let _ = device.config_scroll_set_natural_scroll_enabled(enable); - }), - Setting::TapButtonMap(map) => { - let map = TapButtonMap::try_from(map).unwrap_or(TapButtonMap::Unspecified); - - match map { - TapButtonMap::Unspecified => { - return Err(Status::invalid_argument("unspecified tap button map")); - } - TapButtonMap::LeftRightMiddle => Box::new(|device| { - let _ = device - .config_tap_set_button_map(libinput::TapButtonMap::LeftRightMiddle); - }), - TapButtonMap::LeftMiddleRight => Box::new(|device| { - let _ = device - .config_tap_set_button_map(libinput::TapButtonMap::LeftMiddleRight); - }), - } - } - Setting::TapDrag(enable) => Box::new(move |device| { - let _ = device.config_tap_set_drag_enabled(enable); - }), - Setting::TapDragLock(enable) => Box::new(move |device| { - let _ = device.config_tap_set_drag_lock_enabled(enable); - }), - Setting::Tap(enable) => Box::new(move |device| { - let _ = device.config_tap_set_enabled(enable); - }), - }; - - let f = Box::new(move |state: &mut State| { - for device in state.input_state.libinput_devices.iter_mut() { - apply_setting(device); - } - - state - .input_state - .grpc_libinput_settings - .insert(discriminant, apply_setting); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } -} - -pub struct ProcessService { - pub sender: StateFnSender, -} - -#[tonic::async_trait] -impl pinnacle_api_defs::pinnacle::process::v0alpha1::process_service_server::ProcessService - for ProcessService -{ - type SpawnStream = ResponseStream; - - async fn spawn( - &self, - request: Request, - ) -> Result, Status> { - tracing::debug!("ProcessService.spawn"); - let request = request.into_inner(); - - let once = request.once(); - let has_callback = request.has_callback(); - let mut command = request.args.into_iter(); - let arg0 = command - .next() - .ok_or_else(|| Status::invalid_argument("no args specified"))?; - - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); - - let f = Box::new(move |state: &mut State| { - if once { - state - .system_processes - .refresh_processes_specifics(ProcessRefreshKind::new()); - - let compositor_pid = std::process::id(); - let already_running = - state - .system_processes - .processes_by_exact_name(&arg0) - .any(|proc| { - proc.parent() - .is_some_and(|parent_pid| parent_pid.as_u32() == compositor_pid) - }); - - if already_running { - return; - } - } - - let Ok(mut child) = tokio::process::Command::new(OsString::from(arg0.clone())) - .envs( - [("WAYLAND_DISPLAY", state.socket_name.clone())] - .into_iter() - .chain(state.xdisplay.map(|xdisp| ("DISPLAY", format!(":{xdisp}")))), - ) - .stdin(match has_callback { - true => Stdio::piped(), - false => Stdio::null(), - }) - .stdout(match has_callback { - true => Stdio::piped(), - false => Stdio::null(), - }) - .stderr(match has_callback { - true => Stdio::piped(), - false => Stdio::null(), - }) - .args(command) - .spawn() - else { - tracing::warn!("Tried to run {arg0}, but it doesn't exist",); - return; - }; - - if !has_callback { - return; - } - - let stdout = child.stdout.take(); - let stderr = child.stderr.take(); - - if let Some(stdout) = stdout { - let sender = sender.clone(); - - let mut reader = tokio::io::BufReader::new(stdout).lines(); - - tokio::spawn(async move { - while let Ok(Some(line)) = reader.next_line().await { - let response: Result<_, Status> = Ok(SpawnResponse { - stdout: Some(line), - ..Default::default() - }); - - // TODO: handle error - match sender.send(response) { - Ok(_) => (), - Err(err) => { - tracing::error!(err = ?err); - break; - } - } - } - }); - } - - if let Some(stderr) = stderr { - let sender = sender.clone(); - - let mut reader = tokio::io::BufReader::new(stderr).lines(); - - tokio::spawn(async move { - while let Ok(Some(line)) = reader.next_line().await { - let response: Result<_, Status> = Ok(SpawnResponse { - stderr: Some(line), - ..Default::default() - }); - - // TODO: handle error - match sender.send(response) { - Ok(_) => (), - Err(err) => { - tracing::error!(err = ?err); - break; - } - } - } - }); - } - - tokio::spawn(async move { - match child.wait().await { - Ok(exit_status) => { - let response = Ok(SpawnResponse { - exit_code: exit_status.code(), - exit_message: Some(exit_status.to_string()), - ..Default::default() - }); - // TODO: handle error - let _ = sender.send(response); - } - Err(err) => tracing::warn!("child wait() err: {err}"), - } - }); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); - - Ok(Response::new(Box::pin(receiver_stream))) - } - - async fn set_env(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let key = request - .key - .ok_or_else(|| Status::invalid_argument("no key specified"))?; - let value = request - .value - .ok_or_else(|| Status::invalid_argument("no value specified"))?; - - if key.is_empty() { - return Err(Status::invalid_argument("key was empty")); - } - - if key.contains(['\0', '=']) { - return Err(Status::invalid_argument("key contained NUL or =")); - } - - if value.contains('\0') { - return Err(Status::invalid_argument("value contained NUL")); - } - - std::env::set_var(key, value); - - Ok(Response::new(())) - } -} - -pub struct TagService { - pub sender: StateFnSender, -} - -#[tonic::async_trait] -impl pinnacle_api_defs::pinnacle::tag::v0alpha1::tag_service_server::TagService for TagService { - async fn set_active(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let tag_id = TagId::Some( - request - .tag_id - .ok_or_else(|| Status::invalid_argument("no tag specified"))?, - ); - - let set_or_toggle = match request.set_or_toggle { - Some( - pinnacle_api_defs::pinnacle::tag::v0alpha1::set_active_request::SetOrToggle::Set( - set, - ), - ) => Some(set), - Some( - pinnacle_api_defs::pinnacle::tag::v0alpha1::set_active_request::SetOrToggle::Toggle( - _, - ), - ) => None, - None => return Err(Status::invalid_argument("unspecified set or toggle")), - }; - - let f = Box::new(move |state: &mut State| { - let Some(tag) = tag_id.tag(state) else { - return; - }; - match set_or_toggle { - Some(set) => tag.set_active(set), - None => tag.set_active(!tag.active()), - } - - let Some(output) = tag.output(state) else { - return; - }; - - state.update_windows(&output); - state.update_focus(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn switch_to(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let tag_id = TagId::Some( - request - .tag_id - .ok_or_else(|| Status::invalid_argument("no tag specified"))?, - ); - - let f = Box::new(move |state: &mut State| { - let Some(tag) = tag_id.tag(state) else { return }; - let Some(output) = tag.output(state) else { return }; - - output.with_state(|state| { - for op_tag in state.tags.iter_mut() { - op_tag.set_active(false); - } - tag.set_active(true); - }); - - state.update_windows(&output); - state.update_focus(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn add(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let output_name = OutputName( - request - .output_name - .ok_or_else(|| Status::invalid_argument("no output specified"))?, - ); - - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::(); - - let f = Box::new(move |state: &mut State| { - let new_tags = request - .tag_names - .into_iter() - .map(Tag::new) - .collect::>(); - - let tag_ids = new_tags - .iter() - .map(|tag| tag.id()) - .map(|id| match id { - TagId::None => unreachable!(), - TagId::Some(id) => id, - }) - .collect::>(); - - let _ = sender.send(AddResponse { tag_ids }); - - if let Some(saved_state) = state.config.connector_saved_states.get_mut(&output_name) { - let mut tags = saved_state.tags.clone(); - tags.extend(new_tags.clone()); - saved_state.tags = tags; - } else { - state.config.connector_saved_states.insert( - output_name.clone(), - crate::config::ConnectorSavedState { - tags: new_tags.clone(), - ..Default::default() - }, - ); - } - - let Some(output) = state - .space - .outputs() - .find(|output| output.name() == output_name.0) - else { - return; - }; - - output.with_state(|state| { - state.tags.extend(new_tags.clone()); - tracing::debug!("tags added, are now {:?}", state.tags); - }); - - for tag in new_tags { - for window in state.windows.iter() { - window.with_state(|state| { - for win_tag in state.tags.iter_mut() { - if win_tag.id() == tag.id() { - *win_tag = tag.clone(); - } - } - }); - } - } - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } - - // TODO: test - async fn remove(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let tag_ids = request.tag_ids.into_iter().map(TagId::Some); - - let f = Box::new(move |state: &mut State| { - let tags_to_remove = tag_ids.flat_map(|id| id.tag(state)).collect::>(); - - for output in state.space.outputs().cloned().collect::>() { - // TODO: seriously, convert state.tags into a hashset - output.with_state(|state| { - for tag_to_remove in tags_to_remove.iter() { - state.tags.retain(|tag| tag != tag_to_remove); - } - }); - - state.update_windows(&output); - state.schedule_render(&output); - } - - for conn_saved_state in state.config.connector_saved_states.values_mut() { - for tag_to_remove in tags_to_remove.iter() { - conn_saved_state.tags.retain(|tag| tag != tag_to_remove); - } - } - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_layout(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let tag_id = TagId::Some( - request - .tag_id - .ok_or_else(|| Status::invalid_argument("no tag specified"))?, - ); - - use pinnacle_api_defs::pinnacle::tag::v0alpha1::set_layout_request::Layout; - - // TODO: from impl - let layout = match request.layout() { - Layout::Unspecified => return Err(Status::invalid_argument("unspecified layout")), - Layout::MasterStack => crate::layout::Layout::MasterStack, - Layout::Dwindle => crate::layout::Layout::Dwindle, - Layout::Spiral => crate::layout::Layout::Spiral, - Layout::CornerTopLeft => crate::layout::Layout::CornerTopLeft, - Layout::CornerTopRight => crate::layout::Layout::CornerTopRight, - Layout::CornerBottomLeft => crate::layout::Layout::CornerBottomLeft, - Layout::CornerBottomRight => crate::layout::Layout::CornerBottomRight, - }; - - let f = Box::new(move |state: &mut State| { - let Some(tag) = tag_id.tag(state) else { return }; - - tag.set_layout(layout); - - let Some(output) = tag.output(state) else { return }; - - state.update_windows(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn get( - &self, - _request: Request, - ) -> Result, Status> { - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< - pinnacle_api_defs::pinnacle::tag::v0alpha1::GetResponse, - >(); - - let f = Box::new(move |state: &mut State| { - let tag_ids = state - .space - .outputs() - .flat_map(|op| op.with_state(|state| state.tags.clone())) - .map(|tag| tag.id()) - .map(|id| match id { - TagId::None => unreachable!(), - TagId::Some(id) => id, - }) - .collect::>(); - - let _ = - sender.send(pinnacle_api_defs::pinnacle::tag::v0alpha1::GetResponse { tag_ids }); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } - - async fn get_properties( - &self, - request: Request, - ) -> Result, Status> - { - let request = request.into_inner(); - - let tag_id = TagId::Some( - request - .tag_id - .ok_or_else(|| Status::invalid_argument("no tag specified"))?, - ); - - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< - pinnacle_api_defs::pinnacle::tag::v0alpha1::GetPropertiesResponse, - >(); - - let f = Box::new(move |state: &mut State| { - let tag = tag_id.tag(state); - - let output_name = tag - .as_ref() - .and_then(|tag| tag.output(state)) - .map(|output| output.name()); - let active = tag.as_ref().map(|tag| tag.active()); - let name = tag.as_ref().map(|tag| tag.name()); - - let _ = sender.send( - pinnacle_api_defs::pinnacle::tag::v0alpha1::GetPropertiesResponse { - active, - name, - output_name, - }, - ); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } -} - -pub struct OutputService { - pub sender: StateFnSender, -} - -#[tonic::async_trait] -impl pinnacle_api_defs::pinnacle::output::v0alpha1::output_service_server::OutputService - for OutputService -{ - type ConnectForAllStream = ResponseStream; - - async fn set_location( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let output_name = OutputName( - request - .output_name - .ok_or_else(|| Status::invalid_argument("no output specified"))?, - ); - - let x = request.x; - let y = request.y; - - let f = Box::new(move |state: &mut State| { - if let Some(saved_state) = state.config.connector_saved_states.get_mut(&output_name) { - if let Some(x) = x { - saved_state.loc.x = x; - } - if let Some(y) = y { - saved_state.loc.y = y; - } - } else { - state.config.connector_saved_states.insert( - output_name.clone(), - ConnectorSavedState { - loc: (x.unwrap_or_default(), y.unwrap_or_default()).into(), - ..Default::default() - }, - ); - } - - let Some(output) = output_name.output(state) else { - return; - }; - let mut loc = output.current_location(); - if let Some(x) = x { - loc.x = x; - } - if let Some(y) = y { - loc.y = y; - } - output.change_current_state(None, None, None, Some(loc)); - state.space.map_output(&output, loc); - tracing::debug!("Mapping output {} to {loc:?}", output.name()); - state.update_windows(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - // TODO: remove this and integrate it into a signal/event system - async fn connect_for_all( - &self, - _request: Request, - ) -> Result, Status> { - tracing::trace!("OutputService.connect_for_all"); - let (sender, receiver) = - tokio::sync::mpsc::unbounded_channel::>(); - - let f = Box::new(move |state: &mut State| { - // for output in state.space.outputs() { - // let _ = sender.send(Ok(ConnectForAllResponse { - // output_name: Some(output.name()), - // })); - // tracing::debug!(name = output.name(), "sent connect_for_all"); - // } - - state.config.grpc_output_callback_senders.push(sender); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver); - - Ok(Response::new(Box::pin(receiver_stream))) - } - - async fn get( - &self, - _request: Request, - ) -> Result, Status> { - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< - pinnacle_api_defs::pinnacle::output::v0alpha1::GetResponse, - >(); - - let f = Box::new(move |state: &mut State| { - let output_names = state - .space - .outputs() - .map(|output| output.name()) - .collect::>(); - - let _ = sender - .send(pinnacle_api_defs::pinnacle::output::v0alpha1::GetResponse { output_names }); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } - - async fn get_properties( - &self, - request: Request, - ) -> Result< - Response, - Status, - > { - let request = request.into_inner(); - - let output_name = OutputName( - request - .output_name - .ok_or_else(|| Status::invalid_argument("no output specified"))?, - ); - - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< - pinnacle_api_defs::pinnacle::output::v0alpha1::GetPropertiesResponse, - >(); - - let f = Box::new(move |state: &mut State| { - let output = output_name.output(state); - - let pixel_width = output - .as_ref() - .and_then(|output| output.current_mode().map(|mode| mode.size.w as u32)); - - let pixel_height = output - .as_ref() - .and_then(|output| output.current_mode().map(|mode| mode.size.h as u32)); - - let refresh_rate = output - .as_ref() - .and_then(|output| output.current_mode().map(|mode| mode.refresh as u32)); - - let model = output - .as_ref() - .map(|output| output.physical_properties().model); - - let physical_width = output - .as_ref() - .map(|output| output.physical_properties().size.w as u32); - - let physical_height = output - .as_ref() - .map(|output| output.physical_properties().size.h as u32); - - let make = output - .as_ref() - .map(|output| output.physical_properties().make); - - let x = output.as_ref().map(|output| output.current_location().x); - - let y = output.as_ref().map(|output| output.current_location().y); - - let focused = state - .focus_state - .focused_output - .as_ref() - .and_then(|foc_op| output.as_ref().map(|op| op == foc_op)); - - let tag_ids = output - .as_ref() - .map(|output| { - output.with_state(|state| { - state - .tags - .iter() - .map(|tag| match tag.id() { - TagId::None => unreachable!(), - TagId::Some(id) => id, - }) - .collect::>() - }) - }) - .unwrap_or_default(); - - let _ = sender.send( - pinnacle_api_defs::pinnacle::output::v0alpha1::GetPropertiesResponse { - make, - model, - x, - y, - pixel_width, - pixel_height, - refresh_rate, - physical_width, - physical_height, - focused, - tag_ids, - }, - ); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } -} - -pub struct WindowService { - pub sender: StateFnSender, -} - -#[tonic::async_trait] -impl pinnacle_api_defs::pinnacle::window::v0alpha1::window_service_server::WindowService - for WindowService -{ - async fn close(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { return }; - - match window { - WindowElement::Wayland(window) => window.toplevel().send_close(), - WindowElement::X11(surface) => surface.close().expect("failed to close x11 win"), - WindowElement::X11OverrideRedirect(_) => { - tracing::warn!("tried to close override redirect window"); - } - _ => unreachable!(), - } - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_geometry( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let geometry = request.geometry.unwrap_or_default(); - let x = geometry.x; - let y = geometry.y; - let width = geometry.width; - let height = geometry.height; - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { return }; - - // TODO: with no x or y, defaults unmapped windows to 0, 0 - let mut window_loc = state - .space - .element_location(&window) - .unwrap_or((x.unwrap_or_default(), y.unwrap_or_default()).into()); - window_loc.x = x.unwrap_or(window_loc.x); - window_loc.y = y.unwrap_or(window_loc.y); - - let mut window_size = window.geometry().size; - window_size.w = width.unwrap_or(window_size.w); - window_size.h = height.unwrap_or(window_size.h); - - let rect = Rectangle::from_loc_and_size(window_loc, window_size); - // window.change_geometry(rect); - window.with_state(|state| { - use crate::window::window_state::FloatingOrTiled; - state.floating_or_tiled = match state.floating_or_tiled { - FloatingOrTiled::Floating(_) => FloatingOrTiled::Floating(rect), - FloatingOrTiled::Tiled(_) => FloatingOrTiled::Tiled(Some(rect)), - } - }); - - for output in state.space.outputs_for_element(&window) { - state.update_windows(&output); - state.schedule_render(&output); - } - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_fullscreen( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let set_or_toggle = match request.set_or_toggle { - Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_fullscreen_request::SetOrToggle::Set(set)) => { - Some(set) - } - Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_fullscreen_request::SetOrToggle::Toggle(_)) => { - None - } - None => return Err(Status::invalid_argument("unspecified set or toggle")), - }; - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { - return; - }; - match set_or_toggle { - Some(set) => { - let is_fullscreen = - window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()); - if set != is_fullscreen { - window.toggle_fullscreen(); - } - } - None => window.toggle_fullscreen(), - } - - let Some(output) = window.output(state) else { - return; - }; - - state.update_windows(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_maximized( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let set_or_toggle = match request.set_or_toggle { - Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_maximized_request::SetOrToggle::Set(set)) => { - Some(set) - } - Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_maximized_request::SetOrToggle::Toggle(_)) => None, - None => return Err(Status::invalid_argument("unspecified set or toggle")), - }; - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { - return; - }; - match set_or_toggle { - Some(set) => { - let is_maximized = - window.with_state(|state| state.fullscreen_or_maximized.is_maximized()); - if set != is_maximized { - window.toggle_maximized(); - } - } - None => window.toggle_maximized(), - } - - let Some(output) = window.output(state) else { - return; - }; - - state.update_windows(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_floating( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let set_or_toggle = match request.set_or_toggle { - Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_floating_request::SetOrToggle::Set(set)) => { - Some(set) - } - Some(pinnacle_api_defs::pinnacle::window::v0alpha1::set_floating_request::SetOrToggle::Toggle(_)) => None, - None => return Err(Status::invalid_argument("unspecified set or toggle")), - }; - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { - return; - }; - match set_or_toggle { - Some(set) => { - let is_floating = - window.with_state(|state| state.floating_or_tiled.is_floating()); - if set != is_floating { - window.toggle_floating(); - } - } - None => window.toggle_floating(), - } - - let Some(output) = window.output(state) else { - return; - }; - - state.update_windows(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn move_to_tag( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let tag_id = TagId::Some( - request - .tag_id - .ok_or_else(|| Status::invalid_argument("no tag specified"))?, - ); - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { return }; - let Some(tag) = tag_id.tag(state) else { return }; - window.with_state(|state| { - state.tags = vec![tag.clone()]; - }); - let Some(output) = tag.output(state) else { return }; - state.update_windows(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn set_tag(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let tag_id = TagId::Some( - request - .tag_id - .ok_or_else(|| Status::invalid_argument("no tag specified"))?, - ); - - let set_or_toggle = match request.set_or_toggle { - Some( - pinnacle_api_defs::pinnacle::window::v0alpha1::set_tag_request::SetOrToggle::Set( - set, - ), - ) => Some(set), - Some( - pinnacle_api_defs::pinnacle::window::v0alpha1::set_tag_request::SetOrToggle::Toggle( - _, - ), - ) => None, - None => return Err(Status::invalid_argument("unspecified set or toggle")), - }; - - let f = Box::new(move |state: &mut State| { - let Some(window) = window_id.window(state) else { return }; - let Some(tag) = tag_id.tag(state) else { return }; - - // TODO: turn state.tags into a hashset - match set_or_toggle { - Some(set) => { - if set { - window.with_state(|state| { - state.tags.retain(|tg| tg != &tag); - state.tags.push(tag.clone()); - }) - } else { - window.with_state(|state| { - state.tags.retain(|tg| tg != &tag); - }) - } - } - None => window.with_state(|state| { - if !state.tags.contains(&tag) { - state.tags.push(tag.clone()); - } else { - state.tags.retain(|tg| tg != &tag); - } - }), - } - - let Some(output) = tag.output(state) else { return }; - state.update_windows(&output); - state.schedule_render(&output); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn move_grab(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let button = request - .button - .ok_or_else(|| Status::invalid_argument("no button specified"))?; - - let f = Box::new(move |state: &mut State| { - let Some((FocusTarget::Window(window), _)) = - state.focus_target_under(state.pointer_location) - else { - return; - }; - let Some(wl_surf) = window.wl_surface() else { return }; - let seat = state.seat.clone(); - - // We use the server one and not the client because windows like Steam don't provide - // GrabStartData, so we need to create it ourselves. - crate::grab::move_grab::move_request_server( - state, - &wl_surf, - &seat, - SERIAL_COUNTER.next_serial(), - button, - ); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn resize_grab( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let button = request - .button - .ok_or_else(|| Status::invalid_argument("no button specified"))?; - - let f = Box::new(move |state: &mut State| { - let pointer_loc = state.pointer_location; - let Some((FocusTarget::Window(window), window_loc)) = - state.focus_target_under(pointer_loc) - else { - return; - }; - let Some(wl_surf) = window.wl_surface() else { return }; - - let window_geometry = window.geometry(); - let window_x = window_loc.x as f64; - let window_y = window_loc.y as f64; - let window_width = window_geometry.size.w as f64; - let window_height = window_geometry.size.h as f64; - let half_width = window_x + window_width / 2.0; - let half_height = window_y + window_height / 2.0; - let full_width = window_x + window_width; - let full_height = window_y + window_height; - - let edges = match pointer_loc { - Point { x, y, .. } - if (window_x..=half_width).contains(&x) - && (window_y..=half_height).contains(&y) => - { - server::xdg_toplevel::ResizeEdge::TopLeft - } - Point { x, y, .. } - if (half_width..=full_width).contains(&x) - && (window_y..=half_height).contains(&y) => - { - server::xdg_toplevel::ResizeEdge::TopRight - } - Point { x, y, .. } - if (window_x..=half_width).contains(&x) - && (half_height..=full_height).contains(&y) => - { - server::xdg_toplevel::ResizeEdge::BottomLeft - } - Point { x, y, .. } - if (half_width..=full_width).contains(&x) - && (half_height..=full_height).contains(&y) => - { - server::xdg_toplevel::ResizeEdge::BottomRight - } - _ => server::xdg_toplevel::ResizeEdge::None, - }; - - crate::grab::resize_grab::resize_request_server( - state, - &wl_surf, - &state.seat.clone(), - SERIAL_COUNTER.next_serial(), - edges.into(), - button, - ); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } - - async fn get( - &self, - _request: Request, - ) -> Result, Status> { - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< - pinnacle_api_defs::pinnacle::window::v0alpha1::GetResponse, - >(); - - let f = Box::new(move |state: &mut State| { - let window_ids = state - .windows - .iter() - .map(|win| { - win.with_state(|state| match state.id { - WindowId::None => unreachable!(), - WindowId::Some(id) => id, - }) - }) - .collect::>(); - - let _ = sender - .send(pinnacle_api_defs::pinnacle::window::v0alpha1::GetResponse { window_ids }); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } - - async fn get_properties( - &self, - request: Request, - ) -> Result< - Response, - Status, - > { - let request = request.into_inner(); - - let window_id = WindowId::Some( - request - .window_id - .ok_or_else(|| Status::invalid_argument("no window specified"))?, - ); - - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::< - pinnacle_api_defs::pinnacle::window::v0alpha1::GetPropertiesResponse, - >(); - - let f = Box::new(move |state: &mut State| { - let window = window_id.window(state); - - let width = window.as_ref().map(|win| win.geometry().size.w); - - let height = window.as_ref().map(|win| win.geometry().size.h); - - let x = window - .as_ref() - .and_then(|win| state.space.element_location(win)) - .map(|loc| loc.x); - - let y = window - .as_ref() - .and_then(|win| state.space.element_location(win)) - .map(|loc| loc.y); - - let geometry = if width.is_none() && height.is_none() && x.is_none() && y.is_none() { - None - } else { - Some(Geometry { - x, - y, - width, - height, - }) - }; - - let (class, title) = window.as_ref().map_or((None, None), |win| match &win { - WindowElement::Wayland(_) => { - if let Some(wl_surf) = win.wl_surface() { - compositor::with_states(&wl_surf, |states| { - let lock = states - .data_map - .get::() - .expect("XdgToplevelSurfaceData wasn't in surface's data map") - .lock() - .expect("failed to acquire lock"); - (lock.app_id.clone(), lock.title.clone()) - }) - } else { - (None, None) - } - } - WindowElement::X11(surface) | WindowElement::X11OverrideRedirect(surface) => { - (Some(surface.class()), Some(surface.title())) - } - _ => unreachable!(), - }); - - let focused = window.as_ref().and_then(|win| { - let output = win.output(state)?; - state.focused_window(&output).map(|foc_win| win == &foc_win) - }); - - let floating = window - .as_ref() - .map(|win| win.with_state(|state| state.floating_or_tiled.is_floating())); - - let fullscreen_or_maximized = window - .as_ref() - .map(|win| win.with_state(|state| state.fullscreen_or_maximized)) - .map(|fs_or_max| match fs_or_max { - // TODO: from impl - crate::window::window_state::FullscreenOrMaximized::Neither => { - FullscreenOrMaximized::Neither - } - crate::window::window_state::FullscreenOrMaximized::Fullscreen => { - FullscreenOrMaximized::Fullscreen - } - crate::window::window_state::FullscreenOrMaximized::Maximized => { - FullscreenOrMaximized::Maximized - } - } as i32); - - let tag_ids = window - .as_ref() - .map(|win| { - win.with_state(|state| { - state - .tags - .iter() - .map(|tag| match tag.id() { - TagId::Some(id) => id, - TagId::None => unreachable!(), - }) - .collect::>() - }) - }) - .unwrap_or_default(); - - let _ = sender.send( - pinnacle_api_defs::pinnacle::window::v0alpha1::GetPropertiesResponse { - geometry, - class, - title, - focused, - floating, - fullscreen_or_maximized, - tag_ids, - }, - ); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - let response = receiver - .recv() - .await - .ok_or_else(|| Status::internal("internal state was not running"))?; - - Ok(Response::new(response)) - } - - async fn add_window_rule( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let cond = request - .cond - .ok_or_else(|| Status::invalid_argument("no condition specified"))? - .into(); - - let rule = request - .rule - .ok_or_else(|| Status::invalid_argument("no rule specified"))? - .into(); - - let f = Box::new(move |state: &mut State| { - state.config.window_rules.push((cond, rule)); - }); - - self.sender - .send(f) - .map_err(|_| Status::internal("internal state was not running"))?; - - Ok(Response::new(())) - } -} - -impl From for crate::window::rules::WindowRuleCondition { - fn from(cond: WindowRuleCondition) -> Self { - let cond_any = match cond.any.is_empty() { - true => None, - false => Some( - cond.any - .into_iter() - .map(crate::window::rules::WindowRuleCondition::from) - .collect::>(), - ), - }; - - let cond_all = match cond.all.is_empty() { - true => None, - false => Some( - cond.all - .into_iter() - .map(crate::window::rules::WindowRuleCondition::from) - .collect::>(), - ), - }; - - let class = match cond.classes.is_empty() { - true => None, - false => Some(cond.classes), - }; - - let title = match cond.titles.is_empty() { - true => None, - false => Some(cond.titles), - }; - - let tag = match cond.tags.is_empty() { - true => None, - false => Some(cond.tags.into_iter().map(TagId::Some).collect::>()), - }; - - crate::window::rules::WindowRuleCondition { - cond_any, - cond_all, - class, - title, - tag, - } - } -} - -impl From for crate::window::rules::WindowRule { - fn from(rule: WindowRule) -> Self { - let fullscreen_or_maximized = match rule.fullscreen_or_maximized() { - FullscreenOrMaximized::Unspecified => None, - FullscreenOrMaximized::Neither => { - Some(crate::window::window_state::FullscreenOrMaximized::Neither) - } - FullscreenOrMaximized::Fullscreen => { - Some(crate::window::window_state::FullscreenOrMaximized::Fullscreen) - } - FullscreenOrMaximized::Maximized => { - Some(crate::window::window_state::FullscreenOrMaximized::Maximized) - } - }; - let output = rule.output.map(OutputName); - let tags = match rule.tags.is_empty() { - true => None, - false => Some(rule.tags.into_iter().map(TagId::Some).collect::>()), - }; - let floating_or_tiled = rule.floating.map(|floating| match floating { - true => crate::window::rules::FloatingOrTiled::Floating, - false => crate::window::rules::FloatingOrTiled::Tiled, - }); - let size = rule.width.and_then(|w| { - rule.height.and_then(|h| { - Some(( - NonZeroU32::try_from(w as u32).ok()?, - NonZeroU32::try_from(h as u32).ok()?, - )) - }) - }); - let location = rule.x.and_then(|x| rule.y.map(|y| (x, y))); - - crate::window::rules::WindowRule { - output, - tags, - floating_or_tiled, - fullscreen_or_maximized, - size, - location, - } - } -} diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 32a9b76..c94a8fb 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -72,7 +72,6 @@ use smithay_drm_extras::{ }; use crate::{ - api::msg::{Args, OutgoingMsg}, backend::Backend, config::ConnectorSavedState, output::OutputName, @@ -985,36 +984,11 @@ impl State { output.with_state(|state| state.tags = tags.clone()); } else { // Run any output callbacks - let clone = output.clone(); - self.schedule( - |dt| dt.state.api_state.stream.is_some(), - move |dt| { - let stream = dt - .state - .api_state - .stream - .as_ref() - .expect("stream doesn't exist"); - let mut stream = stream.lock().expect("couldn't lock stream"); - for callback_id in dt.state.config.output_callback_ids.iter() { - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::CallCallback { - callback_id: *callback_id, - args: Some(Args::ConnectForAllOutputs { - output_name: clone.name(), - }), - }, - ) - .expect("Send to client failed"); - } - for grpc_sender in dt.state.config.grpc_output_callback_senders.iter() { - let _ = grpc_sender.send(Ok(ConnectForAllResponse { - output_name: Some(clone.name()), - })); - } - }, - ); + for sender in self.config.output_callback_senders.iter() { + let _ = sender.send(Ok(ConnectForAllResponse { + output_name: Some(output.name()), + })); + } } } diff --git a/src/config.rs b/src/config.rs index 1f87588..50f246c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,8 @@ use crate::{ api::{ - msg::ModifierMask, - protocol::{ - InputService, OutputService, PinnacleService, ProcessService, TagService, WindowService, - }, - PinnacleSocketSource, + InputService, OutputService, PinnacleService, ProcessService, TagService, WindowService, }, + input::ModifierMask, output::OutputName, tag::Tag, window::rules::{WindowRule, WindowRuleCondition}, @@ -14,7 +11,6 @@ use std::{ collections::HashMap, path::{Path, PathBuf}, process::Stdio, - sync::{Arc, Mutex}, }; use anyhow::Context; @@ -32,9 +28,9 @@ use smithay::{ utils::{Logical, Point}, }; use sysinfo::ProcessRefreshKind; +use tokio::sync::mpsc::UnboundedSender; use toml::Table; -use crate::api::msg::{CallbackId, Modifier}; use xkbcommon::xkb::Keysym; use crate::{ @@ -57,8 +53,34 @@ pub struct Metaconfig { #[derive(serde::Deserialize, Debug)] pub struct Keybind { - pub modifiers: Vec, - pub key: Key, + modifiers: Vec, + key: Key, +} + +#[derive(serde::Deserialize, Debug, Clone, Copy)] +enum Modifier { + Shift, + Ctrl, + Alt, + Super, +} + +// TODO: refactor metaconfig input +impl From> for ModifierMask { + fn from(mods: Vec) -> Self { + let mut mask = ModifierMask::empty(); + + for m in mods { + match m { + Modifier::Shift => mask |= ModifierMask::SHIFT, + Modifier::Ctrl => mask |= ModifierMask::CTRL, + Modifier::Alt => mask |= ModifierMask::ALT, + Modifier::Super => mask |= ModifierMask::SUPER, + } + } + + mask + } } // TODO: accept xkbcommon names instead @@ -141,10 +163,7 @@ pub enum Key { pub struct Config { /// Window rules and conditions on when those rules should apply pub window_rules: Vec<(WindowRuleCondition, WindowRule)>, - /// All callbacks that should be run when outputs are connected - pub output_callback_ids: Vec, - pub grpc_output_callback_senders: - Vec>>, + pub output_callback_senders: Vec>>, /// Saved states when outputs are disconnected pub connector_saved_states: HashMap, } @@ -214,13 +233,6 @@ impl State { config_join_handle.abort(); } - if let Some(token) = self.api_state.socket_token { - // Should only happen if parsing the metaconfig failed - self.loop_handle.remove(token); - } - - let tx_channel = self.api_state.tx_channel.clone(); - // Love that trailing slash let data_home = PathBuf::from( crate::XDG_BASE_DIRS @@ -255,19 +267,6 @@ impl State { self.start_grpc_server(socket_dir.as_path())?; - self.system_processes - .refresh_processes_specifics(ProcessRefreshKind::new()); - - let multiple_instances = self - .system_processes - .processes_by_exact_name("pinnacle") - .filter(|proc| proc.thread_kind().is_none()) - .count() - > 1; - - let socket_source = PinnacleSocketSource::new(tx_channel, &socket_dir, multiple_instances) - .context("Failed to create socket source")?; - let reload_keybind = metaconfig.reload_keybind; let kill_keybind = metaconfig.kill_keybind; @@ -324,28 +323,9 @@ impl State { let reload_keybind = (reload_mask, Keysym::from(reload_keybind.key as u32)); let kill_keybind = (kill_mask, Keysym::from(kill_keybind.key as u32)); - let socket_token = self - .loop_handle - .insert_source(socket_source, |stream, _, data| { - if let Some(old_stream) = data - .state - .api_state - .stream - .replace(Arc::new(Mutex::new(stream))) - { - old_stream - .lock() - .expect("Couldn't lock old stream") - .shutdown(std::net::Shutdown::Both) - .expect("Couldn't shutdown old stream"); - } - })?; - self.input_state.reload_keybind = Some(reload_keybind); self.input_state.kill_keybind = Some(kill_keybind); - self.api_state.socket_token = Some(socket_token); - self.config_join_handle = Some(tokio::spawn(async move { let _ = child.wait().await; })); diff --git a/src/input.rs b/src/input.rs index 7c3b2ab..b45e13b 100644 --- a/src/input.rs +++ b/src/input.rs @@ -4,12 +4,7 @@ pub mod libinput; use std::{collections::HashMap, mem::Discriminant}; -use crate::{ - api::msg::{CallbackId, Modifier, MouseEdge, OutgoingMsg}, - focus::FocusTarget, - state::WithState, - window::WindowElement, -}; +use crate::{focus::FocusTarget, state::WithState, window::WindowElement}; use pinnacle_api_defs::pinnacle::input::v0alpha1::{ set_libinput_setting_request::Setting, set_mousebind_request, SetKeybindResponse, SetMousebindResponse, @@ -33,8 +28,6 @@ use xkbcommon::xkb::Keysym; use crate::state::State; -use self::libinput::LibinputSetting; - bitflags::bitflags! { #[derive(Debug, Hash, Copy, Clone, PartialEq, Eq)] pub struct ModifierMask: u8 { @@ -85,39 +78,30 @@ impl From<&ModifiersState> for ModifierMask { #[derive(Default)] pub struct InputState { - /// A hashmap of modifier keys and keycodes to callback IDs - pub keybinds: HashMap<(crate::api::msg::ModifierMask, Keysym), CallbackId>, - /// A hashmap of modifier keys and mouse button codes to callback IDs - pub mousebinds: HashMap<(crate::api::msg::ModifierMask, u32, MouseEdge), CallbackId>, - pub reload_keybind: Option<(crate::api::msg::ModifierMask, Keysym)>, - pub kill_keybind: Option<(crate::api::msg::ModifierMask, Keysym)>, - /// User defined libinput settings that will be applied - pub libinput_settings: Vec, + pub reload_keybind: Option<(ModifierMask, Keysym)>, + pub kill_keybind: Option<(ModifierMask, Keysym)>, /// All libinput devices that have been connected pub libinput_devices: Vec, - pub grpc_keybinds: + pub keybinds: HashMap<(ModifierMask, Keysym), UnboundedSender>>, - pub grpc_mousebinds: HashMap< + pub mousebinds: HashMap< (ModifierMask, u32, set_mousebind_request::MouseEdge), UnboundedSender>, >, #[allow(clippy::type_complexity)] - pub grpc_libinput_settings: - HashMap, Box>, + pub libinput_settings: HashMap, Box>, } impl std::fmt::Debug for InputState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("InputState") - .field("keybinds", &self.keybinds) - .field("mousebinds", &self.mousebinds) .field("reload_keybind", &self.reload_keybind) .field("kill_keybind", &self.kill_keybind) - .field("libinput_settings", &self.libinput_settings) .field("libinput_devices", &self.libinput_devices) - .field("grpc_keybinds", &self.grpc_keybinds) - .field("grpc_libinput_settings", &"...") + .field("keybinds", &self.keybinds) + .field("mousebinds", &self.mousebinds) + .field("libinput_settings", &"...") .finish() } } @@ -130,9 +114,7 @@ impl InputState { #[derive(Debug)] enum KeyAction { - /// Call a callback from a config process - CallCallback(CallbackId), - CallGrpcCallback(UnboundedSender>), + CallCallback(UnboundedSender>), Quit, SwitchVt(i32), ReloadConfig, @@ -257,59 +239,23 @@ impl State { |state, modifiers, keysym| { // tracing::debug!(keysym = ?keysym, raw_keysyms = ?keysym.raw_syms(), modified_syms = ?keysym.modified_syms()); if press_state == KeyState::Pressed { - let mut modifier_mask = Vec::::new(); - if modifiers.alt { - modifier_mask.push(Modifier::Alt); - } - if modifiers.shift { - modifier_mask.push(Modifier::Shift); - } - if modifiers.ctrl { - modifier_mask.push(Modifier::Ctrl); - } - if modifiers.logo { - modifier_mask.push(Modifier::Super); - } - let modifier_mask = crate::api::msg::ModifierMask::from(modifier_mask); - - let grpc_modifiers = ModifierMask::from(modifiers); + let mod_mask = ModifierMask::from(modifiers); let raw_sym = keysym.raw_syms().iter().next(); let mod_sym = keysym.modified_sym(); if let (Some(sender), _) | (None, Some(sender)) = ( - state - .input_state - .grpc_keybinds - .get(&(grpc_modifiers, mod_sym)), + state.input_state.keybinds.get(&(mod_mask, mod_sym)), raw_sym.and_then(|raw_sym| { - state - .input_state - .grpc_keybinds - .get(&(grpc_modifiers, *raw_sym)) + state.input_state.keybinds.get(&(mod_mask, *raw_sym)) }), ) { - return FilterResult::Intercept(KeyAction::CallGrpcCallback( - sender.clone(), - )); + return FilterResult::Intercept(KeyAction::CallCallback(sender.clone())); } - let cb_id_mod = state.input_state.keybinds.get(&(modifier_mask, mod_sym)); - - let cb_id_raw = raw_sym.and_then(|raw_sym| { - state.input_state.keybinds.get(&(modifier_mask, *raw_sym)) - }); - - match (cb_id_mod, cb_id_raw) { - (Some(cb_id), _) | (None, Some(cb_id)) => { - return FilterResult::Intercept(KeyAction::CallCallback(*cb_id)); - } - (None, None) => (), - } - - if kill_keybind == Some((modifier_mask, mod_sym)) { + if kill_keybind == Some((mod_mask, mod_sym)) { return FilterResult::Intercept(KeyAction::Quit); - } else if reload_keybind == Some((modifier_mask, mod_sym)) { + } else if reload_keybind == Some((mod_mask, mod_sym)) { return FilterResult::Intercept(KeyAction::ReloadConfig); } else if let mut vt @ keysyms::KEY_XF86Switch_VT_1 ..=keysyms::KEY_XF86Switch_VT_12 = keysym.modified_sym().raw() @@ -325,20 +271,7 @@ impl State { ); match action { - Some(KeyAction::CallCallback(callback_id)) => { - if let Some(stream) = self.api_state.stream.as_ref() { - if let Err(err) = crate::api::send_to_client( - &mut stream.lock().expect("Could not lock stream mutex"), - &OutgoingMsg::CallCallback { - callback_id, - args: None, - }, - ) { - tracing::error!("error sending msg to client: {err}"); - } - } - } - Some(KeyAction::CallGrpcCallback(sender)) => { + Some(KeyAction::CallCallback(sender)) => { let _ = sender.send(Ok(SetKeybindResponse {})); } Some(KeyAction::SwitchVt(vt)) => { @@ -367,42 +300,17 @@ impl State { let pointer_loc = pointer.current_location(); + let mod_mask = ModifierMask::from(keyboard.modifier_state()); + let mouse_edge = match button_state { - ButtonState::Released => MouseEdge::Release, - ButtonState::Pressed => MouseEdge::Press, - }; - let modifier_mask = crate::api::msg::ModifierMask::from(keyboard.modifier_state()); - - let grpc_modifier_mask = ModifierMask::from(keyboard.modifier_state()); - - // If any mousebinds are detected, call the config's callback and return. - if let Some(&callback_id) = - self.input_state - .mousebinds - .get(&(modifier_mask, button, mouse_edge)) - { - if let Some(stream) = self.api_state.stream.as_ref() { - crate::api::send_to_client( - &mut stream.lock().expect("failed to lock api stream"), - &OutgoingMsg::CallCallback { - callback_id, - args: None, - }, - ) - .expect("failed to call callback"); - } - return; - } - - let grpc_mouse_edge = match button_state { ButtonState::Released => set_mousebind_request::MouseEdge::Release, ButtonState::Pressed => set_mousebind_request::MouseEdge::Press, }; - if let Some(stream) = - self.input_state - .grpc_mousebinds - .get(&(grpc_modifier_mask, button, grpc_mouse_edge)) + if let Some(stream) = self + .input_state + .mousebinds + .get(&(mod_mask, button, mouse_edge)) { let _ = stream.send(Ok(SetMousebindResponse {})); } diff --git a/src/input/libinput.rs b/src/input/libinput.rs index 325cb54..f46b4fd 100644 --- a/src/input/libinput.rs +++ b/src/input/libinput.rs @@ -1,104 +1,7 @@ -use smithay::{ - backend::{input::InputEvent, libinput::LibinputInputBackend}, - reexports::input::{self, AccelProfile, ClickMethod, ScrollMethod, TapButtonMap}, -}; +use smithay::backend::{input::InputEvent, libinput::LibinputInputBackend}; use crate::state::State; -#[derive(Debug, serde::Deserialize)] -#[serde(remote = "AccelProfile")] -enum AccelProfileDef { - Flat, - Adaptive, -} - -#[derive(Debug, serde::Deserialize)] -#[serde(remote = "ClickMethod")] -enum ClickMethodDef { - ButtonAreas, - Clickfinger, -} - -#[derive(Debug, serde::Deserialize)] -#[serde(remote = "ScrollMethod")] -enum ScrollMethodDef { - NoScroll, - TwoFinger, - Edge, - OnButtonDown, -} - -#[derive(Debug, serde::Deserialize)] -#[serde(remote = "TapButtonMap")] -enum TapButtonMapDef { - LeftRightMiddle, - LeftMiddleRight, -} - -#[derive(Debug, PartialEq, Copy, Clone, serde::Deserialize)] -pub enum LibinputSetting { - #[serde(with = "AccelProfileDef")] - AccelProfile(AccelProfile), - AccelSpeed(f64), - CalibrationMatrix([f32; 6]), - #[serde(with = "ClickMethodDef")] - ClickMethod(ClickMethod), - DisableWhileTypingEnabled(bool), - LeftHanded(bool), - MiddleEmulationEnabled(bool), - RotationAngle(u32), - #[serde(with = "ScrollMethodDef")] - ScrollMethod(ScrollMethod), - NaturalScrollEnabled(bool), - ScrollButton(u32), - #[serde(with = "TapButtonMapDef")] - TapButtonMap(TapButtonMap), - TapDragEnabled(bool), - TapDragLockEnabled(bool), - TapEnabled(bool), -} - -impl LibinputSetting { - pub fn apply_to_device(&self, device: &mut input::Device) { - let _ = match self { - LibinputSetting::AccelProfile(profile) => device.config_accel_set_profile(*profile), - LibinputSetting::AccelSpeed(speed) => device.config_accel_set_speed(*speed), - LibinputSetting::CalibrationMatrix(matrix) => { - device.config_calibration_set_matrix(*matrix) - } - LibinputSetting::ClickMethod(method) => device.config_click_set_method(*method), - LibinputSetting::DisableWhileTypingEnabled(enabled) => { - device.config_dwt_set_enabled(*enabled) - } - LibinputSetting::LeftHanded(enabled) => device.config_left_handed_set(*enabled), - LibinputSetting::MiddleEmulationEnabled(enabled) => { - device.config_middle_emulation_set_enabled(*enabled) - } - LibinputSetting::RotationAngle(angle) => device.config_rotation_set_angle(*angle), - LibinputSetting::ScrollMethod(method) => device.config_scroll_set_method(*method), - LibinputSetting::NaturalScrollEnabled(enabled) => { - device.config_scroll_set_natural_scroll_enabled(*enabled) - } - LibinputSetting::ScrollButton(button) => device.config_scroll_set_button(*button), - LibinputSetting::TapButtonMap(map) => device.config_tap_set_button_map(*map), - LibinputSetting::TapDragEnabled(enabled) => { - device.config_tap_set_drag_enabled(*enabled) - } - LibinputSetting::TapDragLockEnabled(enabled) => { - device.config_tap_set_drag_lock_enabled(*enabled) - } - LibinputSetting::TapEnabled(enabled) => device.config_tap_set_enabled(*enabled), - }; - } -} - -// We want to completely replace old settings, so we hash only the discriminant. -impl std::hash::Hash for LibinputSetting { - fn hash(&self, state: &mut H) { - core::mem::discriminant(self).hash(state); - } -} - impl State { /// Apply current libinput settings to new devices. pub fn apply_libinput_settings(&mut self, event: &InputEvent) { @@ -117,10 +20,7 @@ impl State { return; } - for setting in self.input_state.libinput_settings.iter() { - setting.apply_to_device(&mut device); - } - for setting in self.input_state.grpc_libinput_settings.values() { + for setting in self.input_state.libinput_settings.values() { setting(&mut device); } diff --git a/src/main.rs b/src/main.rs index 8edef8b..c690d90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ //! While Pinnacle is not a library, this documentation serves to guide those who want to //! contribute or learn how building something like this works. -// #![deny(unused_imports)] // gonna force myself to keep stuff clean +// #![deny(unused_imports)] // this has remained commented out for months lol #![warn(clippy::unwrap_used)] use clap::Parser; diff --git a/src/state.rs b/src/state.rs index 224e28e..b61c23b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,22 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later use crate::{ - api::{msg::Msg, ApiState}, - backend::Backend, - config::Config, - cursor::Cursor, - focus::FocusState, - grab::resize_grab::ResizeSurfaceState, - window::WindowElement, + backend::Backend, config::Config, cursor::Cursor, focus::FocusState, + grab::resize_grab::ResizeSurfaceState, window::WindowElement, }; use smithay::{ desktop::{PopupManager, Space}, input::{keyboard::XkbConfig, pointer::CursorImageStatus, Seat, SeatState}, reexports::{ - calloop::{ - self, channel::Event, generic::Generic, Interest, LoopHandle, LoopSignal, Mode, - PostAction, - }, + calloop::{generic::Generic, Interest, LoopHandle, LoopSignal, Mode, PostAction}, wayland_server::{ backend::{ClientData, ClientId, DisconnectReason}, protocol::wl_surface::WlSurface, @@ -74,8 +66,6 @@ pub struct State { /// The state of key and mousebinds along with libinput settings pub input_state: InputState, - /// The state holding stuff dealing with the api, like the stream - pub api_state: ApiState, /// Keeps track of the focus stack and focused output pub focus_state: FocusState, @@ -159,8 +149,6 @@ impl State { }, )?; - let (tx_channel, rx_channel) = calloop::channel::channel::(); - loop_handle.insert_idle(|data| { if let Err(err) = data.state.start_config(crate::config::get_config_dir()) { panic!("failed to start config: {err}"); @@ -174,16 +162,6 @@ impl State { seat.add_keyboard(XkbConfig::default(), 500, 25)?; - loop_handle.insert_idle(|data| { - data.state - .loop_handle - .insert_source(rx_channel, |msg, _, data| match msg { - Event::Msg(msg) => data.state.handle_msg(msg), - Event::Closed => todo!(), - }) - .expect("failed to insert rx_channel into loop"); - }); - let xwayland = { let (xwayland, channel) = XWayland::new(&display_handle); let clone = display_handle.clone(); @@ -253,11 +231,6 @@ impl State { layer_shell_state: WlrLayerShellState::new::(&display_handle), input_state: InputState::new(), - api_state: ApiState { - stream: None, - socket_token: None, - tx_channel, - }, focus_state: FocusState::new(), config: Config::default(), From 1cdeb59a38ece86d54432b9164ec39e483c54c04 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sun, 21 Jan 2024 23:45:09 -0600 Subject: [PATCH 20/26] Nuke the old Rust API form orbit --- api/{rust_grpc => rust}/.gitignore | 0 api/rust/Cargo.toml | 29 +- .../examples/default_config/main.rs | 0 .../examples/default_config/metaconfig.toml | 0 api/rust/examples/example_config.rs | 210 ------ api/rust/metaconfig.toml | 52 -- .../pinnacle-api-macros/Cargo.toml | 0 .../pinnacle-api-macros/src/lib.rs | 0 api/rust/src/input.rs | 506 +++++++++---- api/rust/src/input/libinput.rs | 118 ++- api/rust/src/lib.rs | 384 +++++----- api/rust/src/msg.rs | 290 ------- api/rust/src/output.rs | 714 ++++++++++++------ api/{rust_grpc => rust}/src/pinnacle.rs | 0 api/rust/src/process.rs | 284 ++++--- api/rust/src/tag.rs | 658 +++++++++++----- api/{rust_grpc => rust}/src/util.rs | 0 api/rust/src/window.rs | 680 ++++++++++++----- api/rust/src/window/rules.rs | 556 +++++++++++--- api/rust_grpc/Cargo.toml | 25 - api/rust_grpc/src/input.rs | 383 ---------- api/rust_grpc/src/input/libinput.rs | 85 --- api/rust_grpc/src/lib.rs | 202 ----- api/rust_grpc/src/output.rs | 515 ------------- api/rust_grpc/src/process.rs | 178 ----- api/rust_grpc/src/tag.rs | 528 ------------- api/rust_grpc/src/window.rs | 530 ------------- api/rust_grpc/src/window/rules.rs | 521 ------------- 28 files changed, 2698 insertions(+), 4750 deletions(-) rename api/{rust_grpc => rust}/.gitignore (100%) rename api/{rust_grpc => rust}/examples/default_config/main.rs (100%) rename api/{rust_grpc => rust}/examples/default_config/metaconfig.toml (100%) delete mode 100644 api/rust/examples/example_config.rs delete mode 100644 api/rust/metaconfig.toml rename api/{rust_grpc => rust}/pinnacle-api-macros/Cargo.toml (100%) rename api/{rust_grpc => rust}/pinnacle-api-macros/src/lib.rs (100%) delete mode 100644 api/rust/src/msg.rs rename api/{rust_grpc => rust}/src/pinnacle.rs (100%) rename api/{rust_grpc => rust}/src/util.rs (100%) delete mode 100644 api/rust_grpc/Cargo.toml delete mode 100644 api/rust_grpc/src/input.rs delete mode 100644 api/rust_grpc/src/input/libinput.rs delete mode 100644 api/rust_grpc/src/lib.rs delete mode 100644 api/rust_grpc/src/output.rs delete mode 100644 api/rust_grpc/src/process.rs delete mode 100644 api/rust_grpc/src/tag.rs delete mode 100644 api/rust_grpc/src/window.rs delete mode 100644 api/rust_grpc/src/window/rules.rs diff --git a/api/rust_grpc/.gitignore b/api/rust/.gitignore similarity index 100% rename from api/rust_grpc/.gitignore rename to api/rust/.gitignore diff --git a/api/rust/Cargo.toml b/api/rust/Cargo.toml index bf259ba..0d0caf2 100644 --- a/api/rust/Cargo.toml +++ b/api/rust/Cargo.toml @@ -1,14 +1,25 @@ [package] -name = "pinnacle_api" -version = "0.0.1" +name = "pinnacle-api" +version = "0.0.2" edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +authors = ["Ottatop "] +description = "The Rust implementation of the Pinnacle compositor's configuration API" +license = "MPL-2.0" +repository = "https://github.com/pinnacle-comp/pinnacle" +keywords = ["compositor", "pinnacle", "api", "config"] +categories = ["api-bindings", "config"] [dependencies] -serde = { version = "1.0.188", features = ["derive"] } -rmp = { version = "0.8.12" } -rmp-serde = { version = "1.1.2" } -anyhow = { version = "1.0.75", features = ["backtrace"] } -lazy_static = "1.4.0" +pinnacle-api-defs = { path = "../../pinnacle-api-defs" } +pinnacle-api-macros = { path = "./pinnacle-api-macros" } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] } +async-net = "2.0.0" +async-compat = "0.2.3" +tonic = "0.10.2" +tower = { version = "0.4.13", features = ["util"] } +futures = "0.3.30" +num_enum = "0.7.2" xkbcommon = "0.7.0" + +[workspace] +members = ["pinnacle-api-macros"] diff --git a/api/rust_grpc/examples/default_config/main.rs b/api/rust/examples/default_config/main.rs similarity index 100% rename from api/rust_grpc/examples/default_config/main.rs rename to api/rust/examples/default_config/main.rs diff --git a/api/rust_grpc/examples/default_config/metaconfig.toml b/api/rust/examples/default_config/metaconfig.toml similarity index 100% rename from api/rust_grpc/examples/default_config/metaconfig.toml rename to api/rust/examples/default_config/metaconfig.toml diff --git a/api/rust/examples/example_config.rs b/api/rust/examples/example_config.rs deleted file mode 100644 index ea5b8cb..0000000 --- a/api/rust/examples/example_config.rs +++ /dev/null @@ -1,210 +0,0 @@ -// You should glob import these to prevent your config from getting cluttered. -use pinnacle_api::prelude::*; -use pinnacle_api::*; - -fn main() { - // Connect to the Pinnacle server. - // This needs to be called before you start calling any config functions. - pinnacle_api::connect().unwrap(); - - let mod_key = Modifier::Ctrl; // This is set to Ctrl to not conflict with your WM/DE keybinds. - - let terminal = "alacritty"; - - process::set_env("MOZ_ENABLE_WAYLAND", "1"); - - // You must create a callback_vec to hold your callbacks. - // Rust is not Lua, so it takes a bit more work to get captures working. - // - // Anything that requires a callback will also require a mut reference to this struct. - // - // Additionally, all callbacks also take in `&mut CallbackVec`. - // This allows you to call functions that need callbacks within other callbacks. - let mut callback_vec = CallbackVec::new(); - - // Keybinds ------------------------------------------------------ - - input::mousebind( - &[mod_key], - MouseButton::Left, - MouseEdge::Press, - move |_| { - window::begin_move(MouseButton::Left); - }, - &mut callback_vec, - ); - - input::mousebind( - &[mod_key], - MouseButton::Right, - MouseEdge::Press, - move |_| { - window::begin_resize(MouseButton::Right); - }, - &mut callback_vec, - ); - - input::keybind( - &[mod_key, Modifier::Alt], - 'q', - |_| pinnacle_api::quit(), - &mut callback_vec, - ); - - input::keybind( - &[mod_key, Modifier::Alt], - 'c', - move |_| { - if let Some(window) = window::get_focused() { - window.close(); - } - }, - &mut callback_vec, - ); - - input::keybind( - &[mod_key], - xkbcommon::xkb::keysyms::KEY_Return, - move |_| { - process::spawn(vec![terminal]).unwrap(); - }, - &mut callback_vec, - ); - - input::keybind( - &[mod_key, Modifier::Alt], - xkbcommon::xkb::keysyms::KEY_space, - move |_| { - if let Some(window) = window::get_focused() { - window.toggle_floating(); - } - }, - &mut callback_vec, - ); - - input::keybind( - &[mod_key], - 'f', - move |_| { - if let Some(window) = window::get_focused() { - window.toggle_fullscreen(); - } - }, - &mut callback_vec, - ); - - input::keybind( - &[mod_key], - 'm', - move |_| { - if let Some(window) = window::get_focused() { - window.toggle_maximized(); - } - }, - &mut callback_vec, - ); - - // Output stuff ------------------------------------------------------- - - let tags = ["1", "2", "3", "4", "5"]; - - output::connect_for_all( - move |output, _| { - tag::add(&output, tags.as_slice()); - tag::get("1", Some(&output)).unwrap().toggle(); - }, - &mut callback_vec, - ); - - // Layouts ----------------------------------------------------------- - - // Create a `LayoutCycler` to cycle your layouts. - let mut layout_cycler = tag::layout_cycler(&[ - Layout::MasterStack, - Layout::Dwindle, - Layout::Spiral, - Layout::CornerTopLeft, - Layout::CornerTopRight, - Layout::CornerBottomLeft, - Layout::CornerBottomRight, - ]); - - // Cycle forward. - input::keybind( - &[mod_key], - xkbcommon::xkb::keysyms::KEY_space, - move |_| { - (layout_cycler.next)(None); - }, - &mut callback_vec, - ); - - // Cycle backward. - input::keybind( - &[mod_key, Modifier::Shift], - xkbcommon::xkb::keysyms::KEY_space, - move |_| { - (layout_cycler.prev)(None); - }, - &mut callback_vec, - ); - - // Keybinds for tags ------------------------------------------ - - for tag_name in tags.iter().map(|t| t.to_string()) { - // mod_key + 1-5 to switch to tag - let t = tag_name.clone(); - let num = tag_name.chars().next().unwrap(); - input::keybind( - &[mod_key], - num, - move |_| { - tag::get(&t, None).unwrap().switch_to(); - }, - &mut callback_vec, - ); - - // mod_key + Shift + 1-5 to toggle tag - let t = tag_name.clone(); - input::keybind( - &[mod_key, Modifier::Shift], - num, - move |_| { - tag::get(&t, None).unwrap().toggle(); - }, - &mut callback_vec, - ); - - // mod_key + Alt + 1-5 to move focused window to tag - let t = tag_name.clone(); - input::keybind( - &[mod_key, Modifier::Alt], - num, - move |_| { - if let Some(window) = window::get_focused() { - window.move_to_tag(&tag::get(&t, None).unwrap()); - } - }, - &mut callback_vec, - ); - - // mod_key + Shift + Alt + 1-5 to toggle tag on focused window - let t = tag_name.clone(); - input::keybind( - &[mod_key, Modifier::Shift, Modifier::Alt], - num, - move |_| { - if let Some(window) = window::get_focused() { - window.toggle_tag(&tag::get(&t, None).unwrap()); - } - }, - &mut callback_vec, - ); - } - - // At the very end of your config, you will need to start listening to Pinnacle in order for - // your callbacks to be correctly called. - // - // This will not return unless an error occurs. - pinnacle_api::listen(callback_vec); -} diff --git a/api/rust/metaconfig.toml b/api/rust/metaconfig.toml deleted file mode 100644 index aa70e7d..0000000 --- a/api/rust/metaconfig.toml +++ /dev/null @@ -1,52 +0,0 @@ -# This metaconfig.toml file dictates what config Pinnacle will run. -# -# When running Pinnacle, the compositor will look in the following directories for a metaconfig.toml file, -# in order from top to bottom: -# $PINNACLE_CONFIG_DIR -# $XDG_CONFIG_HOME/pinnacle/ -# ~/.config/pinnacle/ -# -# When Pinnacle finds a metaconfig.toml file, it will execute the command provided to `command`. -# For now, the only thing that should be here is `lua` with a path to the main config file. -# In the future, there will be a Rust API that can be run using `cargo run`. -# -# Because configuration is done using an external process, if it ever crashes, you lose all of your keybinds. -# In order prevent you from getting stuck in the compositor, you must define keybinds to reload your config -# and kill Pinnacle. -# -# More details on each setting can be found below. - -# The command Pinnacle will run on startup and when you reload your config. -# Paths are relative to the directory the metaconfig.toml file is in. -# This must be an array. -command = ["cargo", "run", "--example", "example_config"] - -### Keybinds ### -# Each keybind takes in a table with two fields: `modifiers` and `key`. -# - `modifiers` can be one of "Ctrl", "Alt", "Shift", or "Super". -# - `key` can be a string of any lowercase letter, number, -# "numN" where N is a number for numpad keys, or "esc"/"escape". -# Support for any xkbcommon key is planned for a future update. - -# The keybind that will reload your config. -reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } -# The keybind that will kill Pinnacle. -kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } - -### Socket directory ### -# Pinnacle will open a Unix socket at `$XDG_RUNTIME_DIR` by default, falling back to `/tmp` if it doesn't exist. -# If you want/need to change this, use the `socket_dir` setting set to the directory of your choosing. -# -# socket_dir = "/your/dir/here/" - -### Environment Variables ### -# You may need to specify to Lua where Pinnacle's Lua API library is. -# This is currently done using the `envs` table, with keys as the name of the environment variable and -# the value as the variable value. This supports $var expansion, and paths are relative to this metaconfig.toml file. -# -# Pinnacle will run your config with the additional PINNACLE_DIR environment variable. -# -# Here, LUA_PATH and LUA_CPATH are used to tell Lua the path to the library. -[envs] -# LUA_PATH = "$PINNACLE_LIB_DIR/lua/?.lua;$PINNACLE_LIB_DIR/lua/?/init.lua;$PINNACLE_LIB_DIR/lua/lib/?.lua;$PINNACLE_LIB_DIR/lua/lib/?/init.lua;$LUA_PATH" -# LUA_CPATH = "$PINNACLE_LIB_DIR/lua/lib/?.so;$LUA_CPATH" diff --git a/api/rust_grpc/pinnacle-api-macros/Cargo.toml b/api/rust/pinnacle-api-macros/Cargo.toml similarity index 100% rename from api/rust_grpc/pinnacle-api-macros/Cargo.toml rename to api/rust/pinnacle-api-macros/Cargo.toml diff --git a/api/rust_grpc/pinnacle-api-macros/src/lib.rs b/api/rust/pinnacle-api-macros/src/lib.rs similarity index 100% rename from api/rust_grpc/pinnacle-api-macros/src/lib.rs rename to api/rust/pinnacle-api-macros/src/lib.rs diff --git a/api/rust/src/input.rs b/api/rust/src/input.rs index e80fba1..5127c0a 100644 --- a/api/rust/src/input.rs +++ b/api/rust/src/input.rs @@ -1,165 +1,383 @@ //! Input management. +//! +//! This module provides [`Input`], a struct that gives you several different +//! methods for setting key- and mousebinds, changing xkeyboard settings, and more. +//! View the struct's documentation for more information. + +use futures::{ + channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, +}; +use num_enum::TryFromPrimitive; +use pinnacle_api_defs::pinnacle::input::{ + self, + v0alpha1::{ + input_service_client::InputServiceClient, + set_libinput_setting_request::{CalibrationMatrix, Setting}, + SetKeybindRequest, SetLibinputSettingRequest, SetMousebindRequest, SetRepeatRateRequest, + SetXkbConfigRequest, + }, +}; +use tonic::transport::Channel; +use xkbcommon::xkb::Keysym; + +use self::libinput::LibinputSetting; pub mod libinput; -use xkbcommon::xkb::Keysym; - -use crate::{ - msg::{Args, CallbackId, KeyIntOrString, Msg}, - send_msg, CallbackVec, -}; - -/// Set a keybind. -/// -/// This function takes in four parameters: -/// - `modifiers`: A slice of the modifiers you want held for the keybind to trigger. -/// - `key`: The key that needs to be pressed. This takes `impl Into` and can -/// take the following three types: -/// - [`char`]: A character of the key you want. This can be `a`, `~`, `@`, and so on. -/// - [`u32`]: The key in numeric form. You can use the keys defined in [`xkbcommon::xkb::keysyms`] for this. -/// - [`Keysym`]: The key in `Keysym` form, from [xkbcommon::xkb::Keysym]. -/// - `action`: What you want to run. -/// - `callback_vec`: Your [`CallbackVec`] to insert `action` into. -/// -/// `action` takes in a `&mut `[`CallbackVec`] for use in the closure. -pub fn keybind<'a, F>( - modifiers: &[Modifier], - key: impl Into, - mut action: F, - callback_vec: &mut CallbackVec<'a>, -) where - F: FnMut(&mut CallbackVec) + 'a, -{ - let args_callback = move |_: Option, callback_vec: &mut CallbackVec<'_>| { - action(callback_vec); - }; - - let len = callback_vec.callbacks.len(); - callback_vec.callbacks.push(Box::new(args_callback)); - - let key = key.into(); - - let msg = Msg::SetKeybind { - key, - modifiers: modifiers.to_vec(), - callback_id: CallbackId(len as u32), - }; - - send_msg(msg).unwrap(); -} - -/// Set a mousebind. If called with an already existing mousebind, it gets replaced. -/// -/// The mousebind can happen either on button press or release, so you must -/// specify which edge you desire. -/// -/// `action` takes in a `&mut `[`CallbackVec`] for use in the closure. -pub fn mousebind<'a, F>( - modifiers: &[Modifier], - button: MouseButton, - edge: MouseEdge, - mut action: F, - callback_vec: &mut CallbackVec<'a>, -) where - F: FnMut(&mut CallbackVec) + 'a, -{ - let args_callback = move |_: Option, callback_vec: &mut CallbackVec<'_>| { - action(callback_vec); - }; - - let len = callback_vec.callbacks.len(); - callback_vec.callbacks.push(Box::new(args_callback)); - - let msg = Msg::SetMousebind { - modifiers: modifiers.to_vec(), - button: button as u32, - edge, - callback_id: CallbackId(len as u32), - }; - - send_msg(msg).unwrap(); -} - -/// Set the xkbconfig for your keyboard. -/// -/// Parameters set to `None` will be set to their default values. -/// -/// Read `xkeyboard-config(7)` for more information. -pub fn set_xkb_config( - rules: Option<&str>, - model: Option<&str>, - layout: Option<&str>, - variant: Option<&str>, - options: Option<&str>, -) { - let msg = Msg::SetXkbConfig { - rules: rules.map(|s| s.to_string()), - variant: variant.map(|s| s.to_string()), - layout: layout.map(|s| s.to_string()), - model: model.map(|s| s.to_string()), - options: options.map(|s| s.to_string()), - }; - - send_msg(msg).unwrap(); -} - /// A mouse button. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum MouseButton { - /// The left mouse button. + /// The left mouse button Left = 0x110, - /// The right mouse button. - Right, - /// The middle mouse button, pressed usually by clicking the scroll wheel. - Middle, - /// - Side, - /// - Extra, - /// - Forward, - /// - Back, + /// The right mouse button + Right = 0x111, + /// The middle mouse button + Middle = 0x112, + /// The side mouse button + Side = 0x113, + /// The extra mouse button + Extra = 0x114, + /// The forward mouse button + Forward = 0x115, + /// The backward mouse button + Back = 0x116, } -/// The edge on which you want things to trigger. -#[derive(Debug, Hash, serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq)] +/// Keyboard modifiers. +#[repr(i32)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] +pub enum Mod { + /// The shift key + Shift = 1, + /// The ctrl key + Ctrl, + /// The alt key + Alt, + /// The super key, aka meta, win, mod4 + Super, +} + +/// Press or release. +#[repr(i32)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] pub enum MouseEdge { - /// Actions will be triggered on button press. - Press, - /// Actions will be triggered on button release. + /// Perform actions on button press + Press = 1, + /// Perform actions on button release Release, } -impl From for KeyIntOrString { - fn from(value: char) -> Self { - Self::String(value.to_string()) - } +/// A struct that lets you define xkeyboard config options. +/// +/// See `xkeyboard-config(7)` for more information. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +pub struct XkbConfig { + /// Files of rules to be used for keyboard mapping composition + pub rules: Option<&'static str>, + /// Name of the model of your keyboard type + pub model: Option<&'static str>, + /// Layout(s) you intend to use + pub layout: Option<&'static str>, + /// Variant(s) of the layout you intend to use + pub variant: Option<&'static str>, + /// Extra xkb configuration options + pub options: Option<&'static str>, } -impl From for KeyIntOrString { - fn from(value: u32) -> Self { - Self::Int(value) - } +/// The `Input` struct. +/// +/// This struct contains methods that allow you to set key- and mousebinds, +/// change xkeyboard and libinput settings, and change the keyboard's repeat rate. +#[derive(Debug, Clone)] +pub struct Input { + channel: Channel, + fut_sender: UnboundedSender>, } -impl From for KeyIntOrString { - fn from(value: Keysym) -> Self { - Self::Int(value.raw()) +impl Input { + pub(crate) fn new( + channel: Channel, + fut_sender: UnboundedSender>, + ) -> Self { + Self { + channel, + fut_sender, + } } -} -/// A modifier key. -#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)] -pub enum Modifier { - /// The shift key. - Shift, - /// The control key. - Ctrl, - /// The alt key. - Alt, - /// The super key. + fn create_input_client(&self) -> InputServiceClient { + InputServiceClient::new(self.channel.clone()) + } + + /// Set a keybind. /// - /// This is also known as the Windows key, meta, or Mod4 for those coming from Xorg. - Super, + /// If called with an already set keybind, it gets replaced. + /// + /// You must supply: + /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger. + /// - `key`: The key that needs to be pressed. This can be anything that implements the [Key] trait: + /// - `char` + /// - `&str` and `String`: This is any name from + /// [xkbcommon-keysyms.h](https://xkbcommon.org/doc/current/xkbcommon-keysyms_8h.html) + /// without the `XKB_KEY_` prefix. + /// - `u32`: The numerical key code from the website above. + /// - A [`keysym`][Keysym] from the [`xkbcommon`] re-export. + /// - `action`: A closure that will be run when the keybind is triggered. + /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate + /// something, consider using channels or [`Box::leak`]. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::Mod; + /// + /// // Set `Super + Shift + c` to close the focused window + /// input.keybind([Mod::Super, Mod::Shift], 'c', || { + /// if let Some(win) = window.get_focused() { + /// win.close(); + /// } + /// }); + /// + /// // With a string key + /// input.keybind([], "BackSpace", || { /* ... */ }); + /// + /// // With a numeric key + /// input.keybind([], 65, || { /* ... */ }); // 65 = 'A' + /// + /// // With a `Keysym` + /// input.keybind([], pinnacle_api::xkbcommon::xkb::Keysym::Return, || { /* ... */ }); + /// ``` + pub fn keybind( + &self, + mods: impl IntoIterator, + key: impl Key + Send + 'static, + mut action: impl FnMut() + Send + 'static, + ) { + let mut client = self.create_input_client(); + + let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); + + self.fut_sender + .unbounded_send( + async move { + let mut stream = client + .set_keybind(SetKeybindRequest { + modifiers, + key: Some(input::v0alpha1::set_keybind_request::Key::RawCode( + key.into_keysym().raw(), + )), + }) + .await + .unwrap() + .into_inner(); + + while let Some(Ok(_response)) = stream.next().await { + action(); + } + } + .boxed(), + ) + .unwrap(); + } + + /// Set a mousebind. + /// + /// If called with an already set mousebind, it gets replaced. + /// + /// You must supply: + /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger. + /// - `button`: A [`MouseButton`]. + /// - `edge`: A [`MouseEdge`]. This allows you to trigger the bind on either mouse press or release. + /// - `action`: A closure that will be run when the mousebind is triggered. + /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate + /// something, consider using channels or [`Box::leak`]. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; + /// + /// // Set `Super + left click` to start moving a window + /// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || { + /// window.begin_move(MouseButton::Press); + /// }); + /// ``` + pub fn mousebind( + &self, + mods: impl IntoIterator, + button: MouseButton, + edge: MouseEdge, + mut action: impl FnMut() + 'static + Send, + ) { + let mut client = self.create_input_client(); + + let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); + + self.fut_sender + .unbounded_send( + async move { + let mut stream = client + .set_mousebind(SetMousebindRequest { + modifiers, + button: Some(button as u32), + edge: Some(edge as i32), + }) + .await + .unwrap() + .into_inner(); + + while let Some(Ok(_response)) = stream.next().await { + action(); + } + } + .boxed(), + ) + .unwrap(); + } + + /// Set the xkeyboard config. + /// + /// This allows you to set several xkeyboard options like `layout` and `rules`. + /// + /// See `xkeyboard-config(7)` for more information. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::XkbConfig; + /// + /// input.set_xkb_config(Xkbconfig { + /// layout: Some("us,fr,ge"), + /// options: Some("ctrl:swapcaps,caps:shift"), + /// ..Default::default() + /// }); + /// ``` + pub fn set_xkb_config(&self, xkb_config: XkbConfig) { + let mut client = self.create_input_client(); + + block_on(client.set_xkb_config(SetXkbConfigRequest { + rules: xkb_config.rules.map(String::from), + variant: xkb_config.variant.map(String::from), + layout: xkb_config.layout.map(String::from), + model: xkb_config.model.map(String::from), + options: xkb_config.options.map(String::from), + })) + .unwrap(); + } + + /// Set the keyboard's repeat rate. + /// + /// This allows you to set the time between holding down a key and it repeating + /// as well as the time between each repeat. + /// + /// Units are in milliseconds. + /// + /// # Examples + /// + /// ``` + /// // Set keyboard to repeat after holding down for half a second, + /// // and repeat once every 25ms (40 times a second) + /// input.set_repeat_rate(25, 500); + /// ``` + pub fn set_repeat_rate(&self, rate: i32, delay: i32) { + let mut client = self.create_input_client(); + + block_on(client.set_repeat_rate(SetRepeatRateRequest { + rate: Some(rate), + delay: Some(delay), + })) + .unwrap(); + } + + /// Set a libinput setting. + /// + /// From [freedesktop.org](https://www.freedesktop.org/wiki/Software/libinput/): + /// > libinput is a library to handle input devices in Wayland compositors + /// + /// As such, this method allows you to set various settings related to input devices. + /// This includes things like pointer acceleration and natural scrolling. + /// + /// See [`LibinputSetting`] for all the settings you can change. + /// + /// Note: currently Pinnacle applies anything set here to *every* device, regardless of what it + /// actually is. This will be fixed in the future. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::libinput::*; + /// + /// // Set pointer acceleration to flat + /// input.set_libinput_setting(LibinputSetting::AccelProfile(AccelProfile::Flat)); + /// + /// // Enable natural scrolling (reverses scroll direction; usually used with trackpads) + /// input.set_libinput_setting(LibinputSetting::NaturalScroll(true)); + /// ``` + pub fn set_libinput_setting(&self, setting: LibinputSetting) { + let mut client = self.create_input_client(); + + let setting = match setting { + LibinputSetting::AccelProfile(profile) => Setting::AccelProfile(profile as i32), + LibinputSetting::AccelSpeed(speed) => Setting::AccelSpeed(speed), + LibinputSetting::CalibrationMatrix(matrix) => { + Setting::CalibrationMatrix(CalibrationMatrix { + matrix: matrix.to_vec(), + }) + } + LibinputSetting::ClickMethod(method) => Setting::ClickMethod(method as i32), + LibinputSetting::DisableWhileTyping(disable) => Setting::DisableWhileTyping(disable), + LibinputSetting::LeftHanded(enable) => Setting::LeftHanded(enable), + LibinputSetting::MiddleEmulation(enable) => Setting::MiddleEmulation(enable), + LibinputSetting::RotationAngle(angle) => Setting::RotationAngle(angle), + LibinputSetting::ScrollButton(button) => Setting::RotationAngle(button), + LibinputSetting::ScrollButtonLock(enable) => Setting::ScrollButtonLock(enable), + LibinputSetting::ScrollMethod(method) => Setting::ScrollMethod(method as i32), + LibinputSetting::NaturalScroll(enable) => Setting::NaturalScroll(enable), + LibinputSetting::TapButtonMap(map) => Setting::TapButtonMap(map as i32), + LibinputSetting::TapDrag(enable) => Setting::TapDrag(enable), + LibinputSetting::TapDragLock(enable) => Setting::TapDragLock(enable), + LibinputSetting::Tap(enable) => Setting::Tap(enable), + }; + + block_on(client.set_libinput_setting(SetLibinputSettingRequest { + setting: Some(setting), + })) + .unwrap(); + } +} + +/// A trait that designates anything that can be converted into a [`Keysym`]. +pub trait Key { + /// Convert this into a [`Keysym`]. + fn into_keysym(self) -> Keysym; +} + +impl Key for Keysym { + fn into_keysym(self) -> Keysym { + self + } +} + +impl Key for char { + fn into_keysym(self) -> Keysym { + Keysym::from_char(self) + } +} + +impl Key for &str { + fn into_keysym(self) -> Keysym { + xkbcommon::xkb::keysym_from_name(self, xkbcommon::xkb::KEYSYM_NO_FLAGS) + } +} + +impl Key for String { + fn into_keysym(self) -> Keysym { + xkbcommon::xkb::keysym_from_name(&self, xkbcommon::xkb::KEYSYM_NO_FLAGS) + } +} + +impl Key for u32 { + fn into_keysym(self) -> Keysym { + Keysym::from(self) + } } diff --git a/api/rust/src/input/libinput.rs b/api/rust/src/input/libinput.rs index 23dddeb..5e24df7 100644 --- a/api/rust/src/input/libinput.rs +++ b/api/rust/src/input/libinput.rs @@ -1,40 +1,35 @@ -//! Libinput settings. +//! Types for libinput configuration. -use crate::{msg::Msg, send_msg}; - -/// Set a libinput setting. -/// -/// This takes a [`LibinputSetting`] containing what you want set. -pub fn set(setting: LibinputSetting) { - let msg = Msg::SetLibinputSetting(setting); - send_msg(msg).unwrap(); -} - -/// The acceleration profile. -#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)] +/// Pointer acceleration profile +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AccelProfile { - /// Flat pointer acceleration. - Flat, - /// Adaptive pointer acceleration. + /// A flat acceleration profile. /// - /// This is the default for most devices. + /// Pointer motion is accelerated by a constant (device-specific) factor, depending on the current speed. + Flat = 1, + /// An adaptive acceleration profile. + /// + /// Pointer acceleration depends on the input speed. This is the default profile for most devices. Adaptive, } -/// The click method for a touchpad. -#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)] +/// The click method defines when to generate software-emulated buttons, usually on a device +/// that does not have a specific physical button available. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ClickMethod { /// Use software-button areas to generate button events. - ButtonAreas, + ButtonAreas = 1, /// The number of fingers decides which button press to generate. Clickfinger, } -/// The scroll method for a touchpad. -#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)] +/// The scroll method of a device selects when to generate scroll axis events instead of pointer motion events. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ScrollMethod { - /// Never send scroll events. - NoScroll, + /// Never send scroll events instead of pointer motion events. + /// + /// This has no effect on events generated by scroll wheels. + NoScroll = 1, /// Send scroll events when two fingers are logically down on the device. TwoFinger, /// Send scroll events when a finger moves along the bottom or right edge of a device. @@ -43,63 +38,48 @@ pub enum ScrollMethod { OnButtonDown, } -/// The mapping between finger count and button event for a touchpad. -#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)] +/// Map 1/2/3 finger tips to buttons. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TapButtonMap { - /// 1/2/3 finger tap is mapped to left/right/middle click. + /// 1/2/3 finger tap maps to left/right/middle LeftRightMiddle, - /// 1/2/3 finger tap is mapped to left/middle/right click. + /// 1/2/3 finger tap maps to left/middle/right LeftMiddleRight, } -/// Libinput settings. -#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize)] +/// Possible settings for libinput. +#[derive(Debug, Clone, Copy, PartialEq)] pub enum LibinputSetting { - /// Set the acceleration profile. + /// Set the pointer acceleration profile AccelProfile(AccelProfile), - /// Set the acceleration speed. - /// - /// This should be a float from -1.0 to 1.0. + /// Set pointer acceleration speed AccelSpeed(f64), - /// Set the calibration matrix. + /// Set the calibration matrix CalibrationMatrix([f32; 6]), - /// Set the click method. - /// - /// The click method defines when to generate software-emulated buttons, usually on a device - /// that does not have a specific physical button available. + /// Set the [`ClickMethod`] ClickMethod(ClickMethod), - /// Set whether or not the device will be disabled while typing. - DisableWhileTypingEnabled(bool), - /// Set device left-handedness. + /// Set whether the device gets disabled while typing + DisableWhileTyping(bool), + /// Set left handed mode LeftHanded(bool), - /// Set whether or not the middle click can be emulated. - MiddleEmulationEnabled(bool), - /// Set the rotation angle of a device. + /// Allow or disallow middle mouse button emulation + MiddleEmulation(bool), + /// Set the rotation angle RotationAngle(u32), - /// Set the scroll method. - ScrollMethod(ScrollMethod), - /// Set whether or not natural scroll is enabled. - /// - /// This reverses the direction of scrolling and is mainly used with touchpads. - NaturalScrollEnabled(bool), - /// Set the scroll button. + /// Set the scroll button ScrollButton(u32), - /// Set the tap button map, - /// - /// This determines whether taps with 2 and 3 fingers register as right and middle clicks or - /// the reverse. + /// Set whether the scroll button should be a drag or toggle + ScrollButtonLock(bool), + /// Set the [`ScrollMethod`] + ScrollMethod(ScrollMethod), + /// Enable or disable natural scrolling + NaturalScroll(bool), + /// Set the [`TapButtonMap`] TapButtonMap(TapButtonMap), - /// Set whether or not tap-and-drag is enabled. - /// - /// When enabled, a single-finger tap immediately followed by a finger down results in - /// a button down event, and subsequent finger motion thus triggers a drag. - /// The button is released on finger up. - TapDragEnabled(bool), - /// Set whether or not tap drag lock is enabled. - /// - /// When enabled, a finger may be lifted and put back on the touchpad within a timeout and the drag process - /// continues. When disabled, lifting the finger during a tap-and-drag will immediately stop the drag. - TapDragLockEnabled(bool), - /// Set whether or not tap-to-click is enabled. - TapEnabled(bool), + /// Enable or disable tap-to-drag + TapDrag(bool), + /// Enable or disable a timeout where lifting a finger off the device will not stop dragging + TapDragLock(bool), + /// Enable or disable tap-to-click + Tap(bool), } diff --git a/api/rust/src/lib.rs b/api/rust/src/lib.rs index b403eec..e573209 100644 --- a/api/rust/src/lib.rs +++ b/api/rust/src/lib.rs @@ -1,234 +1,202 @@ -//! The Rust implementation of the configuration API for Pinnacle, -//! a [Smithay](https://github.com/Smithay/smithay)-based Wayland compositor -//! inspired by [AwesomeWM](https://github.com/awesomeWM/awesome). - #![warn(missing_docs)] +//! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s +//! configuration API. +//! +//! This library allows to to interface with the Pinnacle compositor and configure various aspects +//! like input and the tag system. +//! +//! # Configuration +//! +//! ## 1. Create a cargo project +//! To create your own Rust config, create a Cargo project in `~/.config/pinnacle`. +//! +//! ## 2. Create `metaconfig.toml` +//! Then, create a file named `metaconfig.toml`. This is the file Pinnacle will use to determine +//! what to run, kill and reload-config keybinds, an optional socket directory, and any environment +//! variables to give the config client. +//! +//! In `metaconfig.toml`, put the following: +//! ```toml +//! # `command` will tell Pinnacle to run `cargo run` in your config directory. +//! # You can add stuff like "--release" here if you want to. +//! command = ["cargo", "run"] +//! +//! # You must define a keybind to reload your config if it crashes, otherwise you'll get stuck if +//! # the Lua config doesn't kick in properly. +//! reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } +//! +//! # Similarly, you must define a keybind to kill Pinnacle. +//! kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } +//! +//! # You can specify an optional socket directory if you need to place the socket Pinnacle will +//! # use for configuration in a different place. +//! # socket_dir = "your/dir/here" +//! +//! # If you need to set any environment variables for the config process, you can do so here if +//! # you don't want to do it in the config itself. +//! [envs] +//! # key = "value" +//! ``` +//! +//! ## 3. Set up dependencies +//! In your `Cargo.toml`, add a dependency to `pinnacle-api`: +//! +//! ```toml +//! # Cargo.toml +//! +//! [dependencies] +//! pinnacle-api = { git = "https://github.com/pinnacle-comp/pinnacle" } +//! ``` +//! +//! ## 4. Set up the main function +//! In `main.rs`, change `fn main()` to `async fn main()` and annotate it with the +//! [`pinnacle_api::config`][`crate::config`] macro. Pass in the identifier you want to bind the +//! config modules to: +//! +//! ``` +//! use pinnacle_api::ApiModules; +//! +//! #[pinnacle_api::config(modules)] +//! async fn main() { +//! // `modules` is now available in the function body. +//! // You can deconstruct `ApiModules` to get all the config structs. +//! let ApiModules { +//! pinnacle, +//! process, +//! window, +//! input, +//! output, +//! tag, +//! } = modules; +//! } +//! ``` +//! +//! ## 5. Begin crafting your config! +//! You can peruse the documentation for things to configure. + +use std::sync::OnceLock; + +use futures::{ + channel::mpsc::UnboundedReceiver, future::BoxFuture, stream::FuturesUnordered, StreamExt, +}; +use input::Input; +use output::Output; +use pinnacle::Pinnacle; +use process::Process; +use tag::Tag; +use tonic::transport::{Endpoint, Uri}; +use tower::service_fn; +use window::Window; + pub mod input; -mod msg; pub mod output; +pub mod pinnacle; pub mod process; pub mod tag; +pub mod util; pub mod window; -/// The xkbcommon crate, re-exported for your convenience. +pub use pinnacle_api_macros::config; +pub use tokio; pub use xkbcommon; -/// The prelude for the Pinnacle API. +static PINNACLE: OnceLock = OnceLock::new(); +static PROCESS: OnceLock = OnceLock::new(); +static WINDOW: OnceLock = OnceLock::new(); +static INPUT: OnceLock = OnceLock::new(); +static OUTPUT: OnceLock = OnceLock::new(); +static TAG: OnceLock = OnceLock::new(); + +/// A struct containing static references to all of the configuration structs. +#[derive(Debug, Clone, Copy)] +pub struct ApiModules { + /// The [`Pinnacle`] struct + pub pinnacle: &'static Pinnacle, + /// The [`Process`] struct + pub process: &'static Process, + /// The [`Window`] struct + pub window: &'static Window, + /// The [`Input`] struct + pub input: &'static Input, + /// The [`Output`] struct + pub output: &'static Output, + /// The [`Tag`] struct + pub tag: &'static Tag, +} + +/// Connects to Pinnacle and builds the configuration structs. /// -/// This contains useful imports that you will likely need. -/// To that end, you can do `use pinnacle_api::prelude::*` to -/// prevent your config file from being cluttered with imports. -pub mod prelude { - pub use crate::input::libinput::*; - pub use crate::input::Modifier; - pub use crate::input::MouseButton; - pub use crate::input::MouseEdge; - pub use crate::output::AlignmentHorizontal; - pub use crate::output::AlignmentVertical; - pub use crate::tag::Layout; - pub use crate::window::rules::WindowRule; - pub use crate::window::rules::WindowRuleCondition; - pub use crate::window::FloatingOrTiled; - pub use crate::window::FullscreenOrMaximized; -} +/// This function is inserted at the top of your config through the [`config`] macro. +/// You should use that macro instead of this function directly. +pub async fn connect( +) -> Result<(ApiModules, UnboundedReceiver>), Box> { + let channel = Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket + .connect_with_connector(service_fn(|_: Uri| { + tokio::net::UnixStream::connect( + std::env::var("PINNACLE_GRPC_SOCKET") + .expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"), + ) + })) + .await?; -use std::{ - collections::{hash_map::Entry, HashMap}, - convert::Infallible, - io::{Read, Write}, - os::unix::net::UnixStream, - path::PathBuf, - sync::{atomic::AtomicU32, Mutex, OnceLock}, -}; + let (fut_sender, fut_recv) = futures::channel::mpsc::unbounded::>(); -use msg::{Args, CallbackId, IncomingMsg, Msg, Request, RequestResponse}; + let output = Output::new(channel.clone(), fut_sender.clone()); -use crate::msg::RequestId; + let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(channel.clone())); + let process = PROCESS.get_or_init(|| Process::new(channel.clone(), fut_sender.clone())); + let window = WINDOW.get_or_init(|| Window::new(channel.clone())); + let input = INPUT.get_or_init(|| Input::new(channel.clone(), fut_sender.clone())); + let tag = TAG.get_or_init(|| Tag::new(channel.clone(), fut_sender.clone())); + let output = OUTPUT.get_or_init(|| output); -static STREAM: OnceLock> = OnceLock::new(); -lazy_static::lazy_static! { - static ref UNREAD_CALLBACK_MSGS: Mutex> = Mutex::new(HashMap::new()); - static ref UNREAD_REQUEST_MSGS: Mutex> = Mutex::new(HashMap::new()); -} - -static REQUEST_ID_COUNTER: AtomicU32 = AtomicU32::new(0); - -fn send_msg(msg: Msg) -> anyhow::Result<()> { - let mut msg = rmp_serde::encode::to_vec_named(&msg)?; - let mut msg_len = (msg.len() as u32).to_ne_bytes(); - - let mut stream = STREAM.get().unwrap().lock().unwrap(); - - stream.write_all(msg_len.as_mut_slice())?; - stream.write_all(msg.as_mut_slice())?; - - Ok(()) -} - -fn read_msg(request_id: Option) -> IncomingMsg { - loop { - if let Some(request_id) = request_id { - if let Some(msg) = UNREAD_REQUEST_MSGS.lock().unwrap().remove(&request_id) { - return msg; - } - } - - let mut stream = STREAM.get().unwrap().lock().unwrap(); - let mut msg_len_bytes = [0u8; 4]; - stream.read_exact(msg_len_bytes.as_mut_slice()).unwrap(); - - let msg_len = u32::from_ne_bytes(msg_len_bytes); - let mut msg_bytes = vec![0u8; msg_len as usize]; - stream.read_exact(msg_bytes.as_mut_slice()).unwrap(); - - let incoming_msg: IncomingMsg = rmp_serde::from_slice(msg_bytes.as_slice()).unwrap(); - - if let Some(request_id) = request_id { - match &incoming_msg { - IncomingMsg::CallCallback { - callback_id, - args: _, - } => { - UNREAD_CALLBACK_MSGS - .lock() - .unwrap() - .insert(*callback_id, incoming_msg); - } - IncomingMsg::RequestResponse { - request_id: req_id, - response: _, - } => { - if req_id != &request_id { - UNREAD_REQUEST_MSGS - .lock() - .unwrap() - .insert(*req_id, incoming_msg); - } else { - return incoming_msg; - } - } - } - } else { - return incoming_msg; - } - } -} - -fn request(request: Request) -> RequestResponse { - use std::sync::atomic::Ordering; - let request_id = REQUEST_ID_COUNTER.fetch_add(1, Ordering::Relaxed); - - let msg = Msg::Request { - request_id: RequestId(request_id), - request, - }; - send_msg(msg).unwrap(); // TODO: propogate - - let IncomingMsg::RequestResponse { - request_id: _, - response, - } = read_msg(Some(RequestId(request_id))) - else { - unreachable!() + let modules = ApiModules { + pinnacle, + process, + window, + input, + output, + tag, }; - response + Ok((modules, fut_recv)) } -/// Connect to Pinnacle. This needs to be called before you begin calling config functions. +/// Listen to Pinnacle for incoming messages. /// -/// This will open up a connection to the Unix socket at `$PINNACLE_SOCKET`, -/// which should be set when you start the compositor. -pub fn connect() -> anyhow::Result<()> { - STREAM - .set(Mutex::new( - UnixStream::connect(PathBuf::from( - std::env::var("PINNACLE_SOCKET").unwrap_or("/tmp/pinnacle_socket".to_string()), - )) - .unwrap(), - )) - .unwrap(); - - Ok(()) -} - -/// Begin listening for messages coming from Pinnacle. +/// This will run all futures returned by configuration methods that take in callbacks in order to +/// call them. /// -/// This needs to be called at the very end of your `setup` function. -pub fn listen(mut callback_vec: CallbackVec) -> Infallible { - loop { - let mut unread_callback_msgs = UNREAD_CALLBACK_MSGS.lock().unwrap(); +/// This function is inserted at the end of your config through the [`config`] macro. +/// You should use the macro instead of this function directly. +pub async fn listen( + fut_recv: UnboundedReceiver>, // api_modules: ApiModules<'a>, +) { + let mut future_set = FuturesUnordered::< + BoxFuture<( + Option>, + Option>>, + )>, + >::new(); - for cb_id in unread_callback_msgs.keys().copied().collect::>() { - let Entry::Occupied(entry) = unread_callback_msgs.entry(cb_id) else { - unreachable!(); - }; - let IncomingMsg::CallCallback { callback_id, args } = entry.remove() else { - unreachable!(); - }; + future_set.push(Box::pin(async move { + let (fut, stream) = fut_recv.into_future().await; + (fut, Some(stream)) + })); - // Take the callback out and replace it with a dummy callback - // to allow callback_vec to be used mutably below. - let mut callback = std::mem::replace( - &mut callback_vec.callbacks[callback_id.0 as usize], - Box::new(|_, _| {}), - ); - - callback(args, &mut callback_vec); - - // Put it back. - callback_vec.callbacks[callback_id.0 as usize] = callback; + while let Some((fut, stream)) = future_set.next().await { + if let Some(fut) = fut { + future_set.push(Box::pin(async move { + fut.await; + (None, None) + })); + } + if let Some(stream) = stream { + future_set.push(Box::pin(async move { + let (fut, stream) = stream.into_future().await; + (fut, Some(stream)) + })) } - - let incoming_msg = read_msg(None); - - let IncomingMsg::CallCallback { callback_id, args } = incoming_msg else { - unreachable!(); - }; - - let mut callback = std::mem::replace( - &mut callback_vec.callbacks[callback_id.0 as usize], - Box::new(|_, _| {}), - ); - - callback(args, &mut callback_vec); - - callback_vec.callbacks[callback_id.0 as usize] = callback; - } -} - -/// Quit Pinnacle. -pub fn quit() { - send_msg(Msg::Quit).unwrap(); -} - -/// A wrapper around a vector that holds all of your callbacks. -/// -/// You will need to create this before you can start calling config functions -/// that require callbacks. -/// -/// Because your callbacks can capture things, we need a non-static way to hold them. -/// That's where this struct comes in. -/// -/// Every function that needs you to provide a callback will also need you to -/// provide a `&mut CallbackVec`. This will insert the callback for use in [`listen`]. -/// -/// Additionally, all callbacks will also take in `&mut CallbackVec`. This is so you can -/// call functions that need it inside of other callbacks. -/// -/// At the end of your config, you will need to call [`listen`] to begin listening for -/// messages from Pinnacle that will call your callbacks. Here, you must in pass your -/// `CallbackVec`. -#[derive(Default)] -pub struct CallbackVec<'a> { - #[allow(clippy::type_complexity)] - pub(crate) callbacks: Vec, &mut CallbackVec) + 'a>>, -} - -impl<'a> CallbackVec<'a> { - /// Create a new, empty `CallbackVec`. - pub fn new() -> Self { - Default::default() } } diff --git a/api/rust/src/msg.rs b/api/rust/src/msg.rs deleted file mode 100644 index 9aae141..0000000 --- a/api/rust/src/msg.rs +++ /dev/null @@ -1,290 +0,0 @@ -use std::num::NonZeroU32; - -use crate::{ - input::{libinput::LibinputSetting, Modifier, MouseEdge}, - output::OutputName, - tag::{Layout, TagId}, - window::{FloatingOrTiled, FullscreenOrMaximized, WindowId}, -}; - -#[derive(Debug, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize, Clone, Copy)] -pub(crate) struct CallbackId(pub u32); - -#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub(crate) struct WindowRuleCondition { - /// This condition is met when any of the conditions provided is met. - #[serde(default)] - pub cond_any: Option>, - /// This condition is met when all of the conditions provided are met. - #[serde(default)] - pub cond_all: Option>, - /// This condition is met when the class matches. - #[serde(default)] - pub class: Option>, - /// This condition is met when the title matches. - #[serde(default)] - pub title: Option>, - /// This condition is met when the tag matches. - #[serde(default)] - pub tag: Option>, -} - -#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize)] -pub(crate) struct WindowRule { - /// Set the output the window will open on. - #[serde(default)] - pub output: Option, - /// Set the tags the output will have on open. - #[serde(default)] - pub tags: Option>, - /// Set the window to floating or tiled on open. - #[serde(default)] - pub floating_or_tiled: Option, - /// Set the window to fullscreen, maximized, or force it to neither. - #[serde(default)] - pub fullscreen_or_maximized: Option, - /// Set the window's initial size. - #[serde(default)] - pub size: Option<(NonZeroU32, NonZeroU32)>, - /// Set the window's initial location. If the window is tiled, it will snap to this position - /// when set to floating. - #[serde(default)] - pub location: Option<(i32, i32)>, -} - -#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct RequestId(pub u32); - -#[derive(Debug, serde::Serialize)] -pub(crate) enum Msg { - // Input - SetKeybind { - key: KeyIntOrString, - modifiers: Vec, - callback_id: CallbackId, - }, - SetMousebind { - modifiers: Vec, - button: u32, - edge: MouseEdge, - callback_id: CallbackId, - }, - - // Window management - CloseWindow { - window_id: WindowId, - }, - SetWindowSize { - window_id: WindowId, - #[serde(default)] - width: Option, - #[serde(default)] - height: Option, - }, - MoveWindowToTag { - window_id: WindowId, - tag_id: TagId, - }, - ToggleTagOnWindow { - window_id: WindowId, - tag_id: TagId, - }, - ToggleFloating { - window_id: WindowId, - }, - ToggleFullscreen { - window_id: WindowId, - }, - ToggleMaximized { - window_id: WindowId, - }, - AddWindowRule { - cond: WindowRuleCondition, - rule: WindowRule, - }, - WindowMoveGrab { - button: u32, - }, - WindowResizeGrab { - button: u32, - }, - - // Tag management - ToggleTag { - tag_id: TagId, - }, - SwitchToTag { - tag_id: TagId, - }, - AddTags { - /// The name of the output you want these tags on. - output_name: OutputName, - tag_names: Vec, - }, - // TODO: - RemoveTags { - /// The name of the output you want these tags removed from. - tag_ids: Vec, - }, - SetLayout { - tag_id: TagId, - layout: Layout, - }, - - // Output management - ConnectForAllOutputs { - callback_id: CallbackId, - }, - SetOutputLocation { - output_name: OutputName, - #[serde(default)] - x: Option, - #[serde(default)] - y: Option, - }, - - // Process management - /// Spawn a program with an optional callback. - Spawn { - command: Vec, - #[serde(default)] - callback_id: Option, - }, - /// Spawn a program with an optional callback only if it isn't running. - SpawnOnce { - command: Vec, - #[serde(default)] - callback_id: Option, - }, - SetEnv { - key: String, - value: String, - }, - - // Pinnacle management - /// Quit the compositor. - Quit, - - // Input management - SetXkbConfig { - #[serde(default)] - rules: Option, - #[serde(default)] - variant: Option, - #[serde(default)] - layout: Option, - #[serde(default)] - model: Option, - #[serde(default)] - options: Option, - }, - - SetLibinputSetting(LibinputSetting), - - Request { - request_id: RequestId, - request: Request, - }, -} - -#[allow(clippy::enum_variant_names)] -#[derive(Debug, serde::Serialize, serde::Deserialize)] -/// Messages that require a server response, usually to provide some data. -pub(crate) enum Request { - // Windows - GetWindows, - GetWindowProps { window_id: WindowId }, - // Outputs - GetOutputs, - GetOutputProps { output_name: String }, - // Tags - GetTags, - GetTagProps { tag_id: TagId }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] -pub enum KeyIntOrString { - Int(u32), - String(String), -} - -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] -pub enum Args { - /// Send a message with lines from the spawned process. - Spawn { - #[serde(default)] - stdout: Option, - #[serde(default)] - stderr: Option, - #[serde(default)] - exit_code: Option, - #[serde(default)] - exit_msg: Option, - }, - ConnectForAllOutputs { - output_name: String, - }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub(crate) enum IncomingMsg { - CallCallback { - callback_id: CallbackId, - #[serde(default)] - args: Option, - }, - RequestResponse { - request_id: RequestId, - response: RequestResponse, - }, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub(crate) enum RequestResponse { - Window { - window_id: Option, - }, - Windows { - window_ids: Vec, - }, - WindowProps { - size: Option<(i32, i32)>, - loc: Option<(i32, i32)>, - class: Option, - title: Option, - focused: Option, - floating: Option, - fullscreen_or_maximized: Option, - }, - Output { - output_name: Option, - }, - Outputs { - output_names: Vec, - }, - OutputProps { - /// The make of the output. - make: Option, - /// The model of the output. - model: Option, - /// The location of the output in the space. - loc: Option<(i32, i32)>, - /// The resolution of the output. - res: Option<(i32, i32)>, - /// The refresh rate of the output. - refresh_rate: Option, - /// The size of the output, in millimeters. - physical_size: Option<(i32, i32)>, - /// Whether the output is focused or not. - focused: Option, - tag_ids: Option>, - }, - Tags { - tag_ids: Vec, - }, - TagProps { - active: Option, - name: Option, - output_name: Option, - }, -} diff --git a/api/rust/src/output.rs b/api/rust/src/output.rs index e94dc20..6ef6d9b 100644 --- a/api/rust/src/output.rs +++ b/api/rust/src/output.rs @@ -1,301 +1,515 @@ //! Output management. +//! +//! An output is Pinnacle's terminology for a monitor. +//! +//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different +//! connected monitors and set them up. -use crate::{ - msg::{Args, CallbackId, Msg, Request, RequestResponse}, - request, send_msg, - tag::TagHandle, - CallbackVec, +use futures::{ + channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, }; +use pinnacle_api_defs::pinnacle::{ + output::{ + self, + v0alpha1::{ + output_service_client::OutputServiceClient, ConnectForAllRequest, SetLocationRequest, + }, + }, + tag::v0alpha1::tag_service_client::TagServiceClient, +}; +use tonic::transport::Channel; -/// A unique identifier for an output. +use crate::tag::TagHandle; + +/// A struct that allows you to get handles to connected outputs and set them up. /// -/// An empty string represents an invalid output. -#[derive(Debug, Hash, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] -pub(crate) struct OutputName(pub String); - -/// Get an [`OutputHandle`] by its name. -/// -/// `name` is the name of the port the output is plugged in to. -/// This is something like `HDMI-1` or `eDP-0`. -pub fn get_by_name(name: &str) -> Option { - let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else { - unreachable!() - }; - - output_names - .into_iter() - .find(|s| s == name) - .map(|s| OutputHandle(OutputName(s))) +/// See [`OutputHandle`] for more information. +#[derive(Debug, Clone)] +pub struct Output { + channel: Channel, + fut_sender: UnboundedSender>, } -/// Get a handle to all connected outputs. -pub fn get_all() -> impl Iterator { - let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else { - unreachable!() - }; - - output_names - .into_iter() - .map(|name| OutputHandle(OutputName(name))) -} - -/// Get the currently focused output. -/// -/// This is currently defined as the one with the cursor on it. -pub fn get_focused() -> Option { - let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else { - unreachable!() - }; - - output_names - .into_iter() - .map(|s| OutputHandle(OutputName(s))) - .find(|op| op.properties().focused == Some(true)) -} - -/// Connect a function to be run on all current and future outputs. -/// -/// When called, `connect_for_all` will run `func` with all currently connected outputs. -/// If a new output is connected, `func` will also be called with it. -/// -/// `func` takes in two parameters: -/// - `0`: An [`OutputHandle`] you can act on. -/// - `1`: A `&mut `[`CallbackVec`] for use in the closure. -/// -/// This will *not* be called if it has already been called for a given connector. -/// This means turning your monitor off and on or unplugging and replugging it *to the same port* -/// won't trigger `func`. Plugging it in to a new port *will* trigger `func`. -/// This is intended to prevent duplicate setup. -/// -/// Please note: this function will be run *after* Pinnacle processes your entire config. -/// For example, if you define tags in `func` but toggle them directly after `connect_for_all`, -/// nothing will happen as the tags haven't been added yet. -pub fn connect_for_all<'a, F>(mut func: F, callback_vec: &mut CallbackVec<'a>) -where - F: FnMut(OutputHandle, &mut CallbackVec) + 'a, -{ - let args_callback = move |args: Option, callback_vec: &mut CallbackVec<'_>| { - if let Some(Args::ConnectForAllOutputs { output_name }) = args { - func(OutputHandle(OutputName(output_name)), callback_vec); +impl Output { + pub(crate) fn new( + channel: Channel, + fut_sender: UnboundedSender>, + ) -> Self { + Self { + channel, + fut_sender, } - }; + } - let len = callback_vec.callbacks.len(); - callback_vec.callbacks.push(Box::new(args_callback)); + fn create_output_client(&self) -> OutputServiceClient { + OutputServiceClient::new(self.channel.clone()) + } - let msg = Msg::ConnectForAllOutputs { - callback_id: CallbackId(len as u32), - }; + fn create_tag_client(&self) -> TagServiceClient { + TagServiceClient::new(self.channel.clone()) + } - send_msg(msg).unwrap(); + /// Get a handle to all connected outputs. + /// + /// # Examples + /// + /// ``` + /// let outputs = output.get_all(); + /// ``` + pub fn get_all(&self) -> impl Iterator { + let mut client = self.create_output_client(); + let tag_client = self.create_tag_client(); + block_on(client.get(output::v0alpha1::GetRequest {})) + .unwrap() + .into_inner() + .output_names + .into_iter() + .map(move |name| OutputHandle { + client: client.clone(), + tag_client: tag_client.clone(), + name, + }) + } + + /// Get a handle to the output with the given name. + /// + /// By "name", we mean the name of the connector the output is connected to. + /// + /// # Examples + /// + /// ``` + /// let op = output.get_by_name("eDP-1")?; + /// let op2 = output.get_by_name("HDMI-2")?; + /// ``` + pub fn get_by_name(&self, name: impl Into) -> Option { + let name: String = name.into(); + self.get_all().find(|output| output.name == name) + } + + /// Get a handle to the focused output. + /// + /// This is currently implemented as the one that has had the most recent pointer movement. + /// + /// # Examples + /// + /// ``` + /// let op = output.get_focused()?; + /// ``` + pub fn get_focused(&self) -> Option { + self.get_all() + .find(|output| matches!(output.props().focused, Some(true))) + } + + /// Connect a closure to be run on all current and future outputs. + /// + /// When called, `connect_for_all` will do two things: + /// 1. Immediately run `for_all` with all currently connected outputs. + /// 2. Create a future that will call `for_all` with any newly connected outputs. + /// + /// Note that `for_all` will *not* run with outputs that have been unplugged and replugged. + /// This is to prevent duplicate setup. Instead, the compositor keeps track of any tags and + /// state the output had when unplugged and restores them on replug. + /// + /// # Examples + /// + /// ``` + /// // Add tags 1-3 to all outputs and set tag "1" to active + /// output.connect_for_all(|op| { + /// let tags = tag.add(&op, ["1", "2", "3"]); + /// tags.next().unwrap().set_active(true); + /// }); + /// ``` + pub fn connect_for_all(&self, mut for_all: impl FnMut(OutputHandle) + Send + 'static) { + for output in self.get_all() { + for_all(output); + } + + let mut client = self.create_output_client(); + let tag_client = self.create_tag_client(); + + self.fut_sender + .unbounded_send( + async move { + let mut stream = client + .connect_for_all(ConnectForAllRequest {}) + .await + .unwrap() + .into_inner(); + + while let Some(Ok(response)) = stream.next().await { + let Some(output_name) = response.output_name else { + continue; + }; + + let output = OutputHandle { + client: client.clone(), + tag_client: tag_client.clone(), + name: output_name, + }; + + for_all(output); + } + } + .boxed(), + ) + .unwrap(); + } } -/// An output handle. +/// A handle to an output. /// -/// This is a handle to one of your monitors. -/// It serves to make it easier to deal with them, defining methods for getting properties and -/// helpers for things like positioning multiple monitors. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct OutputHandle(pub(crate) OutputName); +/// This allows you to manipulate outputs and get their properties. +#[derive(Clone, Debug)] +pub struct OutputHandle { + pub(crate) client: OutputServiceClient, + pub(crate) tag_client: TagServiceClient, + pub(crate) name: String, +} -/// Properties of an output. -pub struct OutputProperties { - /// The make. - pub make: Option, - /// The model. - /// - /// This is something like `27GL850` or whatever gibberish monitor manufacturers name their - /// displays. - pub model: Option, - /// The location of the output in the global space. - pub loc: Option<(i32, i32)>, - /// The resolution of the output in pixels, where `res.0` is the width and `res.1` is the - /// height. - pub res: Option<(i32, i32)>, - /// The refresh rate of the output in millihertz. - /// - /// For example, 60Hz is returned as 60000. - pub refresh_rate: Option, - /// The physical size of the output in millimeters. - pub physical_size: Option<(i32, i32)>, - /// Whether or not the output is focused. - pub focused: Option, - /// The tags on this output. - pub tags: Vec, +/// The alignment to use for [`OutputHandle::set_loc_adj_to`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Alignment { + /// Set above, align left borders + TopAlignLeft, + /// Set above, align centers + TopAlignCenter, + /// Set above, align right borders + TopAlignRight, + /// Set below, align left borders + BottomAlignLeft, + /// Set below, align centers + BottomAlignCenter, + /// Set below, align right borders + BottomAlignRight, + /// Set to left, align top borders + LeftAlignTop, + /// Set to left, align centers + LeftAlignCenter, + /// Set to left, align bottom borders + LeftAlignBottom, + /// Set to right, align top borders + RightAlignTop, + /// Set to right, align centers + RightAlignCenter, + /// Set to right, align bottom borders + RightAlignBottom, } impl OutputHandle { - /// Get this output's name. - pub fn name(&self) -> String { - self.0 .0.clone() + /// Set the location of this output in the global space. + /// + /// On startup, Pinnacle will lay out all connected outputs starting at (0, 0) + /// and going to the right, with their top borders aligned. + /// + /// This method allows you to move outputs where necessary. + /// + /// Note: If you leave space between two outputs when setting their locations, + /// the pointer will not be able to move between them. + /// + /// # Examples + /// + /// ``` + /// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions: + /// // - "DP-1": ┌─────┐ + /// // │ │1920x1080 + /// // └─────┘ + /// // - "HDMI-1": ┌───────┐ + /// // │ 2560x │ + /// // │ 1440 │ + /// // └───────┘ + /// + /// output.get_by_name("DP-1")?.set_location(0, 0); + /// output.get_by_name("HDMI-1")?.set_location(1920, -360); + /// + /// // Results in: + /// // x=0 ┌───────┐y=-360 + /// // y=0┌─────┤ │ + /// // │DP-1 │HDMI-1 │ + /// // └─────┴───────┘ + /// // ^x=1920 + /// ``` + pub fn set_location(&self, x: impl Into>, y: impl Into>) { + let mut client = self.client.clone(); + block_on(client.set_location(SetLocationRequest { + output_name: Some(self.name.clone()), + x: x.into(), + y: y.into(), + })) + .unwrap(); } - // TODO: Make OutputProperties an option, make non null fields not options - /// Get all properties of this output. - pub fn properties(&self) -> OutputProperties { - let RequestResponse::OutputProps { - make, - model, - loc, - res, - refresh_rate, - physical_size, - focused, - tag_ids, - } = request(Request::GetOutputProps { - output_name: self.0 .0.clone(), - }) - else { - unreachable!() + /// Set this output adjacent to another one. + /// + /// This is a helper method over [`OutputHandle::set_location`] to make laying out outputs + /// easier. + /// + /// `alignment` is an [`Alignment`] of how you want this output to be placed. + /// For example, [`TopAlignLeft`][Alignment::TopAlignLeft] will place this output + /// above `other` and align the left borders. + /// Similarly, [`RightAlignCenter`][Alignment::RightAlignCenter] will place this output + /// to the right of `other` and align their centers. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::output::Alignment; + /// + /// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions: + /// // - "DP-1": ┌─────┐ + /// // │ │1920x1080 + /// // └─────┘ + /// // - "HDMI-1": ┌───────┐ + /// // │ 2560x │ + /// // │ 1440 │ + /// // └───────┘ + /// + /// output.get_by_name("DP-1")?.set_loc_adj_to(output.get_by_name("HDMI-1")?, Alignment::BottomAlignRight); + /// + /// // Results in: + /// // ┌───────┐ + /// // │ │ + /// // │HDMI-1 │ + /// // └──┬────┤ + /// // │DP-1│ + /// // └────┘ + /// // Notice that "DP-1" now has the coordinates (2280, 1440) because "DP-1" is getting moved, not "HDMI-1". + /// // "HDMI-1" was placed at (1920, 0) during the compositor's initial output layout. + /// ``` + pub fn set_loc_adj_to(&self, other: &OutputHandle, alignment: Alignment) { + let self_props = self.props(); + let other_props = other.props(); + + // poor man's try {} + let attempt_set_loc = || -> Option<()> { + let other_x = other_props.x?; + let other_y = other_props.y?; + let other_width = other_props.pixel_width? as i32; + let other_height = other_props.pixel_height? as i32; + + let self_width = self_props.pixel_width? as i32; + let self_height = self_props.pixel_height? as i32; + + use Alignment::*; + + let x: i32; + let y: i32; + + if let TopAlignLeft | TopAlignCenter | TopAlignRight | BottomAlignLeft + | BottomAlignCenter | BottomAlignRight = alignment + { + if let TopAlignLeft | TopAlignCenter | TopAlignRight = alignment { + y = other_y - self_height; + } else { + // bottom + y = other_y + other_height; + } + + match alignment { + TopAlignLeft | BottomAlignLeft => x = other_x, + TopAlignCenter | BottomAlignCenter => { + x = other_x + (other_width - self_width) / 2; + } + TopAlignRight | BottomAlignRight => x = other_x + (other_width - self_width), + _ => unreachable!(), + } + } else { + if let LeftAlignTop | LeftAlignCenter | LeftAlignBottom = alignment { + x = other_x - self_width; + } else { + x = other_x + other_width; + } + + match alignment { + LeftAlignTop | RightAlignTop => y = other_y, + LeftAlignCenter | RightAlignCenter => { + y = other_y + (other_height - self_height) / 2; + } + LeftAlignBottom | RightAlignBottom => { + y = other_y + (other_height - self_height); + } + _ => unreachable!(), + } + } + + self.set_location(Some(x), Some(y)); + + Some(()) }; + attempt_set_loc(); + } + + /// Get all properties of this output. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::output::OutputProperties; + /// + /// let OutputProperties { + /// make, + /// model, + /// x, + /// y, + /// pixel_width, + /// pixel_height, + /// refresh_rate, + /// physical_width, + /// physical_height, + /// focused, + /// tags, + /// } = output.get_focused()?.props(); + /// ``` + pub fn props(&self) -> OutputProperties { + let mut client = self.client.clone(); + let response = block_on( + client.get_properties(output::v0alpha1::GetPropertiesRequest { + output_name: Some(self.name.clone()), + }), + ) + .unwrap() + .into_inner(); + OutputProperties { - make, - model, - loc, - res, - refresh_rate, - physical_size, - focused, - tags: tag_ids - .unwrap_or(vec![]) + make: response.make, + model: response.model, + x: response.x, + y: response.y, + pixel_width: response.pixel_width, + pixel_height: response.pixel_height, + refresh_rate: response.refresh_rate, + physical_width: response.physical_width, + physical_height: response.physical_height, + focused: response.focused, + tags: response + .tag_ids .into_iter() - .map(TagHandle) + .map(|id| TagHandle { + client: self.tag_client.clone(), + output_client: self.client.clone(), + id, + }) .collect(), } } - /// Add tags with the given `names` to this output. - pub fn add_tags(&self, names: &[&str]) { - crate::tag::add(self, names); - } + // TODO: make a macro for the following or something - /// Set this output's location in the global space. - pub fn set_loc(&self, x: Option, y: Option) { - let msg = Msg::SetOutputLocation { - output_name: self.0.clone(), - x, - y, - }; - - send_msg(msg).unwrap(); - } - - /// Set this output's location to the right of `other`. + /// Get this output's make. /// - /// It will be aligned vertically based on the given `alignment`. - pub fn set_loc_right_of(&self, other: &OutputHandle, alignment: AlignmentVertical) { - self.set_loc_horizontal(other, LeftOrRight::Right, alignment); + /// Shorthand for `self.props().make`. + pub fn make(&self) -> Option { + self.props().make } - /// Set this output's location to the left of `other`. + /// Get this output's model. /// - /// It will be aligned vertically based on the given `alignment`. - pub fn set_loc_left_of(&self, other: &OutputHandle, alignment: AlignmentVertical) { - self.set_loc_horizontal(other, LeftOrRight::Left, alignment); + /// Shorthand for `self.props().make`. + pub fn model(&self) -> Option { + self.props().model } - /// Set this output's location to the top of `other`. + /// Get this output's x position in the global space. /// - /// It will be aligned horizontally based on the given `alignment`. - pub fn set_loc_top_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) { - self.set_loc_vertical(other, TopOrBottom::Top, alignment); + /// Shorthand for `self.props().x`. + pub fn x(&self) -> Option { + self.props().x } - /// Set this output's location to the bottom of `other`. + /// Get this output's y position in the global space. /// - /// It will be aligned horizontally based on the given `alignment`. - pub fn set_loc_bottom_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) { - self.set_loc_vertical(other, TopOrBottom::Bottom, alignment); + /// Shorthand for `self.props().y`. + pub fn y(&self) -> Option { + self.props().y } - fn set_loc_horizontal( - &self, - other: &OutputHandle, - left_or_right: LeftOrRight, - alignment: AlignmentVertical, - ) { - let op1_props = self.properties(); - let op2_props = other.properties(); - - let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) = - (op1_props.loc, op1_props.res, op2_props.loc, op2_props.res) - else { - return; - }; - - let x = match left_or_right { - LeftOrRight::Left => other_loc.0 - self_res.0, - LeftOrRight::Right => other_loc.0 + self_res.0, - }; - - let y = match alignment { - AlignmentVertical::Top => other_loc.1, - AlignmentVertical::Center => other_loc.1 + (other_res.1 - self_res.1) / 2, - AlignmentVertical::Bottom => other_loc.1 + (other_res.1 - self_res.1), - }; - - self.set_loc(Some(x), Some(y)); + /// Get this output's screen width in pixels. + /// + /// Shorthand for `self.props().pixel_width`. + pub fn pixel_width(&self) -> Option { + self.props().pixel_width } - fn set_loc_vertical( - &self, - other: &OutputHandle, - top_or_bottom: TopOrBottom, - alignment: AlignmentHorizontal, - ) { - let op1_props = self.properties(); - let op2_props = other.properties(); + /// Get this output's screen height in pixels. + /// + /// Shorthand for `self.props().pixel_height`. + pub fn pixel_height(&self) -> Option { + self.props().pixel_height + } - let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) = - (op1_props.loc, op1_props.res, op2_props.loc, op2_props.res) - else { - return; - }; + /// Get this output's refresh rate in millihertz. + /// + /// For example, 144Hz will be returned as 144000. + /// + /// Shorthand for `self.props().refresh_rate`. + pub fn refresh_rate(&self) -> Option { + self.props().refresh_rate + } - let y = match top_or_bottom { - TopOrBottom::Top => other_loc.1 - self_res.1, - TopOrBottom::Bottom => other_loc.1 + other_res.1, - }; + /// Get this output's physical width in millimeters. + /// + /// Shorthand for `self.props().physical_width`. + pub fn physical_width(&self) -> Option { + self.props().physical_width + } - let x = match alignment { - AlignmentHorizontal::Left => other_loc.0, - AlignmentHorizontal::Center => other_loc.0 + (other_res.0 - self_res.0) / 2, - AlignmentHorizontal::Right => other_loc.0 + (other_res.0 - self_res.0), - }; + /// Get this output's physical height in millimeters. + /// + /// Shorthand for `self.props().physical_height`. + pub fn physical_height(&self) -> Option { + self.props().physical_height + } - self.set_loc(Some(x), Some(y)); + /// Get whether this output is focused or not. + /// + /// This is currently implemented as the output with the most recent pointer motion. + /// + /// Shorthand for `self.props().focused`. + pub fn focused(&self) -> Option { + self.props().focused + } + + /// Get the tags this output has. + /// + /// Shorthand for `self.props().tags` + pub fn tags(&self) -> Vec { + self.props().tags + } + + /// Get this output's unique name (the name of its connector). + pub fn name(&self) -> &str { + &self.name } } -enum TopOrBottom { - Top, - Bottom, -} - -enum LeftOrRight { - Left, - Right, -} - -/// Horizontal alignment. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum AlignmentHorizontal { - /// Align the outputs such that the left edges are in line. - Left, - /// Center the outputs horizontally. - Center, - /// Align the outputs such that the right edges are in line. - Right, -} - -/// Vertical alignment. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum AlignmentVertical { - /// Align the outputs such that the top edges are in line. - Top, - /// Center the outputs vertically. - Center, - /// Align the outputs such that the bottom edges are in line. - Bottom, +/// The properties of an output. +#[derive(Clone, Debug)] +pub struct OutputProperties { + /// The make of the output + pub make: Option, + /// The model of the output + /// + /// This is something like "27GL83A" or whatever crap monitor manufacturers name their monitors + /// these days. + pub model: Option, + /// The x position of the output in the global space + pub x: Option, + /// The y position of the output in the global space + pub y: Option, + /// The output's screen width in pixels + pub pixel_width: Option, + /// The output's screen height in pixels + pub pixel_height: Option, + /// The output's refresh rate in millihertz + pub refresh_rate: Option, + /// The output's physical width in millimeters + pub physical_width: Option, + /// The output's physical height in millimeters + pub physical_height: Option, + /// Whether this output is focused or not + /// + /// This is currently implemented as the output with the most recent pointer motion. + pub focused: Option, + /// The tags this output has + pub tags: Vec, } diff --git a/api/rust_grpc/src/pinnacle.rs b/api/rust/src/pinnacle.rs similarity index 100% rename from api/rust_grpc/src/pinnacle.rs rename to api/rust/src/pinnacle.rs diff --git a/api/rust/src/process.rs b/api/rust/src/process.rs index ae412a3..121f2ec 100644 --- a/api/rust/src/process.rs +++ b/api/rust/src/process.rs @@ -1,132 +1,178 @@ //! Process management. +//! +//! This module provides [`Process`], which allows you to spawn processes and set environment +//! variables. -use crate::{ - msg::{Args, CallbackId, Msg}, - send_msg, CallbackVec, +use futures::{ + channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, }; +use pinnacle_api_defs::pinnacle::process::v0alpha1::{ + process_service_client::ProcessServiceClient, SetEnvRequest, SpawnRequest, +}; +use tonic::transport::Channel; -/// Spawn a process. -/// -/// This will use Rust's (more specifically `async_process`'s) `Command` to spawn the provided -/// arguments. If you are using any shell syntax like `~`, you may need to spawn a shell -/// instead. If so, you may *also* need to correctly escape the input. -pub fn spawn(command: Vec<&str>) -> anyhow::Result<()> { - let msg = Msg::Spawn { - command: command.into_iter().map(|s| s.to_string()).collect(), - callback_id: None, - }; - - send_msg(msg) +/// A struct containing methods to spawn processes with optional callbacks and set environment +/// variables. +#[derive(Debug, Clone)] +pub struct Process { + channel: Channel, + fut_sender: UnboundedSender>, } -/// Spawn a process only if it isn't already running. -/// -/// This will use Rust's (more specifically `async_process`'s) `Command` to spawn the provided -/// arguments. If you are using any shell syntax like `~`, you may need to spawn a shell -/// instead. If so, you may *also* need to correctly escape the input. -pub fn spawn_once(command: Vec<&str>) -> anyhow::Result<()> { - let msg = Msg::SpawnOnce { - command: command.into_iter().map(|s| s.to_string()).collect(), - callback_id: None, - }; - - send_msg(msg) +/// Optional callbacks to be run when a spawned process prints to stdout or stderr or exits. +pub struct SpawnCallbacks { + /// A callback that will be run when a process prints to stdout with a line + pub stdout: Option>, + /// A callback that will be run when a process prints to stderr with a line + pub stderr: Option>, + /// A callback that will be run when a process exits with a status code and message + #[allow(clippy::type_complexity)] + pub exit: Option, String) + Send>>, } -/// Spawn a process with an optional callback for its stdout, stderr, and exit information. -/// -/// `callback` has the following parameters: -/// - `0`: The process's stdout printed this line. -/// - `1`: The process's stderr printed this line. -/// - `2`: The process exited with this code. -/// - `3`: The process exited with this message. -/// - `4`: A `&mut `[`CallbackVec`] for use inside the closure. -/// -/// You must also pass in a mutable reference to a [`CallbackVec`] in order to store your callback. -pub fn spawn_with_callback<'a, F>( - command: Vec<&str>, - mut callback: F, - callback_vec: &mut CallbackVec<'a>, -) -> anyhow::Result<()> -where - F: FnMut(Option, Option, Option, Option, &mut CallbackVec) + 'a, -{ - let args_callback = move |args: Option, callback_vec: &mut CallbackVec<'_>| { - if let Some(Args::Spawn { - stdout, - stderr, - exit_code, - exit_msg, - }) = args - { - callback(stdout, stderr, exit_code, exit_msg, callback_vec); +impl Process { + pub(crate) fn new( + channel: Channel, + fut_sender: UnboundedSender>, + ) -> Process { + Self { + channel, + fut_sender, } - }; + } - let len = callback_vec.callbacks.len(); - callback_vec.callbacks.push(Box::new(args_callback)); + fn create_process_client(&self) -> ProcessServiceClient { + ProcessServiceClient::new(self.channel.clone()) + } - let msg = Msg::Spawn { - command: command.into_iter().map(|s| s.to_string()).collect(), - callback_id: Some(CallbackId(len as u32)), - }; + /// Spawn a process. + /// + /// Note that windows spawned *before* tags are added will not be displayed. + /// This will be changed in the future to be more like Awesome, where windows with no tags are + /// displayed on every tag instead. + /// + /// # Examples + /// + /// ``` + /// process.spawn(["alacritty"]); + /// process.spawn(["bash", "-c", "swaybg -i ~/path_to_wallpaper"]); + /// ``` + pub fn spawn(&self, args: impl IntoIterator>) { + self.spawn_inner(args, false, None); + } - send_msg(msg) -} - -// TODO: literally copy pasted from above, but will be rewritten so meh -/// Spawn a process with an optional callback for its stdout, stderr, and exit information, -/// only if it isn't already running. -/// -/// `callback` has the following parameters: -/// - `0`: The process's stdout printed this line. -/// - `1`: The process's stderr printed this line. -/// - `2`: The process exited with this code. -/// - `3`: The process exited with this message. -/// - `4`: A `&mut `[`CallbackVec`] for use inside the closure. -/// -/// You must also pass in a mutable reference to a [`CallbackVec`] in order to store your callback. -pub fn spawn_once_with_callback<'a, F>( - command: Vec<&str>, - mut callback: F, - callback_vec: &mut CallbackVec<'a>, -) -> anyhow::Result<()> -where - F: FnMut(Option, Option, Option, Option, &mut CallbackVec) + 'a, -{ - let args_callback = move |args: Option, callback_vec: &mut CallbackVec<'_>| { - if let Some(Args::Spawn { - stdout, - stderr, - exit_code, - exit_msg, - }) = args - { - callback(stdout, stderr, exit_code, exit_msg, callback_vec); - } - }; - - let len = callback_vec.callbacks.len(); - callback_vec.callbacks.push(Box::new(args_callback)); - - let msg = Msg::SpawnOnce { - command: command.into_iter().map(|s| s.to_string()).collect(), - callback_id: Some(CallbackId(len as u32)), - }; - - send_msg(msg) -} - -/// Set an environment variable for Pinnacle. All future processes spawned will have this env set. -/// -/// Note that this will only set the variable for the compositor, not the running config process. -/// If you need to set an environment variable for this config, place them in the `metaconfig.toml` file instead -/// or use [`std::env::set_var`]. -pub fn set_env(key: &str, value: &str) { - let msg = Msg::SetEnv { - key: key.to_string(), - value: value.to_string(), - }; - - send_msg(msg).unwrap(); + /// Spawn a process with callbacks for its stdout, stderr, and exit information. + /// + /// See [`SpawnCallbacks`] for the passed in struct. + /// + /// Note that windows spawned *before* tags are added will not be displayed. + /// This will be changed in the future to be more like Awesome, where windows with no tags are + /// displayed on every tag instead. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::process::SpawnCallbacks; + /// + /// process.spawn_with_callbacks(["alacritty"], SpawnCallbacks { + /// stdout: Some(Box::new(|line| println!("stdout: {line}"))), + /// stderr: Some(Box::new(|line| println!("stderr: {line}"))), + /// stdout: Some(Box::new(|code, msg| println!("exit code: {code:?}, exit_msg: {msg}"))), + /// }); + /// ``` + pub fn spawn_with_callbacks( + &self, + args: impl IntoIterator>, + callbacks: SpawnCallbacks, + ) { + self.spawn_inner(args, false, Some(callbacks)); + } + + /// Spawn a process only if it isn't already running. + /// + /// This is useful for startup programs. + /// + /// See [`Process::spawn`] for details. + pub fn spawn_once(&self, args: impl IntoIterator>) { + self.spawn_inner(args, true, None); + } + + /// Spawn a process only if it isn't already running with optional callbacks for its stdout, + /// stderr, and exit information. + /// + /// This is useful for startup programs. + /// + /// See [`Process::spawn_with_callbacks`] for details. + pub fn spawn_once_with_callbacks( + &self, + args: impl IntoIterator>, + callbacks: SpawnCallbacks, + ) { + self.spawn_inner(args, true, Some(callbacks)); + } + + fn spawn_inner( + &self, + args: impl IntoIterator>, + once: bool, + callbacks: Option, + ) { + let mut client = self.create_process_client(); + + let args = args.into_iter().map(Into::into).collect::>(); + + let request = SpawnRequest { + args, + once: Some(once), + has_callback: Some(callbacks.is_some()), + }; + + self.fut_sender + .unbounded_send( + async move { + let mut stream = client.spawn(request).await.unwrap().into_inner(); + let Some(mut callbacks) = callbacks else { return }; + while let Some(Ok(response)) = stream.next().await { + if let Some(line) = response.stdout { + if let Some(stdout) = callbacks.stdout.as_mut() { + stdout(line); + } + } + if let Some(line) = response.stderr { + if let Some(stderr) = callbacks.stderr.as_mut() { + stderr(line); + } + } + if let Some(exit_msg) = response.exit_message { + if let Some(exit) = callbacks.exit.as_mut() { + exit(response.exit_code, exit_msg); + } + } + } + } + .boxed(), + ) + .unwrap(); + } + + /// Set an environment variable for the compositor. + /// This will cause any future spawned processes to have this environment variable. + /// + /// # Examples + /// + /// ``` + /// process.set_env("ENV", "a value lalala"); + /// ``` + pub fn set_env(&self, key: impl Into, value: impl Into) { + let key = key.into(); + let value = value.into(); + + let mut client = self.create_process_client(); + + block_on(client.set_env(SetEnvRequest { + key: Some(key), + value: Some(value), + })) + .unwrap(); + } } diff --git a/api/rust/src/tag.rs b/api/rust/src/tag.rs index 45714d9..256fdd3 100644 --- a/api/rust/src/tag.rs +++ b/api/rust/src/tag.rs @@ -1,208 +1,528 @@ //! Tag management. +//! +//! This module allows you to interact with Pinnacle's tag system. +//! +//! # The Tag System +//! Many Wayland compositors use workspaces for window management. +//! Each window is assigned to a workspace and will only show up if that workspace is being +//! viewed. This is a find way to manage windows, but it's not that powerful. +//! +//! Instead, Pinnacle works with a tag system similar to window managers like [dwm](https://dwm.suckless.org/) +//! and, the window manager Pinnacle takes inspiration from, [awesome](https://awesomewm.org/). +//! +//! In a tag system, there are no workspaces. Instead, each window can be tagged with zero or more +//! tags, and zero or more tags can be displayed on a monitor at once. This allows you to, for +//! example, bring in your browsers on the same screen as your IDE by toggling the "Browser" tag. +//! +//! Workspaces can be emulated by only displaying one tag at a time. Combining this feature with +//! the ability to tag windows with multiple tags allows you to have one window show up on multiple +//! different "workspaces". As you can see, this system is much more powerful than workspaces +//! alone. +//! +//! # Configuration +//! `tag` contains the [`Tag`] struct, which allows you to add new tags +//! and get handles to already defined ones. +//! +//! These [`TagHandle`]s allow you to manipulate individual tags and get their properties. -use std::collections::HashMap; - -use crate::{ - msg::{Msg, Request, RequestResponse}, - output::{OutputHandle, OutputName}, - request, send_msg, +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, }; -/// Get a tag by its name and output. If `output` is `None`, the currently focused output will -/// be used instead. -/// -/// If multiple tags have the same name, this returns the first one. -pub fn get(name: &str, output: Option<&OutputHandle>) -> Option { - get_all() - .filter(|tag| { - tag.properties().output.is_some_and(|op| match output { - Some(output) => &op == output, - None => Some(op) == crate::output::get_focused(), - }) +use futures::{channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture}; +use num_enum::TryFromPrimitive; +use pinnacle_api_defs::pinnacle::{ + output::v0alpha1::output_service_client::OutputServiceClient, + tag::{ + self, + v0alpha1::{ + tag_service_client::TagServiceClient, AddRequest, RemoveRequest, SetActiveRequest, + SetLayoutRequest, SwitchToRequest, + }, + }, +}; +use tonic::transport::Channel; + +use crate::output::{Output, OutputHandle}; + +/// A struct that allows you to add and remove tags and get [`TagHandle`]s. +#[derive(Clone, Debug)] +pub struct Tag { + channel: Channel, + fut_sender: UnboundedSender>, +} + +impl Tag { + pub(crate) fn new( + channel: Channel, + fut_sender: UnboundedSender>, + ) -> Self { + Self { + channel, + fut_sender, + } + } + + fn create_tag_client(&self) -> TagServiceClient { + TagServiceClient::new(self.channel.clone()) + } + + fn create_output_client(&self) -> OutputServiceClient { + OutputServiceClient::new(self.channel.clone()) + } + + /// Add tags to the specified output. + /// + /// This will add tags with the given names to `output` and return [`TagHandle`]s to all of + /// them. + /// + /// # Examples + /// + /// ``` + /// // Add tags 1-5 to the focused output + /// if let Some(op) = output.get_focused() { + /// let tags = tag.add(&op, ["1", "2", "3", "4", "5"]); + /// } + /// ``` + pub fn add( + &self, + output: &OutputHandle, + tag_names: impl IntoIterator>, + ) -> impl Iterator { + let mut client = self.create_tag_client(); + let output_client = self.create_output_client(); + + let tag_names = tag_names.into_iter().map(Into::into).collect(); + + let response = block_on(client.add(AddRequest { + output_name: Some(output.name.clone()), + tag_names, + })) + .unwrap() + .into_inner(); + + response.tag_ids.into_iter().map(move |id| TagHandle { + client: client.clone(), + output_client: output_client.clone(), + id, }) - .find(|tag| tag.properties().name.is_some_and(|s| s == name)) -} + } -/// Get all tags. -pub fn get_all() -> impl Iterator { - let RequestResponse::Tags { tag_ids } = request(Request::GetTags) else { - unreachable!() - }; + /// Get handles to all tags across all outputs. + /// + /// # Examples + /// + /// ``` + /// let all_tags = tag.get_all(); + /// ``` + pub fn get_all(&self) -> impl Iterator { + let mut client = self.create_tag_client(); + let output_client = self.create_output_client(); - tag_ids.into_iter().map(TagHandle) -} + let response = block_on(client.get(tag::v0alpha1::GetRequest {})) + .unwrap() + .into_inner(); -// TODO: return taghandles here -/// Add tags with the names from `names` to `output`. -pub fn add(output: &OutputHandle, names: &[&str]) { - let msg = Msg::AddTags { - output_name: output.0.clone(), - tag_names: names.iter().map(|s| s.to_string()).collect(), - }; + response.tag_ids.into_iter().map(move |id| TagHandle { + client: client.clone(), + output_client: output_client.clone(), + id, + }) + } - send_msg(msg).unwrap(); -} + /// Get a handle to the first tag with the given name on `output`. + /// + /// If `output` is `None`, the focused output will be used. + /// + /// # Examples + /// + /// ``` + /// // Get tag "1" on output "HDMI-1" + /// if let Some(op) = output.get_by_name("HDMI-1") { + /// let tg = tag.get("1", &op); + /// } + /// + /// // Get tag "Thing" on the focused output + /// let tg = tag.get("Thing", None); + /// ``` + pub fn get<'a>( + &self, + name: impl Into, + output: impl Into>, + ) -> Option { + let name = name.into(); + let output: Option<&OutputHandle> = output.into(); + let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); -/// Create a `LayoutCycler` to cycle layouts on tags. -/// -/// Given a slice of layouts, this will create a `LayoutCycler` with two methods; -/// one will cycle forward the layout for the active tag, and one will cycle backward. -/// -/// # Example -/// ``` -/// todo!() -/// ``` -pub fn layout_cycler(layouts: &[Layout]) -> LayoutCycler { - let indices = std::rc::Rc::new(std::cell::RefCell::new(HashMap::::new())); - let indices_clone = indices.clone(); - let layouts = layouts.to_vec(); - let layouts_clone = layouts.clone(); - let len = layouts.len(); - let next = move |output: Option<&OutputHandle>| { - let Some(output) = output.cloned().or_else(crate::output::get_focused) else { - return; + self.get_all().find(|tag| { + let props = tag.props(); + + let same_tag_name = props.name.as_ref() == Some(&name); + let same_output = props.output.is_some_and(|op| { + Some(op.name) + == output + .map(|o| o.name.clone()) + .or_else(|| output_module.get_focused().map(|o| o.name)) + }); + + same_tag_name && same_output + }) + } + + /// Remove the given tags from their outputs. + /// + /// # Examples + /// + /// ``` + /// let tags = tag.add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]); + /// + /// tag.remove(tags); // "DP-1" no longer has any tags + /// ``` + pub fn remove(&self, tags: impl IntoIterator) { + let tag_ids = tags.into_iter().map(|handle| handle.id).collect::>(); + + let mut client = self.create_tag_client(); + + block_on(client.remove(RemoveRequest { tag_ids })).unwrap(); + } + + /// Create a [`LayoutCycler`] to cycle layouts on outputs. + /// + /// This will create a `LayoutCycler` with two functions: one to cycle forward the layout for + /// the first active tag on the specified output, and one to cycle backward. + /// + /// If you do not specify an output for `LayoutCycler` functions, it will default to the + /// focused output. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::tag::{Layout, LayoutCycler}; + /// use pinnacle_api::xkbcommon::xkb::Keysym; + /// use pinnacle_api::input::Mod; + /// + /// // Create a layout cycler that cycles through the listed layouts + /// let LayoutCycler { + /// prev: layout_prev, + /// next: layout_next, + /// } = tag.new_layout_cycler([ + /// Layout::MasterStack, + /// Layout::Dwindle, + /// Layout::Spiral, + /// Layout::CornerTopLeft, + /// Layout::CornerTopRight, + /// Layout::CornerBottomLeft, + /// Layout::CornerBottomRight, + /// ]); + /// + /// // Cycle layouts forward on the focused output + /// layout_next(None); + /// + /// // Cycle layouts backward on the focused output + /// layout_prev(None); + /// + /// // Cycle layouts forward on "eDP-1" + /// layout_next(output.get_by_name("eDP-1")?); + /// ``` + pub fn new_layout_cycler(&self, layouts: impl IntoIterator) -> LayoutCycler { + let indices = Arc::new(Mutex::new(HashMap::::new())); + let indices_clone = indices.clone(); + + let layouts = layouts.into_iter().collect::>(); + let layouts_clone = layouts.clone(); + let len = layouts.len(); + + let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); + let output_module_clone = output_module.clone(); + + let next = move |output: Option<&OutputHandle>| { + let Some(output) = output.cloned().or_else(|| output_module.get_focused()) else { + return; + }; + + let Some(first_tag) = output + .props() + .tags + .into_iter() + .find(|tag| tag.active() == Some(true)) + else { + return; + }; + + let mut indices = indices.lock().expect("layout next mutex lock failed"); + let index = indices.entry(first_tag.id).or_insert(0); + + if *index + 1 >= len { + *index = 0; + } else { + *index += 1; + } + + first_tag.set_layout(layouts[*index]); }; - let Some(tag) = output - .properties() - .tags - .into_iter() - .find(|tag| tag.properties().active == Some(true)) - else { - return; + let prev = move |output: Option<&OutputHandle>| { + let Some(output) = output + .cloned() + .or_else(|| output_module_clone.get_focused()) + else { + return; + }; + + let Some(first_tag) = output + .props() + .tags + .into_iter() + .find(|tag| tag.active() == Some(true)) + else { + return; + }; + + let mut indices = indices_clone.lock().expect("layout next mutex lock failed"); + let index = indices.entry(first_tag.id).or_insert(0); + + if index.checked_sub(1).is_none() { + *index = len - 1; + } else { + *index -= 1; + } + + first_tag.set_layout(layouts_clone[*index]); }; - let mut indices = indices.borrow_mut(); - let index = indices.entry(tag.0).or_insert(0); - - if *index + 1 >= len { - *index = 0; - } else { - *index += 1; + LayoutCycler { + prev: Box::new(prev), + next: Box::new(next), } - - tag.set_layout(layouts[*index]); - }; - let prev = move |output: Option<&OutputHandle>| { - let Some(output) = output.cloned().or_else(crate::output::get_focused) else { - return; - }; - - let Some(tag) = output - .properties() - .tags - .into_iter() - .find(|tag| tag.properties().active == Some(true)) - else { - return; - }; - - let mut indices = indices_clone.borrow_mut(); - let index = indices.entry(tag.0).or_insert(0); - - if index.wrapping_sub(1) == usize::MAX { - *index = len - 1; - } else { - *index -= 1; - } - - tag.set_layout(layouts_clone[*index]); - }; - - LayoutCycler { - next: Box::new(next), - prev: Box::new(prev), } } -/// A layout cycler that keeps track of tags and their layouts and provides methods to cycle +/// A layout cycler that keeps track of tags and their layouts and provides functions to cycle /// layouts on them. #[allow(clippy::type_complexity)] pub struct LayoutCycler { /// Cycle to the next layout on the given output, or the focused output if `None`. - pub next: Box)>, + pub prev: Box) + Send + Sync + 'static>, /// Cycle to the previous layout on the given output, or the focused output if `None`. - pub prev: Box)>, -} - -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)] -pub(crate) enum TagId { - None, - #[serde(untagged)] - Some(u32), + pub next: Box) + Send + Sync + 'static>, } /// A handle to a tag. -pub struct TagHandle(pub(crate) TagId); - -/// Properties of a tag, retrieved through [`TagHandle::properties`]. -#[derive(Debug)] -pub struct TagProperties { - /// Whether or not the tag is active. - pub active: Option, - /// The tag's name. - pub name: Option, - /// The output the tag is on. - pub output: Option, +/// +/// This handle allows you to do things like switch to tags and get their properties. +#[derive(Debug, Clone)] +pub struct TagHandle { + pub(crate) client: TagServiceClient, + pub(crate) output_client: OutputServiceClient, + pub(crate) id: u32, } -impl TagHandle { - /// Get this tag's [`TagProperties`]. - pub fn properties(&self) -> TagProperties { - let RequestResponse::TagProps { - active, - name, - output_name, - } = request(Request::GetTagProps { tag_id: self.0 }) - else { - unreachable!() - }; - - TagProperties { - active, - name, - output: output_name.map(|name| OutputHandle(OutputName(name))), - } - } - - /// Toggle this tag. - pub fn toggle(&self) { - let msg = Msg::ToggleTag { tag_id: self.0 }; - send_msg(msg).unwrap(); - } - - /// Switch to this tag, deactivating all others on its output. - pub fn switch_to(&self) { - let msg = Msg::SwitchToTag { tag_id: self.0 }; - send_msg(msg).unwrap(); - } - - /// Set this tag's [`Layout`]. - pub fn set_layout(&self, layout: Layout) { - let msg = Msg::SetLayout { - tag_id: self.0, - layout, - }; - - send_msg(msg).unwrap() - } -} - -/// Layouts for tags. -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] +/// Various static layouts. +#[repr(i32)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] pub enum Layout { - /// One master window on the left with all other windows stacked to the right. - MasterStack, - /// Windows split in half towards the bottom right corner. + /// One master window on the left with all other windows stacked to the right + MasterStack = 1, + /// Windows split in half towards the bottom right corner Dwindle, /// Windows split in half in a spiral Spiral, - /// One main corner window in the top left with a column of windows on the right and a row on the bottom. + /// One main corner window in the top left with a column of windows on the right and a row on the bottom CornerTopLeft, - /// One main corner window in the top right with a column of windows on the left and a row on the bottom. + /// One main corner window in the top right with a column of windows on the left and a row on the bottom CornerTopRight, /// One main corner window in the bottom left with a column of windows on the right and a row on the top. CornerBottomLeft, /// One main corner window in the bottom right with a column of windows on the left and a row on the top. CornerBottomRight, } + +impl TagHandle { + /// Activate this tag and deactivate all other ones on the same output. + /// + /// This essentially emulates what a traditional workspace is. + /// + /// # Examples + /// + /// ``` + /// // Assume the focused output has the following inactive tags and windows: + /// // "1": Alacritty + /// // "2": Firefox, Discord + /// // "3": Steam + /// tag.get("2")?.switch_to(); // Displays Firefox and Discord + /// tag.get("3")?.switch_to(); // Displays Steam + /// ``` + pub fn switch_to(&self) { + let mut client = self.client.clone(); + block_on(client.switch_to(SwitchToRequest { + tag_id: Some(self.id), + })) + .unwrap(); + } + + /// Set this tag to active or not. + /// + /// While active, windows with this tag will be displayed. + /// + /// While inactive, windows with this tag will not be displayed unless they have other active + /// tags. + /// + /// # Examples + /// + /// ``` + /// // Assume the focused output has the following inactive tags and windows: + /// // "1": Alacritty + /// // "2": Firefox, Discord + /// // "3": Steam + /// tag.get("2")?.set_active(true); // Displays Firefox and Discord + /// tag.get("3")?.set_active(true); // Displays Firefox, Discord, and Steam + /// tag.get("2")?.set_active(false); // Displays Steam + /// ``` + pub fn set_active(&self, set: bool) { + let mut client = self.client.clone(); + block_on(client.set_active(SetActiveRequest { + tag_id: Some(self.id), + set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Set(set)), + })) + .unwrap(); + } + + /// Toggle this tag between active and inactive. + /// + /// While active, windows with this tag will be displayed. + /// + /// While inactive, windows with this tag will not be displayed unless they have other active + /// tags. + /// + /// # Examples + /// + /// ``` + /// // Assume the focused output has the following inactive tags and windows: + /// // "1": Alacritty + /// // "2": Firefox, Discord + /// // "3": Steam + /// tag.get("2")?.toggle(); // Displays Firefox and Discord + /// tag.get("3")?.toggle(); // Displays Firefox, Discord, and Steam + /// tag.get("3")?.toggle(); // Displays Firefox, Discord + /// tag.get("2")?.toggle(); // Displays nothing + /// ``` + pub fn toggle_active(&self) { + let mut client = self.client.clone(); + block_on(client.set_active(SetActiveRequest { + tag_id: Some(self.id), + set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Toggle(())), + })) + .unwrap(); + } + + /// Remove this tag from its output. + /// + /// # Examples + /// + /// ``` + /// let tags = tag + /// .add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]) + /// .collect::>; + /// + /// tags[1].remove(); + /// tags[3].remove(); + /// // "DP-1" now only has tags "1" and "Buckle" + /// ``` + pub fn remove(mut self) { + block_on(self.client.remove(RemoveRequest { + tag_ids: vec![self.id], + })) + .unwrap(); + } + + /// Set this tag's layout. + /// + /// Layouting only applies to tiled windows (windows that are not floating, maximized, or + /// fullscreen). If multiple tags are active on an output, the first active tag's layout will + /// determine the layout strategy. + /// + /// See [`Layout`] for the different static layouts Pinnacle currently has to offer. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::tag::Layout; + /// + /// // Set the layout of tag "1" on the focused output to "corner top left". + /// tag.get("1", None)?.set_layout(Layout::CornerTopLeft); + /// ``` + pub fn set_layout(&self, layout: Layout) { + let mut client = self.client.clone(); + block_on(client.set_layout(SetLayoutRequest { + tag_id: Some(self.id), + layout: Some(layout as i32), + })) + .unwrap(); + } + + /// Get all properties of this tag. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::tag::TagProperties; + /// + /// let TagProperties { + /// active, + /// name, + /// output, + /// } = tag.get("1", None)?.props(); + /// ``` + pub fn props(&self) -> TagProperties { + let mut client = self.client.clone(); + let output_client = self.output_client.clone(); + + let response = block_on(client.get_properties(tag::v0alpha1::GetPropertiesRequest { + tag_id: Some(self.id), + })) + .unwrap() + .into_inner(); + + TagProperties { + active: response.active, + name: response.name, + output: response.output_name.map(|name| OutputHandle { + client: output_client, + tag_client: client, + name, + }), + } + } + + /// Get this tag's active status. + /// + /// Shorthand for `self.props().active`. + pub fn active(&self) -> Option { + self.props().active + } + + /// Get this tag's name. + /// + /// Shorthand for `self.props().name`. + pub fn name(&self) -> Option { + self.props().name + } + + /// Get a handle to the output this tag is on. + /// + /// Shorthand for `self.props().output`. + pub fn output(&self) -> Option { + self.props().output + } +} + +/// Properties of a tag. +pub struct TagProperties { + /// Whether the tag is active or not + pub active: Option, + /// The name of the tag + pub name: Option, + /// The output the tag is on + pub output: Option, +} diff --git a/api/rust_grpc/src/util.rs b/api/rust/src/util.rs similarity index 100% rename from api/rust_grpc/src/util.rs rename to api/rust/src/util.rs diff --git a/api/rust/src/window.rs b/api/rust/src/window.rs index 8423ebc..c120ce8 100644 --- a/api/rust/src/window.rs +++ b/api/rust/src/window.rs @@ -1,206 +1,530 @@ //! Window management. +//! +//! This module provides [`Window`], which allows you to get [`WindowHandle`]s and move and resize +//! windows using the mouse. +//! +//! [`WindowHandle`]s allow you to do things like resize and move windows, toggle them between +//! floating and tiled, close them, and more. +//! +//! This module also allows you to set window rules; see the [rules] module for more information. + +use futures::executor::block_on; +use num_enum::TryFromPrimitive; +use pinnacle_api_defs::pinnacle::{ + output::v0alpha1::output_service_client::OutputServiceClient, + tag::v0alpha1::tag_service_client::TagServiceClient, + window::v0alpha1::{ + window_service_client::WindowServiceClient, AddWindowRuleRequest, CloseRequest, + MoveToTagRequest, SetTagRequest, + }, + window::{ + self, + v0alpha1::{ + GetRequest, MoveGrabRequest, ResizeGrabRequest, SetFloatingRequest, + SetFullscreenRequest, SetMaximizedRequest, + }, + }, +}; +use tonic::transport::Channel; + +use crate::{input::MouseButton, tag::TagHandle, util::Geometry}; + +use self::rules::{WindowRule, WindowRuleCondition}; pub mod rules; -use crate::{ - input::MouseButton, - msg::{Msg, Request, RequestResponse}, - request, send_msg, - tag::TagHandle, -}; - -/// A unique identifier for each window. -#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub(crate) enum WindowId { - /// A config API returned an invalid window. It should be using this variant. - None, - /// A valid window id. - #[serde(untagged)] - Some(u32), -} - -/// Get all windows with the class `class`. -pub fn get_by_class(class: &str) -> impl Iterator + '_ { - get_all().filter(|win| win.properties().class.as_deref() == Some(class)) -} - -/// Get the currently focused window, or `None` if there isn't one. -pub fn get_focused() -> Option { - get_all().find(|win| win.properties().focused.is_some_and(|focused| focused)) -} - -/// Get all windows. -pub fn get_all() -> impl Iterator { - let RequestResponse::Windows { window_ids } = request(Request::GetWindows) else { - unreachable!() - }; - - window_ids.into_iter().map(WindowHandle) -} - -/// Begin a window move. +/// A struct containing methods that get [`WindowHandle`]s and move windows with the mouse. /// -/// This will start a window move grab with the provided button on the window the pointer -/// is currently hovering over. Once `button` is let go, the move will end. -pub fn begin_move(button: MouseButton) { - let msg = Msg::WindowMoveGrab { - button: button as u32, - }; - - send_msg(msg).unwrap(); +/// See [`WindowHandle`] for more information. +#[derive(Debug, Clone)] +pub struct Window { + channel: Channel, } -/// Begin a window resize. -/// -/// This will start a window resize grab with the provided button on the window the -/// pointer is currently hovering over. Once `button` is let go, the resize will end. -pub fn begin_resize(button: MouseButton) { - let msg = Msg::WindowResizeGrab { - button: button as u32, - }; - - send_msg(msg).unwrap(); -} - -/// A handle to a window. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WindowHandle(WindowId); - -/// Properties of a window, retrieved through [`WindowHandle::properties`]. -#[derive(Debug)] -pub struct WindowProperties { - /// The size of the window, in pixels. - pub size: Option<(i32, i32)>, - /// The location of the window in the global space. - pub loc: Option<(i32, i32)>, - /// The window's class. - pub class: Option, - /// The window's title. - pub title: Option, - /// Whether or not the window is focused. - pub focused: Option, - /// Whether or not the window is floating. - pub floating: Option, - /// Whether the window is fullscreen, maximized, or neither. - pub fullscreen_or_maximized: Option, -} - -impl WindowHandle { - /// Toggle this window between floating and tiled. - pub fn toggle_floating(&self) { - send_msg(Msg::ToggleFloating { window_id: self.0 }).unwrap(); +impl Window { + pub(crate) fn new(channel: Channel) -> Self { + Self { channel } } - /// Toggle this window's fullscreen status. + fn create_window_client(&self) -> WindowServiceClient { + WindowServiceClient::new(self.channel.clone()) + } + + fn create_tag_client(&self) -> TagServiceClient { + TagServiceClient::new(self.channel.clone()) + } + + fn create_output_client(&self) -> OutputServiceClient { + OutputServiceClient::new(self.channel.clone()) + } + + /// Start moving the window with the mouse. /// - /// If used while not fullscreen, it becomes fullscreen. - /// If used while fullscreen, it becomes unfullscreen. - /// If used while maximized, it becomes fullscreen. - pub fn toggle_fullscreen(&self) { - send_msg(Msg::ToggleFullscreen { window_id: self.0 }).unwrap(); - } - - /// Toggle this window's maximized status. + /// This will begin moving the window under the pointer using the specified [`MouseButton`]. + /// The button must be held down at the time this method is called for the move to start. /// - /// If used while not maximized, it becomes maximized. - /// If used while maximized, it becomes unmaximized. - /// If used while fullscreen, it becomes maximized. - pub fn toggle_maximized(&self) { - send_msg(Msg::ToggleMaximized { window_id: self.0 }).unwrap(); - } - - /// Set this window's size. None parameters will be ignored. - pub fn set_size(&self, width: Option, height: Option) { - send_msg(Msg::SetWindowSize { - window_id: self.0, - width, - height, - }) + /// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind]. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; + /// + /// // Set `Super + left click` to begin moving a window + /// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || { + /// window.begin_move(MouseButton::Left); + /// }); + /// ``` + pub fn begin_move(&self, button: MouseButton) { + let mut client = self.create_window_client(); + block_on(client.move_grab(MoveGrabRequest { + button: Some(button as u32), + })) .unwrap(); } - /// Send a close event to this window. - pub fn close(&self) { - send_msg(Msg::CloseWindow { window_id: self.0 }).unwrap(); + /// Start resizing the window with the mouse. + /// + /// This will begin resizing the window under the pointer using the specified [`MouseButton`]. + /// The button must be held down at the time this method is called for the resize to start. + /// + /// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind]. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; + /// + /// // Set `Super + right click` to begin moving a window + /// input.mousebind([Mod::Super], MouseButton::Right, MouseEdge::Press, || { + /// window.begin_resize(MouseButton::Right); + /// }); + /// ``` + pub fn begin_resize(&self, button: MouseButton) { + let mut client = self.create_window_client(); + block_on(client.resize_grab(ResizeGrabRequest { + button: Some(button as u32), + })) + .unwrap(); } - /// Get this window's [`WindowProperties`]. - pub fn properties(&self) -> WindowProperties { - let RequestResponse::WindowProps { - size, - loc, - class, - title, - focused, - floating, - fullscreen_or_maximized, - } = request(Request::GetWindowProps { window_id: self.0 }) - else { - unreachable!() - }; + /// Get all windows. + /// + /// # Examples + /// + /// ``` + /// let windows = window.get_all(); + /// ``` + pub fn get_all(&self) -> impl Iterator { + let mut client = self.create_window_client(); + let tag_client = self.create_tag_client(); + let output_client = self.create_output_client(); + block_on(client.get(GetRequest {})) + .unwrap() + .into_inner() + .window_ids + .into_iter() + .map(move |id| WindowHandle { + client: client.clone(), + id, + tag_client: tag_client.clone(), + output_client: output_client.clone(), + }) + } + + /// Get the currently focused window. + /// + /// # Examples + /// + /// ``` + /// let focused_window = window.get_focused()?; + /// ``` + pub fn get_focused(&self) -> Option { + self.get_all() + .find(|window| matches!(window.props().focused, Some(true))) + } + + /// Add a window rule. + /// + /// A window rule is a set of criteria that a window must open with. + /// For it to apply, a [`WindowRuleCondition`] must evaluate to true for the window in question. + /// + /// TODO: + pub fn add_window_rule(&self, cond: WindowRuleCondition, rule: WindowRule) { + let mut client = self.create_window_client(); + + block_on(client.add_window_rule(AddWindowRuleRequest { + cond: Some(cond.0), + rule: Some(rule.0), + })) + .unwrap(); + } +} + +/// A handle to a window. +/// +/// This allows you to manipulate the window and get its properties. +#[derive(Debug, Clone)] +pub struct WindowHandle { + pub(crate) client: WindowServiceClient, + pub(crate) id: u32, + pub(crate) tag_client: TagServiceClient, + pub(crate) output_client: OutputServiceClient, +} + +/// Whether a window is fullscreen, maximized, or neither. +#[repr(i32)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] +pub enum FullscreenOrMaximized { + /// The window is neither fullscreen nor maximized + Neither = 1, + /// The window is fullscreen + Fullscreen, + /// The window is maximized + Maximized, +} + +/// Properties of a window. +#[derive(Debug, Clone)] +pub struct WindowProperties { + /// The location and size of the window + pub geometry: Option, + /// The window's class + pub class: Option, + /// The window's title + pub title: Option, + /// Whether the window is focused or not + pub focused: Option, + /// Whether the window is floating or not + /// + /// Note that a window can still be floating even if it's fullscreen or maximized; those two + /// state will just override the floating state. + pub floating: Option, + /// Whether the window is fullscreen, maximized, or neither + pub fullscreen_or_maximized: Option, + /// All the tags on the window + pub tags: Vec, +} + +impl WindowHandle { + /// Send a close request to this window. + /// + /// If the window is unresponsive, it may not close. + /// + /// # Examples + /// + /// ``` + /// // Close the focused window + /// window.get_focused()?.close() + /// ``` + pub fn close(mut self) { + block_on(self.client.close(CloseRequest { + window_id: Some(self.id), + })) + .unwrap(); + } + + /// Set this window to fullscreen or not. + /// + /// If it is maximized, setting it to fullscreen will remove the maximized state. + /// + /// # Examples + /// + /// ``` + /// // Set the focused window to fullscreen. + /// window.get_focused()?.set_fullscreen(true); + /// ``` + pub fn set_fullscreen(&self, set: bool) { + let mut client = self.client.clone(); + block_on(client.set_fullscreen(SetFullscreenRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Set( + set, + )), + })) + .unwrap(); + } + + /// Toggle this window between fullscreen and not. + /// + /// If it is maximized, toggling it to fullscreen will remove the maximized state. + /// + /// # Examples + /// + /// ``` + /// // Toggle the focused window to and from fullscreen. + /// window.get_focused()?.toggle_fullscreen(); + /// ``` + pub fn toggle_fullscreen(&self) { + let mut client = self.client.clone(); + block_on(client.set_fullscreen(SetFullscreenRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Toggle(())), + })) + .unwrap(); + } + + /// Set this window to maximized or not. + /// + /// If it is fullscreen, setting it to maximized will remove the fullscreen state. + /// + /// # Examples + /// + /// ``` + /// // Set the focused window to maximized. + /// window.get_focused()?.set_maximized(true); + /// ``` + pub fn set_maximized(&self, set: bool) { + let mut client = self.client.clone(); + block_on(client.set_maximized(SetMaximizedRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Set( + set, + )), + })) + .unwrap(); + } + + /// Toggle this window between maximized and not. + /// + /// If it is fullscreen, setting it to maximized will remove the fullscreen state. + /// + /// # Examples + /// + /// ``` + /// // Toggle the focused window to and from maximized. + /// window.get_focused()?.toggle_maximized(); + /// ``` + pub fn toggle_maximized(&self) { + let mut client = self.client.clone(); + block_on(client.set_maximized(SetMaximizedRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Toggle(())), + })) + .unwrap(); + } + + /// Set this window to floating or not. + /// + /// Floating windows will not be tiled and can be moved around and resized freely. + /// + /// Note that fullscreen and maximized windows can still be floating; those two states will + /// just override the floating state. + /// + /// # Examples + /// + /// ``` + /// // Set the focused window to floating. + /// window.get_focused()?.set_floating(true); + /// ``` + pub fn set_floating(&self, set: bool) { + let mut client = self.client.clone(); + block_on(client.set_floating(SetFloatingRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Set( + set, + )), + })) + .unwrap(); + } + + /// Toggle this window to and from floating. + /// + /// Floating windows will not be tiled and can be moved around and resized freely. + /// + /// Note that fullscreen and maximized windows can still be floating; those two states will + /// just override the floating state. + /// + /// # Examples + /// + /// ``` + /// // Toggle the focused window to and from floating. + /// window.get_focused()?.toggle_floating(); + /// ``` + pub fn toggle_floating(&self) { + let mut client = self.client.clone(); + block_on(client.set_floating(SetFloatingRequest { + window_id: Some(self.id), + set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Toggle( + (), + )), + })) + .unwrap(); + } + + /// Move this window to the given `tag`. + /// + /// This will remove all tags from this window then tag it with `tag`, essentially moving the + /// window to that tag. + /// + /// # Examples + /// + /// ``` + /// // Move the focused window to tag "Code" on the focused output + /// window.get_focused()?.move_to_tag(&tag.get("Code", None)?); + /// ``` + pub fn move_to_tag(&self, tag: &TagHandle) { + let mut client = self.client.clone(); + + block_on(client.move_to_tag(MoveToTagRequest { + window_id: Some(self.id), + tag_id: Some(tag.id), + })) + .unwrap(); + } + + /// Set or unset a tag on this window. + /// + /// # Examples + /// + /// ``` + /// let focused = window.get_focused()?; + /// let tg = tag.get("Potato", None)?; + /// + /// focused.set_tag(&tg, true); // `focused` now has tag "Potato" + /// focused.set_tag(&tg, false); // `focused` no longer has tag "Potato" + /// ``` + pub fn set_tag(&self, tag: &TagHandle, set: bool) { + let mut client = self.client.clone(); + + block_on(client.set_tag(SetTagRequest { + window_id: Some(self.id), + tag_id: Some(tag.id), + set_or_toggle: Some(window::v0alpha1::set_tag_request::SetOrToggle::Set(set)), + })) + .unwrap(); + } + + /// Toggle a tag on this window. + /// + /// # Examples + /// + /// ``` + /// let focused = window.get_focused()?; + /// let tg = tag.get("Potato", None)?; + /// + /// // Assume `focused` does not have tag `tg` + /// + /// focused.toggle_tag(&tg); // `focused` now has tag "Potato" + /// focused.toggle_tag(&tg); // `focused` no longer has tag "Potato" + /// ``` + pub fn toggle_tag(&self, tag: &TagHandle) { + let mut client = self.client.clone(); + + block_on(client.set_tag(SetTagRequest { + window_id: Some(self.id), + tag_id: Some(tag.id), + set_or_toggle: Some(window::v0alpha1::set_tag_request::SetOrToggle::Toggle(())), + })) + .unwrap(); + } + + /// Get all properties of this window. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::WindowProperties; + /// + /// let WindowProperties { + /// geometry, + /// class, + /// title, + /// focused, + /// floating, + /// fullscreen_or_maximized, + /// tags, + /// } = window.get_focused()?.props(); + /// ``` + pub fn props(&self) -> WindowProperties { + let mut client = self.client.clone(); + let tag_client = self.tag_client.clone(); + let response = block_on( + client.get_properties(window::v0alpha1::GetPropertiesRequest { + window_id: Some(self.id), + }), + ) + .unwrap() + .into_inner(); + + let fullscreen_or_maximized = response + .fullscreen_or_maximized + .unwrap_or_default() + .try_into() + .ok(); + + let geometry = response.geometry.map(|geo| Geometry { + x: geo.x(), + y: geo.y(), + width: geo.width() as u32, + height: geo.height() as u32, + }); WindowProperties { - size, - loc, - class, - title, - focused, - floating, + geometry, + class: response.class, + title: response.title, + focused: response.focused, + floating: response.floating, fullscreen_or_maximized, + tags: response + .tag_ids + .into_iter() + .map(|id| TagHandle { + client: tag_client.clone(), + output_client: self.output_client.clone(), + id, + }) + .collect(), } } - /// Toggle `tag` on this window. - pub fn toggle_tag(&self, tag: &TagHandle) { - let msg = Msg::ToggleTagOnWindow { - window_id: self.0, - tag_id: tag.0, - }; - - send_msg(msg).unwrap(); + /// Get this window's location and size. + /// + /// Shorthand for `self.props().geometry`. + pub fn geometry(&self) -> Option { + self.props().geometry } - /// Move this window to `tag`. + /// Get this window's class. /// - /// This will remove all other tags on this window. - pub fn move_to_tag(&self, tag: &TagHandle) { - let msg = Msg::MoveWindowToTag { - window_id: self.0, - tag_id: tag.0, - }; + /// Shorthand for `self.props().class`. + pub fn class(&self) -> Option { + self.props().class + } - send_msg(msg).unwrap(); + /// Get this window's title. + /// + /// Shorthand for `self.props().title`. + pub fn title(&self) -> Option { + self.props().title + } + + /// Get whether or not this window is focused. + /// + /// Shorthand for `self.props().focused`. + pub fn focused(&self) -> Option { + self.props().focused + } + + /// Get whether or not this window is floating. + /// + /// Shorthand for `self.props().floating`. + pub fn floating(&self) -> Option { + self.props().floating + } + + /// Get whether this window is fullscreen, maximized, or neither. + /// + /// Shorthand for `self.props().fullscreen_or_maximized`. + pub fn fullscreen_or_maximized(&self) -> Option { + self.props().fullscreen_or_maximized + } + + /// Get all the tags on this window. + /// + /// Shorthand for `self.props().tags`. + pub fn tags(&self) -> Vec { + self.props().tags } } - -/// Whether or not a window is floating or tiled. -#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize)] -pub enum FloatingOrTiled { - /// The window is floating. - /// - /// It can be freely moved around and resized and will not respond to layouts. - Floating, - /// The window is tiled. - /// - /// It cannot be resized and can only move by swapping places with other tiled windows. - Tiled, -} - -/// Whether the window is fullscreen, maximized, or neither. -/// -/// These three states are mutually exclusive. Setting a window to maximized while it is fullscreen -/// will make it stop being fullscreen and vice versa. -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum FullscreenOrMaximized { - /// The window is not fullscreen or maximized. - Neither, - /// The window is fullscreen. - /// - /// It will be the only rendered window on screen and will fill the output it resides on. - /// Layer surfaces will also not be rendered while a window is fullscreen. - Fullscreen, - /// The window is maximized. - /// - /// It will fill up as much space on its output as it can, respecting any layer surfaces. - Maximized, -} diff --git a/api/rust/src/window/rules.rs b/api/rust/src/window/rules.rs index e2b2d17..86e5a0a 100644 --- a/api/rust/src/window/rules.rs +++ b/api/rust/src/window/rules.rs @@ -1,83 +1,222 @@ -//! Window rules. +//! Types for window rules. +//! +//! A window rule is a way to set the properties of a window on open. +//! +//! They are comprised of two parts: the [condition][WindowRuleCondition] and the actual [rule][WindowRule]. +//! +//! # [`WindowRuleCondition`]s +//! `WindowRuleCondition`s are conditions that the window needs to open with in order to apply a +//! rule. For example, you may want to set a window to maximized if it has the class "steam", or +//! you might want to open all Firefox instances on tag "3". +//! +//! To do this, you must build a `WindowRuleCondition` to tell the compositor when to apply any +//! rules. +//! +//! ## Building `WindowRuleCondition`s +//! A condition is created through [`WindowRuleCondition::new`]: +//! ``` +//! let cond = WindowRuleCondition::new(); +//! ``` +//! +//! In order to understand conditions, you must understand the concept of "any" and "all". +//! +//! **"Any"** +//! +//! "Any" conditions only need one of their constituent items to be true for the whole condition to +//! evaluate to true. Think of it as one big `if a || b || c || d || ... {}` block. +//! +//! **"All"** +//! +//! "All" conditions need *all* of their constituent items to be true for the condition to evaluate +//! to true. This is like a big `if a && b && c && d && ... {}` block. +//! +//! Note that any items in a top level `WindowRuleCondition` fall under "all", so all those items +//! must be true. +//! +//! With that out of the way, we can get started building conditions. +//! +//! ### `WindowRuleCondition::classes` +//! With [`WindowRuleCondition::classes`], you can specify what classes a window needs to have for +//! a rule to apply. +//! +//! The following will apply to windows with the class "firefox": +//! ``` +//! let cond = WindowRuleCondition::new().classes(["firefox"]); +//! ``` +//! +//! Note that you pass in some `impl IntoIterator>`. This means you can +//! pass in more than one class here: +//! ``` +//! let failing_cond = WindowRuleCondition::new().classes(["firefox", "steam"]); +//! ``` +//! *HOWEVER*: this will not work. Recall that top level conditions are implicitly "all". This +//! means the above would require windows to have *both classes*, which is impossible. Thus, the +//! condition above will never be true. +//! +//! ### `WindowRuleCondition::titles` +//! Like `classes`, you can use `titles` to specify that the window needs to open with a specific +//! title for the condition to apply. +//! +//! ``` +//! let cond = WindowRuleCondition::new().titles(["Steam"]); +//! ``` +//! +//! Like `classes`, passing in multiple titles at the top level will cause the condition to always +//! fail. +//! +//! ### `WindowRuleCondition::tags` +//! You can specify that the window needs to open on the given tags in order to apply a rule. +//! +//! ``` +//! let cond = WindowRuleCondition::new().tags([&tag.get("3", output.get_by_name("HDMI-1")?)?]); +//! ``` +//! +//! Here, if you have tag "3" active on "HDMI-1" and spawn a window on that output, this condition +//! will apply. +//! +//! Unlike `classes` and `titles`, you can specify multiple tags at the top level: +//! +//! ``` +//! let op = output.get_by_name("HDMI-1")?; +//! let tag1 = tag.get("1", &op)?; +//! let tag2 = tag.get("2", &op)?; +//! +//! let cond = WindowRuleCondition::new().tags([&tag1, &tag2]); +//! ``` +//! +//! Now, you must have both tags "1" and "2" active and spawn a window for the condition to apply. +//! +//! ### `WindowRuleCondition::any` +//! Now we can get to ways to compose more complex conditions. +//! +//! `WindowRuleCondition::any` takes in conditions and will evaluate to true if *anything* in those +//! conditions are true. +//! +//! ``` +//! let cond = WindowRuleCondition::new() +//! .any([ +//! WindowRuleCondition::new().classes(["Alacritty"]), +//! WindowRuleCondition::new().tags([&tag.get("2", None)?]), +//! ]); +//! ``` +//! +//! This condition will apply if the window is *either* "Alacritty" *or* opens on tag "2". +//! +//! ### `WindowRuleCondition::all` +//! With `WindowRuleCondition::all`, *all* specified conditions must be true for the condition to +//! be true. +//! +//! ``` +//! let cond = WindowRuleCondition::new() +//! .all([ +//! WindowRuleCondition::new().classes(["Alacritty"]), +//! WindowRuleCondition::new().tags([&tag.get("2", None)?]), +//! ]); +//! ``` +//! +//! This condition applies if the window has the class "Alacritty" *and* opens on tag "2". +//! +//! You can write the above a bit shorter, as top level conditions are already "all": +//! +//! ``` +//! let cond = WindowRuleCondition::new() +//! .classes(["Alacritty"]) +//! .tags([&tag.get("2", None)?]); +//! ``` +//! +//! ## Complex condition composition +//! You can arbitrarily nest `any` and `all` to achieve desired logic. +//! +//! ``` +//! let op = output.get_by_name("HDMI-1")?; +//! let tag1 = tag.get("1", &op)?; +//! let tag2 = tag.get("2", &op)?; +//! +//! let complex_cond = WindowRuleCondition::new() +//! .any([ +//! WindowRuleCondition::new().all([ +//! WindowRuleCondition::new() +//! .classes("Alacritty") +//! .tags([&tag1, &tag2]) +//! ]), +//! WindowRuleCondition::new().all([ +//! WindowRuleCondition::new().any([ +//! WindowRuleCondition::new().titles(["nvim", "emacs", "nano"]), +//! ]), +//! WindowRuleCondition::new().any([ +//! WindowRuleCondition::new().tags([&tag1, &tag2]), +//! ]), +//! ]) +//! ]) +//! ``` +//! +//! The above is true if either of the following are true: +//! - The window has class "Alacritty" and opens on both tags "1" and "2", or +//! - The window's class is either "nvim", "emacs", or "nano" *and* it opens on either tag "1" or +//! "2". +//! +//! # [`WindowRule`]s +//! `WindowRuleCondition`s are half of a window rule. The other half is the [`WindowRule`] itself. +//! +//! A `WindowRule` is what will apply to a window if a condition is true. +//! +//! ## Building `WindowRule`s +//! +//! Create a new window rule with [`WindowRule::new`]: +//! +//! ``` +//! let rule = WindowRule::new(); +//! ``` +//! +//! There are several rules you can set currently. +//! +//! ### [`WindowRule::output`] +//! This will cause the window to open on the specified output. +//! +//! ### [`WindowRule::tags`] +//! This will cause the window to open with the given tags. +//! +//! ### [`WindowRule::floating`] +//! This will cause the window to open either floating or tiled. +//! +//! ### [`WindowRule::fullscreen_or_maximized`] +//! This will cause the window to open either fullscreen, maximized, or neither. +//! +//! ### [`WindowRule::x`] +//! This will cause the window to open at the given x-coordinate. +//! +//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by +//! layouting. +//! +//! ### [`WindowRule::y`] +//! This will cause the window to open at the given y-coordinate. +//! +//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by +//! layouting. +//! +//! ### [`WindowRule::width`] +//! This will cause the window to open with the given width in pixels. +//! +//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by +//! layouting. +//! +//! ### [`WindowRule::height`] +//! This will cause the window to open with the given height in pixels. +//! +//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by +//! layouting. -use std::num::NonZeroU32; +use pinnacle_api_defs::pinnacle::window; -use crate::{msg::Msg, output::OutputHandle, send_msg, tag::TagHandle}; +use crate::{output::OutputHandle, tag::TagHandle}; -use super::{FloatingOrTiled, FullscreenOrMaximized}; - -/// Add a window rule. -pub fn add(cond: WindowRuleCondition, rule: WindowRule) { - let msg = Msg::AddWindowRule { - cond: cond.0, - rule: rule.0, - }; - - send_msg(msg).unwrap(); -} - -/// A window rule. -/// -/// This is what will be applied to a window if it meets a [`WindowRuleCondition`]. -/// -/// `WindowRule`s are built using the builder pattern. -/// // TODO: show example -#[derive(Default)] -pub struct WindowRule(crate::msg::WindowRule); - -impl WindowRule { - /// Create a new, empty window rule. - pub fn new() -> Self { - Default::default() - } - - /// This rule will force windows to open on the provided `output`. - pub fn output(mut self, output: &OutputHandle) -> Self { - self.0.output = Some(output.0.clone()); - self - } - - /// This rule will force windows to open with the provided `tags`. - pub fn tags(mut self, tags: &[TagHandle]) -> Self { - self.0.tags = Some(tags.iter().map(|tag| tag.0).collect()); - self - } - - /// This rule will force windows to open either floating or tiled. - pub fn floating_or_tiled(mut self, floating_or_tiled: FloatingOrTiled) -> Self { - self.0.floating_or_tiled = Some(floating_or_tiled); - self - } - - /// This rule will force windows to open either fullscreen, maximized, or neither. - pub fn fullscreen_or_maximized( - mut self, - fullscreen_or_maximized: FullscreenOrMaximized, - ) -> Self { - self.0.fullscreen_or_maximized = Some(fullscreen_or_maximized); - self - } - - /// This rule will force windows to open with a specific size. - /// - /// This will only actually be visible if the window is also floating. - pub fn size(mut self, width: NonZeroU32, height: NonZeroU32) -> Self { - self.0.size = Some((width, height)); - self - } - - /// This rule will force windows to open at a specific location. - /// - /// This will only actually be visible if the window is also floating. - pub fn location(mut self, x: i32, y: i32) -> Self { - self.0.location = Some((x, y)); - self - } -} +use super::FullscreenOrMaximized; /// A condition for a [`WindowRule`] to apply to a window. -#[derive(Default, Debug)] -pub struct WindowRuleCondition(crate::msg::WindowRuleCondition); +/// +/// `WindowRuleCondition`s are built using the builder pattern. +#[derive(Default, Debug, Clone)] +pub struct WindowRuleCondition(pub(super) window::v0alpha1::WindowRuleCondition); impl WindowRuleCondition { /// Create a new, empty `WindowRuleCondition`. @@ -86,14 +225,41 @@ impl WindowRuleCondition { } /// This condition requires that at least one provided condition is true. - pub fn any(mut self, conds: &[WindowRuleCondition]) -> Self { - self.0.cond_any = Some(conds.iter().map(|cond| cond.0.clone()).collect()); + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// // `cond` will be true if the window opens with *either* class "Alacritty" or "firefox" + /// // *or* with title "Steam" + /// let cond = WindowRuleCondition::new() + /// .any([ + /// WindowRuleCondition::new().classes(["Alacritty", "firefox"]), + /// WindowRuleCondition::new().titles(["Steam"]). + /// ]); + /// ``` + pub fn any(mut self, conds: impl IntoIterator) -> Self { + self.0.any = conds.into_iter().map(|cond| cond.0).collect(); self } /// This condition requires that all provided conditions are true. - pub fn all(mut self, conds: &[WindowRuleCondition]) -> Self { - self.0.cond_all = Some(conds.iter().map(|cond| cond.0.clone()).collect()); + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// // `cond` will be true if the window opens with class "Alacritty" *and* on tag "1" + /// let cond = WindowRuleCondition::new() + /// .any([ + /// WindowRuleCondition::new().tags([tag.get("1", None)?]), + /// WindowRuleCondition::new().titles(["Alacritty"]). + /// ]); + /// ``` + pub fn all(mut self, conds: impl IntoIterator) -> Self { + self.0.all = conds.into_iter().map(|cond| cond.0).collect(); self } @@ -104,8 +270,26 @@ impl WindowRuleCondition { /// /// When used in [`WindowRuleCondition::any`], at least one of the /// provided classes must match. - pub fn class(mut self, classes: &[&str]) -> Self { - self.0.class = Some(classes.iter().map(|s| s.to_string()).collect()); + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// // `cond` will be true if the window opens with class "Alacritty" + /// let cond = WindowRuleCondition::new().classes(["Alacritty"]); + /// + /// // Top level conditions need all items to be true, + /// // so the following will never be true as windows can't have two classes at once + /// let always_false = WindowRuleCondition::new().classes(["Alacritty", "firefox"]); + /// + /// // To make the above work, use [`WindowRuleCondition::any`]. + /// // The following will be true if the window is "Alacritty" or "firefox" + /// let any_class = WindowRuleCondition::new() + /// .any([ WindowRuleCondition::new().classes(["Alacritty", "firefox"]) ]); + /// ``` + pub fn classes(mut self, classes: impl IntoIterator>) -> Self { + self.0.classes = classes.into_iter().map(Into::into).collect(); self } @@ -116,8 +300,26 @@ impl WindowRuleCondition { /// /// When used in [`WindowRuleCondition::any`], at least one of the /// provided titles must match. - pub fn title(mut self, titles: &[&str]) -> Self { - self.0.title = Some(titles.iter().map(|s| s.to_string()).collect()); + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// // `cond` will be true if the window opens with title "vim" + /// let cond = WindowRuleCondition::new().titles(["vim"]); + /// + /// // Top level conditions need all items to be true, + /// // so the following will never be true as windows can't have two titles at once + /// let always_false = WindowRuleCondition::new().titles(["vim", "emacs"]); + /// + /// // To make the above work, use [`WindowRuleCondition::any`]. + /// // The following will be true if the window has the title "vim" or "emacs" + /// let any_title = WindowRuleCondition::new() + /// .any([WindowRuleCondition::new().titles(["vim", "emacs"])]); + /// ``` + pub fn titles(mut self, titles: impl IntoIterator>) -> Self { + self.0.titles = titles.into_iter().map(Into::into).collect(); self } @@ -128,8 +330,192 @@ impl WindowRuleCondition { /// /// When used in [`WindowRuleCondition::any`], the window must open on at least /// one of the given tags. - pub fn tag(mut self, tags: &[TagHandle]) -> Self { - self.0.tag = Some(tags.iter().map(|tag| tag.0).collect()); + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRuleCondition; + /// + /// let tag1 = tag.get("1", None)?; + /// let tag2 = tag.get("2", None)?; + /// + /// // `cond` will be true if the window opens with tag "1" + /// let cond = WindowRuleCondition::new().tags([&tag1]); + /// + /// // Top level conditions need all items to be true, + /// // so the following will be true if the window opens with both tags "1" and "2" + /// let all_tags = WindowRuleCondition::new().tags([&tag1, &tag2]); + /// + /// // This does the same as the above + /// let all_tags = WindowRuleCondition::new() + /// .all([WindowRuleCondition::new().tags([&tag1, &tag2])]); + /// + /// // The following will be true if the window opens with *either* tag "1" or "2" + /// let any_tag = WindowRuleCondition::new() + /// .any([WindowRuleCondition::new().tags([&tag1, &tag2])]); + /// ``` + pub fn tags<'a>(mut self, tags: impl IntoIterator) -> Self { + self.0.tags = tags.into_iter().map(|tag| tag.id).collect(); + self + } +} + +/// A window rule. +/// +/// This is what will be applied to a window if it meets a [`WindowRuleCondition`]. +/// +/// `WindowRule`s are built using the builder pattern. +#[derive(Clone, Debug, Default)] +pub struct WindowRule(pub(super) window::v0alpha1::WindowRule); + +impl WindowRule { + /// Create a new, empty window rule. + pub fn new() -> Self { + Default::default() + } + + /// This rule will force windows to open on the provided `output`. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open on "HDMI-1" + /// let rule = WindowRule::new().output(output.get_by_name("HDMI-1")?); + /// ``` + pub fn output(mut self, output: &OutputHandle) -> Self { + self.0.output = Some(output.name.clone()); + self + } + + /// This rule will force windows to open with the provided `tags`. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// let op = output.get_by_name("HDMI-1")?; + /// let tag1 = tag.get("1", &op)?; + /// let tag2 = tag.get("2", &op)?; + /// + /// // Force the window to open with tags "1" and "2" + /// let rule = WindowRule::new().tags([&tag1, &tag2]); + /// ``` + pub fn tags<'a>(mut self, tags: impl IntoIterator) -> Self { + self.0.tags = tags.into_iter().map(|tag| tag.id).collect(); + self + } + + /// This rule will force windows to open either floating or not. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open floating + /// let rule = WindowRule::new().floating(true); + /// + /// // Force the window to open tiled + /// let rule = WindowRule::new().floating(false); + /// ``` + pub fn floating(mut self, floating: bool) -> Self { + self.0.floating = Some(floating); + self + } + + /// This rule will force windows to open either fullscreen, maximized, or neither. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// use pinnacle_api::window::FullscreenOrMaximized; + /// + /// // Force the window to open fullscreen + /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Fullscreen); + /// + /// // Force the window to open maximized + /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Maximized); + /// + /// // Force the window to open not fullscreen nor maximized + /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Neither); + /// ``` + pub fn fullscreen_or_maximized( + mut self, + fullscreen_or_maximized: FullscreenOrMaximized, + ) -> Self { + self.0.fullscreen_or_maximized = Some(fullscreen_or_maximized as i32); + self + } + + /// This rule will force windows to open at a specific x-coordinate. + /// + /// This will only actually be visible if the window is also floating. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open at x = 480 + /// let rule = WindowRule::new().x(480); + /// ``` + pub fn x(mut self, x: i32) -> Self { + self.0.x = Some(x); + self + } + + /// This rule will force windows to open at a specific y-coordinate. + /// + /// This will only actually be visible if the window is also floating. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open at y = 240 + /// let rule = WindowRule::new().y(240); + /// ``` + pub fn y(mut self, y: i32) -> Self { + self.0.y = Some(y); + self + } + + /// This rule will force windows to open with a specific width. + /// + /// This will only actually be visible if the window is also floating. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open with a width of 500 pixels + /// let rule = WindowRule::new().width(500); + /// ``` + pub fn width(mut self, width: u32) -> Self { + self.0.width = Some(width as i32); + self + } + + /// This rule will force windows to open with a specific height. + /// + /// This will only actually be visible if the window is also floating. + /// + /// # Examples + /// + /// ``` + /// use pinnacle_api::window::rules::WindowRule; + /// + /// // Force the window to open with a height of 250 pixels + /// let rule = WindowRule::new().height(250); + /// ``` + pub fn height(mut self, height: u32) -> Self { + self.0.height = Some(height as i32); self } } diff --git a/api/rust_grpc/Cargo.toml b/api/rust_grpc/Cargo.toml deleted file mode 100644 index 0d0caf2..0000000 --- a/api/rust_grpc/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "pinnacle-api" -version = "0.0.2" -edition = "2021" -authors = ["Ottatop "] -description = "The Rust implementation of the Pinnacle compositor's configuration API" -license = "MPL-2.0" -repository = "https://github.com/pinnacle-comp/pinnacle" -keywords = ["compositor", "pinnacle", "api", "config"] -categories = ["api-bindings", "config"] - -[dependencies] -pinnacle-api-defs = { path = "../../pinnacle-api-defs" } -pinnacle-api-macros = { path = "./pinnacle-api-macros" } -tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "net"] } -async-net = "2.0.0" -async-compat = "0.2.3" -tonic = "0.10.2" -tower = { version = "0.4.13", features = ["util"] } -futures = "0.3.30" -num_enum = "0.7.2" -xkbcommon = "0.7.0" - -[workspace] -members = ["pinnacle-api-macros"] diff --git a/api/rust_grpc/src/input.rs b/api/rust_grpc/src/input.rs deleted file mode 100644 index 5127c0a..0000000 --- a/api/rust_grpc/src/input.rs +++ /dev/null @@ -1,383 +0,0 @@ -//! Input management. -//! -//! This module provides [`Input`], a struct that gives you several different -//! methods for setting key- and mousebinds, changing xkeyboard settings, and more. -//! View the struct's documentation for more information. - -use futures::{ - channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, -}; -use num_enum::TryFromPrimitive; -use pinnacle_api_defs::pinnacle::input::{ - self, - v0alpha1::{ - input_service_client::InputServiceClient, - set_libinput_setting_request::{CalibrationMatrix, Setting}, - SetKeybindRequest, SetLibinputSettingRequest, SetMousebindRequest, SetRepeatRateRequest, - SetXkbConfigRequest, - }, -}; -use tonic::transport::Channel; -use xkbcommon::xkb::Keysym; - -use self::libinput::LibinputSetting; - -pub mod libinput; - -/// A mouse button. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] -pub enum MouseButton { - /// The left mouse button - Left = 0x110, - /// The right mouse button - Right = 0x111, - /// The middle mouse button - Middle = 0x112, - /// The side mouse button - Side = 0x113, - /// The extra mouse button - Extra = 0x114, - /// The forward mouse button - Forward = 0x115, - /// The backward mouse button - Back = 0x116, -} - -/// Keyboard modifiers. -#[repr(i32)] -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] -pub enum Mod { - /// The shift key - Shift = 1, - /// The ctrl key - Ctrl, - /// The alt key - Alt, - /// The super key, aka meta, win, mod4 - Super, -} - -/// Press or release. -#[repr(i32)] -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] -pub enum MouseEdge { - /// Perform actions on button press - Press = 1, - /// Perform actions on button release - Release, -} - -/// A struct that lets you define xkeyboard config options. -/// -/// See `xkeyboard-config(7)` for more information. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] -pub struct XkbConfig { - /// Files of rules to be used for keyboard mapping composition - pub rules: Option<&'static str>, - /// Name of the model of your keyboard type - pub model: Option<&'static str>, - /// Layout(s) you intend to use - pub layout: Option<&'static str>, - /// Variant(s) of the layout you intend to use - pub variant: Option<&'static str>, - /// Extra xkb configuration options - pub options: Option<&'static str>, -} - -/// The `Input` struct. -/// -/// This struct contains methods that allow you to set key- and mousebinds, -/// change xkeyboard and libinput settings, and change the keyboard's repeat rate. -#[derive(Debug, Clone)] -pub struct Input { - channel: Channel, - fut_sender: UnboundedSender>, -} - -impl Input { - pub(crate) fn new( - channel: Channel, - fut_sender: UnboundedSender>, - ) -> Self { - Self { - channel, - fut_sender, - } - } - - fn create_input_client(&self) -> InputServiceClient { - InputServiceClient::new(self.channel.clone()) - } - - /// Set a keybind. - /// - /// If called with an already set keybind, it gets replaced. - /// - /// You must supply: - /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger. - /// - `key`: The key that needs to be pressed. This can be anything that implements the [Key] trait: - /// - `char` - /// - `&str` and `String`: This is any name from - /// [xkbcommon-keysyms.h](https://xkbcommon.org/doc/current/xkbcommon-keysyms_8h.html) - /// without the `XKB_KEY_` prefix. - /// - `u32`: The numerical key code from the website above. - /// - A [`keysym`][Keysym] from the [`xkbcommon`] re-export. - /// - `action`: A closure that will be run when the keybind is triggered. - /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate - /// something, consider using channels or [`Box::leak`]. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::input::Mod; - /// - /// // Set `Super + Shift + c` to close the focused window - /// input.keybind([Mod::Super, Mod::Shift], 'c', || { - /// if let Some(win) = window.get_focused() { - /// win.close(); - /// } - /// }); - /// - /// // With a string key - /// input.keybind([], "BackSpace", || { /* ... */ }); - /// - /// // With a numeric key - /// input.keybind([], 65, || { /* ... */ }); // 65 = 'A' - /// - /// // With a `Keysym` - /// input.keybind([], pinnacle_api::xkbcommon::xkb::Keysym::Return, || { /* ... */ }); - /// ``` - pub fn keybind( - &self, - mods: impl IntoIterator, - key: impl Key + Send + 'static, - mut action: impl FnMut() + Send + 'static, - ) { - let mut client = self.create_input_client(); - - let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); - - self.fut_sender - .unbounded_send( - async move { - let mut stream = client - .set_keybind(SetKeybindRequest { - modifiers, - key: Some(input::v0alpha1::set_keybind_request::Key::RawCode( - key.into_keysym().raw(), - )), - }) - .await - .unwrap() - .into_inner(); - - while let Some(Ok(_response)) = stream.next().await { - action(); - } - } - .boxed(), - ) - .unwrap(); - } - - /// Set a mousebind. - /// - /// If called with an already set mousebind, it gets replaced. - /// - /// You must supply: - /// - `mods`: A list of [`Mod`]s. These must be held down for the keybind to trigger. - /// - `button`: A [`MouseButton`]. - /// - `edge`: A [`MouseEdge`]. This allows you to trigger the bind on either mouse press or release. - /// - `action`: A closure that will be run when the mousebind is triggered. - /// - Currently, any captures must be both `Send` and `'static`. If you want to mutate - /// something, consider using channels or [`Box::leak`]. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; - /// - /// // Set `Super + left click` to start moving a window - /// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || { - /// window.begin_move(MouseButton::Press); - /// }); - /// ``` - pub fn mousebind( - &self, - mods: impl IntoIterator, - button: MouseButton, - edge: MouseEdge, - mut action: impl FnMut() + 'static + Send, - ) { - let mut client = self.create_input_client(); - - let modifiers = mods.into_iter().map(|modif| modif as i32).collect(); - - self.fut_sender - .unbounded_send( - async move { - let mut stream = client - .set_mousebind(SetMousebindRequest { - modifiers, - button: Some(button as u32), - edge: Some(edge as i32), - }) - .await - .unwrap() - .into_inner(); - - while let Some(Ok(_response)) = stream.next().await { - action(); - } - } - .boxed(), - ) - .unwrap(); - } - - /// Set the xkeyboard config. - /// - /// This allows you to set several xkeyboard options like `layout` and `rules`. - /// - /// See `xkeyboard-config(7)` for more information. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::input::XkbConfig; - /// - /// input.set_xkb_config(Xkbconfig { - /// layout: Some("us,fr,ge"), - /// options: Some("ctrl:swapcaps,caps:shift"), - /// ..Default::default() - /// }); - /// ``` - pub fn set_xkb_config(&self, xkb_config: XkbConfig) { - let mut client = self.create_input_client(); - - block_on(client.set_xkb_config(SetXkbConfigRequest { - rules: xkb_config.rules.map(String::from), - variant: xkb_config.variant.map(String::from), - layout: xkb_config.layout.map(String::from), - model: xkb_config.model.map(String::from), - options: xkb_config.options.map(String::from), - })) - .unwrap(); - } - - /// Set the keyboard's repeat rate. - /// - /// This allows you to set the time between holding down a key and it repeating - /// as well as the time between each repeat. - /// - /// Units are in milliseconds. - /// - /// # Examples - /// - /// ``` - /// // Set keyboard to repeat after holding down for half a second, - /// // and repeat once every 25ms (40 times a second) - /// input.set_repeat_rate(25, 500); - /// ``` - pub fn set_repeat_rate(&self, rate: i32, delay: i32) { - let mut client = self.create_input_client(); - - block_on(client.set_repeat_rate(SetRepeatRateRequest { - rate: Some(rate), - delay: Some(delay), - })) - .unwrap(); - } - - /// Set a libinput setting. - /// - /// From [freedesktop.org](https://www.freedesktop.org/wiki/Software/libinput/): - /// > libinput is a library to handle input devices in Wayland compositors - /// - /// As such, this method allows you to set various settings related to input devices. - /// This includes things like pointer acceleration and natural scrolling. - /// - /// See [`LibinputSetting`] for all the settings you can change. - /// - /// Note: currently Pinnacle applies anything set here to *every* device, regardless of what it - /// actually is. This will be fixed in the future. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::input::libinput::*; - /// - /// // Set pointer acceleration to flat - /// input.set_libinput_setting(LibinputSetting::AccelProfile(AccelProfile::Flat)); - /// - /// // Enable natural scrolling (reverses scroll direction; usually used with trackpads) - /// input.set_libinput_setting(LibinputSetting::NaturalScroll(true)); - /// ``` - pub fn set_libinput_setting(&self, setting: LibinputSetting) { - let mut client = self.create_input_client(); - - let setting = match setting { - LibinputSetting::AccelProfile(profile) => Setting::AccelProfile(profile as i32), - LibinputSetting::AccelSpeed(speed) => Setting::AccelSpeed(speed), - LibinputSetting::CalibrationMatrix(matrix) => { - Setting::CalibrationMatrix(CalibrationMatrix { - matrix: matrix.to_vec(), - }) - } - LibinputSetting::ClickMethod(method) => Setting::ClickMethod(method as i32), - LibinputSetting::DisableWhileTyping(disable) => Setting::DisableWhileTyping(disable), - LibinputSetting::LeftHanded(enable) => Setting::LeftHanded(enable), - LibinputSetting::MiddleEmulation(enable) => Setting::MiddleEmulation(enable), - LibinputSetting::RotationAngle(angle) => Setting::RotationAngle(angle), - LibinputSetting::ScrollButton(button) => Setting::RotationAngle(button), - LibinputSetting::ScrollButtonLock(enable) => Setting::ScrollButtonLock(enable), - LibinputSetting::ScrollMethod(method) => Setting::ScrollMethod(method as i32), - LibinputSetting::NaturalScroll(enable) => Setting::NaturalScroll(enable), - LibinputSetting::TapButtonMap(map) => Setting::TapButtonMap(map as i32), - LibinputSetting::TapDrag(enable) => Setting::TapDrag(enable), - LibinputSetting::TapDragLock(enable) => Setting::TapDragLock(enable), - LibinputSetting::Tap(enable) => Setting::Tap(enable), - }; - - block_on(client.set_libinput_setting(SetLibinputSettingRequest { - setting: Some(setting), - })) - .unwrap(); - } -} - -/// A trait that designates anything that can be converted into a [`Keysym`]. -pub trait Key { - /// Convert this into a [`Keysym`]. - fn into_keysym(self) -> Keysym; -} - -impl Key for Keysym { - fn into_keysym(self) -> Keysym { - self - } -} - -impl Key for char { - fn into_keysym(self) -> Keysym { - Keysym::from_char(self) - } -} - -impl Key for &str { - fn into_keysym(self) -> Keysym { - xkbcommon::xkb::keysym_from_name(self, xkbcommon::xkb::KEYSYM_NO_FLAGS) - } -} - -impl Key for String { - fn into_keysym(self) -> Keysym { - xkbcommon::xkb::keysym_from_name(&self, xkbcommon::xkb::KEYSYM_NO_FLAGS) - } -} - -impl Key for u32 { - fn into_keysym(self) -> Keysym { - Keysym::from(self) - } -} diff --git a/api/rust_grpc/src/input/libinput.rs b/api/rust_grpc/src/input/libinput.rs deleted file mode 100644 index 5e24df7..0000000 --- a/api/rust_grpc/src/input/libinput.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! Types for libinput configuration. - -/// Pointer acceleration profile -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum AccelProfile { - /// A flat acceleration profile. - /// - /// Pointer motion is accelerated by a constant (device-specific) factor, depending on the current speed. - Flat = 1, - /// An adaptive acceleration profile. - /// - /// Pointer acceleration depends on the input speed. This is the default profile for most devices. - Adaptive, -} - -/// The click method defines when to generate software-emulated buttons, usually on a device -/// that does not have a specific physical button available. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ClickMethod { - /// Use software-button areas to generate button events. - ButtonAreas = 1, - /// The number of fingers decides which button press to generate. - Clickfinger, -} - -/// The scroll method of a device selects when to generate scroll axis events instead of pointer motion events. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ScrollMethod { - /// Never send scroll events instead of pointer motion events. - /// - /// This has no effect on events generated by scroll wheels. - NoScroll = 1, - /// Send scroll events when two fingers are logically down on the device. - TwoFinger, - /// Send scroll events when a finger moves along the bottom or right edge of a device. - Edge, - /// Send scroll events when a button is down and the device moves along a scroll-capable axis. - OnButtonDown, -} - -/// Map 1/2/3 finger tips to buttons. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum TapButtonMap { - /// 1/2/3 finger tap maps to left/right/middle - LeftRightMiddle, - /// 1/2/3 finger tap maps to left/middle/right - LeftMiddleRight, -} - -/// Possible settings for libinput. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum LibinputSetting { - /// Set the pointer acceleration profile - AccelProfile(AccelProfile), - /// Set pointer acceleration speed - AccelSpeed(f64), - /// Set the calibration matrix - CalibrationMatrix([f32; 6]), - /// Set the [`ClickMethod`] - ClickMethod(ClickMethod), - /// Set whether the device gets disabled while typing - DisableWhileTyping(bool), - /// Set left handed mode - LeftHanded(bool), - /// Allow or disallow middle mouse button emulation - MiddleEmulation(bool), - /// Set the rotation angle - RotationAngle(u32), - /// Set the scroll button - ScrollButton(u32), - /// Set whether the scroll button should be a drag or toggle - ScrollButtonLock(bool), - /// Set the [`ScrollMethod`] - ScrollMethod(ScrollMethod), - /// Enable or disable natural scrolling - NaturalScroll(bool), - /// Set the [`TapButtonMap`] - TapButtonMap(TapButtonMap), - /// Enable or disable tap-to-drag - TapDrag(bool), - /// Enable or disable a timeout where lifting a finger off the device will not stop dragging - TapDragLock(bool), - /// Enable or disable tap-to-click - Tap(bool), -} diff --git a/api/rust_grpc/src/lib.rs b/api/rust_grpc/src/lib.rs deleted file mode 100644 index e573209..0000000 --- a/api/rust_grpc/src/lib.rs +++ /dev/null @@ -1,202 +0,0 @@ -#![warn(missing_docs)] - -//! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s -//! configuration API. -//! -//! This library allows to to interface with the Pinnacle compositor and configure various aspects -//! like input and the tag system. -//! -//! # Configuration -//! -//! ## 1. Create a cargo project -//! To create your own Rust config, create a Cargo project in `~/.config/pinnacle`. -//! -//! ## 2. Create `metaconfig.toml` -//! Then, create a file named `metaconfig.toml`. This is the file Pinnacle will use to determine -//! what to run, kill and reload-config keybinds, an optional socket directory, and any environment -//! variables to give the config client. -//! -//! In `metaconfig.toml`, put the following: -//! ```toml -//! # `command` will tell Pinnacle to run `cargo run` in your config directory. -//! # You can add stuff like "--release" here if you want to. -//! command = ["cargo", "run"] -//! -//! # You must define a keybind to reload your config if it crashes, otherwise you'll get stuck if -//! # the Lua config doesn't kick in properly. -//! reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } -//! -//! # Similarly, you must define a keybind to kill Pinnacle. -//! kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } -//! -//! # You can specify an optional socket directory if you need to place the socket Pinnacle will -//! # use for configuration in a different place. -//! # socket_dir = "your/dir/here" -//! -//! # If you need to set any environment variables for the config process, you can do so here if -//! # you don't want to do it in the config itself. -//! [envs] -//! # key = "value" -//! ``` -//! -//! ## 3. Set up dependencies -//! In your `Cargo.toml`, add a dependency to `pinnacle-api`: -//! -//! ```toml -//! # Cargo.toml -//! -//! [dependencies] -//! pinnacle-api = { git = "https://github.com/pinnacle-comp/pinnacle" } -//! ``` -//! -//! ## 4. Set up the main function -//! In `main.rs`, change `fn main()` to `async fn main()` and annotate it with the -//! [`pinnacle_api::config`][`crate::config`] macro. Pass in the identifier you want to bind the -//! config modules to: -//! -//! ``` -//! use pinnacle_api::ApiModules; -//! -//! #[pinnacle_api::config(modules)] -//! async fn main() { -//! // `modules` is now available in the function body. -//! // You can deconstruct `ApiModules` to get all the config structs. -//! let ApiModules { -//! pinnacle, -//! process, -//! window, -//! input, -//! output, -//! tag, -//! } = modules; -//! } -//! ``` -//! -//! ## 5. Begin crafting your config! -//! You can peruse the documentation for things to configure. - -use std::sync::OnceLock; - -use futures::{ - channel::mpsc::UnboundedReceiver, future::BoxFuture, stream::FuturesUnordered, StreamExt, -}; -use input::Input; -use output::Output; -use pinnacle::Pinnacle; -use process::Process; -use tag::Tag; -use tonic::transport::{Endpoint, Uri}; -use tower::service_fn; -use window::Window; - -pub mod input; -pub mod output; -pub mod pinnacle; -pub mod process; -pub mod tag; -pub mod util; -pub mod window; - -pub use pinnacle_api_macros::config; -pub use tokio; -pub use xkbcommon; - -static PINNACLE: OnceLock = OnceLock::new(); -static PROCESS: OnceLock = OnceLock::new(); -static WINDOW: OnceLock = OnceLock::new(); -static INPUT: OnceLock = OnceLock::new(); -static OUTPUT: OnceLock = OnceLock::new(); -static TAG: OnceLock = OnceLock::new(); - -/// A struct containing static references to all of the configuration structs. -#[derive(Debug, Clone, Copy)] -pub struct ApiModules { - /// The [`Pinnacle`] struct - pub pinnacle: &'static Pinnacle, - /// The [`Process`] struct - pub process: &'static Process, - /// The [`Window`] struct - pub window: &'static Window, - /// The [`Input`] struct - pub input: &'static Input, - /// The [`Output`] struct - pub output: &'static Output, - /// The [`Tag`] struct - pub tag: &'static Tag, -} - -/// Connects to Pinnacle and builds the configuration structs. -/// -/// This function is inserted at the top of your config through the [`config`] macro. -/// You should use that macro instead of this function directly. -pub async fn connect( -) -> Result<(ApiModules, UnboundedReceiver>), Box> { - let channel = Endpoint::try_from("http://[::]:50051")? // port doesn't matter, we use a unix socket - .connect_with_connector(service_fn(|_: Uri| { - tokio::net::UnixStream::connect( - std::env::var("PINNACLE_GRPC_SOCKET") - .expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"), - ) - })) - .await?; - - let (fut_sender, fut_recv) = futures::channel::mpsc::unbounded::>(); - - let output = Output::new(channel.clone(), fut_sender.clone()); - - let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(channel.clone())); - let process = PROCESS.get_or_init(|| Process::new(channel.clone(), fut_sender.clone())); - let window = WINDOW.get_or_init(|| Window::new(channel.clone())); - let input = INPUT.get_or_init(|| Input::new(channel.clone(), fut_sender.clone())); - let tag = TAG.get_or_init(|| Tag::new(channel.clone(), fut_sender.clone())); - let output = OUTPUT.get_or_init(|| output); - - let modules = ApiModules { - pinnacle, - process, - window, - input, - output, - tag, - }; - - Ok((modules, fut_recv)) -} - -/// Listen to Pinnacle for incoming messages. -/// -/// This will run all futures returned by configuration methods that take in callbacks in order to -/// call them. -/// -/// This function is inserted at the end of your config through the [`config`] macro. -/// You should use the macro instead of this function directly. -pub async fn listen( - fut_recv: UnboundedReceiver>, // api_modules: ApiModules<'a>, -) { - let mut future_set = FuturesUnordered::< - BoxFuture<( - Option>, - Option>>, - )>, - >::new(); - - future_set.push(Box::pin(async move { - let (fut, stream) = fut_recv.into_future().await; - (fut, Some(stream)) - })); - - while let Some((fut, stream)) = future_set.next().await { - if let Some(fut) = fut { - future_set.push(Box::pin(async move { - fut.await; - (None, None) - })); - } - if let Some(stream) = stream { - future_set.push(Box::pin(async move { - let (fut, stream) = stream.into_future().await; - (fut, Some(stream)) - })) - } - } -} diff --git a/api/rust_grpc/src/output.rs b/api/rust_grpc/src/output.rs deleted file mode 100644 index 6ef6d9b..0000000 --- a/api/rust_grpc/src/output.rs +++ /dev/null @@ -1,515 +0,0 @@ -//! Output management. -//! -//! An output is Pinnacle's terminology for a monitor. -//! -//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different -//! connected monitors and set them up. - -use futures::{ - channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, -}; -use pinnacle_api_defs::pinnacle::{ - output::{ - self, - v0alpha1::{ - output_service_client::OutputServiceClient, ConnectForAllRequest, SetLocationRequest, - }, - }, - tag::v0alpha1::tag_service_client::TagServiceClient, -}; -use tonic::transport::Channel; - -use crate::tag::TagHandle; - -/// A struct that allows you to get handles to connected outputs and set them up. -/// -/// See [`OutputHandle`] for more information. -#[derive(Debug, Clone)] -pub struct Output { - channel: Channel, - fut_sender: UnboundedSender>, -} - -impl Output { - pub(crate) fn new( - channel: Channel, - fut_sender: UnboundedSender>, - ) -> Self { - Self { - channel, - fut_sender, - } - } - - fn create_output_client(&self) -> OutputServiceClient { - OutputServiceClient::new(self.channel.clone()) - } - - fn create_tag_client(&self) -> TagServiceClient { - TagServiceClient::new(self.channel.clone()) - } - - /// Get a handle to all connected outputs. - /// - /// # Examples - /// - /// ``` - /// let outputs = output.get_all(); - /// ``` - pub fn get_all(&self) -> impl Iterator { - let mut client = self.create_output_client(); - let tag_client = self.create_tag_client(); - block_on(client.get(output::v0alpha1::GetRequest {})) - .unwrap() - .into_inner() - .output_names - .into_iter() - .map(move |name| OutputHandle { - client: client.clone(), - tag_client: tag_client.clone(), - name, - }) - } - - /// Get a handle to the output with the given name. - /// - /// By "name", we mean the name of the connector the output is connected to. - /// - /// # Examples - /// - /// ``` - /// let op = output.get_by_name("eDP-1")?; - /// let op2 = output.get_by_name("HDMI-2")?; - /// ``` - pub fn get_by_name(&self, name: impl Into) -> Option { - let name: String = name.into(); - self.get_all().find(|output| output.name == name) - } - - /// Get a handle to the focused output. - /// - /// This is currently implemented as the one that has had the most recent pointer movement. - /// - /// # Examples - /// - /// ``` - /// let op = output.get_focused()?; - /// ``` - pub fn get_focused(&self) -> Option { - self.get_all() - .find(|output| matches!(output.props().focused, Some(true))) - } - - /// Connect a closure to be run on all current and future outputs. - /// - /// When called, `connect_for_all` will do two things: - /// 1. Immediately run `for_all` with all currently connected outputs. - /// 2. Create a future that will call `for_all` with any newly connected outputs. - /// - /// Note that `for_all` will *not* run with outputs that have been unplugged and replugged. - /// This is to prevent duplicate setup. Instead, the compositor keeps track of any tags and - /// state the output had when unplugged and restores them on replug. - /// - /// # Examples - /// - /// ``` - /// // Add tags 1-3 to all outputs and set tag "1" to active - /// output.connect_for_all(|op| { - /// let tags = tag.add(&op, ["1", "2", "3"]); - /// tags.next().unwrap().set_active(true); - /// }); - /// ``` - pub fn connect_for_all(&self, mut for_all: impl FnMut(OutputHandle) + Send + 'static) { - for output in self.get_all() { - for_all(output); - } - - let mut client = self.create_output_client(); - let tag_client = self.create_tag_client(); - - self.fut_sender - .unbounded_send( - async move { - let mut stream = client - .connect_for_all(ConnectForAllRequest {}) - .await - .unwrap() - .into_inner(); - - while let Some(Ok(response)) = stream.next().await { - let Some(output_name) = response.output_name else { - continue; - }; - - let output = OutputHandle { - client: client.clone(), - tag_client: tag_client.clone(), - name: output_name, - }; - - for_all(output); - } - } - .boxed(), - ) - .unwrap(); - } -} - -/// A handle to an output. -/// -/// This allows you to manipulate outputs and get their properties. -#[derive(Clone, Debug)] -pub struct OutputHandle { - pub(crate) client: OutputServiceClient, - pub(crate) tag_client: TagServiceClient, - pub(crate) name: String, -} - -/// The alignment to use for [`OutputHandle::set_loc_adj_to`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Alignment { - /// Set above, align left borders - TopAlignLeft, - /// Set above, align centers - TopAlignCenter, - /// Set above, align right borders - TopAlignRight, - /// Set below, align left borders - BottomAlignLeft, - /// Set below, align centers - BottomAlignCenter, - /// Set below, align right borders - BottomAlignRight, - /// Set to left, align top borders - LeftAlignTop, - /// Set to left, align centers - LeftAlignCenter, - /// Set to left, align bottom borders - LeftAlignBottom, - /// Set to right, align top borders - RightAlignTop, - /// Set to right, align centers - RightAlignCenter, - /// Set to right, align bottom borders - RightAlignBottom, -} - -impl OutputHandle { - /// Set the location of this output in the global space. - /// - /// On startup, Pinnacle will lay out all connected outputs starting at (0, 0) - /// and going to the right, with their top borders aligned. - /// - /// This method allows you to move outputs where necessary. - /// - /// Note: If you leave space between two outputs when setting their locations, - /// the pointer will not be able to move between them. - /// - /// # Examples - /// - /// ``` - /// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions: - /// // - "DP-1": ┌─────┐ - /// // │ │1920x1080 - /// // └─────┘ - /// // - "HDMI-1": ┌───────┐ - /// // │ 2560x │ - /// // │ 1440 │ - /// // └───────┘ - /// - /// output.get_by_name("DP-1")?.set_location(0, 0); - /// output.get_by_name("HDMI-1")?.set_location(1920, -360); - /// - /// // Results in: - /// // x=0 ┌───────┐y=-360 - /// // y=0┌─────┤ │ - /// // │DP-1 │HDMI-1 │ - /// // └─────┴───────┘ - /// // ^x=1920 - /// ``` - pub fn set_location(&self, x: impl Into>, y: impl Into>) { - let mut client = self.client.clone(); - block_on(client.set_location(SetLocationRequest { - output_name: Some(self.name.clone()), - x: x.into(), - y: y.into(), - })) - .unwrap(); - } - - /// Set this output adjacent to another one. - /// - /// This is a helper method over [`OutputHandle::set_location`] to make laying out outputs - /// easier. - /// - /// `alignment` is an [`Alignment`] of how you want this output to be placed. - /// For example, [`TopAlignLeft`][Alignment::TopAlignLeft] will place this output - /// above `other` and align the left borders. - /// Similarly, [`RightAlignCenter`][Alignment::RightAlignCenter] will place this output - /// to the right of `other` and align their centers. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::output::Alignment; - /// - /// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions: - /// // - "DP-1": ┌─────┐ - /// // │ │1920x1080 - /// // └─────┘ - /// // - "HDMI-1": ┌───────┐ - /// // │ 2560x │ - /// // │ 1440 │ - /// // └───────┘ - /// - /// output.get_by_name("DP-1")?.set_loc_adj_to(output.get_by_name("HDMI-1")?, Alignment::BottomAlignRight); - /// - /// // Results in: - /// // ┌───────┐ - /// // │ │ - /// // │HDMI-1 │ - /// // └──┬────┤ - /// // │DP-1│ - /// // └────┘ - /// // Notice that "DP-1" now has the coordinates (2280, 1440) because "DP-1" is getting moved, not "HDMI-1". - /// // "HDMI-1" was placed at (1920, 0) during the compositor's initial output layout. - /// ``` - pub fn set_loc_adj_to(&self, other: &OutputHandle, alignment: Alignment) { - let self_props = self.props(); - let other_props = other.props(); - - // poor man's try {} - let attempt_set_loc = || -> Option<()> { - let other_x = other_props.x?; - let other_y = other_props.y?; - let other_width = other_props.pixel_width? as i32; - let other_height = other_props.pixel_height? as i32; - - let self_width = self_props.pixel_width? as i32; - let self_height = self_props.pixel_height? as i32; - - use Alignment::*; - - let x: i32; - let y: i32; - - if let TopAlignLeft | TopAlignCenter | TopAlignRight | BottomAlignLeft - | BottomAlignCenter | BottomAlignRight = alignment - { - if let TopAlignLeft | TopAlignCenter | TopAlignRight = alignment { - y = other_y - self_height; - } else { - // bottom - y = other_y + other_height; - } - - match alignment { - TopAlignLeft | BottomAlignLeft => x = other_x, - TopAlignCenter | BottomAlignCenter => { - x = other_x + (other_width - self_width) / 2; - } - TopAlignRight | BottomAlignRight => x = other_x + (other_width - self_width), - _ => unreachable!(), - } - } else { - if let LeftAlignTop | LeftAlignCenter | LeftAlignBottom = alignment { - x = other_x - self_width; - } else { - x = other_x + other_width; - } - - match alignment { - LeftAlignTop | RightAlignTop => y = other_y, - LeftAlignCenter | RightAlignCenter => { - y = other_y + (other_height - self_height) / 2; - } - LeftAlignBottom | RightAlignBottom => { - y = other_y + (other_height - self_height); - } - _ => unreachable!(), - } - } - - self.set_location(Some(x), Some(y)); - - Some(()) - }; - - attempt_set_loc(); - } - - /// Get all properties of this output. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::output::OutputProperties; - /// - /// let OutputProperties { - /// make, - /// model, - /// x, - /// y, - /// pixel_width, - /// pixel_height, - /// refresh_rate, - /// physical_width, - /// physical_height, - /// focused, - /// tags, - /// } = output.get_focused()?.props(); - /// ``` - pub fn props(&self) -> OutputProperties { - let mut client = self.client.clone(); - let response = block_on( - client.get_properties(output::v0alpha1::GetPropertiesRequest { - output_name: Some(self.name.clone()), - }), - ) - .unwrap() - .into_inner(); - - OutputProperties { - make: response.make, - model: response.model, - x: response.x, - y: response.y, - pixel_width: response.pixel_width, - pixel_height: response.pixel_height, - refresh_rate: response.refresh_rate, - physical_width: response.physical_width, - physical_height: response.physical_height, - focused: response.focused, - tags: response - .tag_ids - .into_iter() - .map(|id| TagHandle { - client: self.tag_client.clone(), - output_client: self.client.clone(), - id, - }) - .collect(), - } - } - - // TODO: make a macro for the following or something - - /// Get this output's make. - /// - /// Shorthand for `self.props().make`. - pub fn make(&self) -> Option { - self.props().make - } - - /// Get this output's model. - /// - /// Shorthand for `self.props().make`. - pub fn model(&self) -> Option { - self.props().model - } - - /// Get this output's x position in the global space. - /// - /// Shorthand for `self.props().x`. - pub fn x(&self) -> Option { - self.props().x - } - - /// Get this output's y position in the global space. - /// - /// Shorthand for `self.props().y`. - pub fn y(&self) -> Option { - self.props().y - } - - /// Get this output's screen width in pixels. - /// - /// Shorthand for `self.props().pixel_width`. - pub fn pixel_width(&self) -> Option { - self.props().pixel_width - } - - /// Get this output's screen height in pixels. - /// - /// Shorthand for `self.props().pixel_height`. - pub fn pixel_height(&self) -> Option { - self.props().pixel_height - } - - /// Get this output's refresh rate in millihertz. - /// - /// For example, 144Hz will be returned as 144000. - /// - /// Shorthand for `self.props().refresh_rate`. - pub fn refresh_rate(&self) -> Option { - self.props().refresh_rate - } - - /// Get this output's physical width in millimeters. - /// - /// Shorthand for `self.props().physical_width`. - pub fn physical_width(&self) -> Option { - self.props().physical_width - } - - /// Get this output's physical height in millimeters. - /// - /// Shorthand for `self.props().physical_height`. - pub fn physical_height(&self) -> Option { - self.props().physical_height - } - - /// Get whether this output is focused or not. - /// - /// This is currently implemented as the output with the most recent pointer motion. - /// - /// Shorthand for `self.props().focused`. - pub fn focused(&self) -> Option { - self.props().focused - } - - /// Get the tags this output has. - /// - /// Shorthand for `self.props().tags` - pub fn tags(&self) -> Vec { - self.props().tags - } - - /// Get this output's unique name (the name of its connector). - pub fn name(&self) -> &str { - &self.name - } -} - -/// The properties of an output. -#[derive(Clone, Debug)] -pub struct OutputProperties { - /// The make of the output - pub make: Option, - /// The model of the output - /// - /// This is something like "27GL83A" or whatever crap monitor manufacturers name their monitors - /// these days. - pub model: Option, - /// The x position of the output in the global space - pub x: Option, - /// The y position of the output in the global space - pub y: Option, - /// The output's screen width in pixels - pub pixel_width: Option, - /// The output's screen height in pixels - pub pixel_height: Option, - /// The output's refresh rate in millihertz - pub refresh_rate: Option, - /// The output's physical width in millimeters - pub physical_width: Option, - /// The output's physical height in millimeters - pub physical_height: Option, - /// Whether this output is focused or not - /// - /// This is currently implemented as the output with the most recent pointer motion. - pub focused: Option, - /// The tags this output has - pub tags: Vec, -} diff --git a/api/rust_grpc/src/process.rs b/api/rust_grpc/src/process.rs deleted file mode 100644 index 121f2ec..0000000 --- a/api/rust_grpc/src/process.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Process management. -//! -//! This module provides [`Process`], which allows you to spawn processes and set environment -//! variables. - -use futures::{ - channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture, FutureExt, StreamExt, -}; -use pinnacle_api_defs::pinnacle::process::v0alpha1::{ - process_service_client::ProcessServiceClient, SetEnvRequest, SpawnRequest, -}; -use tonic::transport::Channel; - -/// A struct containing methods to spawn processes with optional callbacks and set environment -/// variables. -#[derive(Debug, Clone)] -pub struct Process { - channel: Channel, - fut_sender: UnboundedSender>, -} - -/// Optional callbacks to be run when a spawned process prints to stdout or stderr or exits. -pub struct SpawnCallbacks { - /// A callback that will be run when a process prints to stdout with a line - pub stdout: Option>, - /// A callback that will be run when a process prints to stderr with a line - pub stderr: Option>, - /// A callback that will be run when a process exits with a status code and message - #[allow(clippy::type_complexity)] - pub exit: Option, String) + Send>>, -} - -impl Process { - pub(crate) fn new( - channel: Channel, - fut_sender: UnboundedSender>, - ) -> Process { - Self { - channel, - fut_sender, - } - } - - fn create_process_client(&self) -> ProcessServiceClient { - ProcessServiceClient::new(self.channel.clone()) - } - - /// Spawn a process. - /// - /// Note that windows spawned *before* tags are added will not be displayed. - /// This will be changed in the future to be more like Awesome, where windows with no tags are - /// displayed on every tag instead. - /// - /// # Examples - /// - /// ``` - /// process.spawn(["alacritty"]); - /// process.spawn(["bash", "-c", "swaybg -i ~/path_to_wallpaper"]); - /// ``` - pub fn spawn(&self, args: impl IntoIterator>) { - self.spawn_inner(args, false, None); - } - - /// Spawn a process with callbacks for its stdout, stderr, and exit information. - /// - /// See [`SpawnCallbacks`] for the passed in struct. - /// - /// Note that windows spawned *before* tags are added will not be displayed. - /// This will be changed in the future to be more like Awesome, where windows with no tags are - /// displayed on every tag instead. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::process::SpawnCallbacks; - /// - /// process.spawn_with_callbacks(["alacritty"], SpawnCallbacks { - /// stdout: Some(Box::new(|line| println!("stdout: {line}"))), - /// stderr: Some(Box::new(|line| println!("stderr: {line}"))), - /// stdout: Some(Box::new(|code, msg| println!("exit code: {code:?}, exit_msg: {msg}"))), - /// }); - /// ``` - pub fn spawn_with_callbacks( - &self, - args: impl IntoIterator>, - callbacks: SpawnCallbacks, - ) { - self.spawn_inner(args, false, Some(callbacks)); - } - - /// Spawn a process only if it isn't already running. - /// - /// This is useful for startup programs. - /// - /// See [`Process::spawn`] for details. - pub fn spawn_once(&self, args: impl IntoIterator>) { - self.spawn_inner(args, true, None); - } - - /// Spawn a process only if it isn't already running with optional callbacks for its stdout, - /// stderr, and exit information. - /// - /// This is useful for startup programs. - /// - /// See [`Process::spawn_with_callbacks`] for details. - pub fn spawn_once_with_callbacks( - &self, - args: impl IntoIterator>, - callbacks: SpawnCallbacks, - ) { - self.spawn_inner(args, true, Some(callbacks)); - } - - fn spawn_inner( - &self, - args: impl IntoIterator>, - once: bool, - callbacks: Option, - ) { - let mut client = self.create_process_client(); - - let args = args.into_iter().map(Into::into).collect::>(); - - let request = SpawnRequest { - args, - once: Some(once), - has_callback: Some(callbacks.is_some()), - }; - - self.fut_sender - .unbounded_send( - async move { - let mut stream = client.spawn(request).await.unwrap().into_inner(); - let Some(mut callbacks) = callbacks else { return }; - while let Some(Ok(response)) = stream.next().await { - if let Some(line) = response.stdout { - if let Some(stdout) = callbacks.stdout.as_mut() { - stdout(line); - } - } - if let Some(line) = response.stderr { - if let Some(stderr) = callbacks.stderr.as_mut() { - stderr(line); - } - } - if let Some(exit_msg) = response.exit_message { - if let Some(exit) = callbacks.exit.as_mut() { - exit(response.exit_code, exit_msg); - } - } - } - } - .boxed(), - ) - .unwrap(); - } - - /// Set an environment variable for the compositor. - /// This will cause any future spawned processes to have this environment variable. - /// - /// # Examples - /// - /// ``` - /// process.set_env("ENV", "a value lalala"); - /// ``` - pub fn set_env(&self, key: impl Into, value: impl Into) { - let key = key.into(); - let value = value.into(); - - let mut client = self.create_process_client(); - - block_on(client.set_env(SetEnvRequest { - key: Some(key), - value: Some(value), - })) - .unwrap(); - } -} diff --git a/api/rust_grpc/src/tag.rs b/api/rust_grpc/src/tag.rs deleted file mode 100644 index 256fdd3..0000000 --- a/api/rust_grpc/src/tag.rs +++ /dev/null @@ -1,528 +0,0 @@ -//! Tag management. -//! -//! This module allows you to interact with Pinnacle's tag system. -//! -//! # The Tag System -//! Many Wayland compositors use workspaces for window management. -//! Each window is assigned to a workspace and will only show up if that workspace is being -//! viewed. This is a find way to manage windows, but it's not that powerful. -//! -//! Instead, Pinnacle works with a tag system similar to window managers like [dwm](https://dwm.suckless.org/) -//! and, the window manager Pinnacle takes inspiration from, [awesome](https://awesomewm.org/). -//! -//! In a tag system, there are no workspaces. Instead, each window can be tagged with zero or more -//! tags, and zero or more tags can be displayed on a monitor at once. This allows you to, for -//! example, bring in your browsers on the same screen as your IDE by toggling the "Browser" tag. -//! -//! Workspaces can be emulated by only displaying one tag at a time. Combining this feature with -//! the ability to tag windows with multiple tags allows you to have one window show up on multiple -//! different "workspaces". As you can see, this system is much more powerful than workspaces -//! alone. -//! -//! # Configuration -//! `tag` contains the [`Tag`] struct, which allows you to add new tags -//! and get handles to already defined ones. -//! -//! These [`TagHandle`]s allow you to manipulate individual tags and get their properties. - -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; - -use futures::{channel::mpsc::UnboundedSender, executor::block_on, future::BoxFuture}; -use num_enum::TryFromPrimitive; -use pinnacle_api_defs::pinnacle::{ - output::v0alpha1::output_service_client::OutputServiceClient, - tag::{ - self, - v0alpha1::{ - tag_service_client::TagServiceClient, AddRequest, RemoveRequest, SetActiveRequest, - SetLayoutRequest, SwitchToRequest, - }, - }, -}; -use tonic::transport::Channel; - -use crate::output::{Output, OutputHandle}; - -/// A struct that allows you to add and remove tags and get [`TagHandle`]s. -#[derive(Clone, Debug)] -pub struct Tag { - channel: Channel, - fut_sender: UnboundedSender>, -} - -impl Tag { - pub(crate) fn new( - channel: Channel, - fut_sender: UnboundedSender>, - ) -> Self { - Self { - channel, - fut_sender, - } - } - - fn create_tag_client(&self) -> TagServiceClient { - TagServiceClient::new(self.channel.clone()) - } - - fn create_output_client(&self) -> OutputServiceClient { - OutputServiceClient::new(self.channel.clone()) - } - - /// Add tags to the specified output. - /// - /// This will add tags with the given names to `output` and return [`TagHandle`]s to all of - /// them. - /// - /// # Examples - /// - /// ``` - /// // Add tags 1-5 to the focused output - /// if let Some(op) = output.get_focused() { - /// let tags = tag.add(&op, ["1", "2", "3", "4", "5"]); - /// } - /// ``` - pub fn add( - &self, - output: &OutputHandle, - tag_names: impl IntoIterator>, - ) -> impl Iterator { - let mut client = self.create_tag_client(); - let output_client = self.create_output_client(); - - let tag_names = tag_names.into_iter().map(Into::into).collect(); - - let response = block_on(client.add(AddRequest { - output_name: Some(output.name.clone()), - tag_names, - })) - .unwrap() - .into_inner(); - - response.tag_ids.into_iter().map(move |id| TagHandle { - client: client.clone(), - output_client: output_client.clone(), - id, - }) - } - - /// Get handles to all tags across all outputs. - /// - /// # Examples - /// - /// ``` - /// let all_tags = tag.get_all(); - /// ``` - pub fn get_all(&self) -> impl Iterator { - let mut client = self.create_tag_client(); - let output_client = self.create_output_client(); - - let response = block_on(client.get(tag::v0alpha1::GetRequest {})) - .unwrap() - .into_inner(); - - response.tag_ids.into_iter().map(move |id| TagHandle { - client: client.clone(), - output_client: output_client.clone(), - id, - }) - } - - /// Get a handle to the first tag with the given name on `output`. - /// - /// If `output` is `None`, the focused output will be used. - /// - /// # Examples - /// - /// ``` - /// // Get tag "1" on output "HDMI-1" - /// if let Some(op) = output.get_by_name("HDMI-1") { - /// let tg = tag.get("1", &op); - /// } - /// - /// // Get tag "Thing" on the focused output - /// let tg = tag.get("Thing", None); - /// ``` - pub fn get<'a>( - &self, - name: impl Into, - output: impl Into>, - ) -> Option { - let name = name.into(); - let output: Option<&OutputHandle> = output.into(); - let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); - - self.get_all().find(|tag| { - let props = tag.props(); - - let same_tag_name = props.name.as_ref() == Some(&name); - let same_output = props.output.is_some_and(|op| { - Some(op.name) - == output - .map(|o| o.name.clone()) - .or_else(|| output_module.get_focused().map(|o| o.name)) - }); - - same_tag_name && same_output - }) - } - - /// Remove the given tags from their outputs. - /// - /// # Examples - /// - /// ``` - /// let tags = tag.add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]); - /// - /// tag.remove(tags); // "DP-1" no longer has any tags - /// ``` - pub fn remove(&self, tags: impl IntoIterator) { - let tag_ids = tags.into_iter().map(|handle| handle.id).collect::>(); - - let mut client = self.create_tag_client(); - - block_on(client.remove(RemoveRequest { tag_ids })).unwrap(); - } - - /// Create a [`LayoutCycler`] to cycle layouts on outputs. - /// - /// This will create a `LayoutCycler` with two functions: one to cycle forward the layout for - /// the first active tag on the specified output, and one to cycle backward. - /// - /// If you do not specify an output for `LayoutCycler` functions, it will default to the - /// focused output. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::tag::{Layout, LayoutCycler}; - /// use pinnacle_api::xkbcommon::xkb::Keysym; - /// use pinnacle_api::input::Mod; - /// - /// // Create a layout cycler that cycles through the listed layouts - /// let LayoutCycler { - /// prev: layout_prev, - /// next: layout_next, - /// } = tag.new_layout_cycler([ - /// Layout::MasterStack, - /// Layout::Dwindle, - /// Layout::Spiral, - /// Layout::CornerTopLeft, - /// Layout::CornerTopRight, - /// Layout::CornerBottomLeft, - /// Layout::CornerBottomRight, - /// ]); - /// - /// // Cycle layouts forward on the focused output - /// layout_next(None); - /// - /// // Cycle layouts backward on the focused output - /// layout_prev(None); - /// - /// // Cycle layouts forward on "eDP-1" - /// layout_next(output.get_by_name("eDP-1")?); - /// ``` - pub fn new_layout_cycler(&self, layouts: impl IntoIterator) -> LayoutCycler { - let indices = Arc::new(Mutex::new(HashMap::::new())); - let indices_clone = indices.clone(); - - let layouts = layouts.into_iter().collect::>(); - let layouts_clone = layouts.clone(); - let len = layouts.len(); - - let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); - let output_module_clone = output_module.clone(); - - let next = move |output: Option<&OutputHandle>| { - let Some(output) = output.cloned().or_else(|| output_module.get_focused()) else { - return; - }; - - let Some(first_tag) = output - .props() - .tags - .into_iter() - .find(|tag| tag.active() == Some(true)) - else { - return; - }; - - let mut indices = indices.lock().expect("layout next mutex lock failed"); - let index = indices.entry(first_tag.id).or_insert(0); - - if *index + 1 >= len { - *index = 0; - } else { - *index += 1; - } - - first_tag.set_layout(layouts[*index]); - }; - - let prev = move |output: Option<&OutputHandle>| { - let Some(output) = output - .cloned() - .or_else(|| output_module_clone.get_focused()) - else { - return; - }; - - let Some(first_tag) = output - .props() - .tags - .into_iter() - .find(|tag| tag.active() == Some(true)) - else { - return; - }; - - let mut indices = indices_clone.lock().expect("layout next mutex lock failed"); - let index = indices.entry(first_tag.id).or_insert(0); - - if index.checked_sub(1).is_none() { - *index = len - 1; - } else { - *index -= 1; - } - - first_tag.set_layout(layouts_clone[*index]); - }; - - LayoutCycler { - prev: Box::new(prev), - next: Box::new(next), - } - } -} - -/// A layout cycler that keeps track of tags and their layouts and provides functions to cycle -/// layouts on them. -#[allow(clippy::type_complexity)] -pub struct LayoutCycler { - /// Cycle to the next layout on the given output, or the focused output if `None`. - pub prev: Box) + Send + Sync + 'static>, - /// Cycle to the previous layout on the given output, or the focused output if `None`. - pub next: Box) + Send + Sync + 'static>, -} - -/// A handle to a tag. -/// -/// This handle allows you to do things like switch to tags and get their properties. -#[derive(Debug, Clone)] -pub struct TagHandle { - pub(crate) client: TagServiceClient, - pub(crate) output_client: OutputServiceClient, - pub(crate) id: u32, -} - -/// Various static layouts. -#[repr(i32)] -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] -pub enum Layout { - /// One master window on the left with all other windows stacked to the right - MasterStack = 1, - /// Windows split in half towards the bottom right corner - Dwindle, - /// Windows split in half in a spiral - Spiral, - /// One main corner window in the top left with a column of windows on the right and a row on the bottom - CornerTopLeft, - /// One main corner window in the top right with a column of windows on the left and a row on the bottom - CornerTopRight, - /// One main corner window in the bottom left with a column of windows on the right and a row on the top. - CornerBottomLeft, - /// One main corner window in the bottom right with a column of windows on the left and a row on the top. - CornerBottomRight, -} - -impl TagHandle { - /// Activate this tag and deactivate all other ones on the same output. - /// - /// This essentially emulates what a traditional workspace is. - /// - /// # Examples - /// - /// ``` - /// // Assume the focused output has the following inactive tags and windows: - /// // "1": Alacritty - /// // "2": Firefox, Discord - /// // "3": Steam - /// tag.get("2")?.switch_to(); // Displays Firefox and Discord - /// tag.get("3")?.switch_to(); // Displays Steam - /// ``` - pub fn switch_to(&self) { - let mut client = self.client.clone(); - block_on(client.switch_to(SwitchToRequest { - tag_id: Some(self.id), - })) - .unwrap(); - } - - /// Set this tag to active or not. - /// - /// While active, windows with this tag will be displayed. - /// - /// While inactive, windows with this tag will not be displayed unless they have other active - /// tags. - /// - /// # Examples - /// - /// ``` - /// // Assume the focused output has the following inactive tags and windows: - /// // "1": Alacritty - /// // "2": Firefox, Discord - /// // "3": Steam - /// tag.get("2")?.set_active(true); // Displays Firefox and Discord - /// tag.get("3")?.set_active(true); // Displays Firefox, Discord, and Steam - /// tag.get("2")?.set_active(false); // Displays Steam - /// ``` - pub fn set_active(&self, set: bool) { - let mut client = self.client.clone(); - block_on(client.set_active(SetActiveRequest { - tag_id: Some(self.id), - set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Set(set)), - })) - .unwrap(); - } - - /// Toggle this tag between active and inactive. - /// - /// While active, windows with this tag will be displayed. - /// - /// While inactive, windows with this tag will not be displayed unless they have other active - /// tags. - /// - /// # Examples - /// - /// ``` - /// // Assume the focused output has the following inactive tags and windows: - /// // "1": Alacritty - /// // "2": Firefox, Discord - /// // "3": Steam - /// tag.get("2")?.toggle(); // Displays Firefox and Discord - /// tag.get("3")?.toggle(); // Displays Firefox, Discord, and Steam - /// tag.get("3")?.toggle(); // Displays Firefox, Discord - /// tag.get("2")?.toggle(); // Displays nothing - /// ``` - pub fn toggle_active(&self) { - let mut client = self.client.clone(); - block_on(client.set_active(SetActiveRequest { - tag_id: Some(self.id), - set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Toggle(())), - })) - .unwrap(); - } - - /// Remove this tag from its output. - /// - /// # Examples - /// - /// ``` - /// let tags = tag - /// .add(output.get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]) - /// .collect::>; - /// - /// tags[1].remove(); - /// tags[3].remove(); - /// // "DP-1" now only has tags "1" and "Buckle" - /// ``` - pub fn remove(mut self) { - block_on(self.client.remove(RemoveRequest { - tag_ids: vec![self.id], - })) - .unwrap(); - } - - /// Set this tag's layout. - /// - /// Layouting only applies to tiled windows (windows that are not floating, maximized, or - /// fullscreen). If multiple tags are active on an output, the first active tag's layout will - /// determine the layout strategy. - /// - /// See [`Layout`] for the different static layouts Pinnacle currently has to offer. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::tag::Layout; - /// - /// // Set the layout of tag "1" on the focused output to "corner top left". - /// tag.get("1", None)?.set_layout(Layout::CornerTopLeft); - /// ``` - pub fn set_layout(&self, layout: Layout) { - let mut client = self.client.clone(); - block_on(client.set_layout(SetLayoutRequest { - tag_id: Some(self.id), - layout: Some(layout as i32), - })) - .unwrap(); - } - - /// Get all properties of this tag. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::tag::TagProperties; - /// - /// let TagProperties { - /// active, - /// name, - /// output, - /// } = tag.get("1", None)?.props(); - /// ``` - pub fn props(&self) -> TagProperties { - let mut client = self.client.clone(); - let output_client = self.output_client.clone(); - - let response = block_on(client.get_properties(tag::v0alpha1::GetPropertiesRequest { - tag_id: Some(self.id), - })) - .unwrap() - .into_inner(); - - TagProperties { - active: response.active, - name: response.name, - output: response.output_name.map(|name| OutputHandle { - client: output_client, - tag_client: client, - name, - }), - } - } - - /// Get this tag's active status. - /// - /// Shorthand for `self.props().active`. - pub fn active(&self) -> Option { - self.props().active - } - - /// Get this tag's name. - /// - /// Shorthand for `self.props().name`. - pub fn name(&self) -> Option { - self.props().name - } - - /// Get a handle to the output this tag is on. - /// - /// Shorthand for `self.props().output`. - pub fn output(&self) -> Option { - self.props().output - } -} - -/// Properties of a tag. -pub struct TagProperties { - /// Whether the tag is active or not - pub active: Option, - /// The name of the tag - pub name: Option, - /// The output the tag is on - pub output: Option, -} diff --git a/api/rust_grpc/src/window.rs b/api/rust_grpc/src/window.rs deleted file mode 100644 index c120ce8..0000000 --- a/api/rust_grpc/src/window.rs +++ /dev/null @@ -1,530 +0,0 @@ -//! Window management. -//! -//! This module provides [`Window`], which allows you to get [`WindowHandle`]s and move and resize -//! windows using the mouse. -//! -//! [`WindowHandle`]s allow you to do things like resize and move windows, toggle them between -//! floating and tiled, close them, and more. -//! -//! This module also allows you to set window rules; see the [rules] module for more information. - -use futures::executor::block_on; -use num_enum::TryFromPrimitive; -use pinnacle_api_defs::pinnacle::{ - output::v0alpha1::output_service_client::OutputServiceClient, - tag::v0alpha1::tag_service_client::TagServiceClient, - window::v0alpha1::{ - window_service_client::WindowServiceClient, AddWindowRuleRequest, CloseRequest, - MoveToTagRequest, SetTagRequest, - }, - window::{ - self, - v0alpha1::{ - GetRequest, MoveGrabRequest, ResizeGrabRequest, SetFloatingRequest, - SetFullscreenRequest, SetMaximizedRequest, - }, - }, -}; -use tonic::transport::Channel; - -use crate::{input::MouseButton, tag::TagHandle, util::Geometry}; - -use self::rules::{WindowRule, WindowRuleCondition}; - -pub mod rules; - -/// A struct containing methods that get [`WindowHandle`]s and move windows with the mouse. -/// -/// See [`WindowHandle`] for more information. -#[derive(Debug, Clone)] -pub struct Window { - channel: Channel, -} - -impl Window { - pub(crate) fn new(channel: Channel) -> Self { - Self { channel } - } - - fn create_window_client(&self) -> WindowServiceClient { - WindowServiceClient::new(self.channel.clone()) - } - - fn create_tag_client(&self) -> TagServiceClient { - TagServiceClient::new(self.channel.clone()) - } - - fn create_output_client(&self) -> OutputServiceClient { - OutputServiceClient::new(self.channel.clone()) - } - - /// Start moving the window with the mouse. - /// - /// This will begin moving the window under the pointer using the specified [`MouseButton`]. - /// The button must be held down at the time this method is called for the move to start. - /// - /// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind]. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; - /// - /// // Set `Super + left click` to begin moving a window - /// input.mousebind([Mod::Super], MouseButton::Left, MouseEdge::Press, || { - /// window.begin_move(MouseButton::Left); - /// }); - /// ``` - pub fn begin_move(&self, button: MouseButton) { - let mut client = self.create_window_client(); - block_on(client.move_grab(MoveGrabRequest { - button: Some(button as u32), - })) - .unwrap(); - } - - /// Start resizing the window with the mouse. - /// - /// This will begin resizing the window under the pointer using the specified [`MouseButton`]. - /// The button must be held down at the time this method is called for the resize to start. - /// - /// This is intended to be used with [`Input::keybind`][crate::input::Input::keybind]. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::input::{Mod, MouseButton, MouseEdge}; - /// - /// // Set `Super + right click` to begin moving a window - /// input.mousebind([Mod::Super], MouseButton::Right, MouseEdge::Press, || { - /// window.begin_resize(MouseButton::Right); - /// }); - /// ``` - pub fn begin_resize(&self, button: MouseButton) { - let mut client = self.create_window_client(); - block_on(client.resize_grab(ResizeGrabRequest { - button: Some(button as u32), - })) - .unwrap(); - } - - /// Get all windows. - /// - /// # Examples - /// - /// ``` - /// let windows = window.get_all(); - /// ``` - pub fn get_all(&self) -> impl Iterator { - let mut client = self.create_window_client(); - let tag_client = self.create_tag_client(); - let output_client = self.create_output_client(); - block_on(client.get(GetRequest {})) - .unwrap() - .into_inner() - .window_ids - .into_iter() - .map(move |id| WindowHandle { - client: client.clone(), - id, - tag_client: tag_client.clone(), - output_client: output_client.clone(), - }) - } - - /// Get the currently focused window. - /// - /// # Examples - /// - /// ``` - /// let focused_window = window.get_focused()?; - /// ``` - pub fn get_focused(&self) -> Option { - self.get_all() - .find(|window| matches!(window.props().focused, Some(true))) - } - - /// Add a window rule. - /// - /// A window rule is a set of criteria that a window must open with. - /// For it to apply, a [`WindowRuleCondition`] must evaluate to true for the window in question. - /// - /// TODO: - pub fn add_window_rule(&self, cond: WindowRuleCondition, rule: WindowRule) { - let mut client = self.create_window_client(); - - block_on(client.add_window_rule(AddWindowRuleRequest { - cond: Some(cond.0), - rule: Some(rule.0), - })) - .unwrap(); - } -} - -/// A handle to a window. -/// -/// This allows you to manipulate the window and get its properties. -#[derive(Debug, Clone)] -pub struct WindowHandle { - pub(crate) client: WindowServiceClient, - pub(crate) id: u32, - pub(crate) tag_client: TagServiceClient, - pub(crate) output_client: OutputServiceClient, -} - -/// Whether a window is fullscreen, maximized, or neither. -#[repr(i32)] -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] -pub enum FullscreenOrMaximized { - /// The window is neither fullscreen nor maximized - Neither = 1, - /// The window is fullscreen - Fullscreen, - /// The window is maximized - Maximized, -} - -/// Properties of a window. -#[derive(Debug, Clone)] -pub struct WindowProperties { - /// The location and size of the window - pub geometry: Option, - /// The window's class - pub class: Option, - /// The window's title - pub title: Option, - /// Whether the window is focused or not - pub focused: Option, - /// Whether the window is floating or not - /// - /// Note that a window can still be floating even if it's fullscreen or maximized; those two - /// state will just override the floating state. - pub floating: Option, - /// Whether the window is fullscreen, maximized, or neither - pub fullscreen_or_maximized: Option, - /// All the tags on the window - pub tags: Vec, -} - -impl WindowHandle { - /// Send a close request to this window. - /// - /// If the window is unresponsive, it may not close. - /// - /// # Examples - /// - /// ``` - /// // Close the focused window - /// window.get_focused()?.close() - /// ``` - pub fn close(mut self) { - block_on(self.client.close(CloseRequest { - window_id: Some(self.id), - })) - .unwrap(); - } - - /// Set this window to fullscreen or not. - /// - /// If it is maximized, setting it to fullscreen will remove the maximized state. - /// - /// # Examples - /// - /// ``` - /// // Set the focused window to fullscreen. - /// window.get_focused()?.set_fullscreen(true); - /// ``` - pub fn set_fullscreen(&self, set: bool) { - let mut client = self.client.clone(); - block_on(client.set_fullscreen(SetFullscreenRequest { - window_id: Some(self.id), - set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Set( - set, - )), - })) - .unwrap(); - } - - /// Toggle this window between fullscreen and not. - /// - /// If it is maximized, toggling it to fullscreen will remove the maximized state. - /// - /// # Examples - /// - /// ``` - /// // Toggle the focused window to and from fullscreen. - /// window.get_focused()?.toggle_fullscreen(); - /// ``` - pub fn toggle_fullscreen(&self) { - let mut client = self.client.clone(); - block_on(client.set_fullscreen(SetFullscreenRequest { - window_id: Some(self.id), - set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Toggle(())), - })) - .unwrap(); - } - - /// Set this window to maximized or not. - /// - /// If it is fullscreen, setting it to maximized will remove the fullscreen state. - /// - /// # Examples - /// - /// ``` - /// // Set the focused window to maximized. - /// window.get_focused()?.set_maximized(true); - /// ``` - pub fn set_maximized(&self, set: bool) { - let mut client = self.client.clone(); - block_on(client.set_maximized(SetMaximizedRequest { - window_id: Some(self.id), - set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Set( - set, - )), - })) - .unwrap(); - } - - /// Toggle this window between maximized and not. - /// - /// If it is fullscreen, setting it to maximized will remove the fullscreen state. - /// - /// # Examples - /// - /// ``` - /// // Toggle the focused window to and from maximized. - /// window.get_focused()?.toggle_maximized(); - /// ``` - pub fn toggle_maximized(&self) { - let mut client = self.client.clone(); - block_on(client.set_maximized(SetMaximizedRequest { - window_id: Some(self.id), - set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Toggle(())), - })) - .unwrap(); - } - - /// Set this window to floating or not. - /// - /// Floating windows will not be tiled and can be moved around and resized freely. - /// - /// Note that fullscreen and maximized windows can still be floating; those two states will - /// just override the floating state. - /// - /// # Examples - /// - /// ``` - /// // Set the focused window to floating. - /// window.get_focused()?.set_floating(true); - /// ``` - pub fn set_floating(&self, set: bool) { - let mut client = self.client.clone(); - block_on(client.set_floating(SetFloatingRequest { - window_id: Some(self.id), - set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Set( - set, - )), - })) - .unwrap(); - } - - /// Toggle this window to and from floating. - /// - /// Floating windows will not be tiled and can be moved around and resized freely. - /// - /// Note that fullscreen and maximized windows can still be floating; those two states will - /// just override the floating state. - /// - /// # Examples - /// - /// ``` - /// // Toggle the focused window to and from floating. - /// window.get_focused()?.toggle_floating(); - /// ``` - pub fn toggle_floating(&self) { - let mut client = self.client.clone(); - block_on(client.set_floating(SetFloatingRequest { - window_id: Some(self.id), - set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Toggle( - (), - )), - })) - .unwrap(); - } - - /// Move this window to the given `tag`. - /// - /// This will remove all tags from this window then tag it with `tag`, essentially moving the - /// window to that tag. - /// - /// # Examples - /// - /// ``` - /// // Move the focused window to tag "Code" on the focused output - /// window.get_focused()?.move_to_tag(&tag.get("Code", None)?); - /// ``` - pub fn move_to_tag(&self, tag: &TagHandle) { - let mut client = self.client.clone(); - - block_on(client.move_to_tag(MoveToTagRequest { - window_id: Some(self.id), - tag_id: Some(tag.id), - })) - .unwrap(); - } - - /// Set or unset a tag on this window. - /// - /// # Examples - /// - /// ``` - /// let focused = window.get_focused()?; - /// let tg = tag.get("Potato", None)?; - /// - /// focused.set_tag(&tg, true); // `focused` now has tag "Potato" - /// focused.set_tag(&tg, false); // `focused` no longer has tag "Potato" - /// ``` - pub fn set_tag(&self, tag: &TagHandle, set: bool) { - let mut client = self.client.clone(); - - block_on(client.set_tag(SetTagRequest { - window_id: Some(self.id), - tag_id: Some(tag.id), - set_or_toggle: Some(window::v0alpha1::set_tag_request::SetOrToggle::Set(set)), - })) - .unwrap(); - } - - /// Toggle a tag on this window. - /// - /// # Examples - /// - /// ``` - /// let focused = window.get_focused()?; - /// let tg = tag.get("Potato", None)?; - /// - /// // Assume `focused` does not have tag `tg` - /// - /// focused.toggle_tag(&tg); // `focused` now has tag "Potato" - /// focused.toggle_tag(&tg); // `focused` no longer has tag "Potato" - /// ``` - pub fn toggle_tag(&self, tag: &TagHandle) { - let mut client = self.client.clone(); - - block_on(client.set_tag(SetTagRequest { - window_id: Some(self.id), - tag_id: Some(tag.id), - set_or_toggle: Some(window::v0alpha1::set_tag_request::SetOrToggle::Toggle(())), - })) - .unwrap(); - } - - /// Get all properties of this window. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::WindowProperties; - /// - /// let WindowProperties { - /// geometry, - /// class, - /// title, - /// focused, - /// floating, - /// fullscreen_or_maximized, - /// tags, - /// } = window.get_focused()?.props(); - /// ``` - pub fn props(&self) -> WindowProperties { - let mut client = self.client.clone(); - let tag_client = self.tag_client.clone(); - let response = block_on( - client.get_properties(window::v0alpha1::GetPropertiesRequest { - window_id: Some(self.id), - }), - ) - .unwrap() - .into_inner(); - - let fullscreen_or_maximized = response - .fullscreen_or_maximized - .unwrap_or_default() - .try_into() - .ok(); - - let geometry = response.geometry.map(|geo| Geometry { - x: geo.x(), - y: geo.y(), - width: geo.width() as u32, - height: geo.height() as u32, - }); - - WindowProperties { - geometry, - class: response.class, - title: response.title, - focused: response.focused, - floating: response.floating, - fullscreen_or_maximized, - tags: response - .tag_ids - .into_iter() - .map(|id| TagHandle { - client: tag_client.clone(), - output_client: self.output_client.clone(), - id, - }) - .collect(), - } - } - - /// Get this window's location and size. - /// - /// Shorthand for `self.props().geometry`. - pub fn geometry(&self) -> Option { - self.props().geometry - } - - /// Get this window's class. - /// - /// Shorthand for `self.props().class`. - pub fn class(&self) -> Option { - self.props().class - } - - /// Get this window's title. - /// - /// Shorthand for `self.props().title`. - pub fn title(&self) -> Option { - self.props().title - } - - /// Get whether or not this window is focused. - /// - /// Shorthand for `self.props().focused`. - pub fn focused(&self) -> Option { - self.props().focused - } - - /// Get whether or not this window is floating. - /// - /// Shorthand for `self.props().floating`. - pub fn floating(&self) -> Option { - self.props().floating - } - - /// Get whether this window is fullscreen, maximized, or neither. - /// - /// Shorthand for `self.props().fullscreen_or_maximized`. - pub fn fullscreen_or_maximized(&self) -> Option { - self.props().fullscreen_or_maximized - } - - /// Get all the tags on this window. - /// - /// Shorthand for `self.props().tags`. - pub fn tags(&self) -> Vec { - self.props().tags - } -} diff --git a/api/rust_grpc/src/window/rules.rs b/api/rust_grpc/src/window/rules.rs deleted file mode 100644 index 86e5a0a..0000000 --- a/api/rust_grpc/src/window/rules.rs +++ /dev/null @@ -1,521 +0,0 @@ -//! Types for window rules. -//! -//! A window rule is a way to set the properties of a window on open. -//! -//! They are comprised of two parts: the [condition][WindowRuleCondition] and the actual [rule][WindowRule]. -//! -//! # [`WindowRuleCondition`]s -//! `WindowRuleCondition`s are conditions that the window needs to open with in order to apply a -//! rule. For example, you may want to set a window to maximized if it has the class "steam", or -//! you might want to open all Firefox instances on tag "3". -//! -//! To do this, you must build a `WindowRuleCondition` to tell the compositor when to apply any -//! rules. -//! -//! ## Building `WindowRuleCondition`s -//! A condition is created through [`WindowRuleCondition::new`]: -//! ``` -//! let cond = WindowRuleCondition::new(); -//! ``` -//! -//! In order to understand conditions, you must understand the concept of "any" and "all". -//! -//! **"Any"** -//! -//! "Any" conditions only need one of their constituent items to be true for the whole condition to -//! evaluate to true. Think of it as one big `if a || b || c || d || ... {}` block. -//! -//! **"All"** -//! -//! "All" conditions need *all* of their constituent items to be true for the condition to evaluate -//! to true. This is like a big `if a && b && c && d && ... {}` block. -//! -//! Note that any items in a top level `WindowRuleCondition` fall under "all", so all those items -//! must be true. -//! -//! With that out of the way, we can get started building conditions. -//! -//! ### `WindowRuleCondition::classes` -//! With [`WindowRuleCondition::classes`], you can specify what classes a window needs to have for -//! a rule to apply. -//! -//! The following will apply to windows with the class "firefox": -//! ``` -//! let cond = WindowRuleCondition::new().classes(["firefox"]); -//! ``` -//! -//! Note that you pass in some `impl IntoIterator>`. This means you can -//! pass in more than one class here: -//! ``` -//! let failing_cond = WindowRuleCondition::new().classes(["firefox", "steam"]); -//! ``` -//! *HOWEVER*: this will not work. Recall that top level conditions are implicitly "all". This -//! means the above would require windows to have *both classes*, which is impossible. Thus, the -//! condition above will never be true. -//! -//! ### `WindowRuleCondition::titles` -//! Like `classes`, you can use `titles` to specify that the window needs to open with a specific -//! title for the condition to apply. -//! -//! ``` -//! let cond = WindowRuleCondition::new().titles(["Steam"]); -//! ``` -//! -//! Like `classes`, passing in multiple titles at the top level will cause the condition to always -//! fail. -//! -//! ### `WindowRuleCondition::tags` -//! You can specify that the window needs to open on the given tags in order to apply a rule. -//! -//! ``` -//! let cond = WindowRuleCondition::new().tags([&tag.get("3", output.get_by_name("HDMI-1")?)?]); -//! ``` -//! -//! Here, if you have tag "3" active on "HDMI-1" and spawn a window on that output, this condition -//! will apply. -//! -//! Unlike `classes` and `titles`, you can specify multiple tags at the top level: -//! -//! ``` -//! let op = output.get_by_name("HDMI-1")?; -//! let tag1 = tag.get("1", &op)?; -//! let tag2 = tag.get("2", &op)?; -//! -//! let cond = WindowRuleCondition::new().tags([&tag1, &tag2]); -//! ``` -//! -//! Now, you must have both tags "1" and "2" active and spawn a window for the condition to apply. -//! -//! ### `WindowRuleCondition::any` -//! Now we can get to ways to compose more complex conditions. -//! -//! `WindowRuleCondition::any` takes in conditions and will evaluate to true if *anything* in those -//! conditions are true. -//! -//! ``` -//! let cond = WindowRuleCondition::new() -//! .any([ -//! WindowRuleCondition::new().classes(["Alacritty"]), -//! WindowRuleCondition::new().tags([&tag.get("2", None)?]), -//! ]); -//! ``` -//! -//! This condition will apply if the window is *either* "Alacritty" *or* opens on tag "2". -//! -//! ### `WindowRuleCondition::all` -//! With `WindowRuleCondition::all`, *all* specified conditions must be true for the condition to -//! be true. -//! -//! ``` -//! let cond = WindowRuleCondition::new() -//! .all([ -//! WindowRuleCondition::new().classes(["Alacritty"]), -//! WindowRuleCondition::new().tags([&tag.get("2", None)?]), -//! ]); -//! ``` -//! -//! This condition applies if the window has the class "Alacritty" *and* opens on tag "2". -//! -//! You can write the above a bit shorter, as top level conditions are already "all": -//! -//! ``` -//! let cond = WindowRuleCondition::new() -//! .classes(["Alacritty"]) -//! .tags([&tag.get("2", None)?]); -//! ``` -//! -//! ## Complex condition composition -//! You can arbitrarily nest `any` and `all` to achieve desired logic. -//! -//! ``` -//! let op = output.get_by_name("HDMI-1")?; -//! let tag1 = tag.get("1", &op)?; -//! let tag2 = tag.get("2", &op)?; -//! -//! let complex_cond = WindowRuleCondition::new() -//! .any([ -//! WindowRuleCondition::new().all([ -//! WindowRuleCondition::new() -//! .classes("Alacritty") -//! .tags([&tag1, &tag2]) -//! ]), -//! WindowRuleCondition::new().all([ -//! WindowRuleCondition::new().any([ -//! WindowRuleCondition::new().titles(["nvim", "emacs", "nano"]), -//! ]), -//! WindowRuleCondition::new().any([ -//! WindowRuleCondition::new().tags([&tag1, &tag2]), -//! ]), -//! ]) -//! ]) -//! ``` -//! -//! The above is true if either of the following are true: -//! - The window has class "Alacritty" and opens on both tags "1" and "2", or -//! - The window's class is either "nvim", "emacs", or "nano" *and* it opens on either tag "1" or -//! "2". -//! -//! # [`WindowRule`]s -//! `WindowRuleCondition`s are half of a window rule. The other half is the [`WindowRule`] itself. -//! -//! A `WindowRule` is what will apply to a window if a condition is true. -//! -//! ## Building `WindowRule`s -//! -//! Create a new window rule with [`WindowRule::new`]: -//! -//! ``` -//! let rule = WindowRule::new(); -//! ``` -//! -//! There are several rules you can set currently. -//! -//! ### [`WindowRule::output`] -//! This will cause the window to open on the specified output. -//! -//! ### [`WindowRule::tags`] -//! This will cause the window to open with the given tags. -//! -//! ### [`WindowRule::floating`] -//! This will cause the window to open either floating or tiled. -//! -//! ### [`WindowRule::fullscreen_or_maximized`] -//! This will cause the window to open either fullscreen, maximized, or neither. -//! -//! ### [`WindowRule::x`] -//! This will cause the window to open at the given x-coordinate. -//! -//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by -//! layouting. -//! -//! ### [`WindowRule::y`] -//! This will cause the window to open at the given y-coordinate. -//! -//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by -//! layouting. -//! -//! ### [`WindowRule::width`] -//! This will cause the window to open with the given width in pixels. -//! -//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by -//! layouting. -//! -//! ### [`WindowRule::height`] -//! This will cause the window to open with the given height in pixels. -//! -//! Note: this only applies to floating windows; tiled windows' geometry will be overridden by -//! layouting. - -use pinnacle_api_defs::pinnacle::window; - -use crate::{output::OutputHandle, tag::TagHandle}; - -use super::FullscreenOrMaximized; - -/// A condition for a [`WindowRule`] to apply to a window. -/// -/// `WindowRuleCondition`s are built using the builder pattern. -#[derive(Default, Debug, Clone)] -pub struct WindowRuleCondition(pub(super) window::v0alpha1::WindowRuleCondition); - -impl WindowRuleCondition { - /// Create a new, empty `WindowRuleCondition`. - pub fn new() -> Self { - Default::default() - } - - /// This condition requires that at least one provided condition is true. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRuleCondition; - /// - /// // `cond` will be true if the window opens with *either* class "Alacritty" or "firefox" - /// // *or* with title "Steam" - /// let cond = WindowRuleCondition::new() - /// .any([ - /// WindowRuleCondition::new().classes(["Alacritty", "firefox"]), - /// WindowRuleCondition::new().titles(["Steam"]). - /// ]); - /// ``` - pub fn any(mut self, conds: impl IntoIterator) -> Self { - self.0.any = conds.into_iter().map(|cond| cond.0).collect(); - self - } - - /// This condition requires that all provided conditions are true. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRuleCondition; - /// - /// // `cond` will be true if the window opens with class "Alacritty" *and* on tag "1" - /// let cond = WindowRuleCondition::new() - /// .any([ - /// WindowRuleCondition::new().tags([tag.get("1", None)?]), - /// WindowRuleCondition::new().titles(["Alacritty"]). - /// ]); - /// ``` - pub fn all(mut self, conds: impl IntoIterator) -> Self { - self.0.all = conds.into_iter().map(|cond| cond.0).collect(); - self - } - - /// This condition requires that the window's class matches. - /// - /// When used in a top level condition or inside of [`WindowRuleCondition::all`], - /// *all* classes must match (this is impossible). - /// - /// When used in [`WindowRuleCondition::any`], at least one of the - /// provided classes must match. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRuleCondition; - /// - /// // `cond` will be true if the window opens with class "Alacritty" - /// let cond = WindowRuleCondition::new().classes(["Alacritty"]); - /// - /// // Top level conditions need all items to be true, - /// // so the following will never be true as windows can't have two classes at once - /// let always_false = WindowRuleCondition::new().classes(["Alacritty", "firefox"]); - /// - /// // To make the above work, use [`WindowRuleCondition::any`]. - /// // The following will be true if the window is "Alacritty" or "firefox" - /// let any_class = WindowRuleCondition::new() - /// .any([ WindowRuleCondition::new().classes(["Alacritty", "firefox"]) ]); - /// ``` - pub fn classes(mut self, classes: impl IntoIterator>) -> Self { - self.0.classes = classes.into_iter().map(Into::into).collect(); - self - } - - /// This condition requires that the window's title matches. - /// - /// When used in a top level condition or inside of [`WindowRuleCondition::all`], - /// *all* titles must match (this is impossible). - /// - /// When used in [`WindowRuleCondition::any`], at least one of the - /// provided titles must match. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRuleCondition; - /// - /// // `cond` will be true if the window opens with title "vim" - /// let cond = WindowRuleCondition::new().titles(["vim"]); - /// - /// // Top level conditions need all items to be true, - /// // so the following will never be true as windows can't have two titles at once - /// let always_false = WindowRuleCondition::new().titles(["vim", "emacs"]); - /// - /// // To make the above work, use [`WindowRuleCondition::any`]. - /// // The following will be true if the window has the title "vim" or "emacs" - /// let any_title = WindowRuleCondition::new() - /// .any([WindowRuleCondition::new().titles(["vim", "emacs"])]); - /// ``` - pub fn titles(mut self, titles: impl IntoIterator>) -> Self { - self.0.titles = titles.into_iter().map(Into::into).collect(); - self - } - - /// This condition requires that the window's is opened on the given tags. - /// - /// When used in a top level condition or inside of [`WindowRuleCondition::all`], - /// the window must open on *all* given tags. - /// - /// When used in [`WindowRuleCondition::any`], the window must open on at least - /// one of the given tags. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRuleCondition; - /// - /// let tag1 = tag.get("1", None)?; - /// let tag2 = tag.get("2", None)?; - /// - /// // `cond` will be true if the window opens with tag "1" - /// let cond = WindowRuleCondition::new().tags([&tag1]); - /// - /// // Top level conditions need all items to be true, - /// // so the following will be true if the window opens with both tags "1" and "2" - /// let all_tags = WindowRuleCondition::new().tags([&tag1, &tag2]); - /// - /// // This does the same as the above - /// let all_tags = WindowRuleCondition::new() - /// .all([WindowRuleCondition::new().tags([&tag1, &tag2])]); - /// - /// // The following will be true if the window opens with *either* tag "1" or "2" - /// let any_tag = WindowRuleCondition::new() - /// .any([WindowRuleCondition::new().tags([&tag1, &tag2])]); - /// ``` - pub fn tags<'a>(mut self, tags: impl IntoIterator) -> Self { - self.0.tags = tags.into_iter().map(|tag| tag.id).collect(); - self - } -} - -/// A window rule. -/// -/// This is what will be applied to a window if it meets a [`WindowRuleCondition`]. -/// -/// `WindowRule`s are built using the builder pattern. -#[derive(Clone, Debug, Default)] -pub struct WindowRule(pub(super) window::v0alpha1::WindowRule); - -impl WindowRule { - /// Create a new, empty window rule. - pub fn new() -> Self { - Default::default() - } - - /// This rule will force windows to open on the provided `output`. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// // Force the window to open on "HDMI-1" - /// let rule = WindowRule::new().output(output.get_by_name("HDMI-1")?); - /// ``` - pub fn output(mut self, output: &OutputHandle) -> Self { - self.0.output = Some(output.name.clone()); - self - } - - /// This rule will force windows to open with the provided `tags`. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// let op = output.get_by_name("HDMI-1")?; - /// let tag1 = tag.get("1", &op)?; - /// let tag2 = tag.get("2", &op)?; - /// - /// // Force the window to open with tags "1" and "2" - /// let rule = WindowRule::new().tags([&tag1, &tag2]); - /// ``` - pub fn tags<'a>(mut self, tags: impl IntoIterator) -> Self { - self.0.tags = tags.into_iter().map(|tag| tag.id).collect(); - self - } - - /// This rule will force windows to open either floating or not. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// // Force the window to open floating - /// let rule = WindowRule::new().floating(true); - /// - /// // Force the window to open tiled - /// let rule = WindowRule::new().floating(false); - /// ``` - pub fn floating(mut self, floating: bool) -> Self { - self.0.floating = Some(floating); - self - } - - /// This rule will force windows to open either fullscreen, maximized, or neither. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// use pinnacle_api::window::FullscreenOrMaximized; - /// - /// // Force the window to open fullscreen - /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Fullscreen); - /// - /// // Force the window to open maximized - /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Maximized); - /// - /// // Force the window to open not fullscreen nor maximized - /// let rule = WindowRule::new().fullscreen_or_maximized(FullscreenOrMaximized::Neither); - /// ``` - pub fn fullscreen_or_maximized( - mut self, - fullscreen_or_maximized: FullscreenOrMaximized, - ) -> Self { - self.0.fullscreen_or_maximized = Some(fullscreen_or_maximized as i32); - self - } - - /// This rule will force windows to open at a specific x-coordinate. - /// - /// This will only actually be visible if the window is also floating. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// // Force the window to open at x = 480 - /// let rule = WindowRule::new().x(480); - /// ``` - pub fn x(mut self, x: i32) -> Self { - self.0.x = Some(x); - self - } - - /// This rule will force windows to open at a specific y-coordinate. - /// - /// This will only actually be visible if the window is also floating. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// // Force the window to open at y = 240 - /// let rule = WindowRule::new().y(240); - /// ``` - pub fn y(mut self, y: i32) -> Self { - self.0.y = Some(y); - self - } - - /// This rule will force windows to open with a specific width. - /// - /// This will only actually be visible if the window is also floating. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// // Force the window to open with a width of 500 pixels - /// let rule = WindowRule::new().width(500); - /// ``` - pub fn width(mut self, width: u32) -> Self { - self.0.width = Some(width as i32); - self - } - - /// This rule will force windows to open with a specific height. - /// - /// This will only actually be visible if the window is also floating. - /// - /// # Examples - /// - /// ``` - /// use pinnacle_api::window::rules::WindowRule; - /// - /// // Force the window to open with a height of 250 pixels - /// let rule = WindowRule::new().height(250); - /// ``` - pub fn height(mut self, height: u32) -> Self { - self.0.height = Some(height as i32); - self - } -} From 699474d2c94dcb9b9939680ccfe8c4d78d4d3ea8 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sun, 21 Jan 2024 23:47:49 -0600 Subject: [PATCH 21/26] Get protoc in doc workflow --- .github/workflows/rustdoc.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rustdoc.yml b/.github/workflows/rustdoc.yml index 3216823..a02769e 100644 --- a/.github/workflows/rustdoc.yml +++ b/.github/workflows/rustdoc.yml @@ -22,6 +22,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Get protoc + run: sudo apt install protobuf-compiler - name: Build docs run: cd ./api/rust && cargo doc - name: Create index.html From 107f67ec64ca2c71a3305702673f5b9597ae1ca3 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 22 Jan 2024 00:03:55 -0600 Subject: [PATCH 22/26] Update README --- README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 18fd23c..66f32d2 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ Pinnacle is a Wayland compositor built in Rust using [Smithay](https://github.co It's my attempt at creating something like [AwesomeWM](https://github.com/awesomeWM/awesome) for Wayland. -It sports extensive configurability through either Lua or Rust, with the ability to add more languages in the future. +It sports extensive configurability through either Lua or Rust, with the ability to add more languages +in the future. And by that I mean other people can do the adding, + I'm already maintaining Lua and Rust lol > ### More video examples below! >
@@ -61,7 +63,7 @@ You will need: - [Rust](https://www.rust-lang.org/) 1.72 or newer, to build the project and use the Rust API - [Lua](https://www.lua.org/) 5.4 or newer, to use the Lua API - Packages for [Smithay](https://github.com/Smithay/smithay): -`libwayland libxkbcommon libudev libinput libgdm libseat`, as well as `xwayland` + `libwayland libxkbcommon libudev libinput libgdm libseat`, as well as `xwayland` - Arch: ```sh sudo pacman -S wayland wayland-protocols libxkbcommon systemd-libs libinput mesa seatd xorg-xwayland @@ -118,10 +120,6 @@ See flags you can pass in by running `cargo run -- --help` (or `-h`). # Configuration Pinnacle is configured in your choice of Lua or Rust. -> [!NOTE] -> Pinnacle is currently in the process of migrating the configuration backend from MessagePack to gRPC. -> The Lua library has already been rewritten, and the Rust API will be rewritten soon. - ## Out-of-the-box configurations If you just want to test Pinnacle out without copying stuff to your config directory, run one of the following in the crate root: @@ -132,13 +130,13 @@ PINNACLE_CONFIG_DIR="./api/lua/examples/default" cargo run PINNACLE_CONFIG_DIR="~/.local/share/pinnacle/default_config" cargo run # For a Rust configuration -PINNACLE_CONFIG_DIR="./api/rust" cargo run +PINNACLE_CONFIG_DIR="./api/rust/examples/default_config" cargo run ``` ## Custom configuration > [!IMPORTANT] -> Pinnacle is under heavy development, and there *will* be major breaking changes to these APIs +> Pinnacle is under development, and there *will* be major breaking changes to these APIs > until I release version 0.1, at which point there will be an API stability spec in place. > > Until then, I recommend you either use the out-of-the-box configs above or prepare for @@ -180,7 +178,7 @@ If you want to use Rust to configure Pinnacle, follow these steps: 1. In `~/.config/pinnacle`, run `cargo init`. 2. In the `Cargo.toml` file, add the following under `[dependencies]`: ```toml -pinnacle_api = { git = "http://github.com/pinnacle-comp/pinnacle" } +pinnacle-api = { git = "http://github.com/pinnacle-comp/pinnacle" } ``` 3. Create the file `metaconfig.toml` at the root. Add the following to the file: ```toml @@ -188,7 +186,7 @@ command = ["cargo", "run"] reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } ``` -4. Copy the contents from [`example_config.rs`](api/rust/examples/example_config.rs) to `src/main.rs`. +4. Copy the [default config](api/rust/examples/default_config/main.rs) to `src/main.rs`. 5. Run Pinnacle! (You may want to run `cargo build` beforehand so you don't have to wait for your config to compile.) ### API Documentation From ba1b3feefb7aaba3bd9124f869372fc3ec354ec5 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 22 Jan 2024 00:09:11 -0600 Subject: [PATCH 23/26] Fix typo --- api/rust/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/rust/src/lib.rs b/api/rust/src/lib.rs index e573209..807c21f 100644 --- a/api/rust/src/lib.rs +++ b/api/rust/src/lib.rs @@ -3,7 +3,7 @@ //! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s //! configuration API. //! -//! This library allows to to interface with the Pinnacle compositor and configure various aspects +//! This library allows you to interface with the Pinnacle compositor and configure various aspects //! like input and the tag system. //! //! # Configuration From 9f067a0996cd9227d570bc1c960e0a3a312f1bb5 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 22 Jan 2024 00:12:32 -0600 Subject: [PATCH 24/26] Fix wording --- api/rust/pinnacle-api-macros/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/rust/pinnacle-api-macros/src/lib.rs b/api/rust/pinnacle-api-macros/src/lib.rs index 36631d6..8b51e7a 100644 --- a/api/rust/pinnacle-api-macros/src/lib.rs +++ b/api/rust/pinnacle-api-macros/src/lib.rs @@ -10,8 +10,7 @@ use syn::{ /// This will cause the function to connect to Pinnacle's gRPC server, run your configuration code, /// then await all necessary futures needed to call callbacks. /// -/// If your config contains anything that has a callback, this function will not return unless an -/// error occurs. +/// This function will not return unless an error occurs. /// /// # Usage /// The function must be marked `async`, as this macro will insert the `#[tokio::main]` macro below From 8356f99d845b095910b4c6011463e851af9e369b Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 22 Jan 2024 18:36:04 -0600 Subject: [PATCH 25/26] Split `tag.get`, impl `PartialEq`, `Eq`, `Hash` for handles --- api/rust/examples/default_config/main.rs | 8 +-- api/rust/src/output.rs | 14 +++++ api/rust/src/tag.rs | 67 +++++++++++++++++------- api/rust/src/window.rs | 14 +++++ 4 files changed, 79 insertions(+), 24 deletions(-) diff --git a/api/rust/examples/default_config/main.rs b/api/rust/examples/default_config/main.rs index c77627a..edf6968 100644 --- a/api/rust/examples/default_config/main.rs +++ b/api/rust/examples/default_config/main.rs @@ -94,19 +94,19 @@ async fn main() { for tag_name in tag_names { input.keybind([mod_key], tag_name, move || { - if let Some(tg) = tag.get(tag_name, None) { + if let Some(tg) = tag.get(tag_name) { tg.switch_to(); } }); input.keybind([mod_key, Mod::Shift], tag_name, move || { - if let Some(tg) = tag.get(tag_name, None) { + if let Some(tg) = tag.get(tag_name) { tg.toggle_active(); } }); input.keybind([mod_key, Mod::Alt], tag_name, move || { - if let Some(tg) = tag.get(tag_name, None) { + if let Some(tg) = tag.get(tag_name) { if let Some(win) = window.get_focused() { win.move_to_tag(&tg); } @@ -114,7 +114,7 @@ async fn main() { }); input.keybind([mod_key, Mod::Shift, Mod::Alt], tag_name, move || { - if let Some(tg) = tag.get(tag_name, None) { + if let Some(tg) = tag.get(tag_name) { if let Some(win) = window.get_focused() { win.toggle_tag(&tg); } diff --git a/api/rust/src/output.rs b/api/rust/src/output.rs index 6ef6d9b..7fca866 100644 --- a/api/rust/src/output.rs +++ b/api/rust/src/output.rs @@ -166,6 +166,20 @@ pub struct OutputHandle { pub(crate) name: String, } +impl PartialEq for OutputHandle { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Eq for OutputHandle {} + +impl std::hash::Hash for OutputHandle { + fn hash(&self, state: &mut H) { + self.name.hash(state); + } +} + /// The alignment to use for [`OutputHandle::set_loc_adj_to`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Alignment { diff --git a/api/rust/src/tag.rs b/api/rust/src/tag.rs index 256fdd3..ddf93eb 100644 --- a/api/rust/src/tag.rs +++ b/api/rust/src/tag.rs @@ -131,40 +131,53 @@ impl Tag { }) } - /// Get a handle to the first tag with the given name on `output`. + /// Get a handle to the first tag with the given name on the focused output. /// - /// If `output` is `None`, the focused output will be used. + /// If you need to get a tag on a specific output, see [`Tag::get_on_output`]. /// /// # Examples /// /// ``` - /// // Get tag "1" on output "HDMI-1" - /// if let Some(op) = output.get_by_name("HDMI-1") { - /// let tg = tag.get("1", &op); - /// } - /// /// // Get tag "Thing" on the focused output - /// let tg = tag.get("Thing", None); + /// let tg = tag.get("Thing"); /// ``` - pub fn get<'a>( - &self, - name: impl Into, - output: impl Into>, - ) -> Option { + pub fn get(&self, name: impl Into) -> Option { let name = name.into(); - let output: Option<&OutputHandle> = output.into(); let output_module = Output::new(self.channel.clone(), self.fut_sender.clone()); + let focused_output = output_module.get_focused(); self.get_all().find(|tag| { let props = tag.props(); let same_tag_name = props.name.as_ref() == Some(&name); - let same_output = props.output.is_some_and(|op| { - Some(op.name) - == output - .map(|o| o.name.clone()) - .or_else(|| output_module.get_focused().map(|o| o.name)) - }); + let same_output = props.output.is_some_and(|op| Some(op) == focused_output); + + same_tag_name && same_output + }) + } + + /// Get a handle to the first tag with the given name on the specified output. + /// + /// If you just need to get a tag on the focused output, see [`Tag::get`]. + /// + /// # Examples + /// + /// ``` + /// // Get tag "Thing" on "HDMI-1" + /// let tg = tag.get_on_output("Thing", output.get_by_name("HDMI-2")?); + /// ``` + pub fn get_on_output( + &self, + name: impl Into, + output: &OutputHandle, + ) -> Option { + let name = name.into(); + + self.get_all().find(|tag| { + let props = tag.props(); + + let same_tag_name = props.name.as_ref() == Some(&name); + let same_output = props.output.is_some_and(|op| &op == output); same_tag_name && same_output }) @@ -318,6 +331,20 @@ pub struct TagHandle { pub(crate) id: u32, } +impl PartialEq for TagHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for TagHandle {} + +impl std::hash::Hash for TagHandle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + /// Various static layouts. #[repr(i32)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)] diff --git a/api/rust/src/window.rs b/api/rust/src/window.rs index c120ce8..fb6f12e 100644 --- a/api/rust/src/window.rs +++ b/api/rust/src/window.rs @@ -172,6 +172,20 @@ pub struct WindowHandle { pub(crate) output_client: OutputServiceClient, } +impl PartialEq for WindowHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for WindowHandle {} + +impl std::hash::Hash for WindowHandle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + /// Whether a window is fullscreen, maximized, or neither. #[repr(i32)] #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] From 236f40b364d1e07a1b08a715c4d7e988af3e3330 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 22 Jan 2024 20:27:22 -0600 Subject: [PATCH 26/26] Add comments, improve macro error msgs --- api/rust/examples/default_config/main.rs | 33 +++++++++++++++++++++--- api/rust/pinnacle-api-macros/src/lib.rs | 19 +++++++++++--- api/rust/src/lib.rs | 4 +-- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/api/rust/examples/default_config/main.rs b/api/rust/examples/default_config/main.rs index edf6968..7462eb5 100644 --- a/api/rust/examples/default_config/main.rs +++ b/api/rust/examples/default_config/main.rs @@ -18,59 +18,80 @@ async fn main() { let mod_key = Mod::Ctrl; + let terminal = "alacritty"; + + // Mousebinds + + // `mod_key + left click` starts moving a window input.mousebind([mod_key], MouseButton::Left, MouseEdge::Press, || { window.begin_move(MouseButton::Left); }); + // `mod_key + right click` starts resizing a window input.mousebind([mod_key], MouseButton::Right, MouseEdge::Press, || { window.begin_resize(MouseButton::Right); }); // Keybinds + // `mod_key + alt + q` quits Pinnacle input.keybind([mod_key, Mod::Alt], 'q', || { pinnacle.quit(); }); + // `mod_key + alt + c` closes the focused window input.keybind([mod_key, Mod::Alt], 'c', || { if let Some(window) = window.get_focused() { window.close(); } }); - input.keybind([mod_key], Keysym::Return, || { - process.spawn(["alacritty"]); + // `mod_key + Return` spawns a terminal + input.keybind([mod_key], Keysym::Return, move || { + process.spawn([terminal]); }); + // `mod_key + alt + space` toggles floating input.keybind([mod_key, Mod::Alt], Keysym::space, || { if let Some(window) = window.get_focused() { window.toggle_floating(); } }); + // `mod_key + f` toggles fullscreen input.keybind([mod_key], 'f', || { if let Some(window) = window.get_focused() { window.toggle_fullscreen(); } }); + // `mod_key + m` toggles maximized input.keybind([mod_key], 'm', || { if let Some(window) = window.get_focused() { window.toggle_maximized(); } }); + // Window rules + // + // You can define window rules to get windows to open with desired properties. + // See `pinnacle_api::window::rules` in the docs for more information. + // Tags let tag_names = ["1", "2", "3", "4", "5"]; + // Setup all monitors with tags "1" through "5" output.connect_for_all(move |op| { let mut tags = tag.add(&op, tag_names); + + // Be sure to set a tag to active or windows won't display tags.next().unwrap().set_active(true); }); - process.spawn_once(["alacritty"]); + process.spawn_once([terminal]); + // Create a layout cycler to cycle through the given layouts let LayoutCycler { prev: layout_prev, next: layout_next, @@ -84,27 +105,32 @@ async fn main() { Layout::CornerBottomRight, ]); + // `mod_key + space` cycles to the next layout input.keybind([mod_key], Keysym::space, move || { layout_next(None); }); + // `mod_key + shift + space` cycles to the previous layout input.keybind([mod_key, Mod::Shift], Keysym::space, move || { layout_prev(None); }); for tag_name in tag_names { + // `mod_key + 1-5` switches to tag "1" to "5" input.keybind([mod_key], tag_name, move || { if let Some(tg) = tag.get(tag_name) { tg.switch_to(); } }); + // `mod_key + shift + 1-5` toggles tag "1" to "5" input.keybind([mod_key, Mod::Shift], tag_name, move || { if let Some(tg) = tag.get(tag_name) { tg.toggle_active(); } }); + // `mod_key + alt + 1-5` moves the focused window to tag "1" to "5" input.keybind([mod_key, Mod::Alt], tag_name, move || { if let Some(tg) = tag.get(tag_name) { if let Some(win) = window.get_focused() { @@ -113,6 +139,7 @@ async fn main() { } }); + // `mod_key + shift + alt + 1-5` toggles tag "1" to "5" on the focused window input.keybind([mod_key, Mod::Shift, Mod::Alt], tag_name, move || { if let Some(tg) = tag.get(tag_name) { if let Some(win) = window.get_focused() { diff --git a/api/rust/pinnacle-api-macros/src/lib.rs b/api/rust/pinnacle-api-macros/src/lib.rs index 8b51e7a..7838045 100644 --- a/api/rust/pinnacle-api-macros/src/lib.rs +++ b/api/rust/pinnacle-api-macros/src/lib.rs @@ -2,7 +2,7 @@ use proc_macro2::{Ident, Span}; use quote::{quote, quote_spanned}; use syn::{ parse::Parse, parse_macro_input, punctuated::Punctuated, spanned::Spanned, Expr, Lit, - MetaNameValue, Token, + MetaNameValue, ReturnType, Stmt, Token, }; /// Transform the annotated function into one used to configure the Pinnacle compositor. @@ -55,7 +55,14 @@ pub fn config( if sig.asyncness.is_none() { return quote_spanned! {sig.fn_token.span()=> - compile_error!("This function must be marked `async` to run a Pinnacle config"); + compile_error!("this function must be marked `async` to run a Pinnacle config"); + } + .into(); + } + + if let ReturnType::Type(_, ty) = sig.output { + return quote_spanned! {ty.span()=> + compile_error!("this function must not have a return type"); } .into(); } @@ -64,6 +71,12 @@ pub fn config( let stmts = item.block.stmts; + if let Some(ret @ Stmt::Expr(Expr::Return(_), _)) = stmts.last() { + return quote_spanned! {ret.span()=> + compile_error!("this function must not return, as it awaits futures after the end of this statement"); + }.into(); + } + let module_ident = macro_input.ident; let options = macro_input.options; @@ -106,7 +119,7 @@ pub fn config( let tokio_attr = internal_tokio.then(|| { quote! { - #[::pinnacle_api::tokio::main] + #[::pinnacle_api::tokio::main(crate = "::pinnacle_api::tokio")] } }); diff --git a/api/rust/src/lib.rs b/api/rust/src/lib.rs index 807c21f..64acc50 100644 --- a/api/rust/src/lib.rs +++ b/api/rust/src/lib.rs @@ -170,9 +170,7 @@ pub async fn connect( /// /// This function is inserted at the end of your config through the [`config`] macro. /// You should use the macro instead of this function directly. -pub async fn listen( - fut_recv: UnboundedReceiver>, // api_modules: ApiModules<'a>, -) { +pub async fn listen(fut_recv: UnboundedReceiver>) { let mut future_set = FuturesUnordered::< BoxFuture<( Option>,