From a8239f171e9d6bd2a3786ac28301022bdf151474 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sun, 14 Jan 2024 18:01:41 -0600 Subject: [PATCH] Add docs to new Lua API 1000% sure I'm gonna have to rewrite my LDoc generation tool to actually work --- .../examples/default/example_config.lua | 33 +- api/lua_grpc/pinnacle/grpc/client.lua | 20 +- api/lua_grpc/pinnacle/grpc/protobuf.lua | 2 + api/lua_grpc/pinnacle/input.lua | 124 ++++-- api/lua_grpc/pinnacle/output.lua | 238 +++++++++++- api/lua_grpc/pinnacle/process.lua | 21 +- api/lua_grpc/pinnacle/tag.lua | 176 ++++++++- api/lua_grpc/pinnacle/window.lua | 362 +++++++++++++++++- api/lua_grpc/test.lua | 41 +- .../pinnacle/window/v0alpha1/window.proto | 1 + src/api/protocol.rs | 19 +- 11 files changed, 910 insertions(+), 127 deletions(-) diff --git a/api/lua_grpc/examples/default/example_config.lua b/api/lua_grpc/examples/default/example_config.lua index 98c80d1..e300794 100644 --- a/api/lua_grpc/examples/default/example_config.lua +++ b/api/lua_grpc/examples/default/example_config.lua @@ -12,47 +12,46 @@ require("pinnacle").setup(function(Pinnacle) local terminal = "alacritty" - Input:set_mousebind({ mod_key }, "left", "press", function() - Window:begin_move("left") + Input:mousebind({ mod_key }, "btn_left", "press", function() + Window:begin_move("btn_left") end) - Input:set_mousebind({ mod_key }, "right", "press", function() - Window:begin_resize("right") + Input:mousebind({ mod_key }, "btn_right", "press", function() + Window:begin_resize("btn_right") end) ------ - Input:set_keybind({ mod_key, "alt" }, "q", function() - print("GOT QUIT") + Input:keybind({ mod_key, "alt" }, "q", function() Pinnacle:quit() end) - Input:set_keybind({ mod_key, "alt" }, "c", function() + Input:keybind({ mod_key, "alt" }, "c", function() local focused = Window:get_focused() if focused then focused:close() end end) - Input:set_keybind({ mod_key }, key.Return, function() + Input:keybind({ mod_key }, key.Return, function() Process:spawn(terminal) end) - Input:set_keybind({ mod_key, "alt" }, key.space, function() + Input:keybind({ mod_key, "alt" }, key.space, function() local focused = Window:get_focused() if focused then focused:toggle_floating() end end) - Input:set_keybind({ mod_key }, "f", function() + Input:keybind({ mod_key }, "f", function() local focused = Window:get_focused() if focused then focused:toggle_fullscreen() end end) - Input:set_keybind({ mod_key }, "m", function() + Input:keybind({ mod_key }, "m", function() local focused = Window:get_focused() if focused then focused:toggle_maximized() @@ -78,14 +77,14 @@ require("pinnacle").setup(function(Pinnacle) "corner_bottom_right", }) - Input:set_keybind({ mod_key }, key.space, function() + Input:keybind({ mod_key }, key.space, function() local focused_op = Output:get_focused() if focused_op then layout_cycler.next(focused_op) end end) - Input:set_keybind({ mod_key, "shift" }, key.space, function() + Input:keybind({ mod_key, "shift" }, key.space, function() local focused_op = Output:get_focused() if focused_op then layout_cycler.prev(focused_op) @@ -94,19 +93,19 @@ require("pinnacle").setup(function(Pinnacle) for _, tag_name in ipairs(tag_names) do -- nil-safety: tags are guaranteed to be on the outputs due to connect_for_all above - Input:set_keybind({ mod_key }, tag_name, function() + Input:keybind({ mod_key }, tag_name, function() Tag:get(tag_name):switch_to() end) - Input:set_keybind({ mod_key, "shift" }, tag_name, function() + Input:keybind({ mod_key, "shift" }, tag_name, function() Tag:get(tag_name):toggle_active() end) - Input:set_keybind({ mod_key, "alt" }, tag_name, function() + Input:keybind({ mod_key, "alt" }, tag_name, function() local focused = Window:get_focused() if focused then focused:move_to_tag(Tag:get(tag_name) --[[@as TagHandle]]) end end) - Input:set_keybind({ mod_key, "shift", "alt" }, tag_name, function() + Input:keybind({ mod_key, "shift", "alt" }, tag_name, function() local focused = Window:get_focused() if focused then focused:toggle_tag(Tag:get(tag_name) --[[@as TagHandle]]) diff --git a/api/lua_grpc/pinnacle/grpc/client.lua b/api/lua_grpc/pinnacle/grpc/client.lua index d455e18..343eadd 100644 --- a/api/lua_grpc/pinnacle/grpc/client.lua +++ b/api/lua_grpc/pinnacle/grpc/client.lua @@ -68,23 +68,13 @@ function Client:unary_request(grpc_request_params) stream:write_headers(create_request_headers(service, method), false) stream:write_chunk(body, true) - -- for chunk in stream:each_chunk() do - -- print(chunk, ":", pb.tohex(chunk)) - -- os.exit(1) - -- end - local response_headers = stream:get_headers() -- TODO: check headers for errors local response_body = stream:get_next_chunk() - print("unary body", response_body, "end") - print(pb.tohex(response_body)) - print("--------------------------------") local trailers = stream:get_headers() - print("trailers", trailers) - print("------------------------------") - if trailers then + if trailers then -- idk if im big dummy or not but there are never any trailers for name, value, never_index in trailers:each() do print(name, value, never_index) end @@ -134,18 +124,10 @@ function Client:server_streaming_request(grpc_request_params, callback) stream:write_chunk(body, true) local response_headers = stream:get_headers() - for name, value, never_index in response_headers:each() do - print(name, value, never_index) - end - -- 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 - print("stream chunk", response_body, "end") - print(pb.tohex(response_body)) - print("-----------------------------------") -- Skip the 1-byte compressed flag and the 4-byte message length local response_body = response_body:sub(6) diff --git a/api/lua_grpc/pinnacle/grpc/protobuf.lua b/api/lua_grpc/pinnacle/grpc/protobuf.lua index caf51a6..73f677e 100644 --- a/api/lua_grpc/pinnacle/grpc/protobuf.lua +++ b/api/lua_grpc/pinnacle/grpc/protobuf.lua @@ -32,6 +32,8 @@ function protobuf.build_protos() pinnacle_pb:close() assert(pb.load(pinnacle_pb_data), "failed to load .pb file") + + pb.option("enum_as_value") end return protobuf diff --git a/api/lua_grpc/pinnacle/input.lua b/api/lua_grpc/pinnacle/input.lua index e5078e3..7bd0d05 100644 --- a/api/lua_grpc/pinnacle/input.lua +++ b/api/lua_grpc/pinnacle/input.lua @@ -34,6 +34,8 @@ local function build_grpc_request_params(method, data) } end +-- This is an @enum and not an @alias because with an @alias the completion replaces tables with a string, +-- which is annoying ---@enum (key) Modifier local modifier_values = { shift = 1, @@ -42,7 +44,6 @@ local modifier_values = { super = 4, } ----@enum (key) MouseButton local mouse_button_values = { --- Left [1] = 0x110, @@ -58,20 +59,38 @@ local mouse_button_values = { [6] = 0x115, --- Back [7] = 0x116, - left = 0x110, - right = 0x111, - middle = 0x112, - side = 0x113, - extra = 0x114, - forward = 0x115, - back = 0x116, + btn_left = 0x110, + btn_right = 0x111, + btn_middle = 0x112, + btn_side = 0x113, + btn_extra = 0x114, + btn_forward = 0x115, + btn_back = 0x116, } +-- This alias is because I can't get @enum completion to work +---@alias MouseButton +---| 1 Left +---| 2 Right +---| 3 Middle +---| 4 Side +---| 5 Extra +---| 6 Forward +---| 7 Back, +---| "btn_left" +---| "btn_right" +---| "btn_middle" +---| "btn_side" +---| "btn_extra" +---| "btn_forward" +---| "btn_back" ----@enum (key) MouseEdge local mouse_edge_values = { press = 1, release = 2, } +---@alias MouseEdge +---| "press" Trigger on mouse button press +---| "release" Trigger on mouse button release ---@class InputModule ---@field private btn table @@ -84,10 +103,44 @@ local Input = { key = require("pinnacle.input.keys"), } ----@param mods Modifier[] ----@param key Key | string ----@param action fun() -function Input:set_keybind(mods, key, action) +---Set a keybind. If called with an already existing keybind, it gets replaced. +--- +---You must provide three arguments: +--- +--- - `mods`: An array of `Modifier`s. If you don't want any, provide an empty table. +--- - `key`: The key that will trigger `action`. You can provide three types of key: +--- - Something from the `Key` table in `Input.key`, which lists every xkbcommon key. The naming pattern is the xkbcommon key without the `KEY_` prefix, unless that would make it start with a number or the reserved lua keyword `function`, in which case the `KEY_` prefix is included. +--- - A single character representing your key. This can be something like "g", "$", "~", "1", and so on. +--- - A string of the key's name. This is the name of the xkbcommon key without the `KEY_` prefix. +--- - `action`: The function that will be run when the keybind is pressed. +--- +---It is important to note that `"a"` is different than `"A"`. Similarly, `key.a` is different than `key.A`. +---Usually, it's best to use the non-modified key to prevent confusion and unintended behavior. +--- +---```lua +---Input:keybind({ "shift" }, "a", function() end) -- This is preferred +---Input:keybind({ "shift" }, "A", function() end) -- over this +--- +--- -- This keybind will only work with capslock on. +---Input:keybind({}, "A", function() end) +--- +--- -- This keybind won't work at all because to get `@` you need to hold shift, +--- -- which this keybind doesn't accept. +---Input:keybind({ "ctrl" }, "@", function() end) +---``` +--- +---### Example +---```lua +--- -- Set `super + Return` to open Alacritty +---Input:keybind({ "super" }, Input.key.Return, function() +--- Process:spawn("alacritty") +---end) +---``` +--- +---@param mods Modifier[] The modifiers that need to be held down for the bind to trigger +---@param key Key | string The key used to trigger the bind +---@param action fun() The function to run when the bind is triggered +function Input:keybind(mods, key, action) local raw_code = nil local xkb_name = nil @@ -112,13 +165,23 @@ function Input:set_keybind(mods, key, action) ) end ----Set a mousebind. +---Set a mousebind. If called with an already existing mousebind, it gets replaced. --- ----@param mods Modifier[] ----@param button MouseButton ----@param edge MouseEdge ----@param action fun() -function Input:set_mousebind(mods, button, edge, action) +---You must specify whether the keybind happens on button press or button release. +--- +---### Example +---```lua +--- -- Set `super + left mouse button` to move a window on press +---Input:mousebind({ "super" }, "btn_left", "press", function() +--- Window:begin_move("btn_left") +---end) +---``` +--- +---@param mods Modifier[] The modifiers that need to be held down for the bind to trigger +---@param button MouseButton The mouse button used to trigger the bind +---@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) local edge = mouse_edge_values[edge] local mod_values = {} @@ -145,15 +208,32 @@ end ---Set the xkbconfig for your keyboard. --- ----@param xkb_config XkbConfig +---Fields not present will be set to their default values. +--- +---Read `xkeyboard-config(7)` for more information. +--- +---### Example +---```lua +---Input:set_xkb_config({ +--- layout = "us,fr,ge", +--- options = "ctrl:swapcaps,caps:shift" +---}) +---``` +--- +---@param xkb_config XkbConfig The new xkbconfig function Input:set_xkb_config(xkb_config) self.config_client:unary_request(build_grpc_request_params("SetXkbConfig", xkb_config)) end ---Set the keyboard's repeat rate and delay. --- ----@param rate integer The time between repeats, in milliseconds ----@param delay integer The duration a key needs to be held down before repeating starts, in milliseconds +---### Example +---```lua +---Input:set_repeat_rate(100, 1000) -- Key must be held down for 1 second, then repeats 10 times per second. +---``` +--- +---@param rate integer The time between repeats in milliseconds +---@param delay integer The duration a key needs to be held down before repeating starts in milliseconds function Input:set_repeat_rate(rate, delay) self.config_client:unary_request(build_grpc_request_params("SetRepeatRate", { rate = rate, diff --git a/api/lua_grpc/pinnacle/output.lua b/api/lua_grpc/pinnacle/output.lua index c652261..d28b220 100644 --- a/api/lua_grpc/pinnacle/output.lua +++ b/api/lua_grpc/pinnacle/output.lua @@ -54,6 +54,11 @@ local Output = {} ---Get all outputs. --- +---### Example +---```lua +---local outputs = Output:get_all() +---``` +--- ---@return OutputHandle[] function Output:get_all() local response = self.config_client:unary_request(build_grpc_request_params("Get", {})) @@ -68,7 +73,14 @@ function Output:get_all() return handles end ----@param name string The name of the port the output is connected to +---Get an output by its name (the connector it's plugged into). +--- +---### Example +---```lua +---local output = Output:get_by_name("eDP-1") +---``` +--- +---@param name string The name of the connector the output is connected to ---@return OutputHandle | nil function Output:get_by_name(name) local handles = self:get_all() @@ -82,6 +94,15 @@ function Output:get_by_name(name) return nil end +---Get the currently focused output. +--- +---This is currently defined as the most recent one that has had pointer motion. +--- +---### Example +---```lua +---local output = Output:get_focused() +---``` +--- ---@return OutputHandle | nil function Output:get_focused() local handles = self:get_all() @@ -95,6 +116,25 @@ function Output:get_focused() return nil end +---Connect a function to be run with all current and future outputs. +--- +---This method does two things: +---1. Immediately runs `callback` with all currently connected outputs. +---2. Calls `callback` whenever a new output is plugged in. +--- +---This will *not* run `callback` with an output that has been unplugged and replugged +---to prevent duplicate setup. Instead, the compositor keeps track of the tags and other +---state associated with that output and restores it when replugged. +--- +---### Example +---```lua +--- -- Add tags "1" through "5" to all outputs +---Output:connect_for_all(function(output) +--- local tags = Tag:add(output, "1", "2", "3", "4", "5") +--- tags[1]:toggle_active() +---end) +---``` +--- ---@param callback fun(output: OutputHandle) function Output:connect_for_all(callback) local handles = self:get_all() @@ -109,7 +149,39 @@ function Output:connect_for_all(callback) end) end +---Set the location of this output in the global space. +--- +---On startup, Pinnacle will lay out all connected outputs starting at (0, 0) +---and going to the right, with their top borders aligned. +--- +---This method allows you to move outputs where necessary. +--- +---Note: If you have space between two outputs when setting their locations, +---the pointer will not be able to move between them. +--- +---### Example +---```lua +--- -- Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions: +--- -- - "DP-1": ┌─────┐ +--- -- │ │1920x1080 +--- -- └─────┘ +--- -- - "HDMI-1": ┌───────┐ +--- -- │ 2560x │ +--- -- │ 1440 │ +--- -- └───────┘ +---Output:get_by_name("DP-1"):set_location({ x = 0, y = 0 }) +---Output:get_by_name("HDMI-1"):set_location({ x = 1920, y = -360 }) +--- -- Results in: +--- -- ┌───────┐ +--- -- ┌─────┤ │ +--- -- │DP-1 │HDMI-1 │ +--- -- └─────┴───────┘ +--- -- Notice that x = 0 aligns with the top of "DP-1", and the top of "HDMI-1" is at x = -360. +---``` +--- ---@param loc { x: integer?, y: integer? } +--- +---@see OutputHandle.set_loc_adj_to function OutputHandle:set_location(loc) self.config_client:unary_request(build_grpc_request_params("SetLocation", { output_name = self.name, @@ -119,19 +191,47 @@ function OutputHandle:set_location(loc) end ---@alias Alignment ----| "top_align_left" ----| "top_align_center" ----| "top_align_right" ----| "bottom_align_left" ----| "bottom_align_center" ----| "bottom_align_right" ----| "left_align_top" ----| "left_align_center" ----| "left_align_bottom" ----| "right_align_top" ----| "right_align_center" ----| "right_align_bottom" +---| "top_align_left" Set above, align left borders +---| "top_align_center" Set above, align centers +---| "top_align_right" Set above, align right borders +---| "bottom_align_left" Set below, align left borders +---| "bottom_align_center" Set below, align centers +---| "bottom_align_right" Set below, align right border +---| "left_align_top" Set to left, align top borders +---| "left_align_center" Set to left, align centers +---| "left_align_bottom" Set to left, align bottom borders +---| "right_align_top" Set to right, align top borders +---| "right_align_center" Set to right, align centers +---| "right_align_bottom" Set to right, align bottom borders +---Set the location of this output adjacent to another one. +--- +---`alignment` is how you want this output to be placed. +---For example, "top_align_left" will place this output above `other` and align the left borders. +---Similarly, "right_align_center" will place this output to the right of `other` and align their centers. +--- +---### Example +---```lua +--- -- Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions: +--- -- - "DP-1": ┌─────┐ +--- -- │ │1920x1080 +--- -- └─────┘ +--- -- - "HDMI-1": ┌───────┐ +--- -- │ 2560x │ +--- -- │ 1440 │ +--- -- └───────┘ +---Output:get_by_name("DP-1"):set_loc_adj_to(Output:get_by_name("HDMI-1"), "bottom_align_right") +--- -- Results in: +--- -- ┌───────┐ +--- -- │ │ +--- -- │HDMI-1 │ +--- -- └──┬────┤ +--- -- │DP-1│ +--- -- └────┘ +--- -- Notice that "DP-1" now has the coordinates (2280, 1440) because "DP-1" is getting moved, not "HDMI-1". +--- -- "HDMI-1" was placed at (1920, 0) during the compositor's initial output layout. +---``` +--- ---@param other OutputHandle ---@param alignment Alignment function OutputHandle:set_loc_adj_to(other, alignment) @@ -203,15 +303,16 @@ end ---@field physical_width integer? ---@field physical_height integer? ---@field focused boolean? ----@field tags TagHandle[] +---@field tags TagHandle[]? ---Get all properties of this output. +--- ---@return OutputProperties function OutputHandle:props() local response = self.config_client:unary_request(build_grpc_request_params("GetProperties", { output_name = self.name })) - local handles = require("pinnacle.tag").handle.new_from_table(self.config_client, response.tag_ids) + local handles = require("pinnacle.tag").handle.new_from_table(self.config_client, response.tag_ids or {}) response.tags = handles response.tag_ids = nil @@ -219,6 +320,113 @@ function OutputHandle:props() return response end +---Get this output's make. +--- +---Note: make and model detection are currently somewhat iffy and may not work. +--- +---Shorthand for `handle:props().make`. +--- +---@return string? +function OutputHandle:make() + return self:props().make +end + +---Get this output's model. +--- +---Note: make and model detection are currently somewhat iffy and may not work. +--- +---Shorthand for `handle:props().model`. +--- +---@return string? +function OutputHandle:model() + return self:props().model +end + +---Get this output's x-coordinate in the global space. +--- +---Shorthand for `handle:props().x`. +--- +---@return integer? +function OutputHandle:x() + return self:props().x +end + +---Get this output's y-coordinate in the global space. +--- +---Shorthand for `handle:props().y`. +--- +---@return integer? +function OutputHandle:y() + return self:props().y +end + +---Get this output's width in pixels. +--- +---Shorthand for `handle:props().pixel_width`. +--- +---@return integer? +function OutputHandle:pixel_width() + return self:props().pixel_width +end + +---Get this output's height in pixels. +--- +---Shorthand for `handle:props().pixel_height`. +--- +---@return integer? +function OutputHandle:pixel_height() + return self:props().pixel_height +end + +---Get this output's refresh rate in millihertz. +--- +---For example, 144Hz is returned as 144000. +--- +---Shorthand for `handle:props().refresh_rate`. +--- +---@return integer? +function OutputHandle:refresh_rate() + return self:props().refresh_rate +end + +---Get this output's physical width in millimeters. +--- +---Shorthand for `handle:props().physical_width`. +--- +---@return integer? +function OutputHandle:physical_width() + return self:props().physical_width +end + +---Get this output's physical height in millimeters. +--- +---Shorthand for `handle:props().physical_height`. +--- +---@return integer? +function OutputHandle:physical_height() + return self:props().physical_height +end + +---Get whether or not this output is focused. +--- +---The focused output is currently implemented as the one that last had pointer motion. +--- +---Shorthand for `handle:props().focused`. +--- +---@return boolean? +function OutputHandle:focused() + return self:props().focused +end + +---Get the tags this output has. +--- +---Shorthand for `handle:props().tags`. +--- +---@return TagHandle[]? +function OutputHandle:tags() + return self:props().tags +end + ---@return Output function output.new(config_client) ---@type Output diff --git a/api/lua_grpc/pinnacle/process.lua b/api/lua_grpc/pinnacle/process.lua index 683aa0d..4a0ac43 100644 --- a/api/lua_grpc/pinnacle/process.lua +++ b/api/lua_grpc/pinnacle/process.lua @@ -67,8 +67,21 @@ local function spawn_inner(config_client, args, callbacks, once) ) end ----@param args string | string[] ----@param callbacks { stdout: fun(line: string)?, stderr: fun(line: string)?, exit: fun(code: integer, msg: string)? }? +---Spawn a program with optional callbacks for its stdout, stderr, and exit information. +--- +---`callbacks` is an optional table with the following optional fields: +--- - `stdout`: function(line: string) +--- - `stderr`: function(line: string) +--- - `exit`: function(code: integer, msg: string) +--- +---Note: if `args` is a string then it will be wrapped in a table and sent to the compositor. +---If you need multiple arguments, use a string array instead. +--- +---Note 2: If you spawn a window before tags are added it will spawn without any tags and +---won't be displayed in the compositor. TODO: Do what awesome does and display on all tags instead +--- +---@param args string | string[] The program arguments; a string instead of an array should be for only 1 argument +---@param callbacks { stdout: fun(line: string)?, stderr: fun(line: string)?, exit: fun(code: integer, msg: string)? }? Callbacks that will be run whenever the program outputs to stdout, stderr, or exits. function Process:spawn(args, callbacks) if type(args) == "string" then args = { args } @@ -77,8 +90,12 @@ function Process:spawn(args, callbacks) spawn_inner(self.config_client, args, callbacks, false) end +---Like `Process:spawn` but will only spawn the program if it isn't already running. +--- ---@param args string | string[] ---@param callbacks { stdout: fun(line: string)?, stderr: fun(line: string)?, exit: fun(code: integer, msg: string)? }? +--- +---@see Process.spawn function Process:spawn_once(args, callbacks) if type(args) == "string" then args = { args } diff --git a/api/lua_grpc/pinnacle/tag.lua b/api/lua_grpc/pinnacle/tag.lua index 015433c..c118871 100644 --- a/api/lua_grpc/pinnacle/tag.lua +++ b/api/lua_grpc/pinnacle/tag.lua @@ -71,8 +71,24 @@ function Tag:get_all() return handles end +---Get the tag with the given name and output. +--- +---If `output` is not specified, this uses the focused output. +--- +---If an output has more than one tag with the same name, this returns the first. +--- +---### Example +---```lua +--- -- Get tags on the focused output +---local tag = Tag:get("Tag") +--- +--- -- Get tags on a specific output +---local tag_on_hdmi1 = Tag:get("Tag", Output:get_by_name("HDMI-1")) +---``` +--- ---@param name string ---@param output OutputHandle? +--- ---@return TagHandle | nil function Tag:get(name, output) output = output or require("pinnacle.output").new(self.config_client):get_focused() @@ -97,10 +113,19 @@ end --- ---Returns handles to the created tags. --- +---### Example +---```lua +---local tags = Tag:add(Output:get_by_name("HDMI-1"), "1", "2", "Buckle", "Shoe") +--- +--- -- With a table +---local tag_names = { "1", "2", "Buckle", "Shoe" } +---local tags = Tag:add(Output:get_by_name("HDMI-1"), tag_names) +---``` +--- ---@param output OutputHandle ---@param ... string --- ----@return TagHandle[] +---@return TagHandle[] tags Handles to the created tags --- ---@overload fun(self: self, output: OutputHandle, tag_names: string[]) function Tag:add(output, ...) @@ -126,6 +151,13 @@ end ---Remove the given tags. --- +---### Example +---```lua +---local tags = Tag:add(Output:get_by_name("HDMI-1"), "1", "2", "Buckle", "Shoe") +--- +---Tag:remove(tags) -- "HDMI-1" no longer has those tags +---``` +--- ---@param tags TagHandle[] function Tag:remove(tags) ---@type integer[] @@ -139,10 +171,44 @@ function Tag:remove(tags) end ---@class LayoutCycler ----@field next fun(output: OutputHandle) ----@field prev fun(output: OutputHandle) +---@field next fun(output: OutputHandle?) +---@field prev fun(output: OutputHandle?) ---- TODO: docs +---Create a layout cycler that will cycle layouts on the given output. +--- +---This returns a `LayoutCycler` table with two fields, both functions that take in an optional `OutputHandle`: +--- - `next`: Cycle to the next layout on the given output +--- - `prev`: Cycle to the previous layout on the given output +--- +---If the output isn't specified then the focused one will be used. +--- +---Internally, this will only change the layout of the first active tag on the output +---because that is the one that determines the layout. +--- +---### Example +---```lua +--- ---@type LayoutCycler[] +---local layouts = { +--- "master_stack", +--- "dwindle", +--- "corner_top_left", +--- "corner_top_right". +---} -- Only cycle between these four layouts +--- +---local layout_cycler = Tag:new_layout_cycler() +--- +--- -- Assume the focused output starts with the "master_stack" layout +---layout_cycler.next() -- Layout is now "dwindle" +---layout_cycler.next() -- Layout is now "corner_top_left" +---layout_cycler.next() -- Layout is now "corner_top_right" +---layout_cycler.next() -- Layout is now "dwindle" +---layout_cycler.next() -- Layout is now "corner_top_right" +--- +--- -- Cycling on another output +---layout_cycler.next(Output:get_by_name("eDP-1")) +---layout_cycler.prev(Output:get_by_name("HDMI-1")) +---``` +--- ---@param layouts Layout[] --- ---@return LayoutCycler @@ -159,6 +225,11 @@ function Tag:new_layout_cycler(layouts) ---@type LayoutCycler return { next = function(output) + local output = output or require("pinnacle.output").new(self.config_client):get_focused() + if not output then + return + end + local tags = output:props().tags for _, tg in ipairs(tags) do @@ -182,6 +253,11 @@ function Tag:new_layout_cycler(layouts) end end, prev = function(output) + local output = output or require("pinnacle.output").new(self.config_client):get_focused() + if not output then + return + end + local tags = output:props().tags for _, tg in ipairs(tags) do @@ -209,11 +285,19 @@ function Tag:new_layout_cycler(layouts) end ---Remove this tag. +--- +---### Example +---```lua +---local tags = Tag:add(Output:get_by_name("HDMI-1"), "1", "2", "Buckle", "Shoe") +--- +---tags[2]:remove() +---tags[4]:remove() +--- -- "HDMI-1" now only has tags "1" and "Buckle" +---``` function TagHandle:remove() self.config_client:unary_request(build_grpc_request_params("Remove", { tag_ids = { self.id } })) end ----@enum (key) Layout local _layouts = { master_stack = 1, dwindle = 2, @@ -223,7 +307,25 @@ local _layouts = { corner_bottom_left = 6, corner_bottom_right = 7, } +---@alias Layout +---| "master_stack" # One master window on the left with all other windows stacked to the right. +---| "dwindle" # Windows split in half towards the bottom right corner. +---| "spiral" # Windows split in half in a spiral. +---| "corner_top_left" # One main corner window in the top left with a column of windows on the right and a row on the bottom. +---| "corner_top_right" # One main corner window in the top right with a column of windows on the left and a row on the bottom. +---| "corner_bottom_left" # One main corner window in the bottom left with a column of windows on the right and a row on the top. +---| "corner_bottom_right" # One main corner window in the bottom right with a column of windows on the left and a row on the top. +---Set this tag's layout. +--- +---If this is the first active tag on its output, its layout will be used to tile windows. +--- +---### Example +---```lua +--- -- Assume the focused output has tag "Tag" +---Tag:get("Tag"):set_layout("dwindle") +---``` +--- ---@param layout Layout function TagHandle:set_layout(layout) local layout = _layouts[layout] @@ -235,26 +337,57 @@ function TagHandle:set_layout(layout) end ---Activate this tag and deactivate all other ones on the same output. +--- +---### Example +---```lua +--- -- Assume the focused output has the following inactive tags and windows: +--- -- - "1": Alacritty +--- -- - "2": Firefox, Discord +--- -- - "3": Steam +---Tag:get("2"):switch_to() -- Displays Firefox and Discord +---Tag:get("3"):switch_to() -- Displays Steam +---``` function TagHandle:switch_to() self.config_client:unary_request(build_grpc_request_params("SwitchTo", { tag_id = self.id })) end ---Set whether or not this tag is active. --- +---### Example +---```lua +--- -- Assume the focused output has the following inactive tags and windows: +--- -- - "1": Alacritty +--- -- - "2": Firefox, Discord +--- -- - "3": Steam +---Tag:get("2"):set_active(true) -- Displays Firefox and Discord +---Tag:get("3"):set_active(true) -- Displays Firefox, Discord, and Steam +---Tag:get("2"):set_active(false) -- Displays Steam +---``` +--- ---@param active boolean function TagHandle:set_active(active) self.config_client:unary_request(build_grpc_request_params("SetActive", { tag_id = self.id, set = active })) end ---Toggle this tag's active state. +--- +---### Example +---```lua +--- -- Assume the focused output has the following inactive tags and windows: +--- -- - "1": Alacritty +--- -- - "2": Firefox, Discord +--- -- - "3": Steam +---Tag:get("2"):toggle_active() -- Displays Firefox and Discord +---Tag:get("2"):toggle_active() -- Displays nothing +---``` function TagHandle:toggle_active() self.config_client:unary_request(build_grpc_request_params("SetActive", { tag_id = self.id, toggle = {} })) end ---@class TagProperties ----@field active boolean? ----@field name string? ----@field output OutputHandle? +---@field active boolean? Whether or not the tag is currently being displayed +---@field name string? The name of the tag +---@field output OutputHandle? The output the tag is on ---Get all properties of this tag. --- @@ -270,6 +403,33 @@ function TagHandle:props() } end +---Get whether or not this tag is being displayed. +--- +---Shorthand for `handle:props().active`. +--- +---@return boolean? +function TagHandle:active() + return self:props().active +end + +---Get this tag's name. +--- +---Shorthand for `handle:props().name`. +--- +---@return string? +function TagHandle:name() + return self:props().name +end + +---Get the output this tag is on. +--- +---Shorthand for `handle:props().output`. +--- +---@return OutputHandle? +function TagHandle:output() + return self:props().output +end + ---@return Tag function tag.new(config_client) ---@type Tag diff --git a/api/lua_grpc/pinnacle/window.lua b/api/lua_grpc/pinnacle/window.lua index 2437860..29f6ed0 100644 --- a/api/lua_grpc/pinnacle/window.lua +++ b/api/lua_grpc/pinnacle/window.lua @@ -60,7 +60,14 @@ local Window = {} ---Get all windows. --- ----@return WindowHandle[] +---### Example +---```lua +---local windows = Window:get_all() +---for _, window in ipairs(windows) do +--- print(window:props().class) +---end +---``` +---@return WindowHandle[] windows Handles to all windows function Window:get_all() local response = self.config_client:unary_request(build_grpc_request_params("Get", {})) @@ -69,7 +76,16 @@ function Window:get_all() return handles end ----@return WindowHandle | nil +---Get the currently focused window. +--- +---### Example +---```lua +---local focused = Window:get_focused() +---if focused then +--- print(focused:props().class) +---end +---``` +---@return WindowHandle | nil window A handle to the currently focused window function Window:get_focused() local handles = self:get_all() @@ -82,15 +98,35 @@ function Window:get_focused() return nil end ---- TODO: docs ----@param button MouseButton +---Begin moving this window using the specified mouse button. +--- +---The button must be pressed at the time this method is called. +---If the button is lifted, the move will end. +--- +---### Example +---```lua +---Input:mousebind({ "super" }, "btn_left", function() +--- Window:begin_move("btn_left") +---end) +---``` +---@param button MouseButton The button that will initiate the move function Window:begin_move(button) local button = require("pinnacle.input").btn[button] self.config_client:unary_request(build_grpc_request_params("MoveGrab", { button = button })) end ---- TODO: docs ----@param button MouseButton +---Begin resizing this window using the specified mouse button. +--- +---The button must be pressed at the time this method is called. +---If the button is lifted, the resize will end. +--- +---### Example +---```lua +---Input:mousebind({ "super" }, "btn_right", function() +--- Window:begin_resize("btn_right") +---end) +---``` +---@param button MouseButton The button that will initiate the resize function Window:begin_resize(button) local button = require("pinnacle.input").btn[button] self.config_client:unary_request(build_grpc_request_params("ResizeGrab", { button = button })) @@ -126,7 +162,95 @@ local _fullscreen_or_maximized_keys = { [3] = "maximized", } ----@param rule { cond: WindowRuleCondition, rule: WindowRule } +---Add a window rule. +--- +---A window rule defines what properties a window will spawn with given certain conditions. +---For example, if Firefox is spawned, you can set it to open on a specific tag. +--- +---This method takes in a table with two keys: +--- +--- - `cond`: The condition for `rule` to apply to a new window. +--- - `rule`: What gets applied to the new window if `cond` is true. +--- +---There are some important mechanics you should know when using window rules: +--- +--- - All children inside an `all` block must be true for the block to be true. +--- - At least one child inside an `any` block must be true for the block to be true. +--- - The outermost block of a window rule condition is implicitly an `all` block. +--- - Within an `all` block, all items in each array must be true for the attribute to be true. +--- - Within an `any` block, only one item in each array needs to be true for the attribute to be true. +--- +---`cond` can be a bit confusing and quite table heavy. Examples are shown below for guidance. +--- +---### Examples +---```lua +--- -- A simple window rule. This one will cause Firefox to open on tag "Browser". +---Window:add_window_rule({ +--- cond = { classes = { "firefox" } }, +--- rule = { tags = { "Browser" } }, +---}) +--- +--- -- To apply rules when *all* provided conditions are true, use `all`. +--- -- `all` takes an array of conditions and checks if all are true. +--- -- The following will open Steam fullscreen only if it opens on tag "5". +---Window:add_window_rule({ +--- cond = { +--- all = { +--- { +--- class = "steam", +--- tag = Tag:get("5"), +--- } +--- } +--- }, +--- rule = { fullscreen_or_maximized = "fullscreen" }, +---}) +--- +--- -- The outermost block of a `cond` is implicitly an `all` block. +--- -- Thus, the above can be shortened to: +---Window:add_window_rule({ +--- cond = { +--- class = "steam", +--- tag = Tag:get("5"), +--- }, +--- rule = { fullscreen_or_maximized = "fullscreen" }, +---}) +--- +--- -- `any` also exists to allow at least one provided condition to match. +--- -- The following will open either xterm or Alacritty floating. +---Window:add_window_rule({ +--- cond = { +--- any = { { classes = { "xterm", "Alacritty" } } } +--- }, +--- rule = { floating = true }, +---}) +--- +--- -- You can arbitrarily nest `any` and `all` to achieve desired logic. +--- -- The following will open Discord, Thunderbird, or Firefox floating if they +--- -- open on either *all* of tags "A", "B", and "C" or both tags "1" and "2". +---Window:add_window_rule({ +--- cond = { +--- all = { -- This `all` block is needed because the outermost block cannot be an array. +--- { any = { +--- { class = { "firefox", "thunderbird", "discord" } } +--- } }, +--- { any = { +--- -- Because `tag` is inside an `all` block, +--- -- the window must have all these tags for this to be true. +--- -- If it was in an `any` block, only one tag would need to match. +--- { all = { +--- { tag = { "A", "B", "C" } } +--- } }, +--- { all = { +--- { tag = { "1", "2" } } +--- } }, +--- } } +--- } +--- }, +--- rule = { floating = true }, +---}) +---``` +--- +---@param rule { cond: WindowRuleCondition, rule: WindowRule } The condition and rule function Window:add_window_rule(rule) if rule.cond.tags then local ids = {} @@ -159,18 +283,57 @@ function Window:add_window_rule(rule) end ---Send a close request to this window. +--- +---### Example +---```lua +---local focused = Window:get_focused() +---if focused then focused:close() end +---``` 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? } +---The coordinate system has the following axes: +---``` +--- ^ -y +--- | +--- -x <--+--> +x +--- | +--- v +y +---``` +--- +---*Tiled windows will not reflect these changes.* +---This method only applies to this window's floating geometry. +--- +---### Example +---```lua +---local focused = Window:get_focused() +---if focused then +--- focused:set_floating(true) -- `set_geometry` only applies to floating geometry. +--- +--- focused:set_geometry({ x = 50, y = 300 }) -- Move this window to (50, 300) +--- focused:set_geometry({ y = 0, height = 1080 }) -- Move this window to y = 0 and make its height 1080 pixels +--- focused:set_geometry({}) -- Do nothing useful +---end +---``` +---@param geo { x: integer?, y: integer, width: integer?, height: integer? } The new location and/or size 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. +--- +---### Example +---```lua +---local focused = Window:get_focused() +---if focused then +--- focused:set_fullscreen(true) +--- focused:set_fullscreen(false) +---end +---``` +--- ---@param fullscreen boolean function WindowHandle:set_fullscreen(fullscreen) self.config_client:unary_request( @@ -178,35 +341,114 @@ function WindowHandle:set_fullscreen(fullscreen) ) end +---Toggle this window to and from fullscreen. +--- +---### Example +---```lua +---local focused = Window:get_focused() +---if focused then +--- focused:toggle_fullscreen() +---end +---``` function WindowHandle:toggle_fullscreen() self.config_client:unary_request(build_grpc_request_params("SetFullscreen", { window_id = self.id, toggle = {} })) end +---Set this window to maximized or not. +--- +---### Example +---```lua +---local focused = Window:get_focused() +---if focused then +--- focused:set_maximized(true) +--- focused:set_maximized(false) +---end +---``` +--- +---@param maximized boolean function WindowHandle:set_maximized(maximized) self.config_client:unary_request( build_grpc_request_params("SetMaximized", { window_id = self.id, set = maximized }) ) end +---Toggle this window to and from maximized. +--- +---### Example +---```lua +---local focused = Window:get_focused() +---if focused then +--- focused:toggle_maximized() +---end +---``` function WindowHandle:toggle_maximized() self.config_client:unary_request(build_grpc_request_params("SetMaximized", { window_id = self.id, toggle = {} })) end +---Set this window to floating or not. +--- +---### Example +---```lua +---local focused = Window:get_focused() +---if focused then +--- focused:set_floating(true) +--- focused:set_floating(false) +---end +---``` +--- +---@param floating boolean function WindowHandle:set_floating(floating) self.config_client:unary_request(build_grpc_request_params("SetFloating", { window_id = self.id, set = floating })) end +---Toggle this window to and from floating. +--- +---### Example +---```lua +---local focused = Window:get_focused() +---if focused then +--- focused:toggle_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 +---Move this window to the specified tag. +--- +---This will remove all tags from this window and tag it with `tag`. +--- +---### Example +---```lua +--- -- Assume the focused output has the tag "Tag" +---local focused = Window:get_focused() +---if focused then +--- focused:move_to_tag(Tag:get("Tag")) +---end +---``` +--- +---@param tag TagHandle The tag to move this window to 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 +--- +---### Example +---```lua +--- -- Assume the focused output has the tag "Tag" +---local focused = Window:get_focused() +---if focused then +--- local tag = Tag:get("Tag") +--- +--- focused:set_tag(tag, true) +--- -- `focused` now has tag "Tag" +--- focused:set_tag(tag, false) +--- -- `focused` no longer has tag "Tag" +---end +---``` +--- +---@param tag TagHandle The tag to set or unset ---@param set boolean function WindowHandle:set_tag(tag, set) self.config_client:unary_request( @@ -215,7 +457,23 @@ function WindowHandle:set_tag(tag, set) end ---Toggle the given tag on this window. ----@param tag TagHandle +--- +---### Example +---```lua +--- -- Assume the focused output has the tag "Tag" +---local focused = Window:get_focused() +---if focused then +--- local tag = Tag:get("Tag") +--- focused:set_tag(tag, false) +--- +--- focused:toggle_tag(tag) +--- -- `focused` now has tag "Tag" +--- focused:toggle_tag(tag) +--- -- `focused` no longer has tag "Tag" +---end +---``` +--- +---@param tag TagHandle The tag to toggle function WindowHandle:toggle_tag(tag) self.config_client:unary_request( build_grpc_request_params("SetTag", { window_id = self.id, tag_id = tag.id, toggle = {} }) @@ -223,13 +481,16 @@ function WindowHandle:toggle_tag(tag) 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? +---@field geometry { x: integer?, y: integer?, width: integer?, height: integer? }? The location and size of the window +---@field class string? The window's class +---@field title string? The window's title +---@field focused boolean? Whether or not the window is focused +---@field floating boolean? Whether or not the window is floating +---@field fullscreen_or_maximized FullscreenOrMaximized? Whether the window is fullscreen, maximized, or neither +---@field tags TagHandle[]? The tags the window has +---Get all the properties of this window. +--- ---@return WindowProperties function WindowHandle:props() local response = @@ -237,9 +498,76 @@ function WindowHandle:props() response.fullscreen_or_maximized = _fullscreen_or_maximized_keys[response.fullscreen_or_maximized] + response.tags = response.tag_ids + and require("pinnacle.tag").handle.new_from_table(self.config_client, response.tag_ids) + response.tag_ids = nil + return response end +---Get this window's location and size. +--- +---Shorthand for `handle:props().geometry`. +--- +---@return { x: integer?, y: integer?, width: integer?, height: integer? }? +function WindowHandle:geometry() + return self:props().geometry +end + +---Get this window's class. +--- +---Shorthand for `handle:props().class`. +--- +---@return string? +function WindowHandle:class() + return self:props().class +end + +---Get this window's title. +--- +---Shorthand for `handle:props().title`. +--- +---@return string? +function WindowHandle:title() + return self:props().title +end + +---Get whether or not this window is focused. +--- +---Shorthand for `handle:props().focused`. +--- +---@return boolean? +function WindowHandle:focused() + return self:props().focused +end + +---Get whether or not this window is floating. +--- +---Shorthand for `handle:props().floating`. +--- +---@return boolean? +function WindowHandle:floating() + return self:props().floating +end + +---Get whether this window is fullscreen, maximized, or neither. +--- +---Shorthand for `handle:props().fullscreen_or_maximized`. +--- +---@return FullscreenOrMaximized? +function WindowHandle:fullscreen_or_maximized() + return self:props().fullscreen_or_maximized +end + +---Get all tags on this window. +--- +---Shorthand for `handle:props().tags`. +--- +---@return TagHandle[]? +function WindowHandle:tags() + return self:props().tags +end + ---@param config_client Client ---@return Window function window.new(config_client) diff --git a/api/lua_grpc/test.lua b/api/lua_grpc/test.lua index 0f6069e..9ec9c3f 100644 --- a/api/lua_grpc/test.lua +++ b/api/lua_grpc/test.lua @@ -1,29 +1,18 @@ -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 +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 - 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) - - input:set_keybind({ 1 }, "Q", function() - pinnacle:quit() + Input:keybind({ "shift" }, "f", function() + local focused = Window:get_focused() + if focused then + print(focused:fullscreen_or_maximized()) + -- assert(focused:fullscreen_or_maximized() == "neither") + focused:set_fullscreen(true) + print(focused:fullscreen_or_maximized()) + -- assert(focused:fullscreen_or_maximized() == "fullscreen") + end end) end) diff --git a/api/protocol/pinnacle/window/v0alpha1/window.proto b/api/protocol/pinnacle/window/v0alpha1/window.proto index 4700e07..c43edcc 100644 --- a/api/protocol/pinnacle/window/v0alpha1/window.proto +++ b/api/protocol/pinnacle/window/v0alpha1/window.proto @@ -77,6 +77,7 @@ message GetPropertiesResponse { optional bool focused = 4; optional bool floating = 5; optional .pinnacle.window.rules.v0alpha1.FullscreenOrMaximized fullscreen_or_maximized = 6; + repeated uint32 tag_ids = 7; } service WindowService { diff --git a/src/api/protocol.rs b/src/api/protocol.rs index 25d440a..4bc705e 100644 --- a/src/api/protocol.rs +++ b/src/api/protocol.rs @@ -1217,7 +1217,7 @@ impl pinnacle_api_defs::pinnacle::window::v0alpha1::window_service_server::Windo window_size.h = height.unwrap_or(window_size.h); let rect = Rectangle::from_loc_and_size(window_loc, window_size); - window.change_geometry(rect); + // window.change_geometry(rect); window.with_state(|state| { use crate::window::window_state::FloatingOrTiled; state.floating_or_tiled = match state.floating_or_tiled { @@ -1727,6 +1727,22 @@ impl pinnacle_api_defs::pinnacle::window::v0alpha1::window_service_server::Windo } } as i32); + let tag_ids = window + .as_ref() + .map(|win| { + win.with_state(|state| { + state + .tags + .iter() + .map(|tag| match tag.id() { + TagId::Some(id) => id, + TagId::None => unreachable!(), + }) + .collect::>() + }) + }) + .unwrap_or_default(); + let _ = sender.send( pinnacle_api_defs::pinnacle::window::v0alpha1::GetPropertiesResponse { geometry, @@ -1735,6 +1751,7 @@ impl pinnacle_api_defs::pinnacle::window::v0alpha1::window_service_server::Windo focused, floating, fullscreen_or_maximized, + tag_ids, }, ); });