From f10bd933ca1b490a578c56d644d66682aaae02f2 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Fri, 12 Jan 2024 20:15:58 -0600 Subject: [PATCH] Write rest of new Lua API --- api/lua_grpc/pinnacle.lua | 8 + api/lua_grpc/pinnacle/grpc/client.lua | 94 +++--- api/lua_grpc/pinnacle/input.lua | 2 +- api/lua_grpc/pinnacle/output.lua | 2 +- api/lua_grpc/pinnacle/process.lua | 75 +++++ api/lua_grpc/pinnacle/tag.lua | 2 +- api/lua_grpc/pinnacle/window.lua | 268 ++++++++++++++++++ api/lua_grpc/test.lua | 29 +- .../pinnacle/process/v0alpha1/process.proto | 8 + src/api/protocol.rs | 28 ++ 10 files changed, 468 insertions(+), 48 deletions(-) create mode 100644 api/lua_grpc/pinnacle/process.lua create mode 100644 api/lua_grpc/pinnacle/window.lua diff --git a/api/lua_grpc/pinnacle.lua b/api/lua_grpc/pinnacle.lua index c2b3c68..0b1bdd8 100644 --- a/api/lua_grpc/pinnacle.lua +++ b/api/lua_grpc/pinnacle.lua @@ -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 }) diff --git a/api/lua_grpc/pinnacle/grpc/client.lua b/api/lua_grpc/pinnacle/grpc/client.lua index 0061487..4fc754c 100644 --- a/api/lua_grpc/pinnacle/grpc/client.lua +++ b/api/lua_grpc/pinnacle/grpc/client.lua @@ -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) diff --git a/api/lua_grpc/pinnacle/input.lua b/api/lua_grpc/pinnacle/input.lua index ec38042..2b863e0 100644 --- a/api/lua_grpc/pinnacle/input.lua +++ b/api/lua_grpc/pinnacle/input.lua @@ -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, } diff --git a/api/lua_grpc/pinnacle/output.lua b/api/lua_grpc/pinnacle/output.lua index 71826b5..5fc3016 100644 --- a/api/lua_grpc/pinnacle/output.lua +++ b/api/lua_grpc/pinnacle/output.lua @@ -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, } diff --git a/api/lua_grpc/pinnacle/process.lua b/api/lua_grpc/pinnacle/process.lua new file mode 100644 index 0000000..bc9f733 --- /dev/null +++ b/api/lua_grpc/pinnacle/process.lua @@ -0,0 +1,75 @@ +---The protobuf absolute path prefix +local prefix = "pinnacle.process." .. require("pinnacle").version .. "." +local service = prefix .. "ProcessService" + +---@type table +---@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 diff --git a/api/lua_grpc/pinnacle/tag.lua b/api/lua_grpc/pinnacle/tag.lua index 93ff82d..b3edfe0 100644 --- a/api/lua_grpc/pinnacle/tag.lua +++ b/api/lua_grpc/pinnacle/tag.lua @@ -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, } diff --git a/api/lua_grpc/pinnacle/window.lua b/api/lua_grpc/pinnacle/window.lua new file mode 100644 index 0000000..6ed7e99 --- /dev/null +++ b/api/lua_grpc/pinnacle/window.lua @@ -0,0 +1,268 @@ +---The protobuf absolute path prefix +local prefix = "pinnacle.prefix." .. require("pinnacle").version .. "." +local service = prefix .. "WindowService" + +---@type table +---@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 diff --git a/api/lua_grpc/test.lua b/api/lua_grpc/test.lua index abffe3e..0f6069e 100644 --- a/api/lua_grpc/test.lua +++ b/api/lua_grpc/test.lua @@ -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) diff --git a/api/protocol/pinnacle/process/v0alpha1/process.proto b/api/protocol/pinnacle/process/v0alpha1/process.proto index 87e0cba..e1817bb 100644 --- a/api/protocol/pinnacle/process/v0alpha1/process.proto +++ b/api/protocol/pinnacle/process/v0alpha1/process.proto @@ -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); } diff --git a/src/api/protocol.rs b/src/api/protocol.rs index af63828..8909998 100644 --- a/src/api/protocol.rs +++ b/src/api/protocol.rs @@ -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) -> Result, 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 {