pinnacle/api/lua/output.lua
2023-10-18 20:31:37 -05:00

616 lines
21 KiB
Lua

-- SPDX-License-Identifier: GPL-3.0-or-later
---@diagnostic disable:redefined-local
---Output management.
---
---An output is what you would call a monitor. It presents windows, your cursor, and other UI elements.
---
---Outputs are uniquely identified by their name, a.k.a. the name of the connector they're plugged in to.
---@class Output
local output = {}
---An output handle.
---
---This is a handle to one of your monitors.
---It serves to make it easier to deal with them, defining methods for getting properties and
---helpers for things like positioning multiple monitors.
---
---This can be retrieved through the various `get` functions in the `Output` module.
---@classmod
---@class OutputHandle A handle to a display.
---@field private _name OutputName The name of this output (or rather, of its connector).
local output_handle = {}
---@param params OutputHandle|string
---@return OutputHandle
local function create_output_from_params(params)
if type(params) == "table" then
return params
end
return output.get_by_name(params --[[@as string]])
end
---Create a new output handle from a name.
---The name is the unique identifier for each output.
---@param name string
---@return OutputHandle
local function create_output(name)
---@type OutputHandle
local o = { _name = name }
-- Copy functions over
for k, v in pairs(output_handle) do
o[k] = v
end
return o
end
---Get this output's name. This is something like "eDP-1" or "HDMI-A-0".
---@return string
function output_handle:name()
return self._name
end
---Get all tags on this output.
---@return TagHandle[]
---@see Output.tags — The corresponding module function
function output_handle:tags()
return output.tags(self)
end
---Add tags to this output.
---@param ... string The names of the tags you want to add. You can also pass in a table.
---@overload fun(self: self, tag_names: string[])
---@see Output.add_tags — The corresponding module function
function output_handle:add_tags(...)
output.add_tags(self, ...)
end
---Get this output's make.
---@return string|nil
---@see Output.make — The corresponding module function
function output_handle:make()
return output.make(self)
end
---Get this output's model.
---@return string|nil
---@see Output.model — The corresponding module function
function output_handle:model()
return output.model(self)
end
---Get this output's location in the global space, in pixels.
---@return { x: integer, y: integer }|nil
---@see Output.loc — The corresponding module function
function output_handle:loc()
return output.loc(self)
end
---Get this output's resolution in pixels.
---@return { w: integer, h: integer }|nil
---@see Output.res — The corresponding module function
function output_handle:res()
return output.res(self)
end
---Get this output's refresh rate in millihertz.
---For example, 60Hz will be returned as 60000.
---@return integer|nil
---@see Output.refresh_rate — The corresponding module function
function output_handle:refresh_rate()
return output.refresh_rate(self)
end
---Get this output's physical size in millimeters.
---@return { w: integer, h: integer }|nil
---@see Output.physical_size — The corresponding module function
function output_handle:physical_size()
return output.physical_size(self)
end
---Get whether or not this output is focused. This is currently defined as having the cursor on it.
---@return boolean|nil
---@see Output.focused — The corresponding module function
function output_handle:focused()
return output.focused(self)
end
---Set this output's location.
---
---### Examples
---```lua
--- -- 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
---dp1:set_loc({ x = 1920, y = 0 })
---dp2:set_loc({ x = 0, y = 0 })
---
--- -- 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
---dp1:set_loc({ x = 0, y = 0 })
---dp2:set_loc({ x = 2560, y = 1440 - 1080 })
---```
---@param loc { x: integer?, y: integer? }
function output_handle:set_loc(loc)
output.set_loc(self, loc)
end
-- TODO: move this into own file or something ---------------------------------------------
---@alias AlignmentVertical
---| "top" Align the tops of the outputs
---| "center" Center the outputs vertically
---| "bottom" Align the bottoms of the outputs
---@alias AlignmentHorizontal
---| "left" Align the left edges of the outputs
---| "center" Center the outputs vertically
---| "right" Align the right edges of the outputs
---@param op1 OutputHandle
---@param op2 OutputHandle
---@param left_or_right "left" | "right"
---@param alignment AlignmentVertical? How you want to align the `self` output. Defaults to `top`.
local function set_loc_horizontal(op1, op2, left_or_right, alignment)
local alignment = alignment or "top"
local self_loc = op1:loc()
local self_res = op1:res()
local other_loc = op2:loc()
local other_res = op2:res()
if self_loc == nil or self_res == nil or other_loc == nil or other_res == nil then
return
end
---@type integer
local x
if left_or_right == "left" then
x = other_loc.x - self_res.w
else
x = other_loc.x + other_res.w
end
if alignment == "top" then
output.set_loc(op1, { x = x, y = other_loc.y })
elseif alignment == "center" then
output.set_loc(op1, { x = x, y = other_loc.y + (other_res.h - self_res.h) // 2 })
elseif alignment == "bottom" then
output.set_loc(op1, { x = x, y = other_loc.y + (other_res.h - self_res.h) })
end
end
---Set this output's location to the right of the specified output.
---
---```
--- top center bottom
--- ┌────────┬──────┐ ┌────────┐ ┌────────┐
--- │op │self │ │op ├──────┐ │op │
--- │ ├──────┘ │ │self │ │ ├──────┐
--- │ │ │ ├──────┘ │ │self │
--- └────────┘ └────────┘ └────────┴──────┘
---```
---This will fail if `op` is an invalid output.
---@param op OutputHandle
---@param alignment AlignmentVertical? How you want to align the `self` output. Defaults to `top`.
---@see OutputHandle.set_loc if you need more granular control
function output_handle:set_loc_right_of(op, alignment)
set_loc_horizontal(self, op, "right", alignment)
end
---Set this output's location to the left of the specified output.
---
---```
--- top center bottom
--- ┌──────┬────────┐ ┌────────┐ ┌────────┐
--- │self │op │ ┌──────┤op │ │op │
--- └──────┤ │ │self │ │ ┌──────┤ │
--- │ │ └──────┤ │ │self │ │
--- └────────┘ └────────┘ └──────┴────────┘
---```
---This will fail if `op` is an invalid output.
---@param op OutputHandle
---@param alignment AlignmentVertical? How you want to align the `self` output. Defaults to `top`.
---@see OutputHandle.set_loc if you need more granular control
function output_handle:set_loc_left_of(op, alignment)
set_loc_horizontal(self, op, "left", alignment)
end
---@param op1 OutputHandle
---@param op2 OutputHandle
---@param top_or_bottom "top" | "bottom"
---@param alignment AlignmentHorizontal? How you want to align the `self` output. Defaults to `top`.
local function set_loc_vertical(op1, op2, top_or_bottom, alignment)
local alignment = alignment or "left"
local self_loc = op1:loc()
local self_res = op1:res()
local other_loc = op2:loc()
local other_res = op2:res()
if self_loc == nil or self_res == nil or other_loc == nil or other_res == nil then
return
end
---@type integer
local y
if top_or_bottom == "top" then
y = other_loc.y - self_res.h
else
y = other_loc.y + other_res.h
end
if alignment == "left" then
output.set_loc(op1, { x = other_loc.x, y = y })
elseif alignment == "center" then
output.set_loc(op1, { x = other_loc.x + (other_res.w - self_res.w) // 2, y = y })
elseif alignment == "right" then
output.set_loc(op1, { x = other_loc.x + (other_res.w - self_res.w), y = y })
end
end
---Set this output's location to the top of the specified output.
---
---```
--- left center right
--- ┌──────┐ ┌──────┐ ┌──────┐
--- │self │ │self │ │self │
--- ├──────┴─┐ ┌┴──────┴┐ ┌─┴──────┤
--- │op │ │op │ │op │
--- │ │ │ │ │ │
--- └────────┘ └────────┘ └────────┘
---```
---This will fail if `op` is an invalid output.
---@param op OutputHandle
---@param alignment AlignmentHorizontal? How you want to align the `self` output. Defaults to `left`.
---@see OutputHandle.set_loc if you need more granular control
function output_handle:set_loc_top_of(op, alignment)
set_loc_vertical(self, op, "top", alignment)
end
---Set this output's location to the bottom of the specified output.
---
---```
--- ┌────────┐ ┌────────┐ ┌────────┐
--- │op │ │op │ │op │
--- │ │ │ │ │ │
--- ├──────┬─┘ └┬──────┬┘ └─┬──────┤
--- │self │ │self │ │self │
--- └──────┘ └──────┘ └──────┘
--- left center right
---```
---This will fail if `op` is an invalid output.
---@param op OutputHandle
---@param alignment AlignmentHorizontal? How you want to align the `self` output. Defaults to `left`.
---@see OutputHandle.set_loc if you need more granular control
function output_handle:set_loc_bottom_of(op, alignment)
set_loc_vertical(self, op, "bottom", alignment)
end
------------------------------------------------------
---Get an output by its name.
---
---"Name" in this sense does not mean its model or manufacturer;
---rather, "name" is the name of the connector the output is connected to.
---This should be something like "HDMI-A-0", "eDP-1", or similar.
---
---### Example
---```lua
---local monitor = output.get_by_name("DP-1")
---print(monitor:name()) -- should print `DP-1`
---```
---@param name string The name of the output.
---@return OutputHandle output The output. If it doesn't exist, a dummy handle with the name "" will be returned.
function output.get_by_name(name)
local response = Request("GetOutputs")
local output_names = response.RequestResponse.response.Outputs.output_names
for _, output_name in pairs(output_names) do
if output_name == name then
return create_output(output_name)
end
end
return create_output("")
end
---Get outputs by their model.
---
---Note: This may or may not be what is reported by other monitor listing utilities.
---Pinnacle currently fails to pick up one of my monitor's models when it is correctly
---picked up by tools like wlr-randr. I'll fix this in the future.
---
---This is something like "DELL E2416H" or whatever gibberish monitor manufacturers call their displays.
---@param model string The model of the output(s).
---@return OutputHandle[] outputs All outputs with this model.
function output.get_by_model(model)
local response = Request("GetOutputs")
local output_names = response.RequestResponse.response.Outputs.output_names
---@type OutputHandle[]
local outputs = {}
for _, output_name in pairs(output_names) do
local o = create_output(output_name)
if o:model() == model then
table.insert(outputs, o)
end
end
return outputs
end
---Get outputs by their resolution.
---
---@param width integer The width of the outputs, in pixels.
---@param height integer The height of the outputs, in pixels.
---@return OutputHandle[] outputs All outputs with this resolution.
function output.get_by_res(width, height)
local response = Request("GetOutputs")
local output_names = response.RequestResponse.response.Outputs.output_names
---@type OutputHandle[]
local outputs = {}
for _, output_name in pairs(output_names) do
local o = create_output(output_name)
if o:res() and o:res().w == width and o:res().h == height then
table.insert(outputs, o)
end
end
return outputs
end
---Get the currently focused output. This is currently implemented as the one with the cursor on it.
---
---If you have no outputs plugged in, this will return a dummy `OutputHandle` with the name "".
---@return OutputHandle output The output, or a dummy handle if none are focused.
function output.get_focused()
local response = Request("GetOutputs")
local output_names = response.RequestResponse.response.Outputs.output_names
for _, output_name in pairs(output_names) do
local o = create_output(output_name)
if o:focused() then
return o
end
end
return create_output("")
end
---Connect a function to be run on all current and future outputs.
---
---When called, `connect_for_all` will immediately run `func` with all currently connected outputs.
---If a new output is connected, `func` will also be called with it.
---
---This will *not* be called if it has already been called for a given connector.
---This means turning your monitor off and on or unplugging and replugging it *to the same port*
---won't trigger `func`. Plugging it in to a new port *will* trigger `func`.
---This is intended to prevent duplicate setup.
---
---Please note: this function will be run *after* Pinnacle processes your entire config.
---For example, if you define tags in `func` but toggle them directly after `connect_for_all`,
---nothing will happen as the tags haven't been added yet.
---@param func fun(output: OutputHandle) The function that will be run.
function output.connect_for_all(func)
---@param args Args
table.insert(CallbackTable, function(args)
local args = args.ConnectForAllOutputs
func(create_output(args.output_name))
end)
SendMsg({
ConnectForAllOutputs = {
callback_id = #CallbackTable,
},
})
end
---Get the output the specified tag is on.
---@param tag TagHandle
---@return OutputHandle
---@see Tag.output — A global method for fully qualified syntax (for you Rustaceans out there)
---@see TagHandle.output — The corresponding object method
function output.get_for_tag(tag)
local response = Request({
GetTagProps = {
tag_id = tag:id(),
},
})
local output_name = response.RequestResponse.response.TagProps.output_name
return create_output(output_name or "")
end
---------Fully-qualified functions
---Get the specified output's make.
---@param op OutputHandle|string The name of the output or an output handle.
---@return string|nil
---@see OutputHandle.make — The corresponding object method
function output.make(op)
local op = create_output_from_params(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
return props.make
end
---Get the specified output's model.
---@param op OutputHandle|string The name of the output or an output object.
---@return string|nil
---@see OutputHandle.model — The corresponding object method
function output.model(op)
local op = create_output_from_params(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
return props.model
end
---Get the specified output's location in the global space, in pixels.
---@param op OutputHandle|string The name of the output or an output object.
---@return { x: integer, y: integer }|nil
---@see OutputHandle.loc — The corresponding object method
function output.loc(op)
local op = create_output_from_params(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
if props.loc == nil then
return nil
else
return { x = props.loc[1], y = props.loc[2] }
end
end
---Get the specified output's resolution in pixels.
---@param op OutputHandle|string The name of the output or an output object.
---@return { w: integer, h: integer }|nil
---@see OutputHandle.res — The corresponding object method
function output.res(op)
local op = create_output_from_params(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
if props.res == nil then
return nil
else
return { w = props.res[1], h = props.res[2] }
end
end
---Get the specified output's refresh rate in millihertz.
---For example, 60Hz will be returned as 60000.
---@param op OutputHandle|string The name of the output or an output object.
---@return integer|nil
---@see OutputHandle.refresh_rate — The corresponding object method
function output.refresh_rate(op)
local op = create_output_from_params(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
return props.refresh_rate
end
---Get the specified output's physical size in millimeters.
---@param op OutputHandle|string The name of the output or an output object.
---@return { w: integer, h: integer }|nil
---@see OutputHandle.physical_size — The corresponding object method
function output.physical_size(op)
local op = create_output_from_params(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
if props.physical_size == nil then
return nil
else
return { w = props.physical_size[1], h = props.physical_size[2] }
end
end
---Get whether or not the specified output is focused. This is currently defined as having the cursor on it.
---@param op OutputHandle|string The name of the output or an output object.
---@return boolean|nil
---@see OutputHandle.focused — The corresponding object method
function output.focused(op)
local op = create_output_from_params(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
return props.focused
end
---Get the specified output's tags.
---@param op OutputHandle|string The name of the output or an output object.
---@return TagHandle[]
---@see Tag.get_on_output — The called function
---@see OutputHandle.tags — The corresponding object method
function output.tags(op)
local op = create_output_from_params(op)
return require("tag").get_on_output(op)
end
---Add tags to the specified output.
---@param op OutputHandle|string The name of the output or an output object.
---@param ... string The names of the tags you want to add. You can also pass in a table.
---@overload fun(op: OutputHandle|string, tag_names: string[])
---@see Tag.add — The called function
---@see OutputHandle.add_tags — The corresponding object method
function output.add_tags(op, ...)
local op = create_output_from_params(op)
require("tag").add(op, ...)
end
---Set the specified output's location.
---
---### Examples
---```lua
--- -- 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
---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
---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
---output.set_loc(dp1, { x = 0, y = 0 })
---output.set_loc(dp2, { x = 2560, y = 1440 - 1080 })
---```
---@param op OutputHandle|string The name of the output or an output object.
---@param loc { x: integer?, y: integer? }
function output.set_loc(op, loc)
local op = create_output_from_params(op)
SendMsg({
SetOutputLocation = {
output_name = op:name(),
x = loc.x,
y = loc.y,
},
})
end
return output