mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2024-12-25 09:59:21 +01:00
Merge branch 'main' into signal_minimal
This commit is contained in:
commit
729ad66429
12 changed files with 637 additions and 161 deletions
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
76
api/lua/pinnacle/util.lua
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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>>,
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue