From f6ad192c86b82bd4fb4fd4419a9f709d99737e37 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 16 Apr 2024 19:25:59 -0500 Subject: [PATCH] Finish Lua setup and setup_locs --- api/lua/examples/default/default_config.lua | 33 +- api/lua/pinnacle/output.lua | 348 +++++--- api/rust/src/output.rs | 2 - api/rust/src/signal.rs | 1 - tests/lua_api.rs | 846 +++++++++++--------- tests/rust_api.rs | 2 +- 6 files changed, 711 insertions(+), 521 deletions(-) diff --git a/api/lua/examples/default/default_config.lua b/api/lua/examples/default/default_config.lua index f474bf8..b77d417 100644 --- a/api/lua/examples/default/default_config.lua +++ b/api/lua/examples/default/default_config.lua @@ -76,12 +76,19 @@ require("pinnacle").setup(function(Pinnacle) end end) - -------------------- - -- Tags -- - -------------------- + ---------------------- + -- Tags and Outputs -- + ---------------------- local tag_names = { "1", "2", "3", "4", "5" } + -- Setup outputs. + -- + -- `Output.setup` allows you to declare things like mode, scale, and tags for outputs. + -- Here we give all outputs tags 1 through 5. + -- + -- Note that output matching functions currently don't infer the type of the parameter, + -- so you may need to add `---@param OutputHandle` above it. Output.setup({ { function(_) @@ -89,23 +96,13 @@ require("pinnacle").setup(function(Pinnacle) end, tag_names = tag_names, }, - { - "DP-2", - scale = 2, - }, - { - "Pinnacle Window", - scale = 0.5, - loc = { x = 300, y = 450 }, - }, }) - -- `connect_for_all` is useful for performing setup on every monitor you have. - -- Here, we add tags with names 1-5 and set tag 1 as active. - -- Output.connect_for_all(function(op) - -- local tags = Tag.add(op, tag_names) - -- tags[1]:set_active(true) - -- end) + -- If you want to declare output locations as well, you can use `Output.setup_locs`. + -- This will additionally allow you to recalculate output locations on signals like + -- output connect, disconnect, and resize. + -- + -- Read the admittedly scuffed docs for more. -- Tag keybinds for _, tag_name in ipairs(tag_names) do diff --git a/api/lua/pinnacle/output.lua b/api/lua/pinnacle/output.lua index f9ad34e..8c58065 100644 --- a/api/lua/pinnacle/output.lua +++ b/api/lua/pinnacle/output.lua @@ -166,7 +166,7 @@ function output.connect_for_all(callback) }) end ----@class OutputSetupArgs +---@class OutputSetup ---@field [1] (string | fun(output: OutputHandle): boolean) ---@field loc ({ x: integer, y: integer } | { [1]: (string | fun(output: OutputHandle): boolean), [2]: Alignment })? ---@field mode Mode? @@ -184,43 +184,132 @@ end ---Declaratively setup outputs. --- ---`Output.setup` allows you to specify output properties that will be applied immediately and ----on output connection. These include location, mode, scale, and tags. +---on output connection. These include mode, scale, tags, and more. --- ----Arguments will be applied from top to bottom. +---Setups will be applied from top to bottom. --- ----`loc` will not be applied to arguments with an output matching function. +---`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. --- ----@param setup OutputSetupArgs[] -function output.setup(setup) +---### Example +---```lua +---Output.setup({ +--- -- Give all outputs tags 1 through 5 +--- { +--- function(_) return true end, +--- tag_names = { "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 +--- end, +--- scale = 2.0, +--- }, +--- -- Additionally give eDP-1 tags 6 and 7 +--- { +--- "eDP-1", +--- tag_names = { "6", "7" }, +--- }, +---}) +---``` +--- +---@param setups OutputSetup[] +function output.setup(setups) ---@param op OutputHandle - local function apply_transformers(op) - for _, args in ipairs(setup) do - if output_matches(op, args[1]) then - if args.mode then - op:set_mode(args.mode.pixel_width, args.mode.pixel_height, args.mode.refresh_rate_millihz) + local function apply_setups(op) + for _, setup in ipairs(setups) do + if output_matches(op, setup[1]) then + if setup.mode then + op:set_mode(setup.mode.pixel_width, setup.mode.pixel_height, setup.mode.refresh_rate_millihz) end - if args.scale then - op:set_scale(args.scale) + if setup.scale then + op:set_scale(setup.scale) end - if args.tag_names then - local tags = require("pinnacle.tag").add(op, args.tag_names) - tags[1]:set_active(true) + if setup.tag_names then + require("pinnacle.tag").add(op, setup.tag_names) end end end + + local tags = op:tags() or {} + if tags[1] then + tags[1]:set_active(true) + end end - -- Apply mode, scale, and transforms first - local outputs = output.get_all() + output.connect_for_all(function(op) + apply_setups(op) + end) +end - for _, op in ipairs(outputs) do - apply_transformers(op) +---@alias OutputLoc +---| { x: integer, y: 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 +---| "disconnect" -- Update output locations on output disconnect +---| "resize" -- Update output locations on output resize + +---Setup locations for outputs. +--- +---This function lets you declare positions for outputs, either as a specific point in the global +---space or relative to another output. +--- +---`update_locs_on` specifies when output positions should be recomputed. It can be `"all"`, signaling you +---want positions to update on all of output connect, disconnect, and resize, or it can be a table +---containing `"connect"`, `"disconnect"`, and/or `"resize"`. +--- +---`setup` is an array of tables of the form `{ [1]: string, loc: OutputLoc }`, where `OutputLoc` is either +---the table `{ x: integer, y: integer }`, `{ [1]: string, [2]: Alignment }`, or an array of the latter table. +---See the examples for information. +--- +---### Example +---```lua +--- -- 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 } }, +--- -- Place HDMI-A-1 below it centered +--- { "HDMI-A-1", loc = { "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" }, +--- }, +--- }, +---}) +--- +--- -- Only relayout on output connect and resize +---Output.setup_locs({ "connect", "resize" }, { ... }) +---``` +--- +---@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 }[]) }[] + 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) + end end local function layout_outputs() local outputs = output.get_all() - ---@type table + ---@type OutputHandle[] local placed_outputs = {} local rightmost_output = { @@ -228,16 +317,15 @@ function output.setup(setup) x = nil, } - local relative_to_outputs_that_are_not_placed = {} - -- Place outputs with a specified location first - for _, args in ipairs(setup) do + ---@diagnostic disable-next-line: redefined-local + for _, setup in ipairs(setups) do for _, op in ipairs(outputs) do - if type(args[1]) == "string" and op.name == args[1] then - if args.loc and args.loc.x and args.loc.y then - local loc = { x = args.loc.x, y = args.loc.y } + 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 } op:set_location(loc) - placed_outputs[op.name] = loc + table.insert(placed_outputs, op) local props = op:props() if not rightmost_output.x or rightmost_output.x < props.x + props.logical_width then @@ -245,107 +333,73 @@ function output.setup(setup) rightmost_output.x = props.x + props.logical_width end end + break end end end - -- Place outputs with no specified location in a line to the rightmost - for _, op in ipairs(outputs) do - local args_contains_op = false - - for _, args in ipairs(setup) do - if type(args[1]) == "string" and op.name == args[1] then - args_contains_op = true - if not args.loc then - if not rightmost_output.output then - op:set_location({ x = 0, y = 0 }) - else - op:set_loc_adj_to(rightmost_output.output, "right_align_top") - end - - local props = op:props() - - local loc = { x = props.x, y = props.y } - rightmost_output.output = op - rightmost_output.x = props.x - - placed_outputs[op.name] = loc - - goto continue_outer - end - end - end - - -- No match, still lay it out - - if not args_contains_op and not placed_outputs[op.name] then - if not rightmost_output.output then - op:set_location({ x = 0, y = 0 }) - else - op:set_loc_adj_to(rightmost_output.output, "right_align_top") - end - - local props = op:props() - - local loc = { x = props.x, y = props.y } - rightmost_output.output = op - rightmost_output.x = props.x - - placed_outputs[op.name] = loc - end - - ::continue_outer:: - end - -- Place outputs that are relative to other outputs - for _, args in ipairs(setup) do - for _, op in ipairs(outputs) do - if type(args[1]) == "string" and op.name == args[1] then - if args.loc and args.loc[1] and args.loc[2] then - local matcher = args.loc[1] - local alignment = args.loc[2] - ---@type OutputHandle? - local relative_to = nil + local function next_output_with_relative_to() + ---@diagnostic disable-next-line: redefined-local + for _, setup in ipairs(setups) do + for _, op in ipairs(outputs) do + for _, placed_op in ipairs(placed_outputs) do + if placed_op.name == op.name then + goto continue + end + end - for _, op in ipairs(outputs) do - if output_matches(op, matcher) then - relative_to = op - break + if op.name ~= setup[1] or type(setup.loc[1]) ~= "table" 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 end - - if not relative_to then - table.insert(relative_to_outputs_that_are_not_placed, op) - goto continue - end - - if not placed_outputs[relative_to.name] then - -- The output it's relative to hasn't been placed yet; - -- Users must place outputs before ones being placed relative to them - table.insert(relative_to_outputs_that_are_not_placed, op) - goto continue - end - - op:set_loc_adj_to(relative_to, alignment) - - local props = op:props() - - local loc = { x = props.x, y = props.y } - - if not rightmost_output.output or rightmost_output.x < props.x + props.logical_width then - rightmost_output.output = op - rightmost_output.x = props.x + props.logical_width - end - - placed_outputs[op.name] = loc end + + goto continue_outer + + ::continue:: end - ::continue:: + ::continue_outer:: + end + + return nil, nil, nil + end + + while true do + local op, relative_to, alignment = next_output_with_relative_to() + if not op then + break + end + + ---@cast relative_to OutputHandle + ---@cast alignment Alignment + + op:set_loc_adj_to(relative_to, alignment) + table.insert(placed_outputs, op) + + local props = op:props() + if not rightmost_output.x or rightmost_output.x < props.x + props.logical_width then + rightmost_output.output = op + rightmost_output.x = props.x + props.logical_width end end -- Place still-not-placed outputs - for _, op in ipairs(relative_to_outputs_that_are_not_placed) do + for _, op in ipairs(outputs) do + for _, placed_op in ipairs(placed_outputs) do + if placed_op.name == op.name then + goto continue + end + end + if not rightmost_output.output then op:set_location({ x = 0, y = 0 }) else @@ -354,30 +408,62 @@ function output.setup(setup) local props = op:props() - local loc = { x = props.x, y = props.y } rightmost_output.output = op rightmost_output.x = props.x - placed_outputs[op.name] = loc + table.insert(placed_outputs, op) + + ::continue:: end end layout_outputs() - output.connect_signal({ - connect = function(op) - -- FIXME: This currently does not duplicate tags because the connect signal does not fire for previously connected - -- | outputs. However, this is unintended behavior, so fix this when you fix that. - apply_transformers(op) - layout_outputs() - end, - disconnect = function(_) - layout_outputs() - end, - resize = function(_, _, _) - layout_outputs() - end, - }) + local layout_on_connect = false + local layout_on_disconnect = false + local layout_on_resize = false + + if update_locs_on == "all" then + layout_on_connect = true + layout_on_disconnect = true + layout_on_resize = true + else + ---@cast update_locs_on UpdateLocsOn[] + + for _, update_on in ipairs(update_locs_on) do + if update_on == "connect" then + layout_on_connect = true + elseif update_on == "disconnect" then + layout_on_disconnect = true + elseif update_on == "resize" then + layout_on_resize = true + end + end + end + + if layout_on_connect then + -- FIXME: This currently does not duplicate tags because the connect signal does not fire for + -- | previously connected outputs. However, this is unintended behavior, so fix this when you fix that. + output.connect_signal({ + connect = function(_) + layout_outputs() + end, + }) + end + if layout_on_disconnect then + output.connect_signal({ + disconnect = function(_) + layout_outputs() + end, + }) + end + if layout_on_resize then + output.connect_signal({ + resize = function(_) + layout_outputs() + end, + }) + end end ---@type table diff --git a/api/rust/src/output.rs b/api/rust/src/output.rs index 41dfbd1..44c6930 100644 --- a/api/rust/src/output.rs +++ b/api/rust/src/output.rs @@ -369,9 +369,7 @@ impl Output { if update_locs_on.contains(UpdateLocsOn::CONNECT) { self.connect_signal(OutputSignal::Connect(Box::new(move |output| { - println!("GOT CONNECT SIGNAL FOR {}", output.name()); layout_outputs_clone2(); - println!("FINISHED CONNECT SIGNAL FOR {}", output.name()); }))); } diff --git a/api/rust/src/signal.rs b/api/rust/src/signal.rs index 319cb19..7081c4c 100644 --- a/api/rust/src/signal.rs +++ b/api/rust/src/signal.rs @@ -70,7 +70,6 @@ macro_rules! signals { callback_sender .send((self.current_id, callback)) - .map_err(|e| { println!("{e}"); e }) .expect("failed to send callback"); let handle = SignalHandle::new(self.current_id, remove_callback_sender); diff --git a/tests/lua_api.rs b/tests/lua_api.rs index 08948e5..5b5befc 100644 --- a/tests/lua_api.rs +++ b/tests/lua_api.rs @@ -5,7 +5,7 @@ use std::{ process::{Command, Stdio}, }; -use crate::common::{sleep_secs, test_api, with_state}; +use crate::common::{output_for_name, sleep_secs, test_api, with_state}; use pinnacle::state::WithState; use test_log::test; @@ -99,26 +99,186 @@ macro_rules! setup_lua { }; } -mod coverage { - use pinnacle::{ - tag::TagId, - window::{ - rules::{WindowRule, WindowRuleCondition}, - window_state::FullscreenOrMaximized, - }, - }; +use pinnacle::{ + tag::TagId, + window::{ + rules::{WindowRule, WindowRuleCondition}, + window_state::FullscreenOrMaximized, + }, +}; + +// Process + +mod process { use super::*; - // Process + #[tokio::main] + #[self::test] + async fn spawn() -> anyhow::Result<()> { + test_api(|sender| { + run_lua! { |Pinnacle| + Pinnacle.process.spawn("foot") + } - mod process { + sleep_secs(1); + with_state(&sender, |state| { + assert_eq!(state.windows.len(), 1); + assert_eq!(state.windows[0].class(), Some("foot".to_string())); + }); + }) + } + + #[tokio::main] + #[self::test] + async fn set_env() -> anyhow::Result<()> { + test_api(|sender| { + run_lua! { |Pinnacle| + Pinnacle.process.set_env("PROCESS_SET_ENV", "env value") + } + + sleep_secs(1); + + with_state(&sender, |_state| { + assert_eq!( + std::env::var("PROCESS_SET_ENV"), + Ok("env value".to_string()) + ); + }); + }) + } +} + +// Window + +mod window { + use super::*; + + #[tokio::main] + #[self::test] + async fn get_all() -> anyhow::Result<()> { + test_api(|_sender| { + run_lua! { |Pinnacle| + assert(#Pinnacle.window.get_all() == 0) + + for i = 1, 5 do + Pinnacle.process.spawn("foot") + end + } + + sleep_secs(1); + + run_lua! { |Pinnacle| + assert(#Pinnacle.window.get_all() == 5) + } + }) + } + + #[tokio::main] + #[self::test] + async fn get_focused() -> anyhow::Result<()> { + test_api(|_sender| { + run_lua! { |Pinnacle| + assert(not Pinnacle.window.get_focused()) + + Pinnacle.tag.add(Pinnacle.output.get_focused(), "1")[1]:set_active(true) + Pinnacle.process.spawn("foot") + } + + sleep_secs(1); + + run_lua! { |Pinnacle| + assert(Pinnacle.window.get_focused()) + } + }) + } + + #[tokio::main] + #[self::test] + async fn add_window_rule() -> anyhow::Result<()> { + test_api(|sender| { + run_lua! { |Pinnacle| + Pinnacle.tag.add(Pinnacle.output.get_focused(), "Tag Name") + Pinnacle.window.add_window_rule({ + cond = { classes = { "firefox" } }, + rule = { tags = { Pinnacle.tag.get("Tag Name") } }, + }) + } + + sleep_secs(1); + + with_state(&sender, |state| { + assert_eq!(state.config.window_rules.len(), 1); + assert_eq!( + state.config.window_rules[0], + ( + WindowRuleCondition { + class: Some(vec!["firefox".to_string()]), + ..Default::default() + }, + WindowRule { + tags: Some(vec![TagId(0)]), + ..Default::default() + } + ) + ); + }); + + run_lua! { |Pinnacle| + Pinnacle.tag.add(Pinnacle.output.get_focused(), "Tag Name 2") + Pinnacle.window.add_window_rule({ + cond = { + all = { + { + classes = { "steam" }, + tags = { + Pinnacle.tag.get("Tag Name"), + Pinnacle.tag.get("Tag Name 2"), + }, + } + } + }, + rule = { fullscreen_or_maximized = "fullscreen" }, + }) + } + + sleep_secs(1); + + with_state(&sender, |state| { + assert_eq!(state.config.window_rules.len(), 2); + assert_eq!( + state.config.window_rules[1], + ( + WindowRuleCondition { + cond_all: Some(vec![WindowRuleCondition { + class: Some(vec!["steam".to_string()]), + tag: Some(vec![TagId(0), TagId(1)]), + ..Default::default() + }]), + ..Default::default() + }, + WindowRule { + fullscreen_or_maximized: Some(FullscreenOrMaximized::Fullscreen), + ..Default::default() + } + ) + ); + }); + }) + } + + // TODO: window_begin_move + // TODO: window_begin_resize + + mod handle { use super::*; + // WindowHandle + #[tokio::main] #[self::test] - async fn spawn() -> anyhow::Result<()> { + async fn close() -> anyhow::Result<()> { test_api(|sender| { run_lua! { |Pinnacle| Pinnacle.process.spawn("foot") @@ -128,424 +288,374 @@ mod coverage { with_state(&sender, |state| { assert_eq!(state.windows.len(), 1); - assert_eq!(state.windows[0].class(), Some("foot".to_string())); + }); + + run_lua! { |Pinnacle| + Pinnacle.window.get_all()[1]:close() + } + + sleep_secs(1); + + with_state(&sender, |state| { + assert_eq!(state.windows.len(), 0); }); }) } #[tokio::main] #[self::test] - async fn set_env() -> anyhow::Result<()> { + async fn move_to_tag() -> anyhow::Result<()> { test_api(|sender| { run_lua! { |Pinnacle| - Pinnacle.process.set_env("PROCESS_SET_ENV", "env value") + local tags = Pinnacle.tag.add(Pinnacle.output.get_focused(), "1", "2", "3") + tags[1]:set_active(true) + tags[2]:set_active(true) + Pinnacle.process.spawn("foot") } sleep_secs(1); - with_state(&sender, |_state| { + with_state(&sender, |state| { assert_eq!( - std::env::var("PROCESS_SET_ENV"), - Ok("env value".to_string()) + state.windows[0].with_state(|st| st + .tags + .iter() + .map(|tag| tag.name()) + .collect::>()), + vec!["1", "2"] + ); + }); + + // Correct usage + run_lua! { |Pinnacle| + Pinnacle.window.get_all()[1]:move_to_tag(Pinnacle.tag.get("3")) + } + + sleep_secs(1); + + with_state(&sender, |state| { + assert_eq!( + state.windows[0].with_state(|st| st + .tags + .iter() + .map(|tag| tag.name()) + .collect::>()), + vec!["3"] + ); + }); + + // Move to the same tag + run_lua! { |Pinnacle| + Pinnacle.window.get_all()[1]:move_to_tag(Pinnacle.tag.get("3")) + } + + sleep_secs(1); + + with_state(&sender, |state| { + assert_eq!( + state.windows[0].with_state(|st| st + .tags + .iter() + .map(|tag| tag.name()) + .collect::>()), + vec!["3"] ); }); }) } } +} - // Window +mod tag { + use super::*; - mod window { + mod handle { use super::*; #[tokio::main] #[self::test] - async fn get_all() -> anyhow::Result<()> { + async fn props() -> anyhow::Result<()> { test_api(|_sender| { run_lua! { |Pinnacle| - assert(#Pinnacle.window.get_all() == 0) - - for i = 1, 5 do - Pinnacle.process.spawn("foot") - end + Pinnacle.output.connect_for_all(function(op) + local tags = Pinnacle.tag.add(op, "First", "Mungus", "Potato") + tags[1]:set_active(true) + tags[3]:set_active(true) + end) } sleep_secs(1); run_lua! { |Pinnacle| - assert(#Pinnacle.window.get_all() == 5) - } - }) - } - - #[tokio::main] - #[self::test] - async fn get_focused() -> anyhow::Result<()> { - test_api(|_sender| { - run_lua! { |Pinnacle| - assert(not Pinnacle.window.get_focused()) - - Pinnacle.tag.add(Pinnacle.output.get_focused(), "1")[1]:set_active(true) + Pinnacle.process.spawn("foot") Pinnacle.process.spawn("foot") } sleep_secs(1); run_lua! { |Pinnacle| - assert(Pinnacle.window.get_focused()) + local first_props = Pinnacle.tag.get("First"):props() + assert(first_props.active == true) + assert(first_props.name == "First") + assert(first_props.output.name == "Pinnacle Window") + assert(#first_props.windows == 2) + assert(first_props.windows[1]:class() == "foot") + assert(first_props.windows[2]:class() == "foot") + + local mungus_props = Pinnacle.tag.get("Mungus"):props() + assert(mungus_props.active == false) + assert(mungus_props.name == "Mungus") + assert(mungus_props.output.name == "Pinnacle Window") + assert(#mungus_props.windows == 0) + + local potato_props = Pinnacle.tag.get("Potato"):props() + assert(potato_props.active == true) + assert(potato_props.name == "Potato") + assert(potato_props.output.name == "Pinnacle Window") + assert(#potato_props.windows == 2) + assert(potato_props.windows[1]:class() == "foot") + assert(potato_props.windows[2]:class() == "foot") } }) } - - #[tokio::main] - #[self::test] - async fn add_window_rule() -> anyhow::Result<()> { - test_api(|sender| { - run_lua! { |Pinnacle| - Pinnacle.tag.add(Pinnacle.output.get_focused(), "Tag Name") - Pinnacle.window.add_window_rule({ - cond = { classes = { "firefox" } }, - rule = { tags = { Pinnacle.tag.get("Tag Name") } }, - }) - } - - sleep_secs(1); - - with_state(&sender, |state| { - assert_eq!(state.config.window_rules.len(), 1); - assert_eq!( - state.config.window_rules[0], - ( - WindowRuleCondition { - class: Some(vec!["firefox".to_string()]), - ..Default::default() - }, - WindowRule { - tags: Some(vec![TagId(0)]), - ..Default::default() - } - ) - ); - }); - - run_lua! { |Pinnacle| - Pinnacle.tag.add(Pinnacle.output.get_focused(), "Tag Name 2") - Pinnacle.window.add_window_rule({ - cond = { - all = { - { - classes = { "steam" }, - tags = { - Pinnacle.tag.get("Tag Name"), - Pinnacle.tag.get("Tag Name 2"), - }, - } - } - }, - rule = { fullscreen_or_maximized = "fullscreen" }, - }) - } - - sleep_secs(1); - - with_state(&sender, |state| { - assert_eq!(state.config.window_rules.len(), 2); - assert_eq!( - state.config.window_rules[1], - ( - WindowRuleCondition { - cond_all: Some(vec![WindowRuleCondition { - class: Some(vec!["steam".to_string()]), - tag: Some(vec![TagId(0), TagId(1)]), - ..Default::default() - }]), - ..Default::default() - }, - WindowRule { - fullscreen_or_maximized: Some(FullscreenOrMaximized::Fullscreen), - ..Default::default() - } - ) - ); - }); - }) - } - - // TODO: window_begin_move - // TODO: window_begin_resize - - mod handle { - use super::*; - - // WindowHandle - - #[tokio::main] - #[self::test] - async fn close() -> anyhow::Result<()> { - test_api(|sender| { - run_lua! { |Pinnacle| - Pinnacle.process.spawn("foot") - } - - sleep_secs(1); - - with_state(&sender, |state| { - assert_eq!(state.windows.len(), 1); - }); - - run_lua! { |Pinnacle| - Pinnacle.window.get_all()[1]:close() - } - - sleep_secs(1); - - with_state(&sender, |state| { - assert_eq!(state.windows.len(), 0); - }); - }) - } - - #[tokio::main] - #[self::test] - async fn move_to_tag() -> anyhow::Result<()> { - test_api(|sender| { - run_lua! { |Pinnacle| - local tags = Pinnacle.tag.add(Pinnacle.output.get_focused(), "1", "2", "3") - tags[1]:set_active(true) - tags[2]:set_active(true) - Pinnacle.process.spawn("foot") - } - - sleep_secs(1); - - with_state(&sender, |state| { - assert_eq!( - state.windows[0].with_state(|st| st - .tags - .iter() - .map(|tag| tag.name()) - .collect::>()), - vec!["1", "2"] - ); - }); - - // Correct usage - run_lua! { |Pinnacle| - Pinnacle.window.get_all()[1]:move_to_tag(Pinnacle.tag.get("3")) - } - - sleep_secs(1); - - with_state(&sender, |state| { - assert_eq!( - state.windows[0].with_state(|st| st - .tags - .iter() - .map(|tag| tag.name()) - .collect::>()), - vec!["3"] - ); - }); - - // Move to the same tag - run_lua! { |Pinnacle| - Pinnacle.window.get_all()[1]:move_to_tag(Pinnacle.tag.get("3")) - } - - sleep_secs(1); - - with_state(&sender, |state| { - assert_eq!( - state.windows[0].with_state(|st| st - .tags - .iter() - .map(|tag| tag.name()) - .collect::>()), - vec!["3"] - ); - }); - }) - } - } } +} - mod tag { - use super::*; +mod output { + use smithay::{output::Output, utils::Rectangle}; - mod handle { - use super::*; + use super::*; - #[tokio::main] - #[self::test] - async fn props() -> anyhow::Result<()> { - test_api(|_sender| { - run_lua! { |Pinnacle| - Pinnacle.output.connect_for_all(function(op) - local tags = Pinnacle.tag.add(op, "First", "Mungus", "Potato") - tags[1]:set_active(true) - tags[3]:set_active(true) - end) - } - - sleep_secs(1); - - run_lua! { |Pinnacle| - Pinnacle.process.spawn("foot") - Pinnacle.process.spawn("foot") - } - - sleep_secs(1); - - run_lua! { |Pinnacle| - local first_props = Pinnacle.tag.get("First"):props() - assert(first_props.active == true) - assert(first_props.name == "First") - assert(first_props.output.name == "Pinnacle Window") - assert(#first_props.windows == 2) - assert(first_props.windows[1]:class() == "foot") - assert(first_props.windows[2]:class() == "foot") - - local mungus_props = Pinnacle.tag.get("Mungus"):props() - assert(mungus_props.active == false) - assert(mungus_props.name == "Mungus") - assert(mungus_props.output.name == "Pinnacle Window") - assert(#mungus_props.windows == 0) - - local potato_props = Pinnacle.tag.get("Potato"):props() - assert(potato_props.active == true) - assert(potato_props.name == "Potato") - assert(potato_props.output.name == "Pinnacle Window") - assert(#potato_props.windows == 2) - assert(first_props.windows[1]:class() == "foot") - assert(first_props.windows[2]:class() == "foot") - } + #[tokio::main] + #[self::test] + async fn setup() -> anyhow::Result<()> { + test_api(|sender| { + setup_lua! { |Pinnacle| + Pinnacle.output.setup({ + { + function(_) + return true + end, + tag_names = { "1", "2", "3" }, + }, + { + function(op) + return string.match(op.name, "Test") ~= nil + end, + tag_names = { "Test 4", "Test 5" }, + }, + { + "Second", + scale = 2.0, + mode = { + pixel_width = 6900, + pixel_height = 420, + refresh_rate_millihz = 69420, + }, + }, }) } - } + + sleep_secs(1); + + with_state(&sender, |state| { + state.new_output("First", (300, 200).into()); + state.new_output("Second", (300, 200).into()); + state.new_output("Test Third", (300, 200).into()); + }); + + sleep_secs(1); + + with_state(&sender, |state| { + let original_op = output_for_name(state, "Pinnacle Window"); + let first_op = output_for_name(state, "First"); + let second_op = output_for_name(state, "Second"); + let test_third_op = output_for_name(state, "Test Third"); + + let tags_for = |output: &Output| { + output + .with_state(|state| state.tags.iter().map(|t| t.name()).collect::>()) + }; + + let focused_tags_for = |output: &Output| { + output.with_state(|state| { + state.focused_tags().map(|t| t.name()).collect::>() + }) + }; + + assert_eq!(tags_for(&original_op), vec!["1", "2", "3"]); + assert_eq!(tags_for(&first_op), vec!["1", "2", "3"]); + assert_eq!(tags_for(&second_op), vec!["1", "2", "3"]); + assert_eq!( + tags_for(&test_third_op), + vec!["1", "2", "3", "Test 4", "Test 5"] + ); + + assert_eq!(focused_tags_for(&original_op), vec!["1"]); + assert_eq!(focused_tags_for(&test_third_op), vec!["1"]); + + assert_eq!(second_op.current_scale().fractional_scale(), 2.0); + + let second_mode = second_op.current_mode().unwrap(); + assert_eq!(second_mode.size.w, 6900); + assert_eq!(second_mode.size.h, 420); + assert_eq!(second_mode.refresh, 69420); + }); + }) } - mod output { - use smithay::utils::Rectangle; + #[tokio::main] + #[self::test] + async fn setup_loc_with_cyclic_relative_locs_works() -> anyhow::Result<()> { + 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" } }, + }) + } - use super::*; + sleep_secs(1); - #[tokio::main] - #[self::test] - async fn setup() -> anyhow::Result<()> { - test_api(|sender| { - setup_lua! { |Pinnacle| - Pinnacle.output.setup({ - { - function(_) - return true - end, - tag_names = { "First", "Third", "Schmurd" }, + with_state(&sender, |state| { + state.new_output("First", (300, 200).into()); + }); + + sleep_secs(1); + + with_state(&sender, |state| { + let original_op = output_for_name(state, "Pinnacle Window"); + let first_op = output_for_name(state, "First"); + + let original_geo = state.space.output_geometry(&original_op).unwrap(); + let first_geo = state.space.output_geometry(&first_op).unwrap(); + + assert_eq!( + original_geo, + Rectangle::from_loc_and_size((0, 0), (1920, 1080)) + ); + assert_eq!( + first_geo, + Rectangle::from_loc_and_size((1920, 0), (300, 200)) + ); + + state.new_output("Second", (500, 500).into()); + }); + + sleep_secs(1); + + with_state(&sender, |state| { + let original_op = output_for_name(state, "Pinnacle Window"); + let first_op = output_for_name(state, "First"); + let second_op = output_for_name(state, "Second"); + + let original_geo = state.space.output_geometry(&original_op).unwrap(); + let first_geo = state.space.output_geometry(&first_op).unwrap(); + let second_geo = state.space.output_geometry(&second_op).unwrap(); + + assert_eq!( + original_geo, + Rectangle::from_loc_and_size((0, 0), (1920, 1080)) + ); + assert_eq!( + first_geo, + Rectangle::from_loc_and_size((1920, 0), (300, 200)) + ); + assert_eq!( + second_geo, + Rectangle::from_loc_and_size((1920 + 300, 0), (500, 500)) + ); + }); + }) + } + + #[tokio::main] + #[self::test] + async fn setup_loc_with_relative_locs_with_more_than_one_relative_works() -> anyhow::Result<()> + { + 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", - loc = { x = 300, y = 0 }, - }, - { - "Output 1", - loc = { "Pinnacle Window", "bottom_align_left" }, - } - }) - } + }, + }) + } - sleep_secs(1); + sleep_secs(1); - with_state(&sender, |state| { - state.new_output("Output 1", (960, 540).into()); - }); + with_state(&sender, |state| { + state.new_output("First", (300, 200).into()); + state.new_output("Second", (300, 700).into()); + state.new_output("Third", (300, 400).into()); + }); - sleep_secs(1); + sleep_secs(1); - with_state(&sender, |state| { - let original_op = state - .space - .outputs() - .find(|op| op.name() == "Pinnacle Window") - .unwrap(); - let output_1 = state - .space - .outputs() - .find(|op| op.name() == "Output 1") - .unwrap(); + with_state(&sender, |state| { + let original_op = output_for_name(state, "Pinnacle Window"); + let first_op = output_for_name(state, "First"); + let second_op = output_for_name(state, "Second"); + let third_op = output_for_name(state, "Third"); - let original_op_geo = state.space.output_geometry(original_op).unwrap(); - let output_1_geo = state.space.output_geometry(output_1).unwrap(); + let original_geo = state.space.output_geometry(&original_op).unwrap(); + let first_geo = state.space.output_geometry(&first_op).unwrap(); + let second_geo = state.space.output_geometry(&second_op).unwrap(); + let third_geo = state.space.output_geometry(&third_op).unwrap(); - assert_eq!( - original_op_geo, - Rectangle::from_loc_and_size((300, 0), (1920, 1080)) - ); + assert_eq!( + original_geo, + Rectangle::from_loc_and_size((0, 0), (1920, 1080)) + ); + assert_eq!( + first_geo, + Rectangle::from_loc_and_size((0, 1080), (300, 200)) + ); + assert_eq!( + second_geo, + Rectangle::from_loc_and_size((0, 1080 + 200), (300, 700)) + ); + assert_eq!( + third_geo, + Rectangle::from_loc_and_size((0, 1080 + 200 + 700), (300, 400)) + ); - assert_eq!( - output_1_geo, - Rectangle::from_loc_and_size((300, 1080), (960, 540)) - ); + state.remove_output(&second_op); + }); - assert_eq!( - output_1.with_state(|state| state - .tags - .iter() - .map(|tag| tag.name()) - .collect::>()), - vec!["First", "Third", "Schmurd"] - ); + sleep_secs(1); - state.remove_output(&original_op.clone()); - }); + with_state(&sender, |state| { + let original_op = output_for_name(state, "Pinnacle Window"); + let first_op = output_for_name(state, "First"); + let third_op = output_for_name(state, "Third"); - sleep_secs(1); + let original_geo = state.space.output_geometry(&original_op).unwrap(); + let first_geo = state.space.output_geometry(&first_op).unwrap(); + let third_geo = state.space.output_geometry(&third_op).unwrap(); - with_state(&sender, |state| { - let output_1 = state - .space - .outputs() - .find(|op| op.name() == "Output 1") - .unwrap(); - - let output_1_geo = state.space.output_geometry(output_1).unwrap(); - - assert_eq!( - output_1_geo, - Rectangle::from_loc_and_size((0, 0), (960, 540)) - ); - - state.new_output("Output 2", (300, 500).into()); - }); - - sleep_secs(1); - - with_state(&sender, |state| { - let output_1 = state - .space - .outputs() - .find(|op| op.name() == "Output 1") - .unwrap(); - - let output_2 = state - .space - .outputs() - .find(|op| op.name() == "Output 2") - .unwrap(); - - let output_1_geo = state.space.output_geometry(output_1).unwrap(); - let output_2_geo = state.space.output_geometry(output_2).unwrap(); - - assert_eq!( - output_2_geo, - Rectangle::from_loc_and_size((0, 0), (300, 500)) - ); - - assert_eq!( - output_1_geo, - Rectangle::from_loc_and_size((300, 0), (960, 540)) - ); - }); - }) - } + assert_eq!( + original_geo, + Rectangle::from_loc_and_size((0, 0), (1920, 1080)) + ); + assert_eq!( + first_geo, + Rectangle::from_loc_and_size((0, 1080), (300, 200)) + ); + assert_eq!( + third_geo, + Rectangle::from_loc_and_size((0, 1080 + 200), (300, 400)) + ); + }); + }) } } diff --git a/tests/rust_api.rs b/tests/rust_api.rs index 96e7de5..59acc55 100644 --- a/tests/rust_api.rs +++ b/tests/rust_api.rs @@ -99,7 +99,7 @@ mod output { #[tokio::main] #[self::test] - async fn setup_loc_with_cyclic_relative_locs_work() -> anyhow::Result<()> { + async fn setup_loc_with_cyclic_relative_locs_works() -> anyhow::Result<()> { test_api(|sender| { setup_rust(|api| { api.output.setup_locs(