Merge pull request #65 from Ottatop/window_rules

Add window rules
This commit is contained in:
Ottatop 2023-09-07 21:06:08 -05:00 committed by GitHub
commit 01b6e258ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 792 additions and 316 deletions

View file

@ -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 }}

View file

@ -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[]

View file

@ -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)

View file

@ -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?

View file

@ -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 })
---```

View file

@ -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.

View file

@ -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

View file

@ -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()

View file

@ -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.

238
api/lua/window_rules.lua Normal file
View file

@ -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

View file

@ -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.

View file

@ -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 {

163
src/api/msg/window_rules.rs Normal file
View file

@ -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<Vec<WindowRuleCondition>>,
/// This condition is met when all of the conditions provided are met.
#[serde(default)]
cond_all: Option<Vec<WindowRuleCondition>>,
/// This condition is met when the class matches.
#[serde(default)]
class: Option<Vec<String>>,
/// This condition is met when the title matches.
#[serde(default)]
title: Option<Vec<String>>,
/// This condition is met when the tag matches.
#[serde(default)]
tag: Option<Vec<TagId>>,
}
#[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<OutputName>,
/// Set the tags the output will have on open.
#[serde(default)]
pub tags: Option<Vec<TagId>>,
/// Set the window to floating or tiled on open.
#[serde(default)]
pub floating_or_tiled: Option<FloatingOrTiled>,
/// Set the window to fullscreen, maximized, or force it to neither.
#[serde(default)]
pub fullscreen_or_maximized: Option<FullscreenOrMaximized>,
/// 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,
}

View file

@ -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);

View file

@ -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::<Vec<_>>();
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<i32, Logical>,
_above: Option<smithay::reexports::x11rb::protocol::xproto::Window>,
) {
// tracing::debug!("x11 configure_notify");
let Some(win) = self
.state
.space

View file

@ -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<WlSurface>,
pub windows: Vec<WindowElement>,
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<F1, F2>(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<Con
shellexpand::full_with_context(
&string,
|| std::env::var("HOME").ok(),
// Expand nonexistent vars to an empty string instead of crashing
|var| Ok::<_, ()>(Some(std::env::var(var).unwrap_or("".to_string()))),
)
.ok()?
@ -457,6 +460,8 @@ fn start_config(metaconfig: Metaconfig, config_dir: &Path) -> anyhow::Result<Con
tracing::debug!("Config envs are {:?}", envs);
// Using async_process's Child instead of std::process because I don't have to spawn my own
// thread to wait for the child
let child = async_process::Command::new(arg1)
.args(command)
.envs(envs)
@ -494,6 +499,7 @@ impl State {
tracing::debug!("Clearing mouse- and keybinds");
self.input_state.keybinds.clear();
self.input_state.mousebinds.clear();
self.window_rules.clear();
tracing::debug!("Killing old config");
if let Err(err) = self.config_process.kill() {
@ -528,6 +534,7 @@ pub struct CalloopData {
pub struct ClientState {
pub compositor_state: CompositorClientState,
}
impl ClientData for ClientState {
fn initialized(&self, _client_id: ClientId) {}
@ -588,7 +595,7 @@ impl ApiState {
}
pub trait WithState {
type State: Default;
type State;
fn with_state<F, T>(&self, func: F) -> T
where
F: FnMut(&mut Self::State) -> T;

View file

@ -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 } => {

View file

@ -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<String> {
match self {
WindowElement::Wayland(window) => {
compositor::with_states(window.toplevel().wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.expect("XdgToplevelSurfaceData wasn't in surface's data map")
.lock()
.expect("Failed to lock Mutex<XdgToplevelSurfaceData>")
.app_id
.clone()
})
}
WindowElement::X11(surface) => Some(surface.class()),
}
}
pub fn title(&self) -> Option<String> {
match self {
WindowElement::Wayland(window) => {
compositor::with_states(window.toplevel().wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.expect("XdgToplevelSurfaceData wasn't in surface's data map")
.lock()
.expect("Failed to lock Mutex<XdgToplevelSurfaceData>")
.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::<Vec<_>>());
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::<Vec<_>>();
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))
});
}
}
}
}
}
}
}

View file

@ -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,