mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2024-12-27 21:58:18 +01:00
574 lines
17 KiB
Lua
574 lines
17 KiB
Lua
-- SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
---Tag management.
|
|
---
|
|
---This module provides utilities for creating and manipulating tags.
|
|
---
|
|
---A tag is a sort of marker for each of your windows. It allows you to present windows in ways that
|
|
---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
|
|
--- 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 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.
|
|
---
|
|
---Many of the functions in this module take Tag|TagTable|TagTableNamed|string.
|
|
---This is a convenience so you don't have to get a tag object every time you want to do
|
|
---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.
|
|
--- - 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.
|
|
--- - 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.
|
|
---
|
|
---If you need to get tags beyond the first with the same name, use a `get` function and find what you need.
|
|
---@class TagModule
|
|
local tag_module = {}
|
|
|
|
---@alias Layout
|
|
---| "MasterStack" # One master window on the left with all other windows stacked to the right.
|
|
---| "Dwindle" # Windows split in half towards the bottom right corner.
|
|
---| "Spiral" # Windows split in half in a spiral.
|
|
---| "CornerTopLeft" # One main corner window in the top left with a column of windows on the right and a row on the bottom.
|
|
---| "CornerTopRight" # One main corner window in the top right with a column of windows on the left and a row on the bottom.
|
|
---| "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 { name: string, output: (string|Output)? }
|
|
|
|
---@alias TagConstructor Tag|TagTable|string
|
|
|
|
---A tag object.
|
|
---
|
|
---This can be retrieved through the various `get` functions in the `TagModule`.
|
|
---@classmod
|
|
---@class Tag
|
|
---@field private _id integer The internal id of this tag.
|
|
local tag = {}
|
|
|
|
---Create a tag from an id.
|
|
---The id is the unique identifier for each tag.
|
|
---@param id TagId
|
|
---@return Tag
|
|
local function create_tag(id)
|
|
---@type Tag
|
|
local t = { _id = id }
|
|
-- Copy functions over
|
|
for k, v in pairs(tag) do
|
|
t[k] = v
|
|
end
|
|
|
|
return t
|
|
end
|
|
|
|
---Get this tag's internal id.
|
|
---***You probably won't need to use this.***
|
|
---@return integer
|
|
function tag:id()
|
|
return self._id
|
|
end
|
|
|
|
---Get this tag's active status.
|
|
---@return boolean|nil active `true` if the tag is active, `false` if not, and `nil` if the tag doesn't exist.
|
|
---@see TagModule.active — The corresponding module function
|
|
function tag:active()
|
|
return tag_module.active(self)
|
|
end
|
|
|
|
---Get this tag's name.
|
|
---@return string|nil name The name of this tag, or nil if it doesn't exist.
|
|
---@see TagModule.name — The corresponding module function
|
|
function tag:name()
|
|
return tag_module.name(self)
|
|
end
|
|
|
|
---Get this tag's output.
|
|
---@return Output|nil output The output this tag is on, or nil if the tag doesn't exist.
|
|
---@see TagModule.output — The corresponding module function
|
|
function tag:output()
|
|
return tag_module.output(self)
|
|
end
|
|
|
|
---Switch to this tag.
|
|
---@see TagModule.switch_to — The corresponding module function
|
|
function tag:switch_to()
|
|
tag_module.switch_to(self)
|
|
end
|
|
|
|
---Toggle this tag.
|
|
---@see TagModule.toggle — The corresponding module function
|
|
function tag:toggle()
|
|
tag_module.toggle(self)
|
|
end
|
|
|
|
---Set this tag's layout.
|
|
---@param layout Layout
|
|
---@see TagModule.set_layout — The corresponding module function
|
|
function tag:set_layout(layout)
|
|
tag_module.set_layout(self, layout)
|
|
end
|
|
|
|
-----------------------------------------------------------
|
|
|
|
---Add tags to the specified output.
|
|
---
|
|
---### Examples
|
|
---```lua
|
|
---local op = output.get_by_name("DP-1")
|
|
---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.
|
|
---local tags = {"Terminal", "Browser", "Code", "Potato", "Email"}
|
|
---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.
|
|
---@overload fun(output: Output, tag_names: string[])
|
|
---@see Output.add_tags — The corresponding object method
|
|
function tag_module.add(output, ...)
|
|
local varargs = { ... }
|
|
if type(varargs[1]) == "string" then
|
|
local tag_names = varargs
|
|
tag_names["n"] = nil -- remove the length to make it a true array for serializing
|
|
|
|
SendMsg({
|
|
AddTags = {
|
|
output_name = output:name(),
|
|
tag_names = tag_names,
|
|
},
|
|
})
|
|
else
|
|
local tag_names = varargs[1] --[=[@as string[]]=]
|
|
|
|
SendMsg({
|
|
AddTags = {
|
|
output_name = output:name(),
|
|
tag_names = tag_names,
|
|
},
|
|
})
|
|
end
|
|
end
|
|
|
|
---Toggle a tag on the specified output. If the output isn't specified, toggle it on the currently focused output instead.
|
|
---
|
|
---### Example
|
|
---
|
|
---```lua
|
|
---local op = output.get_by_name("DP-1")
|
|
---
|
|
---tag.toggle("1") -- Toggle tag 1 on the focused output
|
|
---
|
|
---tag.toggle({ name = "1", output = "DP-1" }) -- Toggle tag 1 on "DP-1"
|
|
---tag.toggle({ name = "1", output = op }) -- Same as above
|
|
---
|
|
--- -- 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 TagConstructor
|
|
---@see Tag.toggle — The corresponding object method
|
|
function tag_module.toggle(t)
|
|
local t = tag_module.get(t)
|
|
|
|
if t then
|
|
SendMsg({
|
|
ToggleTag = {
|
|
tag_id = t:id(),
|
|
},
|
|
})
|
|
end
|
|
end
|
|
|
|
---Switch to a tag on the specified output, deactivating any other active tags on it.
|
|
---If the output is not specified, this uses the currently focused output instead.
|
|
---
|
|
---This is used to replicate what a traditional workspace is on some other Wayland compositors.
|
|
---
|
|
---### Examples
|
|
---```lua
|
|
---local op = output.get_by_name("DP-1")
|
|
---
|
|
---tag.switch_to("1") -- Switch to tag 1 on the focused output
|
|
---
|
|
---tag.switch_to({ name = "1", output = "DP-1" }) -- Switch to tag 1 on "DP-1"
|
|
---tag.switch_to({ name = "1", output = op }) -- Same as above
|
|
---
|
|
--- -- 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 TagConstructor
|
|
---@see Tag.switch_to — The corresponding object method
|
|
function tag_module.switch_to(t)
|
|
local t = tag_module.get(t)
|
|
|
|
if t then
|
|
SendMsg({
|
|
SwitchToTag = {
|
|
tag_id = t:id(),
|
|
},
|
|
})
|
|
end
|
|
end
|
|
|
|
---Set a layout for the tag on the specified output. If no output is provided, set it for the tag on the currently focused one.
|
|
---
|
|
---### Examples
|
|
---```lua
|
|
---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({ 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
|
|
---
|
|
--- -- 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 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.get(t)
|
|
|
|
if t then
|
|
SendMsg({
|
|
SetLayout = {
|
|
tag_id = t:id(),
|
|
layout = layout,
|
|
},
|
|
})
|
|
end
|
|
end
|
|
|
|
---Get a tag with the specified name and optional output.
|
|
---
|
|
---If the output isn't specified, the focused one is used.
|
|
---
|
|
---If you have duplicate tags on an output, this returns the first one.
|
|
---If you need access to all duplicates, use `tag.get_on_output`, `tag.get_by_name`, or `tag.get_all`
|
|
---and filter for what you need.
|
|
---
|
|
---### Examples
|
|
---```lua
|
|
---local t = tag.get("1")
|
|
---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 TagConstructor
|
|
---@return Tag|nil
|
|
---
|
|
---@see TagModule.get_on_output
|
|
---@see TagModule.get_by_name
|
|
---@see TagModule.get_all
|
|
function tag_module.get(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.
|
|
---
|
|
---### Example
|
|
---```lua
|
|
---local op = output.get_focused()
|
|
---if op ~= nil then
|
|
--- local tags = tag.get_on_output(op) -- All tags on the focused output
|
|
---end
|
|
---```
|
|
---@param output Output
|
|
---@return Tag[]
|
|
---
|
|
---@see Output.tags — The corresponding object method
|
|
function tag_module.get_on_output(output)
|
|
local response = Request({
|
|
GetOutputProps = {
|
|
output_name = output:name(),
|
|
},
|
|
})
|
|
|
|
local tag_ids = response.RequestResponse.response.OutputProps.tag_ids
|
|
|
|
---@type Tag[]
|
|
local tags = {}
|
|
|
|
if tag_ids == nil then
|
|
return tags
|
|
end
|
|
|
|
for _, tag_id in pairs(tag_ids) do
|
|
table.insert(tags, create_tag(tag_id))
|
|
end
|
|
|
|
return tags
|
|
end
|
|
|
|
---Get all tags with this name across all outputs.
|
|
---
|
|
---### Example
|
|
---```lua
|
|
--- -- 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...
|
|
---local no_tags = tag.get_by_name("Firefox")
|
|
--- -- ...will have `no_tags` be empty.
|
|
---```
|
|
---@param name string The name of the tag(s) you want.
|
|
---@return Tag[]
|
|
function tag_module.get_by_name(name)
|
|
local t_s = tag_module.get_all()
|
|
|
|
---@type Tag[]
|
|
local tags = {}
|
|
|
|
for _, t in pairs(t_s) do
|
|
if t:name() == name then
|
|
table.insert(tags, t)
|
|
end
|
|
end
|
|
|
|
return tags
|
|
end
|
|
|
|
---Get all tags across all outputs.
|
|
---
|
|
---### Example
|
|
---```lua
|
|
--- -- 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.
|
|
---```
|
|
---@return Tag[]
|
|
function tag_module.get_all()
|
|
local response = Request("GetTags")
|
|
|
|
local tag_ids = response.RequestResponse.response.Tags.tag_ids
|
|
|
|
---@type Tag[]
|
|
local tags = {}
|
|
|
|
for _, tag_id in pairs(tag_ids) do
|
|
table.insert(tags, create_tag(tag_id))
|
|
end
|
|
|
|
return tags
|
|
end
|
|
|
|
---Get the specified tag's name.
|
|
---
|
|
---### Example
|
|
---```lua
|
|
--- -- Assuming the tag `Terminal` exists...
|
|
---print(tag.name(tag.get_by_name("Terminal")[1]))
|
|
--- -- ...should print `Terminal`.
|
|
---```
|
|
---@param t Tag
|
|
---@return string|nil
|
|
---@see Tag.name — The corresponding object method
|
|
function tag_module.name(t)
|
|
local response = Request({
|
|
GetTagProps = {
|
|
tag_id = t:id(),
|
|
},
|
|
})
|
|
local name = response.RequestResponse.response.TagProps.name
|
|
return name
|
|
end
|
|
|
|
---Get whether or not the specified tag is active.
|
|
---@param t Tag
|
|
---@return boolean|nil
|
|
---@see Tag.active — The corresponding object method
|
|
function tag_module.active(t)
|
|
local response = Request({
|
|
GetTagProps = {
|
|
tag_id = t:id(),
|
|
},
|
|
})
|
|
local active = response.RequestResponse.response.TagProps.active
|
|
return active
|
|
end
|
|
|
|
---Get the output the specified tag is on.
|
|
---@param t Tag
|
|
---@return Output|nil
|
|
---@see OutputModule.get_for_tag — The called function
|
|
---@see Tag.output — The corresponding object method
|
|
function tag_module.output(t)
|
|
return require("output").get_for_tag(t)
|
|
end
|
|
|
|
---@class LayoutCycler
|
|
---@field next fun(output: (Output|OutputName)?) Change the first active tag on `output` to its next layout. If `output` is empty, the focused output is used.
|
|
---@field prev fun(output: (Output|OutputName)?) Change the first active tag on `output` to its previous layout. If `output` is empty, the focused output is used.
|
|
|
|
---Given an array of layouts, this will create two functions; one will cycle forward the layout
|
|
---for the provided tag, and one will cycle backward.
|
|
---
|
|
--- ### Example
|
|
---```lua
|
|
---local layout_cycler = tag.layout_cycler({ "Dwindle", "Spiral", "MasterStack" })
|
|
---
|
|
---layout_cycler.next() -- Go to the next layout on the first tag of the focused output
|
|
---layout_cycler.prev() -- Go to the previous layout on the first tag of the focused output
|
|
---
|
|
---layout_cycler.next("DP-1") -- Do the above but on "DP-1" instead
|
|
---layout_cycler.prev(output.get_by_name("DP-1")) -- With an output object
|
|
---```
|
|
---@param layouts Layout[] The available layouts.
|
|
---@return LayoutCycler layout_cycler A table with the functions `next` and `prev`, which will cycle layouts for the given tag.
|
|
function tag_module.layout_cycler(layouts)
|
|
local indices = {}
|
|
|
|
-- Return empty functions if layouts is empty
|
|
if #layouts == 0 then
|
|
return {
|
|
next = function(_) end,
|
|
prev = function(_) end,
|
|
}
|
|
end
|
|
|
|
return {
|
|
---@param output (Output|OutputName)?
|
|
next = function(output)
|
|
if type(output) == "string" then
|
|
output = require("output").get_by_name(output)
|
|
end
|
|
|
|
output = output or require("output").get_focused()
|
|
|
|
if output == nil then
|
|
return
|
|
end
|
|
|
|
local tags = output:tags()
|
|
for _, tg in pairs(tags) do
|
|
if tg:active() then
|
|
local id = tg:id()
|
|
if id == nil then
|
|
return
|
|
end
|
|
|
|
if #layouts == 1 then
|
|
indices[id] = 1
|
|
elseif indices[id] == nil then
|
|
indices[id] = 2
|
|
else
|
|
if indices[id] + 1 > #layouts then
|
|
indices[id] = 1
|
|
else
|
|
indices[id] = indices[id] + 1
|
|
end
|
|
end
|
|
|
|
tg:set_layout(layouts[indices[id]])
|
|
break
|
|
end
|
|
end
|
|
end,
|
|
|
|
---@param output (Output|OutputName)?
|
|
prev = function(output)
|
|
if type(output) == "string" then
|
|
output = require("output").get_by_name(output)
|
|
end
|
|
|
|
output = output or require("output").get_focused()
|
|
|
|
if output == nil then
|
|
return
|
|
end
|
|
|
|
local tags = output:tags()
|
|
for _, tg in pairs(tags) do
|
|
if tg:active() then
|
|
local id = tg:id()
|
|
if id == nil then
|
|
return
|
|
end
|
|
|
|
if #layouts == 1 then
|
|
indices[id] = 1
|
|
elseif indices[id] == nil then
|
|
indices[id] = #layouts - 1
|
|
else
|
|
if indices[id] - 1 < 1 then
|
|
indices[id] = #layouts
|
|
else
|
|
indices[id] = indices[id] - 1
|
|
end
|
|
end
|
|
|
|
tg:set_layout(layouts[indices[id]])
|
|
break
|
|
end
|
|
end
|
|
end,
|
|
}
|
|
end
|
|
|
|
return tag_module
|