Improve Lua output setup API

I really like this compared to the previous iteration
This commit is contained in:
Ottatop 2024-04-17 21:22:54 -05:00
parent 5634cbbfe7
commit a8f393deb3
3 changed files with 216 additions and 87 deletions

View file

@ -168,72 +168,142 @@ function output.connect_for_all(callback)
end end
---@class OutputSetup ---@class OutputSetup
---@field [1] (string | fun(output: OutputHandle): boolean) ---@field filter (fun(output: OutputHandle): boolean)?
---@field loc ({ x: integer, y: integer } | { [1]: (string | fun(output: OutputHandle): boolean), [2]: Alignment })?
---@field mode Mode? ---@field mode Mode?
---@field scale number? ---@field scale number?
---@field tag_names string[]? ---@field tags string[]?
---@field transform Transform?
---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
---Declaratively setup outputs. ---Declaratively setup outputs.
--- ---
---`Output.setup` allows you to specify output properties that will be applied immediately and ---`Output.setup` allows you to specify output properties that will be applied immediately and
---on output connection. These include mode, scale, tags, and more. ---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 ---### Example
---```lua ---```lua
---Output.setup({ ---Output.setup({
--- -- Give all outputs tags 1 through 5 --- -- Give all outputs tags 1 through 5
--- { --- ["1:*"] = {
--- function(_) return true end, --- tags = { "1", "2", "3", "4", "5" },
--- tag_names = { "1", "2", "3", "4", "5" }, --- },
--- }
--- -- Give outputs with a preferred mode of 4K a scale of 2.0 --- -- Give outputs with a preferred mode of 4K a scale of 2.0
--- { --- ["2:*"] = {
--- function(op) --- filter = function(op)
--- return op:preferred_mode().pixel_width == 2160 --- return op:preferred_mode().pixel_height == 2160
--- end, --- end,
--- scale = 2.0, --- scale = 2.0,
--- }, --- },
--- -- Additionally give eDP-1 tags 6 and 7 --- -- Additionally give eDP-1 tags 6 and 7
--- { --- ["eDP-1"] = {
--- "eDP-1", --- tags = { "6", "7" },
--- tag_names = { "6", "7" },
--- }, --- },
---}) ---})
---``` ---```
--- ---
---@param setups OutputSetup[] ---@param setups table<string, OutputSetup>
function output.setup(setups) 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 ---@param op OutputHandle
local function apply_setups(op) local function apply_setups(op)
for _, setup in ipairs(setups) do for _, op_setup in ipairs(op_setups) do
if output_matches(op, setup[1]) then 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 if setup.mode then
op:set_mode(setup.mode.pixel_width, setup.mode.pixel_height, setup.mode.refresh_rate_millihz) op:set_mode(setup.mode.pixel_width, setup.mode.pixel_height, setup.mode.refresh_rate_millihz)
end end
if setup.scale then if setup.scale then
op:set_scale(setup.scale) op:set_scale(setup.scale)
end end
if setup.tag_names then if setup.tags then
require("pinnacle.tag").add(op, setup.tag_names) require("pinnacle.tag").add(op, setup.tags)
end
if setup.transform then
op:set_transform(setup.transform)
end end
end end
::continue::
end end
local tags = op:tags() or {} local tags = op:tags() or {}
@ -248,9 +318,8 @@ function output.setup(setups)
end end
---@alias OutputLoc ---@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
---| { [1]: string, [2]: Alignment }[] -- A location relative to another output with fallbacks
---@alias UpdateLocsOn ---@alias UpdateLocsOn
---| "connect" -- Update output locations on output connect ---| "connect" -- Update output locations on output connect
@ -275,19 +344,22 @@ end
--- -- vvvvv Relayout on output connect, disconnect, and resize --- -- vvvvv Relayout on output connect, disconnect, and resize
---Output.setup_locs("all", { ---Output.setup_locs("all", {
--- -- Anchor eDP-1 to (0, 0) so we can place other outputs relative to it --- -- 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 --- -- 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. --- -- Place HDMI-A-2 below HDMI-A-1.
--- -- Additionally, if HDMI-A-1 isn't connected, fallback to placing --- ["3:HDMI-A-2"] = { "HDMI-A-1", "bottom_align_center" },
--- -- it below eDP-1 instead. --- -- 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" },
--- "HDMI-A-2", ---
--- loc = { --- -- Note that the last two have a number followed by a colon. This dictates the priority of application.
--- { "HDMI-A-1", "bottom_align_center" }, --- -- Because Lua tables with string keys don't index by declaration order, this is needed to specify that.
--- { "eDP-1", "bottom_align_center" }, --- -- 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 --- -- Only relayout on output connect and resize
@ -295,15 +367,40 @@ end
---``` ---```
--- ---
---@param update_locs_on (UpdateLocsOn)[] | "all" ---@param update_locs_on (UpdateLocsOn)[] | "all"
---@param setup { [1]: string, loc: OutputLoc }[] ---@param locs table<string, OutputLoc>
function output.setup_locs(update_locs_on, setup) function output.setup_locs(update_locs_on, locs)
---@type { [1]: string, loc: ({ x: integer, y: integer } | { [1]: string, [2]: Alignment }[]) }[] ---@type { [1]: string, loc: OutputLoc }[]
local setups = {} local setups = {}
for _, s in ipairs(setup) do
if type(s.loc[1]) == "string" then local setup_len = 0
table.insert(setups, { s[1], loc = { s.loc } })
else -- Index entries with an index
table.insert(setups, s) 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
end end
@ -323,8 +420,8 @@ function output.setup_locs(update_locs_on, setup)
for _, setup in ipairs(setups) do for _, setup in ipairs(setups) do
for _, op in ipairs(outputs) do for _, op in ipairs(outputs) do
if op.name == setup[1] then if op.name == setup[1] then
if setup.loc and setup.loc.x and setup.loc.y then if type(setup.loc[1]) == "number" then
local loc = { x = setup.loc.x, y = setup.loc.y } local loc = { x = setup.loc[1], y = setup.loc[2] }
op:set_location(loc) op:set_location(loc)
table.insert(placed_outputs, op) table.insert(placed_outputs, op)
@ -350,17 +447,15 @@ function output.setup_locs(update_locs_on, setup)
end end
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 goto continue
end end
for _, loc in ipairs(setup.loc) do local relative_to_name = setup.loc[1]
local relative_to_name = loc[1] local alignment = setup.loc[2]
local alignment = loc[2] for _, placed_op in ipairs(placed_outputs) do
for _, placed_op in ipairs(placed_outputs) do if placed_op.name == relative_to_name then
if placed_op.name == relative_to_name then return op, placed_op, alignment
return op, placed_op, alignment
end
end end
end end

View file

@ -188,6 +188,7 @@ impl Output {
/// ///
/// ``` /// ```
/// use pinnacle_api::output::OutputSetup; /// use pinnacle_api::output::OutputSetup;
/// use pinnacle_api::output::OutputId;
/// ///
/// output.setup([ /// output.setup([
/// // Give all outputs tags 1 through 5 /// // 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) /// OutputSetup::new_with_matcher(|op| op.preferred_mode().unwrap().pixel_width == 2160)
/// .with_scale(2.0), /// .with_scale(2.0),
/// // Additionally give eDP-1 tags 6 and 7 /// // 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<Item = OutputSetup>) { pub fn setup(&self, setups: impl IntoIterator<Item = OutputSetup>) {

View file

@ -494,26 +494,23 @@ mod output {
test_api(|sender| { test_api(|sender| {
setup_lua! { |Pinnacle| setup_lua! { |Pinnacle|
Pinnacle.output.setup({ Pinnacle.output.setup({
{ ["1:*"] = {
function(_) tags = { "1", "2", "3" },
return true
end,
tag_names = { "1", "2", "3" },
}, },
{ ["2:*"] = {
function(op) filter = function(op)
return string.match(op.name, "Test") ~= nil return string.match(op.name, "Test") ~= nil
end, end,
tag_names = { "Test 4", "Test 5" }, tags = { "Test 4", "Test 5" },
}, },
{ ["Second"] = {
"Second",
scale = 2.0, scale = 2.0,
mode = { mode = {
pixel_width = 6900, pixel_width = 6900,
pixel_height = 420, pixel_height = 420,
refresh_rate_millihz = 69420, refresh_rate_millihz = 69420,
}, },
transform = "90",
}, },
}) })
} }
@ -562,6 +559,47 @@ mod output {
assert_eq!(second_mode.size.w, 6900); assert_eq!(second_mode.size.w, 6900);
assert_eq!(second_mode.size.h, 420); assert_eq!(second_mode.size.h, 420);
assert_eq!(second_mode.refresh, 69420); 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::<Vec<_>>())
};
assert_eq!(tags_for(&first_op), vec!["1", "2", "3", "A", "B"]);
}); });
}) })
} }
@ -572,9 +610,9 @@ mod output {
test_api(|sender| { test_api(|sender| {
setup_lua! { |Pinnacle| setup_lua! { |Pinnacle|
Pinnacle.output.setup_locs("all", { Pinnacle.output.setup_locs("all", {
{ "Pinnacle Window", loc = { x = 0, y = 0 } }, ["Pinnacle Window"] = { x = 0, y = 0 },
{ "First", loc = { "Second", "left_align_top" } }, ["First"] = { "Second", "left_align_top" },
{ "Second", loc = { "First", "right_align_top" } }, ["Second"] = { "First", "right_align_top" },
}) })
} }
@ -639,16 +677,11 @@ mod output {
test_api(|sender| { test_api(|sender| {
setup_lua! { |Pinnacle| setup_lua! { |Pinnacle|
Pinnacle.output.setup_locs("all", { Pinnacle.output.setup_locs("all", {
{ "Pinnacle Window", loc = { x = 0, y = 0 } }, ["Pinnacle Window"] = { 0, 0 },
{ "First", loc = { "Pinnacle Window", "bottom_align_left" } }, ["First"] = { "Pinnacle Window", "bottom_align_left" },
{ "Second", loc = { "First", "bottom_align_left" } }, ["Second"] = { "First", "bottom_align_left" },
{ ["4:Third"] = { "Second", "bottom_align_left" },
"Third", ["5:Third"] = { "First", "bottom_align_left" },
loc = {
{ "Second", "bottom_align_left" },
{ "First", "bottom_align_left" },
},
},
}) })
} }