Write rest of new Lua API

This commit is contained in:
Ottatop 2024-01-12 20:15:58 -06:00
parent 97f9cf82d6
commit f10bd933ca
10 changed files with 468 additions and 48 deletions

View file

@ -10,6 +10,10 @@ local pinnacle = {
---@class Pinnacle
---@field private config_client Client
---@field input Input
---@field output Output
---@field process Process
---@field tag Tag
---@field window Window
local Pinnacle = {}
function Pinnacle:quit()
@ -34,6 +38,10 @@ function pinnacle.setup(config_fn)
local self = {
config_client = config_client,
input = require("pinnacle.input").new(config_client),
process = require("pinnacle.process").new(config_client),
window = require("pinnacle.window").new(config_client),
output = require("pinnacle.output").new(config_client),
tag = require("pinnacle.tag").new(config_client),
}
setmetatable(self, { __index = Pinnacle })

View file

@ -29,7 +29,7 @@ local Client = {}
---@class GrpcRequestParams
---@field service string
---@field method string
---@field request_type string?
---@field request_type string
---@field response_type string?
---@field data table
@ -45,44 +45,6 @@ local Client = {}
function Client:unary_request(grpc_request_params)
local stream = self.conn:new_stream()
local service = grpc_request_params.service
local method = grpc_request_params.method
local request_type = grpc_request_params.request_type or method .. "Request"
local response_type = grpc_request_params.response_type or "google.protobuf.Empty"
local data = grpc_request_params.data
local encoded_protobuf = assert(pb.encode(request_type, data), "wrong table schema")
local packed_prefix = string.pack("I1", 0)
local payload_len = string.pack(">I4", encoded_protobuf:len())
local body = packed_prefix .. payload_len .. encoded_protobuf
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
local response_body = stream:get_next_chunk()
local response = pb.decode(response_type, response_body)
print(inspect(response))
return response
end
---Send a async server streaming request to the compositor.
---
---`callback` will be called with every streamed response.
---
---If `response_type` is not specified then it will default to
---`google.protobuf.Empty`.
---@param grpc_request_params GrpcRequestParams
---@param callback fun(response: table)
function Client:server_streaming_request(grpc_request_params, callback)
local stream = self.conn:new_stream()
local service = grpc_request_params.service
local method = grpc_request_params.method
local request_type = grpc_request_params.request_type
@ -102,9 +64,61 @@ function Client:server_streaming_request(grpc_request_params, callback)
local response_headers = stream:get_headers()
-- TODO: check headers for errors
local response_body = stream:get_next_chunk()
-- Skip the 1-byte compressed flag and the 4-byte message length
local response_body = response_body:sub(6)
local response = pb.decode(response_type, response_body)
print(inspect(response))
return response
end
---Send a async server streaming request to the compositor.
---
---`callback` will be called with every streamed response.
---
---If `response_type` is not specified then it will default to
---`google.protobuf.Empty`.
---@param grpc_request_params GrpcRequestParams
---@param callback fun(response: table)
function Client:server_streaming_request(grpc_request_params, callback)
-- print(inspect(grpc_request_params))
local stream = self.conn:new_stream()
local service = grpc_request_params.service
local method = grpc_request_params.method
local request_type = grpc_request_params.request_type
local response_type = grpc_request_params.response_type or "google.protobuf.Empty"
local data = grpc_request_params.data
local encoded_protobuf = assert(pb.encode(request_type, data), "wrong table schema")
local packed_prefix = string.pack("I1", 0)
local payload_len = string.pack(">I4", encoded_protobuf:len())
local body = packed_prefix .. payload_len .. encoded_protobuf
stream:write_headers(create_request_headers(service, method), false)
stream:write_chunk(body, true)
local response_headers = stream:get_headers()
-- local chunk = stream:get_next_chunk()
-- print(chunk, chunk:len())
-- TODO: check headers for errors
self.loop:wrap(function()
for response_body in stream:each_chunk() do
local response = pb.decode(response_type, response_body)
-- Skip the 1-byte compressed flag and the 4-byte message length
local response_body = response_body:sub(6)
local success, obj = pcall(pb.decode, response_type, response_body)
if not success then
print(obj)
os.exit(1)
end
local response = obj
callback(response)
end
end)

View file

@ -28,7 +28,7 @@ local function build_grpc_request_params(method, data)
return {
service = service,
method = method,
request_type = req_type and prefix .. req_type,
request_type = req_type and prefix .. req_type or prefix .. method .. "Request",
response_type = resp_type and prefix .. resp_type,
data = data,
}

View file

@ -29,7 +29,7 @@ local function build_grpc_request_params(method, data)
return {
service = service,
method = method,
request_type = req_type and prefix .. req_type,
request_type = req_type and prefix .. req_type or prefix .. method .. "Request",
response_type = resp_type and prefix .. resp_type,
data = data,
}

View file

@ -0,0 +1,75 @@
---The protobuf absolute path prefix
local prefix = "pinnacle.process." .. require("pinnacle").version .. "."
local service = prefix .. "ProcessService"
---@type table<string, { request_type: string?, response_type: string? }>
---@enum (key) ProcessServiceMethod
local rpc_types = {
Spawn = {
response_type = "SpawnResponse",
},
SetEnv = {},
}
---Build GrpcRequestParams
---@param method ProcessServiceMethod
---@param data table
---@return GrpcRequestParams
local function build_grpc_request_params(method, data)
local req_type = rpc_types[method].request_type
local resp_type = rpc_types[method].response_type
---@type GrpcRequestParams
return {
service = service,
method = method,
request_type = req_type and prefix .. req_type or prefix .. method .. "Request",
response_type = resp_type and prefix .. resp_type,
data = data,
}
end
---@class ProcessModule
local process = {}
---@class Process
---@field private config_client Client
local Process = {}
---@param args string[]
---@param callbacks { stdout: fun(line: string)?, stderr: fun(line: string)?, exit: fun(code: integer, msg: string)? }?
function Process:spawn(args, callbacks)
local callback = function() end
if callbacks then
callback = function(response)
if callbacks.stdout and response.stdout then
callbacks.stdout(response.stdout)
end
if callbacks.stderr and response.stderr then
callbacks.stderr(response.stderr)
end
if callbacks.exit and (response.exit_code or response.exit_message) then
callbacks.exit(response.exit_code, response.exit_message)
end
end
end
self.config_client:server_streaming_request(
build_grpc_request_params("Spawn", {
args = args,
once = false,
has_callback = callbacks ~= nil,
}),
callback
)
end
function process.new(config_client)
---@type Process
local self = { config_client = config_client }
setmetatable(self, { __index = Process })
return self
end
return process

View file

@ -32,7 +32,7 @@ local function build_grpc_request_params(method, data)
return {
service = service,
method = method,
request_type = req_type and prefix .. req_type,
request_type = req_type and prefix .. req_type or prefix .. method .. "Request",
response_type = resp_type and prefix .. resp_type,
data = data,
}

View file

@ -0,0 +1,268 @@
---The protobuf absolute path prefix
local prefix = "pinnacle.prefix." .. require("pinnacle").version .. "."
local service = prefix .. "WindowService"
---@type table<string, { request_type: string?, response_type: string? }>
---@enum (key) WindowServiceMethod
local rpc_types = {
Close = {},
SetGeometry = {},
SetFullscreen = {},
SetMaximized = {},
SetFloating = {},
MoveToTag = {},
SetTag = {},
MoveGrab = {},
ResizeGrab = {},
Get = {
response_type = "GetResponse",
},
GetProperties = {
response_type = "GetPropertiesResponse",
},
AddWindowRule = {},
}
---Build GrpcRequestParams
---@param method WindowServiceMethod
---@param data table
---@return GrpcRequestParams
local function build_grpc_request_params(method, data)
local req_type = rpc_types[method].request_type
local resp_type = rpc_types[method].response_type
---@type GrpcRequestParams
return {
service = service,
method = method,
request_type = req_type and prefix .. req_type or prefix .. method .. "Request",
response_type = resp_type and prefix .. resp_type,
data = data,
}
end
---@class WindowHandleModule
local window_handle = {}
---@class WindowHandle
---@field private config_client Client
---@field id integer
local WindowHandle = {}
---@class WindowModule
---@field private handle WindowHandleModule
local window = {}
window.handle = window_handle
---@class Window
---@field private config_client Client
local Window = {}
---Get all windows.
---
---@return WindowHandle[]
function Window:get_all()
local response = self.config_client:unary_request(build_grpc_request_params("Get", {}))
local handles = window_handle.new_from_table(self.config_client, response.window_ids)
return handles
end
--- TODO: docs
---@param button MouseButton
function Window:begin_move(button)
self.config_client:unary_request(build_grpc_request_params("MoveGrab", { button = button }))
end
--- TODO: docs
---@param button MouseButton
function Window:begin_resize(button)
self.config_client:unary_request(build_grpc_request_params("ResizeGrab", { button = button }))
end
---@class WindowRuleCondition
---@field any WindowRuleCondition[]?
---@field all WindowRuleCondition[]?
---@field classes string[]?
---@field titles string[]?
---@field tags TagHandle[]?
---@class WindowRule
---@field output OutputHandle?
---@field tags TagHandle[]?
---@field floating boolean?
---@field fullscreen_or_maximized FullscreenOrMaximized?
---@field x integer?
---@field y integer?
---@field width integer?
---@field height integer?
---@enum (key) FullscreenOrMaximized
local _fullscreen_or_maximized = {
neither = 1,
fullscreen = 2,
maximized = 3,
}
local _fullscreen_or_maximized_keys = {
[1] = "neither",
[2] = "fullscreen",
[3] = "maximized",
}
---@param rule { cond: WindowRuleCondition, rule: WindowRule }
function Window:add_window_rule(rule)
if rule.cond.tags then
local ids = {}
for _, tg in pairs(rule.cond.tags) do
table.insert(ids, tg.id)
end
rule.cond.tags = ids
end
if rule.rule.output then
rule.rule.output = rule.rule.output.name
end
if rule.rule.tags then
local ids = {}
for _, tg in pairs(rule.cond.tags) do
table.insert(ids, tg.id)
end
rule.cond.tags = ids
end
if rule.rule.fullscreen_or_maximized then
rule.rule.fullscreen_or_maximized = _fullscreen_or_maximized[rule.rule.fullscreen_or_maximized]
end
self.config_client:unary_request(build_grpc_request_params("AddWindowRule", {
cond = rule.cond,
rule = rule.rule,
}))
end
---Send a close request to this window.
function WindowHandle:close()
self.config_client:unary_request(build_grpc_request_params("Close", { window_id = self.id }))
end
---Set this window's location and/or size.
---
---@param geo { x: integer?, y: integer, width: integer?, height: integer? }
function WindowHandle:set_geometry(geo)
self.config_client:unary_request(build_grpc_request_params("SetGeometry", { window_id = self.id, geometry = geo }))
end
---Set this window to fullscreen or not.
---@param fullscreen boolean
function WindowHandle:set_fullscreen(fullscreen)
self.config_client:unary_request(
build_grpc_request_params("SetFullscreen", { window_id = self.id, set = fullscreen })
)
end
function WindowHandle:toggle_fullscreen()
self.config_client:unary_request(build_grpc_request_params("SetFullscreen", { window_id = self.id, toggle = {} }))
end
function WindowHandle:set_maximized(maximized)
self.config_client:unary_request(
build_grpc_request_params("SetMaximized", { window_id = self.id, set = maximized })
)
end
function WindowHandle:toggle_maximized()
self.config_client:unary_request(build_grpc_request_params("SetMaximized", { window_id = self.id, toggle = {} }))
end
function WindowHandle:set_floating(floating)
self.config_client:unary_request(build_grpc_request_params("SetFloating", { window_id = self.id, set = floating }))
end
function WindowHandle:toggle_floating()
self.config_client:unary_request(build_grpc_request_params("SetFloating", { window_id = self.id, toggle = {} }))
end
---@param tag TagHandle
function WindowHandle:move_to_tag(tag)
self.config_client:unary_request(build_grpc_request_params("MoveToTag", { window_id = self.id, tag_id = tag.id }))
end
---Tag or untag the given tag on this window.
---@param tag TagHandle
---@param set boolean
function WindowHandle:set_tag(tag, set)
self.config_client:unary_request(
build_grpc_request_params("SetTag", { window_id = self.id, tag_id = tag.id, set = set })
)
end
---Toggle the given tag on this window.
---@param tag TagHandle
function WindowHandle:toggle_tag(tag)
self.config_client:unary_request(
build_grpc_request_params("SetTag", { window_id = self.id, tag_id = tag.id, toggle = {} })
)
end
---@class WindowProperties
---@field geometry { x: integer?, y: integer?, width: integer?, height: integer? }?
---@field class string?
---@field title string?
---@field focused boolean?
---@field floating boolean?
---@field fullscreen_or_maximized FullscreenOrMaximized?
---@return WindowProperties
function WindowHandle:props()
local response =
self.config_client:unary_request(build_grpc_request_params("GetProperties", { window_id = self.id }))
response.fullscreen_or_maximized = _fullscreen_or_maximized_keys[response.fullscreen_or_maximized]
return response
end
---@param config_client Client
---@return Window
function window.new(config_client)
---@type Window
local self = {
config_client = config_client,
}
setmetatable(self, { __index = Window })
return self
end
---Create a new `WindowHandle` from an id.
---@param config_client Client
---@param window_id integer
---@return WindowHandle
function window_handle.new(config_client, window_id)
---@type WindowHandle
local self = {
config_client = config_client,
id = window_id,
}
setmetatable(self, { __index = WindowHandle })
return self
end
---@param config_client Client
---@param window_ids integer[]
---
---@return WindowHandle[]
function window_handle.new_from_table(config_client, window_ids)
---@type WindowHandle[]
local handles = {}
for _, id in pairs(window_ids) do
table.insert(handles, window_handle.new(config_client, id))
end
return handles
end
return window

View file

@ -1,10 +1,29 @@
local pinnacle = require("pinnacle")
require("pinnacle").setup(function(pinnacle)
local input = pinnacle.input
local process = pinnacle.process
local output = pinnacle.output
local tag = pinnacle.tag
local window = pinnacle.window
pinnacle.setup(function(pinnacle)
pinnacle.input:set_keybind({ 1 }, "A", function()
print("hi from grpc keybind")
local mods = input.mod
input:set_keybind({ mods.SHIFT }, "A", function()
process:spawn({ "alacritty" }, {
stdout = function(line)
print("stdout")
print(line)
end,
stderr = function(line)
print("stderr")
print(line)
end,
exit = function(code, msg)
print(code, msg)
end,
})
end)
pinnacle.input:set_keybind({ 1 }, "Q", function()
input:set_keybind({ 1 }, "Q", function()
pinnacle:quit()
end)
end)

View file

@ -2,6 +2,8 @@ syntax = "proto2";
package pinnacle.process.v0alpha1;
import "google/protobuf/empty.proto";
message SpawnRequest {
repeated string args = 1;
// Whether or not to spawn `args` if it is already running.
@ -18,6 +20,12 @@ message SpawnResponse {
optional string exit_message = 4;
}
message SetEnvRequest {
optional string key = 1;
optional string value = 2;
}
service ProcessService {
rpc Spawn(SpawnRequest) returns (stream SpawnResponse);
rpc SetEnv(SetEnvRequest) returns (google.protobuf.Empty);
}

View file

@ -5,6 +5,7 @@ use pinnacle_api_defs::pinnacle::{
AccelProfile, ClickMethod, ScrollMethod, TapButtonMap,
},
output::v0alpha1::{ConnectForAllRequest, ConnectForAllResponse, SetLocationRequest},
process::v0alpha1::SetEnvRequest,
tag::v0alpha1::{
AddRequest, AddResponse, RemoveRequest, SetActiveRequest, SetLayoutRequest, SwitchToRequest,
},
@ -524,6 +525,33 @@ impl pinnacle::process::v0alpha1::process_service_server::ProcessService for Pro
Ok(Response::new(Box::pin(receiver_stream)))
}
async fn set_env(&self, request: Request<SetEnvRequest>) -> Result<Response<()>, Status> {
let request = request.into_inner();
let key = request
.key
.ok_or_else(|| Status::invalid_argument("no key specified"))?;
let value = request
.value
.ok_or_else(|| Status::invalid_argument("no value specified"))?;
if key.is_empty() {
return Err(Status::invalid_argument("key was empty"));
}
if key.contains(['\0', '=']) {
return Err(Status::invalid_argument("key contained NUL or ="));
}
if value.contains('\0') {
return Err(Status::invalid_argument("value contained NUL"));
}
std::env::set_var(key, value);
Ok(Response::new(()))
}
}
pub struct TagService {