mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2024-12-27 21:58:18 +01:00
commit
01b6e258ff
19 changed files with 792 additions and 316 deletions
4
.github/workflows/ldoc.yml
vendored
4
.github/workflows/ldoc.yml
vendored
|
@ -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 }}
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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 })
|
||||
---```
|
||||
|
|
|
@ -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.
|
||||
|
|
209
api/lua/tag.lua
209
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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
238
api/lua/window_rules.lua
Normal 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
|
39
api/lua/window_rules_types.lua
Normal file
39
api/lua/window_rules_types.lua
Normal 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.
|
|
@ -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
163
src/api/msg/window_rules.rs
Normal 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,
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
21
src/state.rs
21
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<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;
|
||||
|
|
|
@ -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 } => {
|
||||
|
|
163
src/window.rs
163
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<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))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue