diff --git a/.github/workflows/ldoc.yml b/.github/workflows/ldoc.yml index 6a378cd..41ef286 100644 --- a/.github/workflows/ldoc.yml +++ b/.github/workflows/ldoc.yml @@ -7,12 +7,12 @@ on: branches: - main paths: - - "api/lua/doc/**" + - "api/lua/**" push: branches: - main paths: - - "api/lua/doc/**" + - "api/lua/**" env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} diff --git a/api/lua/example_config.lua b/api/lua/example_config.lua index c42d875..66619c4 100644 --- a/api/lua/example_config.lua +++ b/api/lua/example_config.lua @@ -90,7 +90,26 @@ require("pinnacle").setup(function(pinnacle) op:add_tags("1", "2", "3", "4", "5") -- Same as tag.add(op, "1", "2", "3", "4", "5") - tag.toggle({ "1", op }) + tag.toggle({ name = "1", output = op }) + + -- Window rules + -- Add your own window rules here. Below is an example. + -- + -- These currently need to be added inside of `connect_for_all` because + -- it only runs after the whole config is parsed, so any specified tags won't be available outside + -- of this function. This means that if you have multiple monitors, + -- these rules will be duplicated unless you write in some logic to prevent that. + -- + -- window.rules.add({ + -- cond = { class = "kitty" }, + -- rule = { size = { 300, 300 }, location = { 50, 50 } }, + -- }, { + -- cond = { + -- class = "XTerm", + -- tag = "4", + -- }, + -- rule = { size = { 500, 800 }, floating_or_tiled = "Floating" }, + -- }) end) ---@type Layout[] diff --git a/api/lua/input.lua b/api/lua/input.lua index aa9be54..c486eea 100644 --- a/api/lua/input.lua +++ b/api/lua/input.lua @@ -10,7 +10,7 @@ local input_module = { ---### Example --- ---```lua ------ Set `Super + Return` to open Alacritty +--- -- Set `Super + Return` to open Alacritty ---input.keybind({ "Super" }, input.keys.Return, function() --- process.spawn("Alacritty") ---end) diff --git a/api/lua/msg.lua b/api/lua/msg.lua index 3a02400..66138b5 100644 --- a/api/lua/msg.lua +++ b/api/lua/msg.lua @@ -13,6 +13,7 @@ ---@field ToggleFloating { window_id: WindowId }? ---@field ToggleFullscreen { window_id: WindowId }? ---@field ToggleMaximized { window_id: WindowId }? +---@field AddWindowRule { cond: _WindowRuleCondition, rule: _WindowRule }? -- ---@field Spawn { command: string[], callback_id: integer? }? ---@field Request Request? diff --git a/api/lua/output.lua b/api/lua/output.lua index 46e164b..600bba7 100644 --- a/api/lua/output.lua +++ b/api/lua/output.lua @@ -120,19 +120,19 @@ end --- ---### Examples ---```lua ------ Assuming DP-1 is 2560x1440 and DP-2 is 1920x1080... +--- -- Assuming DP-1 is 2560x1440 and DP-2 is 1920x1080... ---local dp1 = output.get_by_name("DP-1") ---local dp2 = output.get_by_name("DP-2") --- ------ Place DP-2 to the left of DP-1, top borders aligned +--- -- Place DP-2 to the left of DP-1, top borders aligned ---dp1:set_loc({ x = 1920, y = 0 }) ---dp2:set_loc({ x = 0, y = 0 }) --- ------ Do the same as above, with a different origin +--- -- Do the same as above, with a different origin ---dp1:set_loc({ x = 0, y = 0 }) ---dp2:set_loc({ x = -1920, y = 0 }) --- ------ Place DP-2 to the right of DP-1, bottom borders aligned +--- -- Place DP-2 to the right of DP-1, bottom borders aligned ---dp1:set_loc({ x = 0, y = 0 }) ---dp2:set_loc({ x = 2560, y = 1440 - 1080 }) ---``` @@ -650,19 +650,19 @@ end --- ---### Examples ---```lua ------ Assuming DP-1 is 2560x1440 and DP-2 is 1920x1080... +--- -- Assuming DP-1 is 2560x1440 and DP-2 is 1920x1080... ---local dp1 = output.get_by_name("DP-1") ---local dp2 = output.get_by_name("DP-2") --- ------ Place DP-2 to the left of DP-1, top borders aligned +--- -- Place DP-2 to the left of DP-1, top borders aligned ---output.set_loc(dp1, { x = 1920, y = 0 }) ---output.set_loc(dp2, { x = 0, y = 0 }) --- ------ Do the same as above, with a different origin +--- -- Do the same as above, with a different origin ---output.set_loc(dp1, { x = 0, y = 0 }) ---output.set_loc(dp2, { x = -1920, y = 0 }) --- ------ Place DP-2 to the right of DP-1, bottom borders aligned +--- -- Place DP-2 to the right of DP-1, bottom borders aligned ---output.set_loc(dp1, { x = 0, y = 0 }) ---output.set_loc(dp2, { x = 2560, y = 1440 - 1080 }) ---``` diff --git a/api/lua/process.lua b/api/lua/process.lua index 5c5407d..f63b353 100644 --- a/api/lua/process.lua +++ b/api/lua/process.lua @@ -8,6 +8,7 @@ local process_module = {} ---Spawn a process with an optional callback for its stdout, stderr, and exit information. --- ---`callback` has the following parameters: +--- --- - `stdout` - The process's stdout printed this line. --- - `stderr` - The process's stderr printed this line. --- - `exit_code` - The process exited with this code. @@ -47,6 +48,7 @@ end ---Spawn a process only if it isn't already running, with an optional callback for its stdout, stderr, and exit information. --- ---`callback` has the following parameters: +--- --- - `stdout`: The process's stdout printed this line. --- - `stderr`: The process's stderr printed this line. --- - `exit_code`: The process exited with this code. diff --git a/api/lua/tag.lua b/api/lua/tag.lua index d1dde96..a7fdb45 100644 --- a/api/lua/tag.lua +++ b/api/lua/tag.lua @@ -8,11 +8,12 @@ ---traditional workspaces cannot. --- ---More specifically: +--- --- - A window can have multiple tags. ---- - This means that you can have one window show up across multiple "workspaces" if you come +--- - This means that you can have one window show up across multiple "workspaces" if you come --- something like i3. --- - An output can display multiple tags at once. ---- - This allows you to toggle a tag and have windows on both tags display at once. +--- - This allows you to toggle a tag and have windows on both tags display at once. --- This is helpful if you, say, want to reference a browser window while coding; you toggle your --- browser's tag and temporarily reference it while you work without having to change screens. --- @@ -21,12 +22,13 @@ ---something with tags. --- ---Instead, you can pass in either: +--- --- - A string of the tag's name (ex. "1") ---- - This will get the first tag with that name on the focused output. +--- - This will get the first tag with that name on the focused output. --- - A table where [1] is the name and [2] is the output (or its name) (ex. { "1", output.get_by_name("DP-1") }) ---- - This will get the first tag with that name on the specified output. +--- - This will get the first tag with that name on the specified output. --- - The same table as above, but keyed with `name` and `output` (ex. { name = "1", output = "DP-1" }) ---- - This is simply for those who want more clarity in their config. +--- - This is simply for those who want more clarity in their config. --- ---If you need to get tags beyond the first with the same name, use a `get` function and find what you need. ---@class TagModule @@ -41,8 +43,9 @@ local tag_module = {} ---| "CornerBottomLeft" # One main corner window in the bottom left with a column of windows on the right and a row on the top. ---| "CornerBottomRight" # One main corner window in the bottom right with a column of windows on the left and a row on the top. ----@alias TagTable { [1]: string, [2]: (string|Output)? } ----@alias TagTableNamed { name: string, output: (string|Output)? } +---@alias TagTable { name: string, output: (string|Output)? } + +---@alias TagConstructor Tag|TagTable|string ---A tag object. --- @@ -52,92 +55,6 @@ local tag_module = {} ---@field private _id integer The internal id of this tag. local tag = {} ----@nodoc ----***You probably don't need to use this function.*** ---- ----Create a tag from `Tag|TagTable|TagTableNamed|string`. ----@param tb Tag|TagTable|TagTableNamed|string ----@return Tag|nil -function tag_module.create_tag_from_params(tb) - -- If creating from a tag object, just return the obj - if tb.id then - return tb --[[@as Tag]] - end - - -- string passed in - if type(tb) == "string" then - local op = require("output").get_focused() - if op == nil then - return nil - end - - local tags = tag_module.get_by_name(tb) - for _, t in pairs(tags) do - if t:output() and t:output():name() == op:name() then - return t - end - end - - return nil - end - - -- TagTable was passed in - local tag_name = tb[1] - if type(tag_name) == "string" then - local op = tb[2] - if op == nil then - local o = require("output").get_focused() - if o == nil then - return nil - end - op = o - elseif type(op) == "string" then - local o = require("output").get_by_name(op) - if o == nil then - return nil - end - op = o - end - - local tags = tag_module.get_by_name(tag_name) - for _, t in pairs(tags) do - if t:output() and t:output():name() == op:name() then - return t - end - end - - return nil - end - - -- TagTableNamed was passed in - local tb = tb --[[@as TagTableNamed]] - local tag_name = tb.name - local op = tb.output - - if op == nil then - local o = require("output").get_focused() - if o == nil then - return nil - end - op = o - elseif type(op) == "string" then - local o = require("output").get_by_name(op) - if o == nil then - return nil - end - op = o - end - - local tags = tag_module.get_by_name(tag_name) - for _, t in pairs(tags) do - if t:output() and t:output():name() == op:name() then - return t - end - end - - return nil -end - ---Create a tag from an id. ---The id is the unique identifier for each tag. ---@param id TagId @@ -210,11 +127,10 @@ end ---if op ~= nil then --- tag.add(op, "1", "2", "3", "4", "5") -- Add tags with names 1-5 ---end ----``` ----You can also pass in a table. ----```lua +-- +--- -- You can also pass in a table. ---local tags = {"Terminal", "Browser", "Code", "Potato", "Email"} ----tag.add(op, tags) -- Add tags with those names +---tag.add(op, tags) ---``` ---@param output Output The output you want these tags to be added to. ---@param ... string The names of the new tags you want to add. @@ -257,18 +173,18 @@ end ---tag.toggle({ "1", "DP-1" }) -- Toggle tag 1 on DP-1 ---tag.toggle({ "1", op }) -- Same as above --- ------ Verbose versions of the two above +--- -- Verbose versions of the two above ---tag.toggle({ name = "1", output = "DP-1" }) ---tag.toggle({ name = "1", output = op }) --- ------ Using a tag object +--- -- Using a tag object ---local t = tag.get_by_name("1")[1] -- `t` is the first tag with the name "1" ---tag.toggle(t) ---``` ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@see Tag.toggle — The corresponding object method function tag_module.toggle(t) - local t = tag_module.create_tag_from_params(t) + local t = tag_module.get(t) if t then SendMsg({ @@ -294,18 +210,18 @@ end ---tag.switch_to({ "1", "DP-1" }) -- Switch to tag 1 on DP-1 ---tag.switch_to({ "1", op }) -- Same as above --- ------ Verbose versions of the two above +--- -- Verbose versions of the two above ---tag.switch_to({ name = "1", output = "DP-1" }) ---tag.switch_to({ name = "1", output = op }) --- ------ Using a tag object +--- -- Using a tag object ---local t = tag.get_by_name("1")[1] -- `t` is the first tag with the name "1" ---tag.switch_to(t) ---``` ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@see Tag.switch_to — The corresponding object method function tag_module.switch_to(t) - local t = tag_module.create_tag_from_params(t) + local t = tag_module.get(t) if t then SendMsg({ @@ -323,25 +239,20 @@ end ---local op = output.get_by_name("DP-1") --- ---tag.set_layout("1", "Dwindle") -- Set tag 1 on the focused output to "Dwindle" ----tag.set_layout({ "1" }, "Dwindle") -- Same as above --- ----tag.set_layout({ "1", "DP-1" }, "Dwindle") -- Set tag 1 on DP-1 to "Dwindle" ----tag.set_layout({ "1", op }, "Dwindle") -- Same as above +---tag.set_layout({ name = "1", output = "DP-1" }, "Dwindle") -- Set tag 1 on "DP-1" to "Dwindle" +---tag.set_layout({ name = "1", output = op }, "Dwindle") -- Same as above --- ------ Verbose versions of the two above ----tag.set_layout({ name = "1", output = "DP-1" }, "Dwindle") ----tag.set_layout({ name = "1", output = op }, "Dwindle") ---- ------ Using a tag object +--- -- Using a tag object ---local t = tag.get_by_name("1")[1] -- `t` is the first tag with the name "1" ---tag.set_layout(t, "Dwindle") ---``` --- ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@param layout Layout The layout. ---@see Tag.set_layout — The corresponding object method function tag_module.set_layout(t, layout) - local t = tag_module.create_tag_from_params(t) + local t = tag_module.get(t) if t then SendMsg({ @@ -364,22 +275,70 @@ end ---### Examples ---```lua ---local t = tag.get("1") ----local t = tag.get({ "1", "HDMI-A-0" }) ---local t = tag.get({ name = "3" }) +---local t = tag.get({ name = "1", output = "HDMI-A-0" }) --- ---local op = output.get_by_name("DP-2") ---if op ~= nil then --- local t = tag.get({ name = "Code", output = op }) ---end ---``` ----@param params TagTable|TagTableNamed|string +---@param params TagConstructor ---@return Tag|nil --- ---@see TagModule.get_on_output ---@see TagModule.get_by_name ---@see TagModule.get_all function tag_module.get(params) - return tag_module.create_tag_from_params(params) + -- If creating from a tag object, just return the obj + if params.id then + return params --[[@as Tag]] + end + + -- string passed in + if type(params) == "string" then + local op = require("output").get_focused() + if op == nil then + return nil + end + + local tags = tag_module.get_by_name(params) + for _, t in pairs(tags) do + if t:output() and t:output():name() == op:name() then + return t + end + end + + return nil + end + + -- TagTable was passed in + local params = params --[[@as TagTable]] + local tag_name = params.name + local op = params.output + + if op == nil then + local o = require("output").get_focused() + if o == nil then + return nil + end + op = o + elseif type(op) == "string" then + local o = require("output").get_by_name(op) + if o == nil then + return nil + end + op = o + end + + local tags = tag_module.get_by_name(tag_name) + for _, t in pairs(tags) do + if t:output() and t:output():name() == op:name() then + return t + end + end + + return nil end ---Get all tags on the specified output. @@ -422,11 +381,11 @@ end --- ---### Example ---```lua ------ Given one monitor with the tags "OBS", "OBS", "VSCode", and "Spotify"... +--- -- Given one monitor with the tags "OBS", "OBS", "VSCode", and "Spotify"... ---local tags = tag.get_by_name("OBS") ------ ...will have 2 tags in `tags`, while... +--- -- ...will have 2 tags in `tags`, while... ---local no_tags = tag.get_by_name("Firefox") ------ ...will have `no_tags` be empty. +--- -- ...will have `no_tags` be empty. ---``` ---@param name string The name of the tag(s) you want. ---@return Tag[] @@ -449,9 +408,9 @@ end --- ---### Example ---```lua ------ With two monitors with the same tags: "1", "2", "3", "4", and "5"... +--- -- With two monitors with the same tags: "1", "2", "3", "4", and "5"... ---local tags = tag.get_all() ------ ...`tags` should have 10 tags, with 5 pairs of those names across both outputs. +--- -- ...`tags` should have 10 tags, with 5 pairs of those names across both outputs. ---``` ---@return Tag[] function tag_module.get_all() @@ -473,9 +432,9 @@ end --- ---### Example ---```lua ------ Assuming the tag `Terminal` exists... +--- -- Assuming the tag `Terminal` exists... ---print(tag.name(tag.get_by_name("Terminal")[1])) ------ ...should print `Terminal`. +--- -- ...should print `Terminal`. ---``` ---@param t Tag ---@return string|nil diff --git a/api/lua/test_config.lua b/api/lua/test_config.lua index 407dbeb..cd59682 100644 --- a/api/lua/test_config.lua +++ b/api/lua/test_config.lua @@ -27,10 +27,21 @@ require("pinnacle").setup(function(pinnacle) local terminal = "alacritty" + -- Outputs ----------------------------------------------------------------------- + + -- You can set your own monitor layout as I have done below for my monitors. + + -- local lg = output.get_by_name("DP-2") --[[@as Output]] + -- local dell = output.get_by_name("DP-3") --[[@as Output]] + -- + -- dell:set_loc_left_of(lg, "bottom") + -- Keybinds ---------------------------------------------------------------------- + -- mod_key + Alt + q quits the compositor input.keybind({ mod_key, "Alt" }, keys.q, pinnacle.quit) + -- mod_key + Alt + c closes the focused window input.keybind({ mod_key, "Alt" }, keys.c, function() -- The commented out line may crash the config process if you have no windows open. -- There is no nil warning here due to limitations in Lua LS type checking, so check for nil as shown below. @@ -41,6 +52,14 @@ require("pinnacle").setup(function(pinnacle) end end) + -- mod_key + return spawns a terminal + input.keybind({ mod_key }, keys.Return, function() + process.spawn(terminal, function(stdout, stderr, exit_code, exit_msg) + -- do something with the output here + end) + end) + + -- mod_key + Alt + Space toggle floating on the focused window input.keybind({ mod_key, "Alt" }, keys.space, function() local win = window.get_focused() if win ~= nil then @@ -48,138 +67,30 @@ require("pinnacle").setup(function(pinnacle) end end) - input.keybind({ mod_key }, keys.Return, function() - process.spawn(terminal, function(stdout, stderr, exit_code, exit_msg) - -- do something with the output here - end) - end) - - input.keybind({ mod_key }, keys.l, function() - process.spawn("kitty") - end) - input.keybind({ mod_key }, keys.k, function() - process.spawn("foot") - end) - input.keybind({ mod_key }, keys.j, function() - process.spawn("nautilus") - end) - + -- mod_key + f toggles fullscreen on the focused window input.keybind({ mod_key }, keys.f, function() local win = window.get_focused() if win ~= nil then - win:set_status("Fullscreen") + win:toggle_fullscreen() end end) + -- mod_key + m toggles maximized on the focused window input.keybind({ mod_key }, keys.m, function() local win = window.get_focused() if win ~= nil then - win:set_status("Maximized") + win:toggle_maximized() end end) - input.keybind({ mod_key }, keys.t, function() - local win = window.get_focused() - if win ~= nil then - win:set_status("Tiled") - end - end) - - -- Just testing stuff - input.keybind({ mod_key }, keys.h, function() - local dp2 = output.get_by_name("DP-2") - local dp3 = output.get_by_name("DP-3") - - dp2:set_loc_bottom_of(dp3, "right") - - -- local win = window.get_focused() - -- if win ~= nil then - -- win:set_size({ w = 500, h = 500 }) - -- end - - -- local wins = window.get_all() - -- for _, win in pairs(wins) do - -- print("loc: " .. (win:loc() and win:loc().x or "nil") .. ", " .. (win:loc() and win:loc().y or "nil")) - -- print("size: " .. (win:size() and win:size().w or "nil") .. ", " .. (win:size() and win:size().h or "nil")) - -- print("class: " .. (win:class() or "nil")) - -- print("title: " .. (win:title() or "nil")) - -- print("float: " .. tostring(win:floating())) - -- end - -- - -- print("----------------------") - -- - -- local op = output.get_focused() --[[@as Output]] - -- print("res: " .. (op:res() and (op:res().w .. ", " .. op:res().h) or "nil")) - -- print("loc: " .. (op:loc() and (op:loc().x .. ", " .. op:loc().y) or "nil")) - -- print("rr: " .. (op:refresh_rate() or "nil")) - -- print("make: " .. (op:make() or "nil")) - -- print("model: " .. (op:model() or "nil")) - -- print("focused: " .. (tostring(op:focused()))) - -- - -- print("----------------------") - -- - -- local wins = window.get_by_class("Alacritty") - -- for _, win in pairs(wins) do - -- print("loc: " .. (win:loc() and win:loc().x or "nil") .. ", " .. (win:loc() and win:loc().y or "nil")) - -- print("size: " .. (win:size() and win:size().w or "nil") .. ", " .. (win:size() and win:size().h or "nil")) - -- print("class: " .. (win:class() or "nil")) - -- print("title: " .. (win:title() or "nil")) - -- print("float: " .. tostring(win:floating())) - -- end - -- - -- print("----------------------") - -- - -- local wins = window.get_by_title("~/p/pinnacle") - -- for _, win in pairs(wins) do - -- print("loc: " .. (win:loc() and win:loc().x or "nil") .. ", " .. (win:loc() and win:loc().y or "nil")) - -- print("size: " .. (win:size() and win:size().w or "nil") .. ", " .. (win:size() and win:size().h or "nil")) - -- print("class: " .. (win:class() or "nil")) - -- print("title: " .. (win:title() or "nil")) - -- print("float: " .. tostring(win:floating())) - -- end - -- - -- print("----------------------") - -- - -- local tags = tag.get_on_output(output.get_focused() --[[@as Output]]) - -- for _, tg in pairs(tags) do - -- print(tg:name()) - -- print((tg:output() and tg:output():name()) or "nil output") - -- print(tg:active()) - -- end - -- - -- print("----------------------") - -- - -- local tags = tag.get_by_name("2") - -- for _, tg in pairs(tags) do - -- print(tg:name()) - -- print((tg:output() and tg:output():name()) or "nil output") - -- print(tg:active()) - -- end - -- - -- print("----------------------") - -- - -- local tags = tag.get_all() - -- for _, tg in pairs(tags) do - -- print(tg:name()) - -- print((tg:output() and tg:output():name()) or "nil output") - -- print(tg:active()) - -- end - end) - -- Tags --------------------------------------------------------------------------- output.connect_for_all(function(op) + -- Add tags 1, 2, 3, 4 and 5 on all monitors, and toggle tag 1 active by default + op:add_tags("1", "2", "3", "4", "5") -- Same as tag.add(op, "1", "2", "3", "4", "5") - - -- local tags_table = { "Terminal", "Browser", "Code", "Email", "Potato" } - -- op:add_tags(tags_table) - - -- for _, t in pairs(tag.get_by_name("1")) do - -- t:toggle() - -- end - - tag.toggle("1", op) + tag.toggle({ "1", op }) end) ---@type Layout[] @@ -194,6 +105,12 @@ require("pinnacle").setup(function(pinnacle) } local indices = {} + -- Window rules + window.rules.add({ + cond = { class = "kitty" }, + rule = { floating_or_tiled = "Floating" }, + }) + -- Layout cycling -- Yes, this is overly complicated and yes, I'll cook up a way to make it less so. input.keybind({ mod_key }, keys.space, function() diff --git a/api/lua/window.lua b/api/lua/window.lua index 7b7bd9d..b0e5669 100644 --- a/api/lua/window.lua +++ b/api/lua/window.lua @@ -5,7 +5,10 @@ ---This module helps you deal with setting windows to fullscreen and maximized, setting their size, ---moving them between tags, and various other actions. ---@class WindowModule -local window_module = {} +local window_module = { + ---Window rules. + rules = require("window_rules"), +} ---A window object. --- @@ -52,7 +55,7 @@ end --- ---See `WindowModule.move_to_tag` for examples. --- ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@see WindowModule.move_to_tag — The corresponding module function function window:move_to_tag(t) window_module.move_to_tag(self, t) @@ -63,7 +66,7 @@ end ---Note: toggling off all tags currently makes a window not respond to layouting. --- ---See `WindowModule.toggle_tag` for examples. ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@see WindowModule.toggle_tag — The corresponding module function function window:toggle_tag(t) window_module.toggle_tag(self, t) @@ -251,10 +254,10 @@ end ---Toggle the tag with the given name and (optional) output for the specified window. --- ---@param w Window ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@see Window.toggle_tag — The corresponding object method function window_module.toggle_tag(w, t) - local t = require("tag").create_tag_from_params(t) + local t = require("tag").get(t) if t then SendMsg({ @@ -269,10 +272,10 @@ end ---Move the specified window to the tag with the given name and (optional) output. --- ---@param w Window ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@see Window.move_to_tag — The corresponding object method function window_module.move_to_tag(w, t) - local t = require("tag").create_tag_from_params(t) + local t = require("tag").get(t) if t then SendMsg({ @@ -379,9 +382,9 @@ end --- ---### Example ---```lua ------ With a 4K monitor, given a focused fullscreen window `win`... +--- -- With a 4K monitor, given a focused fullscreen window `win`... ---local size = window.size(win) ------ ...should have size equal to `{ w = 3840, h = 2160 }`. +--- -- ...should have size equal to `{ w = 3840, h = 2160 }`. ---``` ---@param win Window ---@return { w: integer, h: integer }|nil size The size of the window, or nil if it doesn't exist. @@ -413,10 +416,10 @@ end --- ---### Example ---```lua ------ With two 1080p monitors side by side and set up as such, ------ if a window `win` is fullscreen on the right one... +--- -- With two 1080p monitors side by side and set up as such, +--- -- if a window `win` is fullscreen on the right one... ---local loc = window.loc(win) ------ ...should have loc equal to `{ x = 1920, y = 0 }`. +--- -- ...should have loc equal to `{ x = 1920, y = 0 }`. ---``` ---@param win Window ---@return { x: integer, y: integer }|nil loc The location of the window, or nil if it's not on-screen or alive. @@ -442,12 +445,12 @@ end --- ---### Example ---```lua ------ With Alacritty focused... +--- -- With Alacritty focused... ---local win = window.get_focused() ---if win ~= nil then --- print(window.class(win)) ---end ------ ...should print "Alacritty". +--- -- ...should print "Alacritty". ---``` ---@param win Window ---@return string|nil class This window's class, or nil if it doesn't exist. @@ -466,12 +469,12 @@ end --- ---### Example ---```lua ------ With Alacritty focused... +--- -- With Alacritty focused... ---local win = window.get_focused() ---if win ~= nil then --- print(window.title(win)) ---end ------ ...should print the directory Alacritty is in or what it's running (what's in its title bar). +--- -- ...should print the directory Alacritty is in or what it's running (what's in its title bar). ---``` ---@param win Window ---@return string|nil title This window's title, or nil if it doesn't exist. diff --git a/api/lua/window_rules.lua b/api/lua/window_rules.lua new file mode 100644 index 0000000..c3b0501 --- /dev/null +++ b/api/lua/window_rules.lua @@ -0,0 +1,238 @@ +---Rules that apply to spawned windows when conditions are met. +---@class WindowRules +local window_rules = {} + +---Convert all tag constructors in `cond` to tag ids for serialization. +---@param cond WindowRuleCondition +---@return _WindowRuleCondition +local function convert_tag_params(cond) + if cond.tag then + local tags = {} + + if type(cond.tag) == "table" then + if cond.tag.name or cond.tag.output then + -- Tag constructor + local tag = require("tag").get(cond.tag) + if tag then + table.insert(tags, tag:id()) + end + else + -- Array of tag constructors + ---@diagnostic disable-next-line + for _, t in pairs(cond.tag) do + local tag = require("tag").get(t) + if tag then + table.insert(tags, tag:id()) + end + end + end + else + -- Tag constructor + local tag = require("tag").get(cond.tag) + if tag then + table.insert(tags, tag:id()) + end + end + + cond.tag = tags + end + + if cond.cond_any then + local conds = {} + if type(cond.cond_any[1]) == "table" then + -- Array of conds + for _, c in pairs(cond.cond_any) do + table.insert(conds, convert_tag_params(c)) + end + else + -- Single cond + table.insert(conds, convert_tag_params(cond.cond_any)) + end + cond.cond_any = conds + end + + if cond.cond_all then + local conds = {} + if type(cond.cond_all[1]) == "table" then + -- Array of conds + for _, c in pairs(cond.cond_all) do + table.insert(conds, convert_tag_params(c)) + end + else + -- Single cond + table.insert(conds, convert_tag_params(cond.cond_all)) + end + cond.cond_all = conds + end + + return cond --[[@as _WindowRuleCondition]] +end + +---These attributes need to be arrays, so this function converts single values into arrays. +---@param cond WindowRuleCondition +---@return WindowRuleCondition +local function convert_single_attrs(cond) + if type(cond.class) == "string" then + -- stylua: ignore start + cond.class = { cond.class --[[@as string]] } + -- stylua: ignore end + end + + if type(cond.title) == "string" then + -- stylua: ignore start + cond.title = { cond.title --[[@as string]] } + -- stylua: ignore end + end + + if cond.cond_any then + local conds = {} + if type(cond.cond_any[1]) == "table" then + -- Array of conds + for _, c in pairs(cond.cond_any) do + table.insert(conds, convert_single_attrs(c)) + end + else + -- Single cond + table.insert(conds, convert_single_attrs(cond.cond_any)) + end + cond.cond_any = conds + end + + if cond.cond_all then + local conds = {} + if type(cond.cond_all[1]) == "table" then + -- Array of conds + for _, c in pairs(cond.cond_all) do + table.insert(conds, convert_single_attrs(c)) + end + else + -- Single cond + table.insert(conds, convert_single_attrs(cond.cond_all)) + end + cond.cond_all = conds + end + + return cond +end + +---Add one or more window rules. +--- +---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 function 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 a `cond_all` block must be true for the block to be true. +--- - At least one child inside a `cond_any` block must be true for the block to be true. +--- - The outermost block of a window rule condition is implicitly a `cond_all` block. +--- - All condition attributes (`tag`, `title`, `class`, etc.) can either be a single value or an array. +--- This includes `cond_all` and `cond_any`. +--- - Within a `cond_all` block, any arrays must have all items be true for the attribute to be true. +--- - Within a `cond_any` block, any arrays only need one item 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.rules.add({ +--- cond = { class = "firefox" }, +--- rule = { tags = { "Browser" } }, +---}) +--- +--- -- To apply rules when *all* provided conditions are true, use `cond_all`. +--- -- `cond_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.rules.add({ +--- cond = { +--- cond_all = { +--- class = "steam", +--- tag = tag.get("5"), +--- } +--- }, +--- rule = { fullscreen_or_maximized = "Fullscreen" }, +---}) +--- +--- -- The outermost block of a `cond` is implicitly a `cond_all`. +--- -- Thus, the above can be shortened to: +---window.rules.add({ +--- cond = { +--- class = "steam", +--- tag = tag.get("5"), +--- }, +--- rule = { fullscreen_or_maximized = "Fullscreen" }, +---}) +--- +--- -- `cond_any` also exists to allow at least one provided condition to match. +--- -- The following will open either xterm or Alacritty floating. +---window.rules.add({ +--- cond = { +--- cond_any = { class = { "xterm", "Alacritty" } } +--- }, +--- rule = { floating_or_tiled = "Floating" } +---}) +--- +--- -- You can arbitrarily nest `cond_any` and `cond_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.rules.add({ +--- cond = { cond_all = { -- This outer `cond_all` block is unnecessary, but it's here for clarity. +--- { cond_any = { class = { "firefox", "thunderbird", "discord" } } }, +--- { cond_any = { +--- -- Because `tag` is inside a `cond_all` block, +--- -- the window must have all these tags for this to be true. +--- -- If it was in a `cond_any` block, only one tag would need to match. +--- { cond_all = { tag = { "A", "B", "C" } } }, +--- { cond_all = { tag = { "1", "2" } } }, +--- } } +--- } }, +--- rule = { floating_or_tiled = "Floating" }, +---}) +---``` +---@param ... { cond: WindowRuleCondition, rule: WindowRule } +function window_rules.add(...) + local rules = { ... } + + for _, rule in pairs(rules) do + rule.cond = convert_single_attrs(rule.cond) + + ---@diagnostic disable-next-line + rule.cond = convert_tag_params(rule.cond) + + if rule.rule.tags then + local tags = {} + for _, tag in pairs(rule.rule.tags) do + local t = require("tag").get(tag) + if t then + ---@diagnostic disable-next-line + t = t:id() + end + table.insert(tags, t) + end + rule.rule.tags = tags + end + + if rule.rule.output and type(rule.rule.output) == "table" then + rule.rule.output = rule + .rule + .output--[[@as Output]] + :name() + end + + SendMsg({ + AddWindowRule = { + -- stylua: ignore start + cond = rule.cond --[[@as _WindowRuleCondition]], + rule = rule.rule --[[@as _WindowRule]], + -- stylua: ignore end + }, + }) + end +end + +return window_rules diff --git a/api/lua/window_rules_types.lua b/api/lua/window_rules_types.lua new file mode 100644 index 0000000..45b3137 --- /dev/null +++ b/api/lua/window_rules_types.lua @@ -0,0 +1,39 @@ +-- SPDX-License-Identifier: GPL-3.0-or-later + +---@meta _ + +---Conditions for window rules. Only one condition can be in the table. +---If you have more than one you need to check for, use `cond_any` or `cond_all` +---to check for any or all conditions. +---@class _WindowRuleCondition +---@field cond_any _WindowRuleCondition[]? At least one provided condition must be true. +---@field cond_all _WindowRuleCondition[]? All provided conditions must be true. +---@field class string[]? The window must have this class. +---@field title string[]? The window must have this title. +---@field tag TagId[]? The window must be on this tag. + +---Conditions for window rules. Only one condition can be in the table. +---If you have more than one you need to check for, use `cond_any` or `cond_all` +---to check for any or all conditions. +---@class WindowRuleCondition +---@field cond_any (WindowRuleCondition|WindowRuleCondition[])? At least one provided condition must be true. +---@field cond_all (WindowRuleCondition|WindowRuleCondition[])? All provided conditions must be true. +---@field class (string|string[])? The window must have this class. +---@field title (string|string[])? The window must have this title. +---@field tag (TagConstructor|TagConstructor[])? The window must be on this tag. + +---@class _WindowRule Attributes the window will be spawned with. +---@field output OutputName? The output this window will be spawned on. TODO: +---@field tags TagId[]? The tags this window will be spawned with. +---@field floating_or_tiled ("Floating"|"Tiled")? Whether or not this window will be spawned floating or tiled. +---@field fullscreen_or_maximized FullscreenOrMaximized? Whether or not this window will be spawned fullscreen, maximized, or forced to neither. +---@field size { [1]: integer, [2]: integer }? The size the window will spawn with, with [1] being width and [2] being height. This must be a strictly positive integer; putting 0 will crash the compositor. +---@field location { [1]: integer, [2]: integer }? The location the window will spawn at. If the window spawns tiled, it will instead snap to this location when set to floating. + +---@class WindowRule Attributes the window will be spawned with. +---@field output (Output|OutputName)? The output this window will be spawned on. TODO: +---@field tags TagConstructor[]? The tags this window will be spawned with. +---@field floating_or_tiled ("Floating"|"Tiled")? Whether or not this window will be spawned floating or tiled. +---@field fullscreen_or_maximized FullscreenOrMaximized? Whether or not this window will be spawned fullscreen, maximized, or forced to neither. +---@field size { [1]: integer, [2]: integer }? The size the window will spawn with, with [1] being width and [2] being height. This must be a strictly positive integer; putting 0 will crash the compositor. +---@field location { [1]: integer, [2]: integer }? The location the window will spawn at. If the window spawns tiled, it will instead snap to this location when set to floating. diff --git a/src/api/msg.rs b/src/api/msg.rs index a8100a9..98005c1 100644 --- a/src/api/msg.rs +++ b/src/api/msg.rs @@ -3,6 +3,8 @@ // The MessagePack format for these is a one-element map where the element's key is the enum name and its // value is a map of the enum's values +pub mod window_rules; + use crate::{ layout::Layout, output::OutputName, @@ -10,6 +12,8 @@ use crate::{ window::window_state::{FullscreenOrMaximized, WindowId}, }; +use self::window_rules::{WindowRule, WindowRuleCondition}; + #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Copy)] pub struct CallbackId(pub u32); @@ -53,6 +57,10 @@ pub enum Msg { ToggleMaximized { window_id: WindowId, }, + AddWindowRule { + cond: WindowRuleCondition, + rule: WindowRule, + }, // Tag management ToggleTag { diff --git a/src/api/msg/window_rules.rs b/src/api/msg/window_rules.rs new file mode 100644 index 0000000..9fa75c4 --- /dev/null +++ b/src/api/msg/window_rules.rs @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::num::NonZeroU32; + +use crate::{ + output::OutputName, + state::{State, WithState}, + tag::TagId, + window::{window_state::FullscreenOrMaximized, WindowElement}, +}; + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct WindowRuleCondition { + /// This condition is met when any of the conditions provided is met. + #[serde(default)] + cond_any: Option>, + /// This condition is met when all of the conditions provided are met. + #[serde(default)] + cond_all: Option>, + /// This condition is met when the class matches. + #[serde(default)] + class: Option>, + /// This condition is met when the title matches. + #[serde(default)] + title: Option>, + /// This condition is met when the tag matches. + #[serde(default)] + tag: Option>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum AllOrAny { + All, + Any, +} + +impl WindowRuleCondition { + /// RefCell Safety: This method uses RefCells on `window`. + pub fn is_met(&self, state: &State, window: &WindowElement) -> bool { + Self::is_met_inner(self, state, window, AllOrAny::All) + } + + fn is_met_inner(&self, state: &State, window: &WindowElement, all_or_any: AllOrAny) -> bool { + tracing::debug!("{self:#?}"); + + let WindowRuleCondition { + cond_any, + cond_all, + class, + title, + tag, + } = self; + + match all_or_any { + AllOrAny::All => { + let cond_any = if let Some(cond_any) = cond_any { + cond_any + .iter() + .any(|cond| Self::is_met_inner(cond, state, window, AllOrAny::Any)) + } else { + true + }; + let cond_all = if let Some(cond_all) = cond_all { + cond_all + .iter() + .all(|cond| Self::is_met_inner(cond, state, window, AllOrAny::All)) + } else { + true + }; + let classes = if let Some(classes) = class { + classes + .iter() + .all(|class| window.class().as_ref() == Some(class)) + } else { + true + }; + let titles = if let Some(titles) = title { + titles + .iter() + .all(|title| window.title().as_ref() == Some(title)) + } else { + true + }; + let tags = if let Some(tag_ids) = tag { + let mut tags = tag_ids.iter().filter_map(|tag_id| tag_id.tag(state)); + tags.all(|tag| window.with_state(|state| state.tags.contains(&tag))) + } else { + true + }; + + tracing::debug!("{cond_all} {cond_any} {classes} {titles} {tags}"); + cond_all && cond_any && classes && titles && tags + } + AllOrAny::Any => { + let cond_any = if let Some(cond_any) = cond_any { + cond_any + .iter() + .any(|cond| Self::is_met_inner(cond, state, window, AllOrAny::Any)) + } else { + false + }; + let cond_all = if let Some(cond_all) = cond_all { + cond_all + .iter() + .all(|cond| Self::is_met_inner(cond, state, window, AllOrAny::All)) + } else { + false + }; + let classes = if let Some(classes) = class { + classes + .iter() + .any(|class| window.class().as_ref() == Some(class)) + } else { + false + }; + let titles = if let Some(titles) = title { + titles + .iter() + .any(|title| window.title().as_ref() == Some(title)) + } else { + false + }; + let tags = if let Some(tag_ids) = tag { + let mut tags = tag_ids.iter().filter_map(|tag_id| tag_id.tag(state)); + tags.any(|tag| window.with_state(|state| state.tags.contains(&tag))) + } else { + false + }; + cond_all || cond_any || classes || titles || tags + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct WindowRule { + /// Set the output the window will open on. + #[serde(default)] + pub output: Option, + /// Set the tags the output will have on open. + #[serde(default)] + pub tags: Option>, + /// Set the window to floating or tiled on open. + #[serde(default)] + pub floating_or_tiled: Option, + /// Set the window to fullscreen, maximized, or force it to neither. + #[serde(default)] + pub fullscreen_or_maximized: Option, + /// Set the window's initial size. + #[serde(default)] + pub size: Option<(NonZeroU32, NonZeroU32)>, + /// Set the window's initial location. If the window is tiled, it will snap to this position + /// when set to floating. + #[serde(default)] + pub location: Option<(i32, i32)>, +} + +// TODO: just skip serializing fields on the other FloatingOrTiled +#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum FloatingOrTiled { + Floating, + Tiled, +} diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 162b7e3..ebbde52 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -112,6 +112,8 @@ impl XdgShellHandler for State { } }, |data| { + data.state.apply_window_rules(&window); + if let Some(focused_output) = data.state.focus_state.focused_output.clone() { data.state.update_windows(&focused_output); BLOCKER_COUNTER.store(1, std::sync::atomic::Ordering::SeqCst); diff --git a/src/handlers/xwayland.rs b/src/handlers/xwayland.rs index 5130feb..1bd23d0 100644 --- a/src/handlers/xwayland.rs +++ b/src/handlers/xwayland.rs @@ -1,8 +1,4 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: GPL-3.0-or-later use smithay::{ reexports::wayland_server::Resource, @@ -154,6 +150,11 @@ impl XwmHandler for CalloopData { .collect::>(); self.state.windows.push(window.clone()); + + self.state.focus_state.set_focus(window.clone()); + + self.state.apply_window_rules(&window); + if let Some(focused_output) = self.state.focus_state.focused_output.clone() { self.state.update_windows(&focused_output); BLOCKER_COUNTER.store(1, std::sync::atomic::Ordering::SeqCst); @@ -182,20 +183,6 @@ impl XwmHandler for CalloopData { .client_compositor_state(&client) .blocker_cleared(&mut data.state, &data.display.handle()) } - - // Schedule the popup to raise when all windows have committed after having - // their blockers cleared - crate::state::schedule_on_commit(data, windows_on_output, move |dt| { - let WindowElement::X11(surface) = &clone else { unreachable!() }; - if should_float(surface) { - if let Some(xwm) = dt.state.xwm.as_mut() { - tracing::debug!("raising x11 popup"); - xwm.raise_window(surface).expect("failed to raise x11 win"); - dt.state.space.raise_element(&clone, true); - dt.state.focus_state.set_focus(clone); - } - } - }); }); }); } @@ -237,12 +224,6 @@ impl XwmHandler for CalloopData { .cloned(); if let Some(win) = win { self.state.space.unmap_elem(&win); - // self.state.windows.retain(|elem| &win != elem); - // if win.with_state(|state| state.floating.is_tiled()) { - // if let Some(output) = win.output(&self.state) { - // self.state.re_layout(&output); - // } - // } } if !window.is_override_redirect() { tracing::debug!("set mapped to false"); @@ -303,7 +284,6 @@ impl XwmHandler for CalloopData { geometry: Rectangle, _above: Option, ) { - // tracing::debug!("x11 configure_notify"); let Some(win) = self .state .space diff --git a/src/state.rs b/src/state.rs index 3a1da88..5c5f752 100644 --- a/src/state.rs +++ b/src/state.rs @@ -12,7 +12,10 @@ use std::{ use crate::{ api::{ - msg::{CallbackId, ModifierMask, Msg}, + msg::{ + window_rules::{WindowRule, WindowRuleCondition}, + CallbackId, ModifierMask, Msg, + }, PinnacleSocketSource, DEFAULT_SOCKET_DIR, }, backend::{udev::Udev, winit::Winit, BackendData}, @@ -123,6 +126,7 @@ pub struct State { pub dnd_icon: Option, pub windows: Vec, + pub window_rules: Vec<(WindowRuleCondition, WindowRule)>, pub async_scheduler: Scheduler<()>, pub config_process: async_process::Child, @@ -145,10 +149,6 @@ where for window in windows.iter().filter(|win| win.alive()) { if window.with_state(|state| !matches!(state.loc_request_state, LocationRequestState::Idle)) { - // tracing::debug!( - // "window state is {:?}", - // window.with_state(|state| state.loc_request_state.clone()) - // ); data.state.loop_handle.insert_idle(|data| { schedule_on_commit(data, windows, on_commit); }); @@ -370,6 +370,7 @@ impl State { config_process: config_child_handle, windows: vec![], + window_rules: vec![], output_callback_ids: vec![], xwayland, @@ -391,7 +392,7 @@ impl State { }); } - // Schedule something to be done when `condition` returns true. + /// Schedule something to be done when `condition` returns true. fn schedule_inner(data: &mut CalloopData, condition: F1, run: F2) where F1: Fn(&mut CalloopData) -> bool + 'static, @@ -418,6 +419,7 @@ fn get_config_dir() -> PathBuf { .to_string_lossy() .to_string() }); + PathBuf::from(shellexpand::tilde(&config_dir).to_string()) } @@ -444,6 +446,7 @@ fn start_config(metaconfig: Metaconfig, config_dir: &Path) -> anyhow::Result(Some(std::env::var(var).unwrap_or("".to_string()))), ) .ok()? @@ -457,6 +460,8 @@ fn start_config(metaconfig: Metaconfig, config_dir: &Path) -> anyhow::Result(&self, func: F) -> T where F: FnMut(&mut Self::State) -> T; diff --git a/src/state/api_handlers.rs b/src/state/api_handlers.rs index 1ef384e..4b4eb60 100644 --- a/src/state/api_handlers.rs +++ b/src/state/api_handlers.rs @@ -117,6 +117,9 @@ impl State { let Some(output) = window.output(self) else { return }; self.update_windows(&output); } + Msg::AddWindowRule { cond, rule } => { + self.window_rules.push((cond, rule)); + } // Tags ---------------------------------------- Msg::ToggleTag { tag_id } => { diff --git a/src/window.rs b/src/window.rs index f05578b..394ef2a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -25,16 +25,20 @@ use smithay::{ }, utils::{user_data::UserDataMap, IsAlive, Logical, Point, Rectangle, Serial, Size}, wayland::{ - compositor::{Blocker, BlockerState, SurfaceData}, + compositor::{self, Blocker, BlockerState, SurfaceData}, dmabuf::DmabufFeedback, seat::WaylandFocus, + shell::xdg::XdgToplevelSurfaceData, }, xwayland::X11Surface, }; -use crate::state::{State, WithState}; +use crate::{ + api::msg::window_rules::{self, WindowRule}, + state::{State, WithState}, +}; -use self::window_state::{LocationRequestState, WindowElementState}; +use self::window_state::{FloatingOrTiled, LocationRequestState, WindowElementState}; pub mod window_state; @@ -221,27 +225,53 @@ impl WindowElement { surface .configure(Rectangle::from_loc_and_size(new_loc, new_size)) .expect("failed to configure x11 win"); - // self.with_state(|state| { - // state.resize_state = WindowResizeState::Acknowledged(new_loc); - // }); + if !surface.is_override_redirect() { surface .set_mapped(true) .expect("failed to set x11 win to mapped"); } space.map_element(self.clone(), new_loc, false); - // if let Some(focused_output) = state.focus_state.focused_output.clone() { - // self.send_frame( - // &focused_output, - // state.clock.now(), - // Some(Duration::ZERO), - // surface_primary_scanout_output, - // ); - // } } } } + pub fn class(&self) -> Option { + match self { + WindowElement::Wayland(window) => { + compositor::with_states(window.toplevel().wl_surface(), |states| { + states + .data_map + .get::() + .expect("XdgToplevelSurfaceData wasn't in surface's data map") + .lock() + .expect("Failed to lock Mutex") + .app_id + .clone() + }) + } + WindowElement::X11(surface) => Some(surface.class()), + } + } + + pub fn title(&self) -> Option { + match self { + WindowElement::Wayland(window) => { + compositor::with_states(window.toplevel().wl_surface(), |states| { + states + .data_map + .get::() + .expect("XdgToplevelSurfaceData wasn't in surface's data map") + .lock() + .expect("Failed to lock Mutex") + .title + .clone() + }) + } + WindowElement::X11(surface) => Some(surface.title()), + } + } + /// Get the output this window is on. /// /// This method gets the first tag the window has and returns its output. @@ -509,3 +539,108 @@ impl Blocker for WindowBlocker { } } } + +impl State { + pub fn apply_window_rules(&mut self, window: &WindowElement) { + tracing::debug!("Applying window rules"); + for (cond, rule) in self.window_rules.iter() { + if cond.is_met(self, window) { + let WindowRule { + output, + tags, + floating_or_tiled, + fullscreen_or_maximized, + size, + location, + } = rule; + + // TODO: If both `output` and `tags` are specified, `tags` will apply over + // | `output`. + + if let Some(output_name) = output { + if let Some(output) = output_name.output(self) { + let tags = output + .with_state(|state| state.focused_tags().cloned().collect::>()); + + window.with_state(|state| state.tags = tags.clone()); + } + } + + if let Some(tag_ids) = tags { + let tags = tag_ids + .iter() + .filter_map(|tag_id| tag_id.tag(self)) + .collect::>(); + + window.with_state(|state| state.tags = tags.clone()); + } + + if let Some(floating_or_tiled) = floating_or_tiled { + match floating_or_tiled { + window_rules::FloatingOrTiled::Floating => { + if window.with_state(|state| state.floating_or_tiled.is_tiled()) { + window.toggle_floating(); + } + } + window_rules::FloatingOrTiled::Tiled => { + if window.with_state(|state| state.floating_or_tiled.is_floating()) { + window.toggle_floating(); + } + } + } + } + + if let Some(fs_or_max) = fullscreen_or_maximized { + window.with_state(|state| state.fullscreen_or_maximized = *fs_or_max); + } + + if let Some((w, h)) = size { + let mut window_size = window.geometry().size; + window_size.w = u32::from(*w) as i32; + window_size.h = u32::from(*h) as i32; + + match window.with_state(|state| state.floating_or_tiled) { + FloatingOrTiled::Floating(mut rect) => { + rect.size = (u32::from(*w) as i32, u32::from(*h) as i32).into(); + window.with_state(|state| { + state.floating_or_tiled = FloatingOrTiled::Floating(rect) + }); + } + FloatingOrTiled::Tiled(mut rect) => { + if let Some(rect) = rect.as_mut() { + rect.size = (u32::from(*w) as i32, u32::from(*h) as i32).into(); + } + window.with_state(|state| { + state.floating_or_tiled = FloatingOrTiled::Tiled(rect) + }); + } + } + } + + if let Some(loc) = location { + match window.with_state(|state| state.floating_or_tiled) { + FloatingOrTiled::Floating(mut rect) => { + rect.loc = (*loc).into(); + window.with_state(|state| { + state.floating_or_tiled = FloatingOrTiled::Floating(rect) + }); + self.space.map_element(window.clone(), *loc, false); + } + FloatingOrTiled::Tiled(rect) => { + // If the window is tiled, don't set the size. Instead, set + // what the size will be when it gets set to floating. + let rect = rect.unwrap_or_else(|| { + let size = window.geometry().size; + Rectangle::from_loc_and_size(Point::from(*loc), size) + }); + + window.with_state(|state| { + state.floating_or_tiled = FloatingOrTiled::Tiled(Some(rect)) + }); + } + } + } + } + } + } +} diff --git a/src/window/window_state.rs b/src/window/window_state.rs index ff81b47..9cf99d2 100644 --- a/src/window/window_state.rs +++ b/src/window/window_state.rs @@ -301,7 +301,7 @@ impl FloatingOrTiled { } } -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum FullscreenOrMaximized { Neither, Fullscreen,