mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-02-05 20:46:27 +01:00
482 lines
14 KiB
Lua
482 lines
14 KiB
Lua
-- 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/.
|
|
|
|
local client = require("pinnacle.grpc.client")
|
|
|
|
---The protobuf absolute path prefix
|
|
local prefix = "pinnacle.output." .. client.version .. "."
|
|
local service = prefix .. "OutputService"
|
|
|
|
---@type table<string, { request_type: string?, response_type: string? }>
|
|
---@enum (key) OutputServiceMethod
|
|
local rpc_types = {
|
|
SetLocation = {},
|
|
ConnectForAll = {
|
|
response_type = "ConnectForAllResponse",
|
|
},
|
|
Get = {
|
|
response_type = "GetResponse",
|
|
},
|
|
GetProperties = {
|
|
response_type = "GetPropertiesResponse",
|
|
},
|
|
}
|
|
|
|
---Build GrpcRequestParams
|
|
---@param method OutputServiceMethod
|
|
---@param data table
|
|
---@return GrpcRequestParams
|
|
local function build_grpc_request_params(method, data)
|
|
local req_type = rpc_types[method].request_type
|
|
local resp_type = rpc_types[method].response_type
|
|
|
|
---@type GrpcRequestParams
|
|
return {
|
|
service = service,
|
|
method = method,
|
|
request_type = req_type and prefix .. req_type or prefix .. method .. "Request",
|
|
response_type = resp_type and prefix .. resp_type,
|
|
data = data,
|
|
}
|
|
end
|
|
|
|
---@nodoc
|
|
---@class OutputHandleModule
|
|
local output_handle = {}
|
|
|
|
---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
|
|
---@field name string The unique name of this output
|
|
local OutputHandle = {}
|
|
|
|
---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
|
|
---@field private handle OutputHandleModule
|
|
local output = {}
|
|
output.handle = output_handle
|
|
|
|
---Get all outputs.
|
|
---
|
|
---### Example
|
|
---```lua
|
|
---local outputs = Output.get_all()
|
|
---```
|
|
---
|
|
---@return OutputHandle[]
|
|
function output.get_all()
|
|
-- Not going to batch these because I doubt people would have that many monitors
|
|
|
|
local response = client.unary_request(build_grpc_request_params("Get", {}))
|
|
|
|
---@type OutputHandle[]
|
|
local handles = {}
|
|
|
|
for _, output_name in ipairs(response.output_names or {}) do
|
|
table.insert(handles, output_handle.new(output_name))
|
|
end
|
|
|
|
return handles
|
|
end
|
|
|
|
---Get an output by its name (the connector it's plugged into).
|
|
---
|
|
---### Example
|
|
---```lua
|
|
---local output = Output.get_by_name("eDP-1")
|
|
---```
|
|
---
|
|
---@param name string The name of the connector the output is connected to
|
|
---@return OutputHandle | nil
|
|
function output.get_by_name(name)
|
|
local handles = output.get_all()
|
|
|
|
for _, handle in ipairs(handles) do
|
|
if handle.name == name then
|
|
return handle
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
---Get the currently focused output.
|
|
---
|
|
---This is currently defined as the most recent one that has had pointer motion.
|
|
---
|
|
---### Example
|
|
---```lua
|
|
---local output = Output.get_focused()
|
|
---```
|
|
---
|
|
---@return OutputHandle | nil
|
|
function output.get_focused()
|
|
local handles = output.get_all()
|
|
|
|
for _, handle in ipairs(handles) do
|
|
if handle:props().focused then
|
|
return handle
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
---Connect a function to be run with all current and future outputs.
|
|
---
|
|
---This method does two things:
|
|
---1. Immediately runs `callback` with all currently connected outputs.
|
|
---2. Calls `callback` whenever a new output is plugged in.
|
|
---
|
|
---This will *not* run `callback` with an output that has been unplugged and replugged
|
|
---to prevent duplicate setup. Instead, the compositor keeps track of the tags and other
|
|
---state associated with that output and restores it when replugged.
|
|
---
|
|
---### Example
|
|
---```lua
|
|
--- -- Add tags "1" through "5" to all outputs
|
|
---Output.connect_for_all(function(output)
|
|
--- local tags = Tag.add(output, "1", "2", "3", "4", "5")
|
|
--- tags[1]:toggle_active()
|
|
---end)
|
|
---```
|
|
---
|
|
---@param callback fun(output: OutputHandle)
|
|
function output.connect_for_all(callback)
|
|
local handles = output.get_all()
|
|
for _, handle in ipairs(handles) do
|
|
callback(handle)
|
|
end
|
|
|
|
output.connect_signal({
|
|
connect = callback,
|
|
})
|
|
end
|
|
|
|
local signal_name_to_SignalName = {
|
|
connect = "OutputConnect",
|
|
}
|
|
|
|
---@class OutputSignal
|
|
---@field connect fun(output: OutputHandle)?
|
|
|
|
---@param signals OutputSignal
|
|
---@return SignalHandles
|
|
function output.connect_signal(signals)
|
|
---@diagnostic disable-next-line: invisible
|
|
local handles = require("pinnacle.signal").handles.new({})
|
|
|
|
for signal, callback in pairs(signals) do
|
|
require("pinnacle.signal").add_callback(signal_name_to_SignalName[signal], callback)
|
|
---@diagnostic disable-next-line: invisible
|
|
local handle = require("pinnacle.signal").handle.new(signal_name_to_SignalName[signal], callback)
|
|
handles[signal] = handle
|
|
end
|
|
|
|
return handles
|
|
end
|
|
|
|
---------------------------------------------------------------------
|
|
|
|
---Set the location of this output in the global space.
|
|
---
|
|
---On startup, Pinnacle will lay out all connected outputs starting at (0, 0)
|
|
---and going to the right, with their top borders aligned.
|
|
---
|
|
---This method allows you to move outputs where necessary.
|
|
---
|
|
---Note: If you have space between two outputs when setting their locations,
|
|
---the pointer will not be able to move between them.
|
|
---
|
|
---### Example
|
|
---```lua
|
|
--- -- Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions:
|
|
--- -- - "DP-1": ┌─────┐
|
|
--- -- │ │1920x1080
|
|
--- -- └─────┘
|
|
--- -- - "HDMI-1": ┌───────┐
|
|
--- -- │ 2560x │
|
|
--- -- │ 1440 │
|
|
--- -- └───────┘
|
|
---Output.get_by_name("DP-1"):set_location({ x = 0, y = 0 })
|
|
---Output.get_by_name("HDMI-1"):set_location({ x = 1920, y = -360 })
|
|
--- -- Results in:
|
|
--- -- ┌───────┐
|
|
--- -- ┌─────┤ │
|
|
--- -- │DP-1 │HDMI-1 │
|
|
--- -- └─────┴───────┘
|
|
--- -- Notice that y = 0 aligns with the top of "DP-1", and the top of "HDMI-1" is at y = -360.
|
|
---```
|
|
---
|
|
---@param loc { x: integer?, y: integer? }
|
|
---
|
|
---@see OutputHandle.set_loc_adj_to
|
|
function OutputHandle:set_location(loc)
|
|
client.unary_request(build_grpc_request_params("SetLocation", {
|
|
output_name = self.name,
|
|
x = loc.x,
|
|
y = loc.y,
|
|
}))
|
|
end
|
|
|
|
---@alias Alignment
|
|
---| "top_align_left" Set above, align left borders
|
|
---| "top_align_center" Set above, align centers
|
|
---| "top_align_right" Set above, align right borders
|
|
---| "bottom_align_left" Set below, align left borders
|
|
---| "bottom_align_center" Set below, align centers
|
|
---| "bottom_align_right" Set below, align right border
|
|
---| "left_align_top" Set to left, align top borders
|
|
---| "left_align_center" Set to left, align centers
|
|
---| "left_align_bottom" Set to left, align bottom borders
|
|
---| "right_align_top" Set to right, align top borders
|
|
---| "right_align_center" Set to right, align centers
|
|
---| "right_align_bottom" Set to right, align bottom borders
|
|
|
|
---Set the location of this output adjacent to another one.
|
|
---
|
|
---`alignment` is how you want this output to be placed.
|
|
---For example, "top_align_left" will place this output above `other` and align the left borders.
|
|
---Similarly, "right_align_center" will place this output to the right of `other` and align their centers.
|
|
---
|
|
---### Example
|
|
---```lua
|
|
--- -- Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions:
|
|
--- -- - "DP-1": ┌─────┐
|
|
--- -- │ │1920x1080
|
|
--- -- └─────┘
|
|
--- -- - "HDMI-1": ┌───────┐
|
|
--- -- │ 2560x │
|
|
--- -- │ 1440 │
|
|
--- -- └───────┘
|
|
---Output.get_by_name("DP-1"):set_loc_adj_to(Output:get_by_name("HDMI-1"), "bottom_align_right")
|
|
--- -- Results in:
|
|
--- -- ┌───────┐
|
|
--- -- │ │
|
|
--- -- │HDMI-1 │
|
|
--- -- └──┬────┤
|
|
--- -- │DP-1│
|
|
--- -- └────┘
|
|
--- -- Notice that "DP-1" now has the coordinates (2280, 1440) because "DP-1" is getting moved, not "HDMI-1".
|
|
--- -- "HDMI-1" was placed at (1920, 0) during the compositor's initial output layout.
|
|
---```
|
|
---
|
|
---@param other OutputHandle
|
|
---@param alignment Alignment
|
|
function OutputHandle:set_loc_adj_to(other, alignment)
|
|
local self_props = self:props()
|
|
local other_props = other:props()
|
|
|
|
if not self_props.x or not other_props.x then
|
|
-- TODO: notify
|
|
return
|
|
end
|
|
|
|
local alignment_parts = {}
|
|
|
|
for str in alignment:gmatch("%a+") do
|
|
table.insert(alignment_parts, str)
|
|
end
|
|
|
|
---@type "top"|"bottom"|"left"|"right"
|
|
local dir = alignment_parts[1]
|
|
---@type "top"|"bottom"|"center"|"left"|"right"
|
|
local align = alignment_parts[3]
|
|
|
|
---@type integer
|
|
local x
|
|
---@type integer
|
|
local y
|
|
|
|
if dir == "top" or dir == "bottom" then
|
|
if dir == "top" then
|
|
y = other_props.y - self_props.pixel_height
|
|
else
|
|
y = other_props.y + other_props.pixel_height
|
|
end
|
|
|
|
if align == "left" then
|
|
x = other_props.x
|
|
elseif align == "center" then
|
|
x = other_props.x + math.floor((other_props.pixel_width - self_props.pixel_width) / 2)
|
|
elseif align == "bottom" then
|
|
x = other_props.x + (other_props.pixel_width - self_props.pixel_width)
|
|
end
|
|
else
|
|
if dir == "left" then
|
|
x = other_props.x - self_props.pixel_width
|
|
else
|
|
x = other_props.x + other_props.pixel_width
|
|
end
|
|
|
|
if align == "top" then
|
|
y = other_props.y
|
|
elseif align == "center" then
|
|
y = other_props.y + math.floor((other_props.pixel_height - self_props.pixel_height) / 2)
|
|
elseif align == "bottom" then
|
|
y = other_props.y + (other_props.pixel_height - self_props.pixel_height)
|
|
end
|
|
end
|
|
|
|
self:set_location({ x = x, y = y })
|
|
end
|
|
|
|
---@class OutputProperties
|
|
---@field make string?
|
|
---@field model string?
|
|
---@field x integer?
|
|
---@field y integer?
|
|
---@field pixel_width integer?
|
|
---@field pixel_height integer?
|
|
---@field refresh_rate integer?
|
|
---@field physical_width integer?
|
|
---@field physical_height integer?
|
|
---@field focused boolean?
|
|
---@field tags TagHandle[]?
|
|
|
|
---Get all properties of this output.
|
|
---
|
|
---@return OutputProperties
|
|
function OutputHandle:props()
|
|
local response = client.unary_request(build_grpc_request_params("GetProperties", { output_name = self.name }))
|
|
|
|
---@diagnostic disable-next-line: invisible
|
|
local handles = require("pinnacle.tag").handle.new_from_table(response.tag_ids or {})
|
|
|
|
response.tags = handles
|
|
response.tag_ids = nil
|
|
|
|
return response
|
|
end
|
|
|
|
---Get this output's make.
|
|
---
|
|
---Note: make and model detection are currently somewhat iffy and may not work.
|
|
---
|
|
---Shorthand for `handle:props().make`.
|
|
---
|
|
---@return string?
|
|
function OutputHandle:make()
|
|
return self:props().make
|
|
end
|
|
|
|
---Get this output's model.
|
|
---
|
|
---Note: make and model detection are currently somewhat iffy and may not work.
|
|
---
|
|
---Shorthand for `handle:props().model`.
|
|
---
|
|
---@return string?
|
|
function OutputHandle:model()
|
|
return self:props().model
|
|
end
|
|
|
|
---Get this output's x-coordinate in the global space.
|
|
---
|
|
---Shorthand for `handle:props().x`.
|
|
---
|
|
---@return integer?
|
|
function OutputHandle:x()
|
|
return self:props().x
|
|
end
|
|
|
|
---Get this output's y-coordinate in the global space.
|
|
---
|
|
---Shorthand for `handle:props().y`.
|
|
---
|
|
---@return integer?
|
|
function OutputHandle:y()
|
|
return self:props().y
|
|
end
|
|
|
|
---Get this output's width in pixels.
|
|
---
|
|
---Shorthand for `handle:props().pixel_width`.
|
|
---
|
|
---@return integer?
|
|
function OutputHandle:pixel_width()
|
|
return self:props().pixel_width
|
|
end
|
|
|
|
---Get this output's height in pixels.
|
|
---
|
|
---Shorthand for `handle:props().pixel_height`.
|
|
---
|
|
---@return integer?
|
|
function OutputHandle:pixel_height()
|
|
return self:props().pixel_height
|
|
end
|
|
|
|
---Get this output's refresh rate in millihertz.
|
|
---
|
|
---For example, 144Hz is returned as 144000.
|
|
---
|
|
---Shorthand for `handle:props().refresh_rate`.
|
|
---
|
|
---@return integer?
|
|
function OutputHandle:refresh_rate()
|
|
return self:props().refresh_rate
|
|
end
|
|
|
|
---Get this output's physical width in millimeters.
|
|
---
|
|
---Shorthand for `handle:props().physical_width`.
|
|
---
|
|
---@return integer?
|
|
function OutputHandle:physical_width()
|
|
return self:props().physical_width
|
|
end
|
|
|
|
---Get this output's physical height in millimeters.
|
|
---
|
|
---Shorthand for `handle:props().physical_height`.
|
|
---
|
|
---@return integer?
|
|
function OutputHandle:physical_height()
|
|
return self:props().physical_height
|
|
end
|
|
|
|
---Get whether or not this output is focused.
|
|
---
|
|
---The focused output is currently implemented as the one that last had pointer motion.
|
|
---
|
|
---Shorthand for `handle:props().focused`.
|
|
---
|
|
---@return boolean?
|
|
function OutputHandle:focused()
|
|
return self:props().focused
|
|
end
|
|
|
|
---Get the tags this output has.
|
|
---
|
|
---Shorthand for `handle:props().tags`.
|
|
---
|
|
---@return TagHandle[]?
|
|
function OutputHandle:tags()
|
|
return self:props().tags
|
|
end
|
|
|
|
---@nodoc
|
|
---Create a new `OutputHandle` from its raw name.
|
|
---@param output_name string
|
|
function output_handle.new(output_name)
|
|
---@type OutputHandle
|
|
local self = {
|
|
name = output_name,
|
|
}
|
|
setmetatable(self, { __index = OutputHandle })
|
|
return self
|
|
end
|
|
|
|
return output
|