From b8c5ec751b3d1123e3e7a29d0c223c70f7d330be Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 20 Feb 2024 21:35:51 -0600 Subject: [PATCH] Add basic API batching for Rust Function coloring is fun --- api/rust/src/output.rs | 138 ++++++++++++++++++++++++------- api/rust/src/process.rs | 1 + api/rust/src/tag.rs | 157 ++++++++++++++++++++++++------------ api/rust/src/util.rs | 166 ++++++++++++++++++++++++++++++++++++++ api/rust/src/window.rs | 174 +++++++++++++++++++++++++++------------- 5 files changed, 496 insertions(+), 140 deletions(-) diff --git a/api/rust/src/output.rs b/api/rust/src/output.rs index d26f7c6..d8b27e3 100644 --- a/api/rust/src/output.rs +++ b/api/rust/src/output.rs @@ -21,15 +21,16 @@ use pinnacle_api_defs::pinnacle::{ }; use tonic::transport::Channel; -use crate::{block_on_tokio, tag::TagHandle}; +use crate::{block_on_tokio, tag::TagHandle, util::Batch}; /// 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>, + output_client: OutputServiceClient, + tag_client: TagServiceClient, } impl Output { @@ -38,19 +39,12 @@ impl Output { fut_sender: UnboundedSender>, ) -> Self { Self { - channel, + output_client: OutputServiceClient::new(channel.clone()), + tag_client: TagServiceClient::new(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 @@ -59,15 +53,22 @@ impl Output { /// 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_tokio(client.get(output::v0alpha1::GetRequest {})) + block_on_tokio(self.get_all_async()) + } + + /// The async version of [`Output::get_all`]. + pub async fn get_all_async(&self) -> impl Iterator { + let mut client = self.output_client.clone(); + let tag_client = self.tag_client.clone(); + client + .get(output::v0alpha1::GetRequest {}) + .await .unwrap() .into_inner() .output_names .into_iter() .map(move |name| OutputHandle { - client: client.clone(), + output_client: client.clone(), tag_client: tag_client.clone(), name, }) @@ -84,8 +85,15 @@ impl Output { /// let op2 = output.get_by_name("HDMI-2")?; /// ``` pub fn get_by_name(&self, name: impl Into) -> Option { + block_on_tokio(self.get_by_name_async(name)) + } + + /// The async version of [`Output::get_by_name`]. + pub async fn get_by_name_async(&self, name: impl Into) -> Option { let name: String = name.into(); - self.get_all().find(|output| output.name == name) + self.get_all_async() + .await + .find(|output| output.name == name) } /// Get a handle to the focused output. @@ -102,6 +110,14 @@ impl Output { .find(|output| matches!(output.props().focused, Some(true))) } + /// The async version of [`Output::get_focused`]. + pub async fn get_focused_async(&self) -> Option { + self.get_all_async().await.batch_find( + |output| output.props_async().boxed(), + |props| props.focused.is_some_and(|focused| focused), + ) + } + /// Connect a closure to be run on all current and future outputs. /// /// When called, `connect_for_all` will do two things: @@ -126,8 +142,8 @@ impl Output { for_all(output); } - let mut client = self.create_output_client(); - let tag_client = self.create_tag_client(); + let mut client = self.output_client.clone(); + let tag_client = self.tag_client.clone(); self.fut_sender .unbounded_send( @@ -144,7 +160,7 @@ impl Output { }; let output = OutputHandle { - client: client.clone(), + output_client: client.clone(), tag_client: tag_client.clone(), name: output_name, }; @@ -163,7 +179,7 @@ impl Output { /// This allows you to manipulate outputs and get their properties. #[derive(Clone, Debug)] pub struct OutputHandle { - pub(crate) client: OutputServiceClient, + pub(crate) output_client: OutputServiceClient, pub(crate) tag_client: TagServiceClient, pub(crate) name: String, } @@ -245,7 +261,7 @@ impl OutputHandle { /// // ^x=1920 /// ``` pub fn set_location(&self, x: impl Into>, y: impl Into>) { - let mut client = self.client.clone(); + let mut client = self.output_client.clone(); block_on_tokio(client.set_location(SetLocationRequest { output_name: Some(self.name.clone()), x: x.into(), @@ -377,14 +393,19 @@ impl OutputHandle { /// } = output.get_focused()?.props(); /// ``` pub fn props(&self) -> OutputProperties { - let mut client = self.client.clone(); - let response = block_on_tokio(client.get_properties( - output::v0alpha1::GetPropertiesRequest { + block_on_tokio(self.props_async()) + } + + /// The async version of [`OutputHandle::props`]. + pub async fn props_async(&self) -> OutputProperties { + let mut client = self.output_client.clone(); + let response = client + .get_properties(output::v0alpha1::GetPropertiesRequest { output_name: Some(self.name.clone()), - }, - )) - .unwrap() - .into_inner(); + }) + .await + .unwrap() + .into_inner(); OutputProperties { make: response.make, @@ -401,8 +422,8 @@ impl OutputHandle { .tag_ids .into_iter() .map(|id| TagHandle { - client: self.tag_client.clone(), - output_client: self.client.clone(), + tag_client: self.tag_client.clone(), + output_client: self.output_client.clone(), id, }) .collect(), @@ -418,6 +439,11 @@ impl OutputHandle { self.props().make } + /// The async version of [`OutputHandle::make`]. + pub async fn make_async(&self) -> Option { + self.props_async().await.make + } + /// Get this output's model. /// /// Shorthand for `self.props().make`. @@ -425,6 +451,11 @@ impl OutputHandle { self.props().model } + /// The async version of [`OutputHandle::model`]. + pub async fn model_async(&self) -> Option { + self.props_async().await.model + } + /// Get this output's x position in the global space. /// /// Shorthand for `self.props().x`. @@ -432,6 +463,11 @@ impl OutputHandle { self.props().x } + /// The async version of [`OutputHandle::x`]. + pub async fn x_async(&self) -> Option { + self.props_async().await.x + } + /// Get this output's y position in the global space. /// /// Shorthand for `self.props().y`. @@ -439,6 +475,11 @@ impl OutputHandle { self.props().y } + /// The async version of [`OutputHandle::y`]. + pub async fn y_async(&self) -> Option { + self.props_async().await.y + } + /// Get this output's screen width in pixels. /// /// Shorthand for `self.props().pixel_width`. @@ -446,6 +487,11 @@ impl OutputHandle { self.props().pixel_width } + /// The async version of [`OutputHandle::pixel_width`]. + pub async fn pixel_width_async(&self) -> Option { + self.props_async().await.pixel_width + } + /// Get this output's screen height in pixels. /// /// Shorthand for `self.props().pixel_height`. @@ -453,6 +499,11 @@ impl OutputHandle { self.props().pixel_height } + /// The async version of [`OutputHandle::pixel_height`]. + pub async fn pixel_height_async(&self) -> Option { + self.props_async().await.pixel_height + } + /// Get this output's refresh rate in millihertz. /// /// For example, 144Hz will be returned as 144000. @@ -462,6 +513,11 @@ impl OutputHandle { self.props().refresh_rate } + /// The async version of [`OutputHandle::refresh_rate`]. + pub async fn refresh_rate_async(&self) -> Option { + self.props_async().await.refresh_rate + } + /// Get this output's physical width in millimeters. /// /// Shorthand for `self.props().physical_width`. @@ -469,6 +525,11 @@ impl OutputHandle { self.props().physical_width } + /// The async version of [`OutputHandle::physical_width`]. + pub async fn physical_width_async(&self) -> Option { + self.props_async().await.physical_width + } + /// Get this output's physical height in millimeters. /// /// Shorthand for `self.props().physical_height`. @@ -476,6 +537,11 @@ impl OutputHandle { self.props().physical_height } + /// The async version of [`OutputHandle::physical_height`]. + pub async fn physical_height_async(&self) -> Option { + self.props_async().await.physical_height + } + /// Get whether this output is focused or not. /// /// This is currently implemented as the output with the most recent pointer motion. @@ -485,6 +551,11 @@ impl OutputHandle { self.props().focused } + /// The async version of [`OutputHandle::focused`]. + pub async fn focused_async(&self) -> Option { + self.props_async().await.focused + } + /// Get the tags this output has. /// /// Shorthand for `self.props().tags` @@ -492,6 +563,11 @@ impl OutputHandle { self.props().tags } + /// The async version of [`OutputHandle::tags`]. + pub async fn tags_async(&self) -> Vec { + self.props_async().await.tags + } + /// Get this output's unique name (the name of its connector). pub fn name(&self) -> &str { &self.name @@ -499,7 +575,7 @@ impl OutputHandle { } /// The properties of an output. -#[derive(Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub struct OutputProperties { /// The make of the output pub make: Option, diff --git a/api/rust/src/process.rs b/api/rust/src/process.rs index f5b1e9b..c93a063 100644 --- a/api/rust/src/process.rs +++ b/api/rust/src/process.rs @@ -24,6 +24,7 @@ pub struct Process { } /// Optional callbacks to be run when a spawned process prints to stdout or stderr or exits. +#[derive(Default)] pub struct SpawnCallbacks { /// A callback that will be run when a process prints to stdout with a line pub stdout: Option>, diff --git a/api/rust/src/tag.rs b/api/rust/src/tag.rs index ce90ff0..e760896 100644 --- a/api/rust/src/tag.rs +++ b/api/rust/src/tag.rs @@ -34,7 +34,7 @@ use std::{ sync::{Arc, Mutex}, }; -use futures::{channel::mpsc::UnboundedSender, future::BoxFuture}; +use futures::{channel::mpsc::UnboundedSender, future::BoxFuture, FutureExt}; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::{ output::v0alpha1::output_service_client::OutputServiceClient, @@ -51,6 +51,7 @@ use tonic::transport::Channel; use crate::{ block_on_tokio, output::{Output, OutputHandle}, + util::Batch, }; /// A struct that allows you to add and remove tags and get [`TagHandle`]s. @@ -58,6 +59,8 @@ use crate::{ pub struct Tag { channel: Channel, fut_sender: UnboundedSender>, + tag_client: TagServiceClient, + output_client: OutputServiceClient, } impl Tag { @@ -66,19 +69,13 @@ impl Tag { fut_sender: UnboundedSender>, ) -> Self { Self { + tag_client: TagServiceClient::new(channel.clone()), + output_client: OutputServiceClient::new(channel.clone()), 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 @@ -97,20 +94,31 @@ impl Tag { output: &OutputHandle, tag_names: impl IntoIterator>, ) -> impl Iterator { - let mut client = self.create_tag_client(); - let output_client = self.create_output_client(); + block_on_tokio(self.add_async(output, tag_names)) + } + + /// The async version of [`Tag::add`]. + pub async fn add_async( + &self, + output: &OutputHandle, + tag_names: impl IntoIterator>, + ) -> impl Iterator { + let mut client = self.tag_client.clone(); + let output_client = self.output_client.clone(); let tag_names = tag_names.into_iter().map(Into::into).collect(); - let response = block_on_tokio(client.add(AddRequest { - output_name: Some(output.name.clone()), - tag_names, - })) - .unwrap() - .into_inner(); + let response = client + .add(AddRequest { + output_name: Some(output.name.clone()), + tag_names, + }) + .await + .unwrap() + .into_inner(); response.tag_ids.into_iter().map(move |id| TagHandle { - client: client.clone(), + tag_client: client.clone(), output_client: output_client.clone(), id, }) @@ -124,15 +132,22 @@ impl Tag { /// 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(); + block_on_tokio(self.get_all_async()) + } - let response = block_on_tokio(client.get(tag::v0alpha1::GetRequest {})) + /// The async version of [`Tag::get_all`]. + pub async fn get_all_async(&self) -> impl Iterator { + let mut client = self.tag_client.clone(); + let output_client = self.output_client.clone(); + + let response = client + .get(tag::v0alpha1::GetRequest {}) + .await .unwrap() .into_inner(); response.tag_ids.into_iter().map(move |id| TagHandle { - client: client.clone(), + tag_client: client.clone(), output_client: output_client.clone(), id, }) @@ -149,18 +164,20 @@ impl Tag { /// let tg = tag.get("Thing"); /// ``` pub fn get(&self, name: impl Into) -> Option { + block_on_tokio(self.get_async(name)) + } + + /// The async version of [`Tag::get`]. + pub async fn get_async(&self, name: impl Into) -> Option { let name = name.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) == focused_output); - - same_tag_name && same_output - }) + if let Some(output) = focused_output { + self.get_on_output_async(name, &output).await + } else { + None + } } /// Get a handle to the first tag with the given name on the specified output. @@ -177,17 +194,27 @@ impl Tag { &self, name: impl Into, output: &OutputHandle, + ) -> Option { + block_on_tokio(self.get_on_output_async(name, output)) + } + + /// The async version of [`Tag::get_on_output`]. + pub async fn get_on_output_async( + &self, + name: impl Into, + output: &OutputHandle, ) -> Option { let name = name.into(); - self.get_all().find(|tag| { - let props = tag.props(); + self.get_all_async().await.batch_find( + |tag| tag.props_async().boxed(), + |props| { + let same_tag_name = props.name.as_ref() == Some(&name); + let same_output = props.output.as_ref().is_some_and(|op| op == output); - 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 - }) + same_tag_name && same_output + }, + ) } /// Remove the given tags from their outputs. @@ -202,7 +229,7 @@ impl Tag { 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(); + let mut client = self.tag_client.clone(); block_on_tokio(client.remove(RemoveRequest { tag_ids })).unwrap(); } @@ -333,9 +360,9 @@ pub struct LayoutCycler { /// 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, + pub(crate) tag_client: TagServiceClient, + pub(crate) output_client: OutputServiceClient, } impl PartialEq for TagHandle { @@ -388,7 +415,7 @@ impl TagHandle { /// tag.get("3")?.switch_to(); // Displays Steam /// ``` pub fn switch_to(&self) { - let mut client = self.client.clone(); + let mut client = self.tag_client.clone(); block_on_tokio(client.switch_to(SwitchToRequest { tag_id: Some(self.id), })) @@ -414,7 +441,7 @@ impl TagHandle { /// tag.get("2")?.set_active(false); // Displays Steam /// ``` pub fn set_active(&self, set: bool) { - let mut client = self.client.clone(); + let mut client = self.tag_client.clone(); block_on_tokio(client.set_active(SetActiveRequest { tag_id: Some(self.id), set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Set(set)), @@ -442,7 +469,7 @@ impl TagHandle { /// tag.get("2")?.toggle(); // Displays nothing /// ``` pub fn toggle_active(&self) { - let mut client = self.client.clone(); + let mut client = self.tag_client.clone(); block_on_tokio(client.set_active(SetActiveRequest { tag_id: Some(self.id), set_or_toggle: Some(tag::v0alpha1::set_active_request::SetOrToggle::Toggle(())), @@ -463,8 +490,9 @@ impl TagHandle { /// tags[3].remove(); /// // "DP-1" now only has tags "1" and "Buckle" /// ``` - pub fn remove(mut self) { - block_on_tokio(self.client.remove(RemoveRequest { + pub fn remove(&self) { + let mut tag_client = self.tag_client.clone(); + block_on_tokio(tag_client.remove(RemoveRequest { tag_ids: vec![self.id], })) .unwrap(); @@ -487,7 +515,7 @@ impl TagHandle { /// tag.get("1", None)?.set_layout(Layout::CornerTopLeft); /// ``` pub fn set_layout(&self, layout: Layout) { - let mut client = self.client.clone(); + let mut client = self.tag_client.clone(); block_on_tokio(client.set_layout(SetLayoutRequest { tag_id: Some(self.id), layout: Some(layout as i32), @@ -509,20 +537,27 @@ impl TagHandle { /// } = tag.get("1", None)?.props(); /// ``` pub fn props(&self) -> TagProperties { - let mut client = self.client.clone(); + block_on_tokio(self.props_async()) + } + + /// The async version of [`TagHandle::props`]. + pub async fn props_async(&self) -> TagProperties { + let mut client = self.tag_client.clone(); let output_client = self.output_client.clone(); - let response = block_on_tokio(client.get_properties(tag::v0alpha1::GetPropertiesRequest { - tag_id: Some(self.id), - })) - .unwrap() - .into_inner(); + let response = client + .get_properties(tag::v0alpha1::GetPropertiesRequest { + tag_id: Some(self.id), + }) + .await + .unwrap() + .into_inner(); TagProperties { active: response.active, name: response.name, output: response.output_name.map(|name| OutputHandle { - client: output_client, + output_client, tag_client: client, name, }), @@ -536,6 +571,11 @@ impl TagHandle { self.props().active } + /// The async version of [`TagHandle::active`]. + pub async fn active_async(&self) -> Option { + self.props_async().await.active + } + /// Get this tag's name. /// /// Shorthand for `self.props().name`. @@ -543,15 +583,26 @@ impl TagHandle { self.props().name } + /// The async version of [`TagHandle::name`]. + pub async fn name_async(&self) -> Option { + self.props_async().await.name + } + /// Get a handle to the output this tag is on. /// /// Shorthand for `self.props().output`. pub fn output(&self) -> Option { self.props().output } + + /// The async version of [`TagHandle::output`]. + pub async fn output_async(&self) -> Option { + self.props_async().await.output + } } /// Properties of a tag. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub struct TagProperties { /// Whether the tag is active or not pub active: Option, diff --git a/api/rust/src/util.rs b/api/rust/src/util.rs index cfbc5ea..ed787dd 100644 --- a/api/rust/src/util.rs +++ b/api/rust/src/util.rs @@ -4,6 +4,15 @@ //! Utility types. +use std::pin::Pin; + +use futures::{stream::FuturesOrdered, Future, StreamExt}; + +use crate::block_on_tokio; + +pub use crate::batch_boxed; +pub use crate::batch_boxed_async; + /// The size and location of something. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Geometry { @@ -16,3 +25,160 @@ pub struct Geometry { /// The height pub height: u32, } + +/// Batch a set of requests that will be sent ot the compositor all at once. +/// +/// # Rationale +/// +/// Normally, all API calls are blocking. For example, calling [`Window::get_all`][crate::window::Window::get_all] +/// then calling [`WindowHandle.props`][crate::window::WindowHandle::props] +/// on each returned window handle will block after each `props` call waiting for the compositor to respond: +/// +/// ``` +/// // This will block after each call to `window.props()`, meaning this all happens synchronously. +/// // If the compositor is running slowly for whatever reason, this will take a long time to complete. +/// let props = window.get_all() +/// .map(|window| window.props()) +/// .collect::>(); +/// ``` +/// +/// In order to mitigate this issue, you can batch up a set of API calls using this function. +/// This will send all requests to the compositor at once without blocking, then wait for the compositor +/// to respond. +/// +/// You'll see that this function takes in an `IntoIterator` of `Future`s. As such, +/// all API calls that return something have an async variant named `*_async` that returns a future. +/// You must pass these futures into the batch function instead of their non-async counterparts. +/// +/// # The `batch_boxed` macro +/// The [`util`][crate::util] module also provides the [`batch_boxed`] macro. +/// +/// The [`batch`] function only accepts one concrete type of future, meaning that you +/// can only batch a collection of futures from one specific function or method. +/// +/// As a convenience, `batch_boxed` accepts one or more different futures that return the same type. +/// It will place provided futures in a `Pin>` to erase the types and pass them along to `batch`. +/// +/// # Examples +/// +/// ``` +/// use pinnacle_api::util::batch; +/// use pinnacle_api::window::WindowProperties; +/// +/// let props: Vec = batch(window.get_all().map(|window| window.props_async())); +/// // Don't forget the `async` ^^^^^ +/// ``` +/// +pub fn batch(requests: impl IntoIterator>) -> Vec { + let results = FuturesOrdered::from_iter(requests).collect::>(); + block_on_tokio(results) +} + +/// The async version of [`batch`]. +/// +/// See [`batch`] for more information. +pub async fn batch_async(requests: impl IntoIterator>) -> Vec { + let results = FuturesOrdered::from_iter(requests).collect::>(); + results.await +} + +/// A convenience macro to batch API calls in different concrete futures. +/// +/// The [`batch`] function only accepts a collection of the same concrete future e.g. +/// from a single async function or method. +/// +/// To support different futures (that still return the same value), this macro will place provided +/// futures in a `Pin>` to erase their type and pass them along to `batch`. +/// +/// # Examples +/// ``` +/// use pinnacle_api::util::batch_boxed; +/// +/// let windows = window.get_all(); +/// let first = windows.next()?; +/// let last = windows.last()?; +/// +/// let classes: Vec = batch_boxed![ +/// async { +/// let mut class = first.class_async().await; +/// class.unwrap_or("no class".to_string()) +/// }, +/// async { +/// last.class_async().await +/// }, +/// ]; +/// ``` +#[macro_export] +macro_rules! batch_boxed { + [ $first:expr, $($request:expr),* ] => { + $crate::util::batch([ + ::std::boxed::Box::pin($first) as ::std::pin::Pin<::std::boxed::Box>>, + $( + ::std::boxed::Box::pin($request), + )* + ]) + }; +} + +/// The async version of [`batch_boxed`]. +/// +/// See [`batch_boxed`] for more information. +#[macro_export] +macro_rules! batch_boxed_async { + [ $first:expr, $($request:expr),* ] => { + $crate::util::batch_async([ + ::std::boxed::Box::pin($first) as ::std::pin::Pin<::std::boxed::Box>>, + $( + ::std::boxed::Box::pin($request), + )* + ]) + }; +} + +/// Methods for batch sending API requests to the compositor. +pub trait Batch { + /// [`batch_map`][Batch::batch_map]s then finds the object for which `f` with the results + /// returns `true`. + fn batch_find(self, map_to_future: M, find: F) -> Option + where + Self: Sized, + M: for<'a> FnMut(&'a I) -> Pin + 'a>>, + F: FnMut(&FutOp) -> bool; + + /// Maps the collection to compositor requests, batching all calls. + fn batch_map(self, map: F) -> impl Iterator + where + Self: Sized, + F: for<'a> FnMut(&'a I) -> Pin + 'a>>; +} + +impl, I> Batch for T { + fn batch_find(self, map_to_future: M, mut find: F) -> Option + where + Self: Sized, + M: for<'a> FnMut(&'a I) -> Pin + 'a>>, + F: FnMut(&FutOp) -> bool, + { + let items = self.into_iter().collect::>(); + let futures = items.iter().map(map_to_future); + let results = crate::util::batch(futures); + + assert_eq!(items.len(), results.len()); + + items + .into_iter() + .zip(results) + .find(|(_, fut_op)| find(fut_op)) + .map(|(item, _)| item) + } + + fn batch_map(self, map: F) -> impl Iterator + where + Self: Sized, + F: for<'a> FnMut(&'a I) -> Pin + 'a>>, + { + let items = self.into_iter().collect::>(); + let futures = items.iter().map(map); + crate::util::batch(futures).into_iter() + } +} diff --git a/api/rust/src/window.rs b/api/rust/src/window.rs index 7bf78c3..f9a84bb 100644 --- a/api/rust/src/window.rs +++ b/api/rust/src/window.rs @@ -12,6 +12,7 @@ //! //! This module also allows you to set window rules; see the [rules] module for more information. +use futures::FutureExt; use num_enum::TryFromPrimitive; use pinnacle_api_defs::pinnacle::{ output::v0alpha1::output_service_client::OutputServiceClient, @@ -30,7 +31,12 @@ use pinnacle_api_defs::pinnacle::{ }; use tonic::transport::Channel; -use crate::{block_on_tokio, input::MouseButton, tag::TagHandle, util::Geometry}; +use crate::{ + block_on_tokio, + input::MouseButton, + tag::TagHandle, + util::{Batch, Geometry}, +}; use self::rules::{WindowRule, WindowRuleCondition}; @@ -41,24 +47,18 @@ pub mod rules; /// See [`WindowHandle`] for more information. #[derive(Debug, Clone)] pub struct Window { - channel: Channel, + window_client: WindowServiceClient, + tag_client: TagServiceClient, + output_client: OutputServiceClient, } 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()) + Self { + window_client: WindowServiceClient::new(channel.clone()), + tag_client: TagServiceClient::new(channel.clone()), + output_client: OutputServiceClient::new(channel), + } } /// Start moving the window with the mouse. @@ -66,7 +66,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`][crate::input::Input::keybind]. + /// This is intended to be used with [`Input::mousebind`][crate::input::Input::mousebind]. /// /// # Examples /// @@ -79,11 +79,12 @@ impl Window { /// }); /// ``` pub fn begin_move(&self, button: MouseButton) { - let mut client = self.create_window_client(); - block_on_tokio(client.move_grab(MoveGrabRequest { + let mut client = self.window_client.clone(); + if let Err(status) = block_on_tokio(client.move_grab(MoveGrabRequest { button: Some(button as u32), - })) - .unwrap(); + })) { + eprintln!("ERROR: {status}"); + } } /// Start resizing the window with the mouse. @@ -91,7 +92,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`][crate::input::Input::keybind]. + /// This is intended to be used with [`Input::mousebind`][crate::input::Input::mousebind]. /// /// # Examples /// @@ -104,7 +105,7 @@ impl Window { /// }); /// ``` pub fn begin_resize(&self, button: MouseButton) { - let mut client = self.create_window_client(); + let mut client = self.window_client.clone(); block_on_tokio(client.resize_grab(ResizeGrabRequest { button: Some(button as u32), })) @@ -119,16 +120,23 @@ impl Window { /// 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_tokio(client.get(GetRequest {})) + block_on_tokio(self.get_all_async()) + } + + /// The async version of [`Window::get_all`]. + pub async fn get_all_async(&self) -> impl Iterator { + let mut client = self.window_client.clone(); + let tag_client = self.tag_client.clone(); + let output_client = self.output_client.clone(); + client + .get(GetRequest {}) + .await .unwrap() .into_inner() .window_ids .into_iter() .map(move |id| WindowHandle { - client: client.clone(), + window_client: client.clone(), id, tag_client: tag_client.clone(), output_client: output_client.clone(), @@ -143,8 +151,15 @@ impl Window { /// let focused_window = window.get_focused()?; /// ``` pub fn get_focused(&self) -> Option { - self.get_all() - .find(|window| matches!(window.props().focused, Some(true))) + block_on_tokio(self.get_focused_async()) + } + + /// The async version of [`Window::get_focused`]. + pub async fn get_focused_async(&self) -> Option { + self.get_all_async().await.batch_find( + |win| win.focused_async().boxed(), + |focused| focused.is_some_and(|focused| focused), + ) } /// Add a window rule. @@ -154,7 +169,7 @@ impl Window { /// /// TODO: pub fn add_window_rule(&self, cond: WindowRuleCondition, rule: WindowRule) { - let mut client = self.create_window_client(); + let mut client = self.window_client.clone(); block_on_tokio(client.add_window_rule(AddWindowRuleRequest { cond: Some(cond.0), @@ -169,10 +184,10 @@ impl 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, + id: u32, + window_client: WindowServiceClient, + tag_client: TagServiceClient, + output_client: OutputServiceClient, } impl PartialEq for WindowHandle { @@ -202,7 +217,7 @@ pub enum FullscreenOrMaximized { } /// Properties of a window. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub struct WindowProperties { /// The location and size of the window pub geometry: Option, @@ -215,7 +230,7 @@ pub struct WindowProperties { /// 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. + /// states will just override the floating state. pub floating: Option, /// Whether the window is fullscreen, maximized, or neither pub fullscreen_or_maximized: Option, @@ -234,8 +249,9 @@ impl WindowHandle { /// // Close the focused window /// window.get_focused()?.close() /// ``` - pub fn close(mut self) { - block_on_tokio(self.client.close(CloseRequest { + pub fn close(&self) { + let mut window_client = self.window_client.clone(); + block_on_tokio(window_client.close(CloseRequest { window_id: Some(self.id), })) .unwrap(); @@ -252,7 +268,7 @@ impl WindowHandle { /// window.get_focused()?.set_fullscreen(true); /// ``` pub fn set_fullscreen(&self, set: bool) { - let mut client = self.client.clone(); + let mut client = self.window_client.clone(); block_on_tokio(client.set_fullscreen(SetFullscreenRequest { window_id: Some(self.id), set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Set( @@ -273,7 +289,7 @@ impl WindowHandle { /// window.get_focused()?.toggle_fullscreen(); /// ``` pub fn toggle_fullscreen(&self) { - let mut client = self.client.clone(); + let mut client = self.window_client.clone(); block_on_tokio(client.set_fullscreen(SetFullscreenRequest { window_id: Some(self.id), set_or_toggle: Some(window::v0alpha1::set_fullscreen_request::SetOrToggle::Toggle(())), @@ -292,7 +308,7 @@ impl WindowHandle { /// window.get_focused()?.set_maximized(true); /// ``` pub fn set_maximized(&self, set: bool) { - let mut client = self.client.clone(); + let mut client = self.window_client.clone(); block_on_tokio(client.set_maximized(SetMaximizedRequest { window_id: Some(self.id), set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Set( @@ -304,7 +320,7 @@ impl WindowHandle { /// Toggle this window between maximized and not. /// - /// If it is fullscreen, setting it to maximized will remove the fullscreen state. + /// If it is fullscreen, toggling it to maximized will remove the fullscreen state. /// /// # Examples /// @@ -313,7 +329,7 @@ impl WindowHandle { /// window.get_focused()?.toggle_maximized(); /// ``` pub fn toggle_maximized(&self) { - let mut client = self.client.clone(); + let mut client = self.window_client.clone(); block_on_tokio(client.set_maximized(SetMaximizedRequest { window_id: Some(self.id), set_or_toggle: Some(window::v0alpha1::set_maximized_request::SetOrToggle::Toggle(())), @@ -335,7 +351,7 @@ impl WindowHandle { /// window.get_focused()?.set_floating(true); /// ``` pub fn set_floating(&self, set: bool) { - let mut client = self.client.clone(); + let mut client = self.window_client.clone(); block_on_tokio(client.set_floating(SetFloatingRequest { window_id: Some(self.id), set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Set( @@ -359,7 +375,7 @@ impl WindowHandle { /// window.get_focused()?.toggle_floating(); /// ``` pub fn toggle_floating(&self) { - let mut client = self.client.clone(); + let mut client = self.window_client.clone(); block_on_tokio(client.set_floating(SetFloatingRequest { window_id: Some(self.id), set_or_toggle: Some(window::v0alpha1::set_floating_request::SetOrToggle::Toggle( @@ -381,7 +397,7 @@ impl WindowHandle { /// window.get_focused()?.move_to_tag(&tag.get("Code", None)?); /// ``` pub fn move_to_tag(&self, tag: &TagHandle) { - let mut client = self.client.clone(); + let mut client = self.window_client.clone(); block_on_tokio(client.move_to_tag(MoveToTagRequest { window_id: Some(self.id), @@ -402,7 +418,7 @@ impl WindowHandle { /// 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(); + let mut client = self.window_client.clone(); block_on_tokio(client.set_tag(SetTagRequest { window_id: Some(self.id), @@ -426,7 +442,7 @@ impl WindowHandle { /// focused.toggle_tag(&tg); // `focused` no longer has tag "Potato" /// ``` pub fn toggle_tag(&self, tag: &TagHandle) { - let mut client = self.client.clone(); + let mut client = self.window_client.clone(); block_on_tokio(client.set_tag(SetTagRequest { window_id: Some(self.id), @@ -454,15 +470,26 @@ impl WindowHandle { /// } = window.get_focused()?.props(); /// ``` pub fn props(&self) -> WindowProperties { - let mut client = self.client.clone(); + block_on_tokio(self.props_async()) + } + + /// The async version of [`props`][Self::props]. + pub async fn props_async(&self) -> WindowProperties { + let mut client = self.window_client.clone(); let tag_client = self.tag_client.clone(); - let response = block_on_tokio(client.get_properties( - window::v0alpha1::GetPropertiesRequest { + + let response = match client + .get_properties(window::v0alpha1::GetPropertiesRequest { window_id: Some(self.id), - }, - )) - .unwrap() - .into_inner(); + }) + .await + { + Ok(response) => response.into_inner(), + Err(status) => { + eprintln!("ERROR: {status}"); + return WindowProperties::default(); + } + }; let fullscreen_or_maximized = response .fullscreen_or_maximized @@ -488,7 +515,7 @@ impl WindowHandle { .tag_ids .into_iter() .map(|id| TagHandle { - client: tag_client.clone(), + tag_client: tag_client.clone(), output_client: self.output_client.clone(), id, }) @@ -503,6 +530,11 @@ impl WindowHandle { self.props().geometry } + /// The async version of [`geometry`][Self::geometry]. + pub async fn geometry_async(&self) -> Option { + self.props_async().await.geometry + } + /// Get this window's class. /// /// Shorthand for `self.props().class`. @@ -510,6 +542,11 @@ impl WindowHandle { self.props().class } + /// The async version of [`class`][Self::class]. + pub async fn class_async(&self) -> Option { + self.props_async().await.class + } + /// Get this window's title. /// /// Shorthand for `self.props().title`. @@ -517,6 +554,11 @@ impl WindowHandle { self.props().title } + /// The async version of [`title`][Self::title]. + pub async fn title_async(&self) -> Option { + self.props_async().await.title + } + /// Get whether or not this window is focused. /// /// Shorthand for `self.props().focused`. @@ -524,6 +566,11 @@ impl WindowHandle { self.props().focused } + /// The async version of [`focused`][Self::focused]. + pub async fn focused_async(&self) -> Option { + self.props_async().await.focused + } + /// Get whether or not this window is floating. /// /// Shorthand for `self.props().floating`. @@ -531,6 +578,11 @@ impl WindowHandle { self.props().floating } + /// The async version of [`floating`][Self::floating] + pub async fn floating_async(&self) -> Option { + self.props_async().await.floating + } + /// Get whether this window is fullscreen, maximized, or neither. /// /// Shorthand for `self.props().fullscreen_or_maximized`. @@ -538,10 +590,20 @@ impl WindowHandle { self.props().fullscreen_or_maximized } + /// The async version of [`fullscreen_or_maximized`][Self::fullscreen_or_maximized]. + pub async fn fullscreen_or_maximized_async(&self) -> Option { + self.props_async().await.fullscreen_or_maximized + } + /// Get all the tags on this window. /// /// Shorthand for `self.props().tags`. pub fn tags(&self) -> Vec { self.props().tags } + + /// The async version of [`tags`][Self::tags]. + pub async fn tags_async(&self) -> Vec { + self.props_async().await.tags + } }