diff --git a/api/lua/pinnacle-api-dev-1.rockspec b/api/lua/pinnacle-api-dev-1.rockspec index a21a245..3a727d9 100644 --- a/api/lua/pinnacle-api-dev-1.rockspec +++ b/api/lua/pinnacle-api-dev-1.rockspec @@ -5,7 +5,7 @@ source = { } description = { homepage = "*** please enter a project homepage ***", - license = "*** please specify a license ***", + license = "MPL 2.0", } dependencies = { "lua ~> 5.4", @@ -25,5 +25,6 @@ build = { ["pinnacle.process"] = "pinnacle/process.lua", ["pinnacle.tag"] = "pinnacle/tag.lua", ["pinnacle.window"] = "pinnacle/window.lua", + ["pinnacle.util"] = "pinnacle/util.lua", }, } diff --git a/api/lua/pinnacle/grpc/client.lua b/api/lua/pinnacle/grpc/client.lua index 8d21d35..eb62da5 100644 --- a/api/lua/pinnacle/grpc/client.lua +++ b/api/lua/pinnacle/grpc/client.lua @@ -11,7 +11,6 @@ local pb = require("pb") ---Create appropriate headers for a gRPC request. ---@param service string The desired service ---@param method string The desired method within the service ----@return HttpHeaders local function create_request_headers(service, method) local req_headers = headers.new() req_headers:append(":method", "POST") @@ -34,6 +33,13 @@ local function new_conn() return conn end +---@class CqueuesLoop +---@field loop function +---@field wrap fun(self: self, fn: function) + +---@class H2Connection +---@field new_stream function + ---@nodoc ---@class Client ---@field conn H2Connection @@ -80,8 +86,8 @@ function client.unary_request(grpc_request_params) stream:write_headers(create_request_headers(service, method), false) stream:write_chunk(body, true) - local response_headers = stream:get_headers() - -- TODO: check headers for errors + -- TODO: check response headers for errors + local _ = stream:get_headers() local response_body = stream:get_next_chunk() @@ -95,6 +101,7 @@ function client.unary_request(grpc_request_params) stream:shutdown() -- Skip the 1-byte compressed flag and the 4-byte message length + ---@diagnostic disable-next-line: redefined-local local response_body = response_body:sub(6) local response = pb.decode(response_type, response_body) @@ -135,14 +142,16 @@ function client.server_streaming_request(grpc_request_params, callback) stream:write_headers(create_request_headers(service, method), false) stream:write_chunk(body, true) - local response_headers = stream:get_headers() - -- TODO: check headers for errors + -- TODO: check response headers for errors + local _ = stream:get_headers() client.loop:wrap(function() for response_body in stream:each_chunk() do -- Skip the 1-byte compressed flag and the 4-byte message length + ---@diagnostic disable-next-line: redefined-local local response_body = response_body:sub(6) + ---@diagnostic disable-next-line: redefined-local local success, obj = pcall(pb.decode, response_type, response_body) if not success then print(obj) diff --git a/api/lua/pinnacle/input.lua b/api/lua/pinnacle/input.lua index c30db6d..d02faee 100644 --- a/api/lua/pinnacle/input.lua +++ b/api/lua/pinnacle/input.lua @@ -103,11 +103,11 @@ local mouse_edge_values = { --- ---This module provides utilities to set key- and mousebinds as well as change keyboard settings. ---@class Input ----@field private btn table +---@field private mouse_button_values table local input = { key = require("pinnacle.input.keys"), } -input.btn = mouse_button_values +input.mouse_button_values = mouse_button_values ---Set a keybind. If called with an already existing keybind, it gets replaced. --- @@ -188,6 +188,7 @@ end ---@param edge MouseEdge "press" or "release" to trigger on button press or release ---@param action fun() The function to run when the bind is triggered function input.mousebind(mods, button, edge, action) + ---@diagnostic disable-next-line: redefined-local local edge = mouse_edge_values[edge] local mod_values = {} diff --git a/api/lua/pinnacle/output.lua b/api/lua/pinnacle/output.lua index 48d26c1..b35da69 100644 --- a/api/lua/pinnacle/output.lua +++ b/api/lua/pinnacle/output.lua @@ -76,6 +76,8 @@ output.handle = output_handle --- ---@return OutputHandle[] function output.get_all() + -- Not going to batch these because I doubt people would have that many monitors + local response = client.unary_request(build_grpc_request_params("Get", {})) ---@type OutputHandle[] @@ -326,6 +328,7 @@ end function OutputHandle:props() local response = client.unary_request(build_grpc_request_params("GetProperties", { output_name = self.name })) + ---@diagnostic disable-next-line: invisible local handles = require("pinnacle.tag").handle.new_from_table(response.tag_ids or {}) response.tags = handles diff --git a/api/lua/pinnacle/tag.lua b/api/lua/pinnacle/tag.lua index 0aac85a..7497389 100644 --- a/api/lua/pinnacle/tag.lua +++ b/api/lua/pinnacle/tag.lua @@ -122,10 +122,20 @@ function tag.get(name, output) local handles = tag.get_all() - for _, handle in ipairs(handles) do - local props = handle:props() - if props.output and props.output.name == output.name and props.name == name then - return handle + ---@type (fun(): TagProperties)[] + local requests = {} + + for i, handle in ipairs(handles) do + requests[i] = function() + return handle:props() + end + end + + local props = require("pinnacle.util").batch(requests) + + for i, prop in ipairs(props) do + if prop.output and prop.output.name == output.name and prop.name == name then + return handles[i] end end @@ -248,12 +258,13 @@ function tag.new_layout_cycler(layouts) ---@type LayoutCycler return { next = function(output) + ---@diagnostic disable-next-line: redefined-local local output = output or require("pinnacle.output").get_focused() if not output then return end - local tags = output:props().tags + local tags = output:props().tags or {} for _, tg in ipairs(tags) do if tg:props().active then @@ -276,12 +287,13 @@ function tag.new_layout_cycler(layouts) end end, prev = function(output) + ---@diagnostic disable-next-line: redefined-local local output = output or require("pinnacle.output").get_focused() if not output then return end - local tags = output:props().tags + local tags = output:props().tags or {} for _, tg in ipairs(tags) do if tg:props().active then @@ -321,7 +333,7 @@ function TagHandle:remove() client.unary_request(build_grpc_request_params("Remove", { tag_ids = { self.id } })) end -local _layouts = { +local layout_name_to_code = { master_stack = 1, dwindle = 2, spiral = 3, @@ -351,7 +363,8 @@ local _layouts = { --- ---@param layout Layout function TagHandle:set_layout(layout) - local layout = _layouts[layout] + ---@diagnostic disable-next-line: redefined-local + local layout = layout_name_to_code[layout] client.unary_request(build_grpc_request_params("SetLayout", { tag_id = self.id, @@ -421,6 +434,7 @@ function TagHandle:props() return { active = response.active, name = response.name, + ---@diagnostic disable-next-line: invisible output = response.output_name and require("pinnacle.output").handle.new(response.output_name), } end diff --git a/api/lua/pinnacle/util.lua b/api/lua/pinnacle/util.lua new file mode 100644 index 0000000..e05b814 --- /dev/null +++ b/api/lua/pinnacle/util.lua @@ -0,0 +1,76 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +---Utility functions. +---@class Util +local util = {} + +---Batch a set of requests that will be sent to the compositor all at once. +--- +---Normally, all API calls are blocking. For example, calling `Window.get_all` +---then calling `WindowHandle.props` on each returned window handle will block +---after each `props` call waiting for the compositor to respond: +--- +---``` +---local handles = Window.get_all() +--- +--- -- Collect all the props into this table +---local props = {} +--- +--- -- This for loop will block after each call. If the compositor is running slowly +--- -- for whatever reason, this will take a long time to complete as it requests +--- -- properties sequentially. +---for i, handle in ipairs(handles) do +--- props[i] = handle:props() +---end +---``` +--- +---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 must wrap each request in a function, otherwise they would just get +---evaluated at the callsite in a blocking manner. +--- +---### Example +---```lua +---local handles = window.get_all() +--- +--- ---@type (fun(): WindowProperties)[] +---local requests = {} +--- +--- -- Wrap each request to `props` in another function +---for i, handle in ipairs(handles) do +--- requests[i] = function() +--- return handle:props() +--- end +---end +--- +--- -- Batch send these requests +---local props = require("pinnacle.util").batch(requests) +--- -- `props` now contains the `WindowProperties` of all the windows above +---``` +--- +---@generic T +--- +---@param requests (fun(): T)[] The requests that you want to batch up, wrapped in a function. +--- +---@return T[] responses The results of each request in the same order that they were in `requests`. +function util.batch(requests) + local loop = require("cqueues").new() + + local responses = {} + + for i, request in ipairs(requests) do + loop:wrap(function() + responses[i] = request() + end) + end + + loop:loop() + + return responses +end + +return util diff --git a/api/lua/pinnacle/window.lua b/api/lua/pinnacle/window.lua index 4cfa23c..7e287fd 100644 --- a/api/lua/pinnacle/window.lua +++ b/api/lua/pinnacle/window.lua @@ -102,9 +102,20 @@ end function window.get_focused() local handles = window.get_all() - for _, handle in ipairs(handles) do - if handle:props().focused then - return handle + ---@type (fun(): WindowProperties)[] + local requests = {} + + for i, handle in ipairs(handles) do + requests[i] = function() + return handle:props() + end + end + + local props = require("pinnacle.util").batch(requests) + + for i, prop in ipairs(props) do + if prop.focused then + return handles[i] end end @@ -124,7 +135,8 @@ end ---``` ---@param button MouseButton The button that will initiate the move function window.begin_move(button) - local button = require("pinnacle.input").btn[button] + ---@diagnostic disable-next-line: redefined-local, invisible + local button = require("pinnacle.input").mouse_button_values[button] client.unary_request(build_grpc_request_params("MoveGrab", { button = button })) end @@ -141,7 +153,8 @@ end ---``` ---@param button MouseButton The button that will initiate the resize function window.begin_resize(button) - local button = require("pinnacle.input").btn[button] + ---@diagnostic disable-next-line: redefined-local, invisible + local button = require("pinnacle.input").mouse_button_values[button] client.unary_request(build_grpc_request_params("ResizeGrab", { button = button })) end @@ -274,6 +287,7 @@ function window.add_window_rule(rule) end if rule.rule.output then + ---@diagnostic disable-next-line: assign-type-mismatch rule.rule.output = rule.rule.output.name end @@ -502,6 +516,7 @@ function WindowHandle:props() response.fullscreen_or_maximized = _fullscreen_or_maximized_keys[response.fullscreen_or_maximized] + ---@diagnostic disable-next-line: invisible response.tags = response.tag_ids and require("pinnacle.tag").handle.new_from_table(response.tag_ids) response.tag_ids = nil 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..db02c23 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,161 @@ 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 { + block_on_tokio(batch_async(requests)) +} + +/// 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 mut windows = window.get_all(); +/// let first = windows.next()?; +/// let last = windows.last()?; +/// +/// let classes: Vec = batch_boxed![ +/// async { +/// let class = first.class_async().await; +/// class.unwrap_or("no class".to_string()) +/// }, +/// async { +/// let mut class = last.class_async().await.unwrap_or("alalala"); +/// class += "hello"; +/// class +/// }, +/// ]; +/// ``` +#[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 + } }