Merge branch 'main' into signal_minimal

This commit is contained in:
Ottatop 2024-02-21 18:09:25 -06:00
commit 729ad66429
12 changed files with 637 additions and 161 deletions

View file

@ -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",
},
}

View file

@ -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)

View file

@ -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 = {}

View file

@ -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

View file

@ -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

76
api/lua/pinnacle/util.lua Normal file
View file

@ -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

View file

@ -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

View file

@ -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<BoxFuture<'static, ()>>,
output_client: OutputServiceClient<Channel>,
tag_client: TagServiceClient<Channel>,
}
impl Output {
@ -38,19 +39,12 @@ impl Output {
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
) -> Self {
Self {
channel,
output_client: OutputServiceClient::new(channel.clone()),
tag_client: TagServiceClient::new(channel),
fut_sender,
}
}
fn create_output_client(&self) -> OutputServiceClient<Channel> {
OutputServiceClient::new(self.channel.clone())
}
fn create_tag_client(&self) -> TagServiceClient<Channel> {
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<Item = OutputHandle> {
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<Item = OutputHandle> {
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<String>) -> Option<OutputHandle> {
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<String>) -> Option<OutputHandle> {
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<OutputHandle> {
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<Channel>,
pub(crate) output_client: OutputServiceClient<Channel>,
pub(crate) tag_client: TagServiceClient<Channel>,
pub(crate) name: String,
}
@ -245,7 +261,7 @@ impl OutputHandle {
/// // ^x=1920
/// ```
pub fn set_location(&self, x: impl Into<Option<i32>>, y: impl Into<Option<i32>>) {
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<String> {
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<String> {
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<i32> {
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<i32> {
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<u32> {
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<u32> {
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<u32> {
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<u32> {
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<u32> {
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<bool> {
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<TagHandle> {
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<String>,

View file

@ -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<Box<dyn FnMut(String) + Send>>,

View file

@ -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<BoxFuture<'static, ()>>,
tag_client: TagServiceClient<Channel>,
output_client: OutputServiceClient<Channel>,
}
impl Tag {
@ -66,19 +69,13 @@ impl Tag {
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
) -> Self {
Self {
tag_client: TagServiceClient::new(channel.clone()),
output_client: OutputServiceClient::new(channel.clone()),
channel,
fut_sender,
}
}
fn create_tag_client(&self) -> TagServiceClient<Channel> {
TagServiceClient::new(self.channel.clone())
}
fn create_output_client(&self) -> OutputServiceClient<Channel> {
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<Item = impl Into<String>>,
) -> impl Iterator<Item = TagHandle> {
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<Item = impl Into<String>>,
) -> impl Iterator<Item = TagHandle> {
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<Item = TagHandle> {
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<Item = TagHandle> {
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<String>) -> Option<TagHandle> {
block_on_tokio(self.get_async(name))
}
/// The async version of [`Tag::get`].
pub async fn get_async(&self, name: impl Into<String>) -> Option<TagHandle> {
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<String>,
output: &OutputHandle,
) -> Option<TagHandle> {
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<String>,
output: &OutputHandle,
) -> Option<TagHandle> {
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<Item = TagHandle>) {
let tag_ids = tags.into_iter().map(|handle| handle.id).collect::<Vec<_>>();
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<Channel>,
pub(crate) output_client: OutputServiceClient<Channel>,
pub(crate) id: u32,
pub(crate) tag_client: TagServiceClient<Channel>,
pub(crate) output_client: OutputServiceClient<Channel>,
}
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<bool> {
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<String> {
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<OutputHandle> {
self.props().output
}
/// The async version of [`TagHandle::output`].
pub async fn output_async(&self) -> Option<OutputHandle> {
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<bool>,

View file

@ -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::<Vec<_>>();
/// ```
///
/// 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<Box<_>>` to erase the types and pass them along to `batch`.
///
/// # Examples
///
/// ```
/// use pinnacle_api::util::batch;
/// use pinnacle_api::window::WindowProperties;
///
/// let props: Vec<WindowProperties> = batch(window.get_all().map(|window| window.props_async()));
/// // Don't forget the `async` ^^^^^
/// ```
///
pub fn batch<T>(requests: impl IntoIterator<Item = impl Future<Output = T>>) -> Vec<T> {
block_on_tokio(batch_async(requests))
}
/// The async version of [`batch`].
///
/// See [`batch`] for more information.
pub async fn batch_async<T>(requests: impl IntoIterator<Item = impl Future<Output = T>>) -> Vec<T> {
let results = FuturesOrdered::from_iter(requests).collect::<Vec<_>>();
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<Box<_>>` 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<String> = 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<dyn std::future::Future<Output = _>>>,
$(
::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<dyn std::future::Future<Output = _>>>,
$(
::std::boxed::Box::pin($request),
)*
])
};
}
/// Methods for batch sending API requests to the compositor.
pub trait Batch<I> {
/// [`batch_map`][Batch::batch_map]s then finds the object for which `f` with the results
/// returns `true`.
fn batch_find<M, F, FutOp>(self, map_to_future: M, find: F) -> Option<I>
where
Self: Sized,
M: for<'a> FnMut(&'a I) -> Pin<Box<dyn Future<Output = FutOp> + 'a>>,
F: FnMut(&FutOp) -> bool;
/// Maps the collection to compositor requests, batching all calls.
fn batch_map<F, FutOp>(self, map: F) -> impl Iterator<Item = FutOp>
where
Self: Sized,
F: for<'a> FnMut(&'a I) -> Pin<Box<dyn Future<Output = FutOp> + 'a>>;
}
impl<T: IntoIterator<Item = I>, I> Batch<I> for T {
fn batch_find<M, F, FutOp>(self, map_to_future: M, mut find: F) -> Option<I>
where
Self: Sized,
M: for<'a> FnMut(&'a I) -> Pin<Box<dyn Future<Output = FutOp> + 'a>>,
F: FnMut(&FutOp) -> bool,
{
let items = self.into_iter().collect::<Vec<_>>();
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<F, FutOp>(self, map: F) -> impl Iterator<Item = FutOp>
where
Self: Sized,
F: for<'a> FnMut(&'a I) -> Pin<Box<dyn Future<Output = FutOp> + 'a>>,
{
let items = self.into_iter().collect::<Vec<_>>();
let futures = items.iter().map(map);
crate::util::batch(futures).into_iter()
}
}

View file

@ -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<Channel>,
tag_client: TagServiceClient<Channel>,
output_client: OutputServiceClient<Channel>,
}
impl Window {
pub(crate) fn new(channel: Channel) -> Self {
Self { channel }
}
fn create_window_client(&self) -> WindowServiceClient<Channel> {
WindowServiceClient::new(self.channel.clone())
}
fn create_tag_client(&self) -> TagServiceClient<Channel> {
TagServiceClient::new(self.channel.clone())
}
fn create_output_client(&self) -> OutputServiceClient<Channel> {
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<Item = WindowHandle> {
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<Item = WindowHandle> {
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<WindowHandle> {
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<WindowHandle> {
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<Channel>,
pub(crate) id: u32,
pub(crate) tag_client: TagServiceClient<Channel>,
pub(crate) output_client: OutputServiceClient<Channel>,
id: u32,
window_client: WindowServiceClient<Channel>,
tag_client: TagServiceClient<Channel>,
output_client: OutputServiceClient<Channel>,
}
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<Geometry>,
@ -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<bool>,
/// Whether the window is fullscreen, maximized, or neither
pub fullscreen_or_maximized: Option<FullscreenOrMaximized>,
@ -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<Geometry> {
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<String> {
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<String> {
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<bool> {
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<bool> {
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<FullscreenOrMaximized> {
self.props_async().await.fullscreen_or_maximized
}
/// Get all the tags on this window.
///
/// Shorthand for `self.props().tags`.
pub fn tags(&self) -> Vec<TagHandle> {
self.props().tags
}
/// The async version of [`tags`][Self::tags].
pub async fn tags_async(&self) -> Vec<TagHandle> {
self.props_async().await.tags
}
}