Add basic API batching for Lua

This commit is contained in:
Ottatop 2024-02-20 15:59:04 -06:00
parent 09e20e3a30
commit 20af3a116c
7 changed files with 140 additions and 21 deletions

View file

@ -5,7 +5,7 @@ source = {
} }
description = { description = {
homepage = "*** please enter a project homepage ***", homepage = "*** please enter a project homepage ***",
license = "*** please specify a license ***", license = "MPL 2.0",
} }
dependencies = { dependencies = {
"lua ~> 5.4", "lua ~> 5.4",
@ -25,5 +25,6 @@ build = {
["pinnacle.process"] = "pinnacle/process.lua", ["pinnacle.process"] = "pinnacle/process.lua",
["pinnacle.tag"] = "pinnacle/tag.lua", ["pinnacle.tag"] = "pinnacle/tag.lua",
["pinnacle.window"] = "pinnacle/window.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. ---Create appropriate headers for a gRPC request.
---@param service string The desired service ---@param service string The desired service
---@param method string The desired method within the service ---@param method string The desired method within the service
---@return HttpHeaders
local function create_request_headers(service, method) local function create_request_headers(service, method)
local req_headers = headers.new() local req_headers = headers.new()
req_headers:append(":method", "POST") req_headers:append(":method", "POST")
@ -34,6 +33,13 @@ local function new_conn()
return conn return conn
end end
---@class CqueuesLoop
---@field loop function
---@field wrap fun(self: self, fn: function)
---@class H2Connection
---@field new_stream function
---@nodoc ---@nodoc
---@class Client ---@class Client
---@field conn H2Connection ---@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_headers(create_request_headers(service, method), false)
stream:write_chunk(body, true) stream:write_chunk(body, true)
local response_headers = stream:get_headers() -- TODO: check response headers for errors
-- TODO: check headers for errors local _ = stream:get_headers()
local response_body = stream:get_next_chunk() local response_body = stream:get_next_chunk()
@ -95,6 +101,7 @@ function client.unary_request(grpc_request_params)
stream:shutdown() stream:shutdown()
-- Skip the 1-byte compressed flag and the 4-byte message length -- 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_body = response_body:sub(6)
local response = pb.decode(response_type, response_body) 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_headers(create_request_headers(service, method), false)
stream:write_chunk(body, true) stream:write_chunk(body, true)
local response_headers = stream:get_headers() -- TODO: check response headers for errors
-- TODO: check headers for errors local _ = stream:get_headers()
client.loop:wrap(function() client.loop:wrap(function()
for response_body in stream:each_chunk() do for response_body in stream:each_chunk() do
-- Skip the 1-byte compressed flag and the 4-byte message length -- 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_body = response_body:sub(6)
---@diagnostic disable-next-line: redefined-local
local success, obj = pcall(pb.decode, response_type, response_body) local success, obj = pcall(pb.decode, response_type, response_body)
if not success then if not success then
print(obj) 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. ---This module provides utilities to set key- and mousebinds as well as change keyboard settings.
---@class Input ---@class Input
---@field private btn table ---@field private mouse_button_values table
local input = { local input = {
key = require("pinnacle.input.keys"), 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. ---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 edge MouseEdge "press" or "release" to trigger on button press or release
---@param action fun() The function to run when the bind is triggered ---@param action fun() The function to run when the bind is triggered
function input.mousebind(mods, button, edge, action) function input.mousebind(mods, button, edge, action)
---@diagnostic disable-next-line: redefined-local
local edge = mouse_edge_values[edge] local edge = mouse_edge_values[edge]
local mod_values = {} local mod_values = {}

View file

@ -76,6 +76,8 @@ output.handle = output_handle
--- ---
---@return OutputHandle[] ---@return OutputHandle[]
function output.get_all() 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", {})) local response = client.unary_request(build_grpc_request_params("Get", {}))
---@type OutputHandle[] ---@type OutputHandle[]
@ -326,6 +328,7 @@ end
function OutputHandle:props() function OutputHandle:props()
local response = client.unary_request(build_grpc_request_params("GetProperties", { output_name = self.name })) 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 {}) local handles = require("pinnacle.tag").handle.new_from_table(response.tag_ids or {})
response.tags = handles response.tags = handles

View file

@ -122,10 +122,20 @@ function tag.get(name, output)
local handles = tag.get_all() local handles = tag.get_all()
for _, handle in ipairs(handles) do ---@type (fun(): TagProperties)[]
local props = handle:props() local requests = {}
if props.output and props.output.name == output.name and props.name == name then
return handle 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
end end
@ -248,12 +258,13 @@ function tag.new_layout_cycler(layouts)
---@type LayoutCycler ---@type LayoutCycler
return { return {
next = function(output) next = function(output)
---@diagnostic disable-next-line: redefined-local
local output = output or require("pinnacle.output").get_focused() local output = output or require("pinnacle.output").get_focused()
if not output then if not output then
return return
end end
local tags = output:props().tags local tags = output:props().tags or {}
for _, tg in ipairs(tags) do for _, tg in ipairs(tags) do
if tg:props().active then if tg:props().active then
@ -276,12 +287,13 @@ function tag.new_layout_cycler(layouts)
end end
end, end,
prev = function(output) prev = function(output)
---@diagnostic disable-next-line: redefined-local
local output = output or require("pinnacle.output").get_focused() local output = output or require("pinnacle.output").get_focused()
if not output then if not output then
return return
end end
local tags = output:props().tags local tags = output:props().tags or {}
for _, tg in ipairs(tags) do for _, tg in ipairs(tags) do
if tg:props().active then if tg:props().active then
@ -321,7 +333,7 @@ function TagHandle:remove()
client.unary_request(build_grpc_request_params("Remove", { tag_ids = { self.id } })) client.unary_request(build_grpc_request_params("Remove", { tag_ids = { self.id } }))
end end
local _layouts = { local layout_name_to_code = {
master_stack = 1, master_stack = 1,
dwindle = 2, dwindle = 2,
spiral = 3, spiral = 3,
@ -351,7 +363,8 @@ local _layouts = {
--- ---
---@param layout Layout ---@param layout Layout
function TagHandle:set_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", { client.unary_request(build_grpc_request_params("SetLayout", {
tag_id = self.id, tag_id = self.id,
@ -421,6 +434,7 @@ function TagHandle:props()
return { return {
active = response.active, active = response.active,
name = response.name, name = response.name,
---@diagnostic disable-next-line: invisible
output = response.output_name and require("pinnacle.output").handle.new(response.output_name), output = response.output_name and require("pinnacle.output").handle.new(response.output_name),
} }
end 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() function window.get_focused()
local handles = window.get_all() local handles = window.get_all()
for _, handle in ipairs(handles) do ---@type (fun(): WindowProperties)[]
if handle:props().focused then local requests = {}
return handle
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
end end
@ -124,7 +135,8 @@ end
---``` ---```
---@param button MouseButton The button that will initiate the move ---@param button MouseButton The button that will initiate the move
function window.begin_move(button) 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 })) client.unary_request(build_grpc_request_params("MoveGrab", { button = button }))
end end
@ -141,7 +153,8 @@ end
---``` ---```
---@param button MouseButton The button that will initiate the resize ---@param button MouseButton The button that will initiate the resize
function window.begin_resize(button) 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 })) client.unary_request(build_grpc_request_params("ResizeGrab", { button = button }))
end end
@ -274,6 +287,7 @@ function window.add_window_rule(rule)
end end
if rule.rule.output then if rule.rule.output then
---@diagnostic disable-next-line: assign-type-mismatch
rule.rule.output = rule.rule.output.name rule.rule.output = rule.rule.output.name
end end
@ -502,6 +516,7 @@ function WindowHandle:props()
response.fullscreen_or_maximized = _fullscreen_or_maximized_keys[response.fullscreen_or_maximized] 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.tags = response.tag_ids and require("pinnacle.tag").handle.new_from_table(response.tag_ids)
response.tag_ids = nil response.tag_ids = nil