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
---@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<string, OutputSetup>
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<string, OutputLoc>
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

View file

@ -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<Item = OutputSetup>) {

View file

@ -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::<Vec<_>>())
};
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" },
})
}