From a8f393deb3af22ce34849cb61f7e50fff7408218 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Wed, 17 Apr 2024 21:22:54 -0500 Subject: [PATCH] Improve Lua output setup API I really like this compared to the previous iteration --- api/lua/pinnacle/output.lua | 221 ++++++++++++++++++++++++++---------- api/rust/src/output.rs | 3 +- tests/lua_api.rs | 79 +++++++++---- 3 files changed, 216 insertions(+), 87 deletions(-) diff --git a/api/lua/pinnacle/output.lua b/api/lua/pinnacle/output.lua index 9e97e87..d5aae5d 100644 --- a/api/lua/pinnacle/output.lua +++ b/api/lua/pinnacle/output.lua @@ -168,72 +168,142 @@ function output.connect_for_all(callback) end ---@class OutputSetup ----@field [1] (string | fun(output: OutputHandle): boolean) ----@field loc ({ x: integer, y: integer } | { [1]: (string | fun(output: OutputHandle): boolean), [2]: Alignment })? +---@field filter (fun(output: OutputHandle): boolean)? ---@field mode Mode? ---@field scale number? ----@field tag_names string[]? - ----comment ----@param op OutputHandle ----@param matcher string | fun(output: OutputHandle): boolean ----@return boolean -local function output_matches(op, matcher) - return (type(matcher) == "string" and matcher == op.name) or (type(matcher) == "function" and matcher(op)) -end +---@field tags string[]? +---@field transform Transform? ---Declaratively setup outputs. --- ---`Output.setup` allows you to specify output properties that will be applied immediately and ---on output connection. These include mode, scale, tags, and more. --- ----Setups will be applied from top to bottom. +---`setups` is a table of output identifier strings to `OutputSetup`s. +--- +---## Keys +--- +---Keys attempt to match outputs. +--- +---Wildcard keys (`"*"`) will match all outputs. You can additionally filter these outputs +---by setting a `filter` function in the setup that returns true if it should apply to the output. +---(See the example.) +--- +---Otherwise, keys will attempt to match the exact name of an output. +--- +---## Setups +--- +---If an output is matched, the corresponding `OutputSetup` entry will be applied to it. +---Any given `tags` will be added, and things like `transform`s, `scale`s, and `mode`s will be set. +--- +---## Ordering setups +--- +---You may need to specify multiple wildcard matches for different setup applications. +---You can't just add another key of `"*"`, because that would overwrite the old `"*"`. +---In this case, you can order setups by prepending `n:` to the key, where n is a priority number. +---`n` should be between `1` and `#setups`. Setting higher priorities without setting lower ones +---will cause entries without priorities to fill up lower priorities in an arbitrary order. Setting +---priorities above `#setups` may cause their entries to not apply. --- ----`setups` is an array of `OutputSetup` tables. ----The table entry at [1] in an `OutputSetup` table should be either a string or a function ----that takes in an `OutputHandle` and returns a boolean. Strings will match output names directly, ----while the function matches outputs based on custom logic. You can specify keys such as ----`tag_names`, `scale`, and others to customize output properties. --- ---### Example ---```lua ---Output.setup({ --- -- Give all outputs tags 1 through 5 ---- { ---- function(_) return true end, ---- tag_names = { "1", "2", "3", "4", "5" }, ---- } +--- ["1:*"] = { +--- tags = { "1", "2", "3", "4", "5" }, +--- }, --- -- Give outputs with a preferred mode of 4K a scale of 2.0 ---- { ---- function(op) ---- return op:preferred_mode().pixel_width == 2160 +--- ["2:*"] = { +--- filter = function(op) +--- return op:preferred_mode().pixel_height == 2160 --- end, --- scale = 2.0, --- }, --- -- Additionally give eDP-1 tags 6 and 7 ---- { ---- "eDP-1", ---- tag_names = { "6", "7" }, +--- ["eDP-1"] = { +--- tags = { "6", "7" }, --- }, ---}) ---``` --- ----@param setups OutputSetup[] +---@param setups table function output.setup(setups) + ---@type { [1]: string, setup: OutputSetup }[] + local op_setups = {} + + local setup_len = 0 + + -- Index entries with an index + for op_id, op_setup in pairs(setups) do + setup_len = setup_len + 1 + + ---@type string|nil + if op_id:match("^%d+:") then + ---@type string + local index = op_id:match("^%d+") + local op_id = op_id:sub(index:len() + 2) + local index = tonumber(index) + + ---@cast index number + + op_setups[index] = { op_id, setup = op_setup } + end + end + + -- Insert *s first + for op_id, op_setup in pairs(setups) do + if op_id:match("^*$") then + -- Fill up holes if there are any + for i = 1, setup_len do + if not op_setups[i] then + print(op_id, i) + op_setups[i] = { op_id, setup = op_setup } + break + end + end + end + end + + -- Insert rest of the entries + for op_id, op_setup in pairs(setups) do + if not op_id:match("^%d+:") and op_id ~= "*" then + -- Fill up holes if there are any + for i = 1, setup_len do + if not op_setups[i] then + print(op_id, i) + op_setups[i] = { op_id, setup = op_setup } + break + end + end + end + end + ---@param op OutputHandle local function apply_setups(op) - for _, setup in ipairs(setups) do - if output_matches(op, setup[1]) then + for _, op_setup in ipairs(op_setups) do + if op_setup[1] == op.name or op_setup[1] == "*" then + local setup = op_setup.setup + + if setup.filter and not setup.filter(op) then + goto continue + end + if setup.mode then op:set_mode(setup.mode.pixel_width, setup.mode.pixel_height, setup.mode.refresh_rate_millihz) end if setup.scale then op:set_scale(setup.scale) end - if setup.tag_names then - require("pinnacle.tag").add(op, setup.tag_names) + if setup.tags then + require("pinnacle.tag").add(op, setup.tags) + end + if setup.transform then + op:set_transform(setup.transform) end end + + ::continue:: end local tags = op:tags() or {} @@ -248,9 +318,8 @@ function output.setup(setups) end ---@alias OutputLoc ----| { x: integer, y: integer } -- A specific point +---| { [1]: integer, [2]: integer } -- A specific point ---| { [1]: string, [2]: Alignment } -- A location relative to another output ----| { [1]: string, [2]: Alignment }[] -- A location relative to another output with fallbacks ---@alias UpdateLocsOn ---| "connect" -- Update output locations on output connect @@ -275,19 +344,22 @@ end --- -- vvvvv Relayout on output connect, disconnect, and resize ---Output.setup_locs("all", { --- -- Anchor eDP-1 to (0, 0) so we can place other outputs relative to it ---- { "eDP-1", loc = { x = 0, y = 0 } }, +--- ["eDP-1"] = { 0, 0 }, --- -- Place HDMI-A-1 below it centered ---- { "HDMI-A-1", loc = { "eDP-1", "bottom_align_center" } }, +--- ["HDMI-A-1"] = { "eDP-1", "bottom_align_center" }, --- -- Place HDMI-A-2 below HDMI-A-1. ---- -- Additionally, if HDMI-A-1 isn't connected, fallback to placing ---- -- it below eDP-1 instead. ---- { ---- "HDMI-A-2", ---- loc = { ---- { "HDMI-A-1", "bottom_align_center" }, ---- { "eDP-1", "bottom_align_center" }, ---- }, ---- }, +--- ["3:HDMI-A-2"] = { "HDMI-A-1", "bottom_align_center" }, +--- -- Additionally, if HDMI-A-1 isn't connected, fallback to placing it below eDP-1 instead. +--- ["4:HDMI-A-2"] = { "eDP-1", "bottom_align_center" }, +--- +--- -- Note that the last two have a number followed by a colon. This dictates the priority of application. +--- -- Because Lua tables with string keys don't index by declaration order, this is needed to specify that. +--- -- You can also put a "1:" and "2:" in front of "eDP-1" and "HDMI-A-1" if you want to be explicit +--- -- about their ordering. +--- -- +--- -- Just note that priorities must be from 1 to the length of the array. Entries without a priority +--- -- will be filled in from 1 upwards, taking any open priorities. Entries with priorities above +--- -- #locs may not be applied. ---}) --- --- -- Only relayout on output connect and resize @@ -295,15 +367,40 @@ end ---``` --- ---@param update_locs_on (UpdateLocsOn)[] | "all" ----@param setup { [1]: string, loc: OutputLoc }[] -function output.setup_locs(update_locs_on, setup) - ---@type { [1]: string, loc: ({ x: integer, y: integer } | { [1]: string, [2]: Alignment }[]) }[] +---@param locs table +function output.setup_locs(update_locs_on, locs) + ---@type { [1]: string, loc: OutputLoc }[] local setups = {} - for _, s in ipairs(setup) do - if type(s.loc[1]) == "string" then - table.insert(setups, { s[1], loc = { s.loc } }) - else - table.insert(setups, s) + + local setup_len = 0 + + -- Index entries with an index + for op_id, op_loc in pairs(locs) do + setup_len = setup_len + 1 + + ---@type string|nil + if op_id:match("^%d+:") then + ---@type string + local index = op_id:match("^%d+") + local op_id = op_id:sub(index:len() + 2) + local index = tonumber(index) + + ---@cast index number + + setups[index] = { op_id, loc = op_loc } + end + end + + -- Insert rest of the entries + for op_id, op_loc in pairs(locs) do + if not op_id:match("^%d+:") then + -- Fill up holes if there are any + for i = 1, setup_len do + if not setups[i] then + setups[i] = { op_id, loc = op_loc } + break + end + end end end @@ -323,8 +420,8 @@ function output.setup_locs(update_locs_on, setup) for _, setup in ipairs(setups) do for _, op in ipairs(outputs) do if op.name == setup[1] then - if setup.loc and setup.loc.x and setup.loc.y then - local loc = { x = setup.loc.x, y = setup.loc.y } + if type(setup.loc[1]) == "number" then + local loc = { x = setup.loc[1], y = setup.loc[2] } op:set_location(loc) table.insert(placed_outputs, op) @@ -350,17 +447,15 @@ function output.setup_locs(update_locs_on, setup) end end - if op.name ~= setup[1] or type(setup.loc[1]) ~= "table" then + if op.name ~= setup[1] or type(setup.loc[1]) == "number" then goto continue end - for _, loc in ipairs(setup.loc) do - local relative_to_name = loc[1] - local alignment = loc[2] - for _, placed_op in ipairs(placed_outputs) do - if placed_op.name == relative_to_name then - return op, placed_op, alignment - end + local relative_to_name = setup.loc[1] + local alignment = setup.loc[2] + for _, placed_op in ipairs(placed_outputs) do + if placed_op.name == relative_to_name then + return op, placed_op, alignment end end diff --git a/api/rust/src/output.rs b/api/rust/src/output.rs index b9643c8..e3e7a41 100644 --- a/api/rust/src/output.rs +++ b/api/rust/src/output.rs @@ -188,6 +188,7 @@ impl Output { /// /// ``` /// use pinnacle_api::output::OutputSetup; + /// use pinnacle_api::output::OutputId; /// /// output.setup([ /// // Give all outputs tags 1 through 5 @@ -196,7 +197,7 @@ impl Output { /// OutputSetup::new_with_matcher(|op| op.preferred_mode().unwrap().pixel_width == 2160) /// .with_scale(2.0), /// // Additionally give eDP-1 tags 6 and 7 - /// OutputSetup::new("eDP-1").with_tags(["6", "7"]), + /// OutputSetup::new(OutputId::name("eDP-1")).with_tags(["6", "7"]), /// ]); /// ``` pub fn setup(&self, setups: impl IntoIterator) { diff --git a/tests/lua_api.rs b/tests/lua_api.rs index bc64fb5..2fce807 100644 --- a/tests/lua_api.rs +++ b/tests/lua_api.rs @@ -494,26 +494,23 @@ mod output { test_api(|sender| { setup_lua! { |Pinnacle| Pinnacle.output.setup({ - { - function(_) - return true - end, - tag_names = { "1", "2", "3" }, + ["1:*"] = { + tags = { "1", "2", "3" }, }, - { - function(op) + ["2:*"] = { + filter = function(op) return string.match(op.name, "Test") ~= nil end, - tag_names = { "Test 4", "Test 5" }, + tags = { "Test 4", "Test 5" }, }, - { - "Second", + ["Second"] = { scale = 2.0, mode = { pixel_width = 6900, pixel_height = 420, refresh_rate_millihz = 69420, }, + transform = "90", }, }) } @@ -562,6 +559,47 @@ mod output { assert_eq!(second_mode.size.w, 6900); assert_eq!(second_mode.size.h, 420); assert_eq!(second_mode.refresh, 69420); + + assert_eq!( + second_op.current_transform(), + smithay::utils::Transform::_90 + ); + }); + }) + } + + #[tokio::main] + #[self::test] + async fn setup_has_wildcard_first() -> anyhow::Result<()> { + test_api(|sender| { + setup_lua! { |Pinnacle| + Pinnacle.output.setup({ + ["*"] = { + tags = { "1", "2", "3" }, + }, + ["First"] = { + tags = { "A", "B" }, + }, + }) + } + + sleep_secs(1); + + with_state(&sender, |state| { + state.new_output("First", (300, 200).into()); + }); + + sleep_secs(1); + + with_state(&sender, |state| { + let first_op = output_for_name(state, "First"); + + let tags_for = |output: &Output| { + output + .with_state(|state| state.tags.iter().map(|t| t.name()).collect::>()) + }; + + assert_eq!(tags_for(&first_op), vec!["1", "2", "3", "A", "B"]); }); }) } @@ -572,9 +610,9 @@ mod output { test_api(|sender| { setup_lua! { |Pinnacle| Pinnacle.output.setup_locs("all", { - { "Pinnacle Window", loc = { x = 0, y = 0 } }, - { "First", loc = { "Second", "left_align_top" } }, - { "Second", loc = { "First", "right_align_top" } }, + ["Pinnacle Window"] = { x = 0, y = 0 }, + ["First"] = { "Second", "left_align_top" }, + ["Second"] = { "First", "right_align_top" }, }) } @@ -639,16 +677,11 @@ mod output { test_api(|sender| { setup_lua! { |Pinnacle| Pinnacle.output.setup_locs("all", { - { "Pinnacle Window", loc = { x = 0, y = 0 } }, - { "First", loc = { "Pinnacle Window", "bottom_align_left" } }, - { "Second", loc = { "First", "bottom_align_left" } }, - { - "Third", - loc = { - { "Second", "bottom_align_left" }, - { "First", "bottom_align_left" }, - }, - }, + ["Pinnacle Window"] = { 0, 0 }, + ["First"] = { "Pinnacle Window", "bottom_align_left" }, + ["Second"] = { "First", "bottom_align_left" }, + ["4:Third"] = { "Second", "bottom_align_left" }, + ["5:Third"] = { "First", "bottom_align_left" }, }) }