Merge pull request #201 from pinnacle-comp/api_enhancements

Add various API enhancements
This commit is contained in:
Ottatop 2024-04-18 16:54:54 -05:00 committed by GitHub
commit 9af11d7b6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 2920 additions and 463 deletions

2
Cargo.lock generated
View file

@ -1767,6 +1767,7 @@ dependencies = [
"image",
"nix",
"pinnacle",
"pinnacle-api",
"pinnacle-api-defs",
"prost",
"serde",
@ -1796,6 +1797,7 @@ dependencies = [
name = "pinnacle-api"
version = "0.0.2"
dependencies = [
"bitflags 2.5.0",
"futures",
"num_enum",
"pinnacle-api-defs",

View file

@ -20,6 +20,7 @@ pinnacle-api-defs = { path = "./pinnacle-api-defs" }
# Misc.
xkbcommon = "0.7.0"
xdg = "2.5.2"
bitflags = "2.5.0"
########################################################################yo😎###########
@ -59,7 +60,7 @@ tokio-stream = { workspace = true }
clap = { version = "4.5.4", features = ["derive"] }
cliclack = "0.2.5"
# Misc.
bitflags = "2.5.0"
bitflags = { workspace = true }
serde = { version = "1.0.197", features = ["derive"] }
toml = "0.8.12"
shellexpand = { version = "3.1.0", features = ["path"] }
@ -109,6 +110,7 @@ temp-env = "0.3.6"
tempfile = "3.10.1"
test-log = { version = "0.2.15", default-features = false, features = ["trace"] }
pinnacle = { path = ".", features = ["testing"] }
pinnacle-api = { path = "./api/rust" }
[features]
testing = [

View file

@ -36,6 +36,11 @@ require("pinnacle").setup(function(Pinnacle)
Pinnacle.quit()
end)
-- mod_key + alt + r = Reload config
Input.keybind({ mod_key, "alt" }, "r", function()
Pinnacle.reload_config()
end)
-- mod_key + alt + c = Close window
Input.keybind({ mod_key, "alt" }, "c", function()
local focused = Window.get_focused()
@ -76,18 +81,26 @@ require("pinnacle").setup(function(Pinnacle)
end
end)
--------------------
-- Tags --
--------------------
----------------------
-- Tags and Outputs --
----------------------
local tag_names = { "1", "2", "3", "4", "5" }
-- `connect_for_all` is useful for performing setup on every monitor you have.
-- Here, we add tags with names 1-5 and set tag 1 as active.
Output.connect_for_all(function(op)
local tags = Tag.add(op, tag_names)
tags[1]:set_active(true)
end)
-- Setup outputs.
--
-- `Output.setup` allows you to declare things like mode, scale, and tags for outputs.
-- Here we give all outputs tags 1 through 5.
Output.setup({
-- "*" matches all outputs
["*"] = { tags = tag_names },
})
-- If you want to declare output locations as well, you can use `Output.setup_locs`.
-- This will additionally allow you to recalculate output locations on signals like
-- output connect, disconnect, and resize.
--
-- Read the admittedly scuffed docs for more.
-- Tag keybinds
for _, tag_name in ipairs(tag_names) do
@ -246,6 +259,10 @@ require("pinnacle").setup(function(Pinnacle)
end
end)
Input.set_libinput_settings({
tap = true,
})
-- Enable sloppy focus
Window.connect_signal({
pointer_enter = function(window)

View file

@ -37,6 +37,16 @@ function pinnacle.quit()
})
end
---Reload the active config.
function pinnacle.reload_config()
client.unary_request({
service = "pinnacle.v0alpha1.PinnacleService",
method = "ReloadConfig",
request_type = "pinnacle.v0alpha1.ReloadConfigRequest",
data = {},
})
end
---Setup a Pinnacle config.
---
---You must pass in a function that takes in the `Pinnacle` table. This table is how you'll access the other config modules.

View file

@ -14,6 +14,7 @@ local rpc_types = {
SetLocation = {},
SetMode = {},
SetScale = {},
SetTransform = {},
ConnectForAll = {
response_type = "ConnectForAllResponse",
},
@ -166,12 +167,465 @@ function output.connect_for_all(callback)
})
end
---@param id_str string
---@param op OutputHandle
---
---@return boolean
local function output_id_matches(id_str, op)
if id_str:match("^serial:") then
local serial = tonumber(id_str:sub(8))
return serial and serial == op:serial() or false
else
return id_str == op.name
end
end
---@class OutputSetup
---@field filter (fun(output: OutputHandle): boolean)? -- A filter for wildcard matches that should return true if this setup should apply to the passed in output.
---@field mode Mode? -- Makes this setup apply the given mode to outputs.
---@field scale number? -- Makes this setup apply the given scale to outputs.
---@field tags string[]? -- Makes this setup add tags with the given name to outputs.
---@field transform Transform? -- Makes this setup applt the given transform to outputs.
---Declaratively setup outputs.
---
---`Output.setup` allows you to specify output properties that will be applied immediately and
---on output connection. These include mode, scale, tags, and more.
---
---`setups` is a table of output identifier strings to `OutputSetup`s.
---
---### Keys
---
---Keys attempt to match outputs.
---
---Wildcard keys (`"*"`) will match all outputs. You can additionally filter these outputs
---by setting a `filter` function in the setup that returns true if it should apply to the output.
---(See the example.)
---
---Otherwise, keys will attempt to match the exact name of an output.
---
---Use "serial:<number>" to match outputs by their EDID serial. For example, "serial:143256".
---Note that not all displays have EDID serials. Also, serials are not guaranteed to be unique.
---If you're unlucky enough to have two displays with the same serial, you'll have to use their names
---or filter with wildcards instead.
---
---### Setups
---
---If an output is matched, the corresponding `OutputSetup` entry will be applied to it.
---Any given `tags` will be added, and things like `transform`s, `scale`s, and `mode`s will be set.
---
---### Ordering setups
---
---You may need to specify multiple wildcard matches for different setup applications.
---You can't just add another key of `"*"`, because that would overwrite the old `"*"`.
---In this case, you can order setups by prepending `n:` to the key, where n is an ordering number.
---`n` should be between `1` and `#setups`. Setting higher orders without setting lower ones
---will cause entries without orders to fill up lower numbers in an arbitrary order. Setting
---orders above `#setups` may cause their entries to not apply.
---
---
---### Example
---```lua
---Output.setup({
--- -- Give all outputs tags 1 through 5
--- ["1:*"] = {
--- tags = { "1", "2", "3", "4", "5" },
--- },
--- -- Give outputs with a preferred mode of 4K a scale of 2.0
--- ["2:*"] = {
--- filter = function(op)
--- return op:preferred_mode().pixel_height == 2160
--- end,
--- scale = 2.0,
--- },
--- -- Additionally give eDP-1 tags 6 and 7
--- ["eDP-1"] = {
--- tags = { "6", "7" },
--- },
--- -- Match an output by its EDID serial number
--- ["serial:235987"] = { ... }
---})
---```
---
---@param setups table<string, OutputSetup>
function output.setup(setups)
---@type { [1]: string, setup: OutputSetup }[]
local op_setups = {}
local setup_len = 0
-- Index entries with an index
for op_id, op_setup in pairs(setups) do
setup_len = setup_len + 1
---@type string|nil
if op_id:match("^%d+:") then
---@type string
local index = op_id:match("^%d+")
---@diagnostic disable-next-line: redefined-local
local op_id = op_id:sub(index:len() + 2)
---@diagnostic disable-next-line: redefined-local
local index = tonumber(index)
---@cast index number
op_setups[index] = { op_id, setup = op_setup }
end
end
-- Insert *s first
for op_id, op_setup in pairs(setups) do
if op_id:match("^*$") then
-- Fill up holes if there are any
for i = 1, setup_len do
if not op_setups[i] then
op_setups[i] = { op_id, setup = op_setup }
break
end
end
end
end
-- Insert rest of the entries
for op_id, op_setup in pairs(setups) do
if not op_id:match("^%d+:") and op_id ~= "*" then
-- Fill up holes if there are any
for i = 1, setup_len do
if not op_setups[i] then
op_setups[i] = { op_id, setup = op_setup }
break
end
end
end
end
---@param op OutputHandle
local function apply_setups(op)
for _, op_setup in ipairs(op_setups) do
if output_id_matches(op_setup[1], op) or op_setup[1] == "*" then
local setup = op_setup.setup
if setup.filter and not setup.filter(op) then
goto continue
end
if setup.mode then
op:set_mode(setup.mode.pixel_width, setup.mode.pixel_height, setup.mode.refresh_rate_millihz)
end
if setup.scale then
op:set_scale(setup.scale)
end
if setup.tags then
require("pinnacle.tag").add(op, setup.tags)
end
if setup.transform then
op:set_transform(setup.transform)
end
end
::continue::
end
local tags = op:tags() or {}
if tags[1] then
tags[1]:set_active(true)
end
end
output.connect_for_all(function(op)
apply_setups(op)
end)
end
---@alias OutputLoc
---| { [1]: integer, [2]: integer } -- A specific point
---| { [1]: string, [2]: Alignment } -- A location relative to another output
---@alias UpdateLocsOn
---| "connect" -- Update output locations on output connect
---| "disconnect" -- Update output locations on output disconnect
---| "resize" -- Update output locations on output resize
---Setup locations for outputs.
---
---This function lets you declare positions for outputs, either as a specific point in the global
---space or relative to another output.
---
---### Choosing when to recompute output positions
---
---`update_locs_on` specifies when output positions should be recomputed. It can be `"all"`, signaling you
---want positions to update on all of output connect, disconnect, and resize, or it can be a table
---containing `"connect"`, `"disconnect"`, and/or `"resize"`.
---
---### Specifying locations
---
---`locs` should be a table of output identifiers to locations.
---
---#### Output identifiers
---
---Keys for `locs` should be output identifiers. These are strings of
---the name of the output, for example "eDP-1" or "HDMI-A-1".
---Additionally, if you want to match the EDID serial of an output,
---prepend the serial with "serial:", for example "serial:174652".
---You can find this by doing `get-edid | edid-decode`.
---
---#### Fallback relative-tos
---
---Sometimes you have an output with a relative location, but the output
---it's relative to isn't connected. In this case you can specify an
---order that locations will be placed by prepending "n:" to the key.
---For example, "4:HDMI-1" will be applied before "5:HDMI-1", allowing
---you to specify more than one relative output. The first connected
---relative output will be chosen for placement. See the example below.
---
---### Example
---```lua
--- -- vvvvv Relayout on output connect, disconnect, and resize
---Output.setup_locs("all", {
--- -- Anchor eDP-1 to (0, 0) so we can place other outputs relative to it
--- ["eDP-1"] = { 0, 0 },
--- -- Place HDMI-A-1 below it centered
--- ["HDMI-A-1"] = { "eDP-1", "bottom_align_center" },
--- -- Place HDMI-A-2 below HDMI-A-1.
--- ["3:HDMI-A-2"] = { "HDMI-A-1", "bottom_align_center" },
--- -- Additionally, if HDMI-A-1 isn't connected, fallback to placing it below eDP-1 instead.
--- ["4:HDMI-A-2"] = { "eDP-1", "bottom_align_center" },
---
--- -- Note that the last two have a number followed by a colon. This dictates the order of application.
--- -- Because Lua tables with string keys don't index by declaration order, this is needed to specify that.
--- -- You can also put a "1:" and "2:" in front of "eDP-1" and "HDMI-A-1" if you want to be explicit
--- -- about their ordering.
--- --
--- -- Just note that orders must be from 1 to the length of the array. Entries without an order
--- -- will be filled in from 1 upwards, taking any open slots. Entries with orders above
--- -- #locs may not be applied.
---})
---
--- -- Only relayout on output connect and resize
---Output.setup_locs({ "connect", "resize" }, { ... })
---
--- -- Use EDID serials for identification.
--- -- You can run
--- -- require("pinnacle").run(function(Pinnacle)
--- -- print(Pinnacle.output.get_focused():serial())
--- -- end)
--- -- in a Lua repl to find the EDID serial of the focused output.
---Output.setup_locs("all" {
--- ["serial:139487"] = { ... },
---})
---```
---
---@param update_locs_on (UpdateLocsOn)[] | "all"
---@param locs table<string, OutputLoc>
function output.setup_locs(update_locs_on, locs)
---@type { [1]: string, loc: OutputLoc }[]
local setups = {}
local setup_len = 0
-- Index entries with an index
for op_id, op_loc in pairs(locs) do
setup_len = setup_len + 1
---@type string|nil
if op_id:match("^%d+:") then
---@type string
local index = op_id:match("^%d+")
---@diagnostic disable-next-line: redefined-local
local op_id = op_id:sub(index:len() + 2)
---@diagnostic disable-next-line: redefined-local
local index = tonumber(index)
---@cast index number
setups[index] = { op_id, loc = op_loc }
end
end
-- Insert rest of the entries
for op_id, op_loc in pairs(locs) do
if not op_id:match("^%d+:") then
-- Fill up holes if there are any
for i = 1, setup_len do
if not setups[i] then
setups[i] = { op_id, loc = op_loc }
break
end
end
end
end
local function layout_outputs()
local outputs = output.get_all()
---@type OutputHandle[]
local placed_outputs = {}
local rightmost_output = {
output = nil,
x = nil,
}
-- Place outputs with a specified location first
---@diagnostic disable-next-line: redefined-local
for _, setup in ipairs(setups) do
for _, op in ipairs(outputs) do
if output_id_matches(setup[1], op) then
if type(setup.loc[1]) == "number" then
local loc = { x = setup.loc[1], y = setup.loc[2] }
op:set_location(loc)
table.insert(placed_outputs, op)
local props = op:props()
if not rightmost_output.x or rightmost_output.x < props.x + props.logical_width then
rightmost_output.output = op
rightmost_output.x = props.x + props.logical_width
end
end
break
end
end
end
-- Place outputs that are relative to other outputs
local function next_output_with_relative_to()
---@diagnostic disable-next-line: redefined-local
for _, setup in ipairs(setups) do
for _, op in ipairs(outputs) do
for _, placed_op in ipairs(placed_outputs) do
if placed_op.name == op.name then
goto continue
end
end
if not output_id_matches(setup[1], op) or type(setup.loc[1]) == "number" then
goto continue
end
local relative_to_name = setup.loc[1]
local alignment = setup.loc[2]
for _, placed_op in ipairs(placed_outputs) do
if placed_op.name == relative_to_name then
return op, placed_op, alignment
end
end
goto continue_outer
::continue::
end
::continue_outer::
end
return nil, nil, nil
end
while true do
local op, relative_to, alignment = next_output_with_relative_to()
if not op then
break
end
---@cast relative_to OutputHandle
---@cast alignment Alignment
op:set_loc_adj_to(relative_to, alignment)
table.insert(placed_outputs, op)
local props = op:props()
if not rightmost_output.x or rightmost_output.x < props.x + props.logical_width then
rightmost_output.output = op
rightmost_output.x = props.x + props.logical_width
end
end
-- Place still-not-placed outputs
for _, op in ipairs(outputs) do
for _, placed_op in ipairs(placed_outputs) do
if placed_op.name == op.name then
goto continue
end
end
if not rightmost_output.output then
op:set_location({ x = 0, y = 0 })
else
op:set_loc_adj_to(rightmost_output.output, "right_align_top")
end
local props = op:props()
rightmost_output.output = op
rightmost_output.x = props.x
table.insert(placed_outputs, op)
::continue::
end
end
layout_outputs()
local layout_on_connect = false
local layout_on_disconnect = false
local layout_on_resize = false
if update_locs_on == "all" then
layout_on_connect = true
layout_on_disconnect = true
layout_on_resize = true
else
---@cast update_locs_on UpdateLocsOn[]
for _, update_on in ipairs(update_locs_on) do
if update_on == "connect" then
layout_on_connect = true
elseif update_on == "disconnect" then
layout_on_disconnect = true
elseif update_on == "resize" then
layout_on_resize = true
end
end
end
if layout_on_connect then
-- FIXME: This currently does not duplicate tags because the connect signal does not fire for
-- | previously connected outputs. However, this is unintended behavior, so fix this when you fix that.
output.connect_signal({
connect = function(_)
layout_outputs()
end,
})
end
if layout_on_disconnect then
output.connect_signal({
disconnect = function(_)
layout_outputs()
end,
})
end
if layout_on_resize then
output.connect_signal({
resize = function(_)
layout_outputs()
end,
})
end
end
---@type table<string, SignalServiceMethod>
local signal_name_to_SignalName = {
connect = "OutputConnect",
disconnect = "OutputDisconnect",
resize = "OutputResize",
move = "OutputMove",
}
---@class OutputSignal Signals related to output events.
---@field connect fun(output: OutputHandle)? An output was connected. FIXME: This currently does not fire for outputs that have been previously connected and disconnected.
---@field disconnect fun(output: OutputHandle)? An output was disconnected.
---@field resize fun(output: OutputHandle, logical_width: integer, logical_height: integer)? An output's logical size changed.
---@field move fun(output: OutputHandle, x: integer, y: integer)? An output moved.
---Connect to an output signal.
---
@ -411,6 +865,41 @@ function OutputHandle:decrease_scale(decrease_by)
client.unary_request(build_grpc_request_params("SetScale", { output_name = self.name, relative = -decrease_by }))
end
---@enum (key) Transform
local transform_name_to_code = {
normal = 1,
["90"] = 2,
["180"] = 3,
["270"] = 4,
flipped = 5,
flipped_90 = 6,
flipped_180 = 7,
flipped_270 = 8,
}
local transform_code_to_name = {
[1] = "normal",
[2] = "90",
[3] = "180",
[4] = "270",
[5] = "flipped",
[6] = "flipped_90",
[7] = "flipped_180",
[8] = "flipped_270",
}
---Set this output's transform.
---
---@param transform Transform
function OutputHandle:set_transform(transform)
client.unary_request(
build_grpc_request_params(
"SetTransform",
{ output_name = self.name, transform = transform_name_to_code[transform] }
)
)
end
---@class Mode
---@field pixel_width integer
---@field pixel_height integer
@ -431,6 +920,8 @@ end
---@field focused boolean?
---@field tags TagHandle[]
---@field scale number?
---@field transform Transform?
---@field serial integer?
---Get all properties of this output.
---
@ -444,6 +935,7 @@ function OutputHandle:props()
response.tags = handles
response.tag_ids = nil
response.modes = response.modes or {}
response.transform = transform_code_to_name[response.transform]
return response
end
@ -580,6 +1072,24 @@ function OutputHandle:scale()
return self:props().scale
end
---Get this output's transform.
---
---Shorthand for `handle:props().transform`.
---
---@return Transform?
function OutputHandle:transform()
return self:props().transform
end
---Get this output's EDID serial number.
---
---Shorthand for `handle:props().serial`.
---
---@return integer?
function OutputHandle:serial()
return self:props().serial
end
---@nodoc
---Create a new `OutputHandle` from its raw name.
---@param output_name string

View file

@ -14,12 +14,26 @@ local rpc_types = {
OutputConnect = {
response_type = "OutputConnectResponse",
},
OutputDisconnect = {
response_type = "OutputDisconnectResponse",
},
OutputResize = {
response_type = "OutputResizeResponse",
},
OutputMove = {
response_type = "OutputMoveResponse",
},
WindowPointerEnter = {
response_type = "WindowPointerEnterResponse",
},
WindowPointerLeave = {
response_type = "WindowPointerLeaveResponse",
},
TagActive = {
response_type = "TagActiveResponse",
},
}
---Build GrpcRequestParams
@ -62,6 +76,39 @@ local signals = {
---@type fun(response: table)
on_response = nil,
},
OutputDisconnect = {
---@nodoc
---@type H2Stream?
sender = nil,
---@nodoc
---@type (fun(output: OutputHandle))[]
callbacks = {},
---@nodoc
---@type fun(response: table)
on_response = nil,
},
OutputResize = {
---@nodoc
---@type H2Stream?
sender = nil,
---@nodoc
---@type (fun(output: OutputHandle, logical_width: integer, logical_height: integer))[]
callbacks = {},
---@nodoc
---@type fun(response: table)
on_response = nil,
},
OutputMove = {
---@nodoc
---@type H2Stream?
sender = nil,
---@nodoc
---@type (fun(output: OutputHandle, x: integer, y: integer))[]
callbacks = {},
---@nodoc
---@type fun(response: table)
on_response = nil,
},
WindowPointerEnter = {
---@nodoc
---@type H2Stream?
@ -84,6 +131,17 @@ local signals = {
---@type fun(response: table)
on_response = nil,
},
TagActive = {
---@nodoc
---@type H2Stream?
sender = nil,
---@nodoc
---@type (fun(tag: TagHandle, active: boolean))[]
callbacks = {},
---@nodoc
---@type fun(response: table)
on_response = nil,
},
}
signals.OutputConnect.on_response = function(response)
@ -94,6 +152,30 @@ signals.OutputConnect.on_response = function(response)
end
end
signals.OutputDisconnect.on_response = function(response)
---@diagnostic disable-next-line: invisible
local handle = require("pinnacle.output").handle.new(response.output_name)
for _, callback in ipairs(signals.OutputDisconnect.callbacks) do
callback(handle)
end
end
signals.OutputResize.on_response = function(response)
---@diagnostic disable-next-line: invisible
local handle = require("pinnacle.output").handle.new(response.output_name)
for _, callback in ipairs(signals.OutputResize.callbacks) do
callback(handle, response.logical_width, response.logical_height)
end
end
signals.OutputMove.on_response = function(response)
---@diagnostic disable-next-line: invisible
local handle = require("pinnacle.output").handle.new(response.output_name)
for _, callback in ipairs(signals.OutputMove.callbacks) do
callback(handle, response.x, response.y)
end
end
signals.WindowPointerEnter.on_response = function(response)
---@diagnostic disable-next-line: invisible
local window_handle = require("pinnacle.window").handle.new(response.window_id)
@ -112,6 +194,15 @@ signals.WindowPointerLeave.on_response = function(response)
end
end
signals.TagActive.on_response = function(response)
---@diagnostic disable-next-line: invisible
local tag_handle = require("pinnacle.tag").handle.new(response.tag_id)
for _, callback in ipairs(signals.TagActive.callbacks) do
callback(tag_handle, response.active)
end
end
-----------------------------------------------------------------------------
---@nodoc

View file

@ -210,6 +210,52 @@ function tag.remove(tags)
client.unary_request(build_grpc_request_params("Remove", { tag_ids = ids }))
end
---@type table<string, SignalServiceMethod>
local signal_name_to_SignalName = {
active = "TagActive",
}
---@class TagSignal Signals related to tag events.
---@field active fun(tag: TagHandle, active: boolean)? A tag was set to active or not active.
---Connect to a tag signal.
---
---The compositor sends signals about various events. Use this function to run a callback when
---some tag signal occurs.
---
---This function returns a table of signal handles with each handle stored at the same key used
---to connect to the signal. See `SignalHandles` for more information.
---
---# Example
---```lua
---Tag.connect_signal({
--- active = function(tag, active)
--- print("Activity for " .. tag:name() .. " was set to", active)
--- end
---})
---```
---
---@param signals TagSignal The signal you want to connect to
---
---@return SignalHandles signal_handles Handles to every signal you connected to wrapped in a table, with keys being the same as the connected signal.
---
---@see SignalHandles.disconnect_all - To disconnect from these signals
function tag.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
--------------------------------------------------------------
---Remove this tag.
---
---### Example
@ -280,6 +326,7 @@ end
---@field active boolean? Whether or not the tag is currently being displayed
---@field name string? The name of the tag
---@field output OutputHandle? The output the tag is on
---@field windows WindowHandle[] The windows that have this tag
---Get all properties of this tag.
---
@ -292,6 +339,8 @@ function TagHandle:props()
name = response.name,
---@diagnostic disable-next-line: invisible
output = response.output_name and require("pinnacle.output").handle.new(response.output_name),
---@diagnostic disable-next-line: invisible
windows = require("pinnacle.window").handle.new_from_table(response.window_ids or {}),
}
end
@ -322,6 +371,15 @@ function TagHandle:output()
return self:props().output
end
---Get the windows that have this tag.
---
---Shorthand for `handle:props().windows`.
---
---@return WindowHandle[]
function TagHandle:windows()
return self:props().windows
end
---@nodoc
---Create a new `TagHandle` from an id.
---@param tag_id integer

View file

@ -1,18 +0,0 @@
require("pinnacle").setup(function(Pinnacle)
local Input = Pinnacle.input
local Process = Pinnacle.process
local Output = Pinnacle.output
local Tag = Pinnacle.tag
local Window = Pinnacle.window
Input:keybind({ "shift" }, "f", function()
local focused = Window:get_focused()
if focused then
print(focused:fullscreen_or_maximized())
-- assert(focused:fullscreen_or_maximized() == "neither")
focused:set_fullscreen(true)
print(focused:fullscreen_or_maximized())
-- assert(focused:fullscreen_or_maximized() == "fullscreen")
end
end)
end)

View file

@ -10,6 +10,18 @@ message Mode {
optional uint32 refresh_rate_millihz = 3;
}
enum Transform {
TRANSFORM_UNSPECIFIED = 0;
TRANSFORM_NORMAL = 1;
TRANSFORM_90 = 2;
TRANSFORM_180 = 3;
TRANSFORM_270 = 4;
TRANSFORM_FLIPPED = 5;
TRANSFORM_FLIPPED_90 = 6;
TRANSFORM_FLIPPED_180 = 7;
TRANSFORM_FLIPPED_270 = 8;
}
message SetLocationRequest {
optional string output_name = 1;
optional int32 x = 2;
@ -32,6 +44,11 @@ message SetScaleRequest {
}
}
message SetTransformRequest {
optional string output_name = 1;
optional Transform transform = 2;
}
message GetRequest {}
message GetResponse {
repeated string output_names = 1;
@ -72,12 +89,18 @@ message GetPropertiesResponse {
optional bool focused = 10;
repeated uint32 tag_ids = 11;
optional float scale = 12;
optional Transform transform = 15;
// NULLABLE
//
// The EDID serial number of this output, if it exists.
optional uint32 serial = 16;
}
service OutputService {
rpc SetLocation(SetLocationRequest) returns (google.protobuf.Empty);
rpc SetMode(SetModeRequest) returns (google.protobuf.Empty);
rpc SetScale(SetScaleRequest) returns (google.protobuf.Empty);
rpc SetTransform(SetTransformRequest) returns (google.protobuf.Empty);
rpc Get(GetRequest) returns (GetResponse);
rpc GetProperties(GetPropertiesRequest) returns (GetPropertiesResponse);
}

View file

@ -16,6 +16,34 @@ message OutputConnectRequest {
message OutputConnectResponse {
optional string output_name = 1;
}
message OutputDisconnectRequest {
optional StreamControl control = 1;
}
message OutputDisconnectResponse {
optional string output_name = 1;
}
message OutputResizeRequest {
optional StreamControl control = 1;
}
// An output's logical size changed
message OutputResizeResponse {
optional string output_name = 1;
optional uint32 logical_width = 2;
optional uint32 logical_height = 3;
}
message OutputMoveRequest {
optional StreamControl control = 1;
}
// An output's location in the global space changed
message OutputMoveResponse {
optional string output_name = 1;
optional int32 x = 2;
optional int32 y = 3;
}
message WindowPointerEnterRequest {
optional StreamControl control = 1;
@ -33,8 +61,23 @@ message WindowPointerLeaveResponse {
optional uint32 window_id = 1;
}
message TagActiveRequest {
optional StreamControl control = 1;
}
message TagActiveResponse {
optional uint32 tag_id = 1;
// The tag was set to active or inactive.
optional bool active = 2;
}
service SignalService {
rpc OutputConnect(stream OutputConnectRequest) returns (stream OutputConnectResponse);
rpc OutputDisconnect(stream OutputDisconnectRequest) returns (stream OutputDisconnectResponse);
rpc OutputResize(stream OutputResizeRequest) returns (stream OutputResizeResponse);
rpc OutputMove(stream OutputMoveRequest) returns (stream OutputMoveResponse);
rpc WindowPointerEnter(stream WindowPointerEnterRequest) returns (stream WindowPointerEnterResponse);
rpc WindowPointerLeave(stream WindowPointerLeaveRequest) returns (stream WindowPointerLeaveResponse);
rpc TagActive(stream TagActiveRequest) returns (stream TagActiveResponse);
}

View file

@ -35,9 +35,14 @@ message GetPropertiesRequest {
optional uint32 tag_id = 1;
}
message GetPropertiesResponse {
// Whether or not this tag is active
optional bool active = 1;
// The name of this tag
optional string name = 2;
// The output this tag is on
optional string output_name = 3;
// All windows that have this tag
repeated uint32 window_ids = 4;
}
service TagService {

View file

@ -21,6 +21,8 @@ enum SetOrToggle {
message QuitRequest {}
message ReloadConfigRequest {}
// A manual ping request independent of any HTTP keepalive.
//
// Tonic does not seems to give you the means to run something
@ -36,5 +38,6 @@ message PingResponse {
service PinnacleService {
rpc Quit(QuitRequest) returns (google.protobuf.Empty);
rpc ReloadConfig(ReloadConfigRequest) returns (google.protobuf.Empty);
rpc Ping(PingRequest) returns (PingResponse);
}

View file

@ -20,3 +20,4 @@ futures = "0.3.30"
num_enum = "0.7.2"
xkbcommon = { workspace = true }
rand = "0.8.5"
bitflags = { workspace = true }

View file

@ -1,7 +1,9 @@
use pinnacle_api::input::libinput::LibinputSetting;
use pinnacle_api::layout::{
CornerLayout, CornerLocation, CyclingLayoutManager, DwindleLayout, FairLayout, MasterSide,
MasterStackLayout, SpiralLayout,
};
use pinnacle_api::output::OutputSetup;
use pinnacle_api::signal::WindowSignal;
use pinnacle_api::util::{Axis, Batch};
use pinnacle_api::xkbcommon::xkb::Keysym;
@ -26,6 +28,7 @@ async fn main() {
tag,
layout,
render,
..
} = modules;
let mod_key = Mod::Ctrl;
@ -55,6 +58,11 @@ async fn main() {
pinnacle.quit();
});
// `mod_key + alt + r` reloads the config
input.keybind([mod_key, Mod::Alt], 'r', || {
pinnacle.reload_config();
});
// `mod_key + alt + c` closes the focused window
input.keybind([mod_key, Mod::Alt], 'c', || {
if let Some(window) = window.get_focused() {
@ -206,12 +214,7 @@ async fn main() {
let tag_names = ["1", "2", "3", "4", "5"];
// Setup all monitors with tags "1" through "5"
output.connect_for_all(move |op| {
let tags = tag.add(op, tag_names);
// Be sure to set a tag to active or windows won't display
tags.first().unwrap().set_active(true);
});
output.setup([OutputSetup::new_with_matcher(|_| true).with_tags(tag_names)]);
for tag_name in tag_names {
// `mod_key + 1-5` switches to tag "1" to "5"
@ -247,6 +250,8 @@ async fn main() {
});
}
input.set_libinput_setting(LibinputSetting::Tap(true));
// Enable sloppy focus
window.connect_signal(WindowSignal::PointerEnter(Box::new(|win| {
win.set_focused(true);

View file

@ -131,11 +131,13 @@ pub fn config(
#(#attrs)*
#tokio_attr
#vis #sig {
let (#module_ident, __fut_receiver) = ::pinnacle_api::connect().await.unwrap();
let (__api, __fut_receiver) = ::pinnacle_api::connect().await.unwrap();
let #module_ident = __api.clone();
#(#stmts)*
::pinnacle_api::listen(__fut_receiver).await;
::pinnacle_api::listen(__api, __fut_receiver).await;
}
}
.into()

View file

@ -8,7 +8,7 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
sync::{Arc, Mutex, OnceLock},
};
use pinnacle_api_defs::pinnacle::layout::v0alpha1::{
@ -26,22 +26,28 @@ use crate::{
tag::TagHandle,
util::{Axis, Geometry},
window::WindowHandle,
OUTPUT, TAG, WINDOW,
ApiModules,
};
/// A struct that allows you to manage layouts.
#[derive(Clone, Debug)]
pub struct Layout {
api: OnceLock<ApiModules>,
layout_client: LayoutServiceClient<Channel>,
}
impl Layout {
pub(crate) fn new(channel: Channel) -> Self {
Self {
api: OnceLock::new(),
layout_client: LayoutServiceClient::new(channel.clone()),
}
}
pub(crate) fn finish_init(&self, api: ApiModules) {
self.api.set(api).unwrap();
}
/// Consume the given [`LayoutManager`] and set it as the global layout handler.
///
/// This returns a [`LayoutRequester`] that allows you to manually request layouts from
@ -61,7 +67,10 @@ impl Layout {
let manager = Arc::new(Mutex::new(manager));
let api = self.api.get().unwrap().clone();
let requester = LayoutRequester {
api: api.clone(),
sender: from_client_clone,
manager: manager.clone(),
};
@ -69,16 +78,16 @@ impl Layout {
let thing = async move {
while let Some(Ok(response)) = from_server.next().await {
let args = LayoutArgs {
output: OUTPUT.get().unwrap().new_handle(response.output_name()),
output: api.output.new_handle(response.output_name()),
windows: response
.window_ids
.into_iter()
.map(|id| WINDOW.get().unwrap().new_handle(id))
.map(|id| api.window.new_handle(id))
.collect(),
tags: response
.tag_ids
.into_iter()
.map(|id| TAG.get().unwrap().new_handle(id))
.map(|id| api.tag.new_handle(id))
.collect(),
output_width: response.output_width.unwrap_or_default(),
output_height: response.output_height.unwrap_or_default(),
@ -225,6 +234,7 @@ impl LayoutManager for CyclingLayoutManager {
/// A struct that can request layouts and provides access to a consumed [`LayoutManager`].
#[derive(Debug)]
pub struct LayoutRequester<T> {
api: ApiModules,
sender: UnboundedSender<LayoutRequest>,
/// The manager that was consumed, wrapped in an `Arc<Mutex>`.
pub manager: Arc<Mutex<T>>,
@ -233,6 +243,7 @@ pub struct LayoutRequester<T> {
impl<T> Clone for LayoutRequester<T> {
fn clone(&self) -> Self {
Self {
api: self.api.clone(),
sender: self.sender.clone(),
manager: self.manager.clone(),
}
@ -245,7 +256,7 @@ impl<T> LayoutRequester<T> {
/// This uses the focused output for the request.
/// If you want to layout a specific output, see [`LayoutRequester::request_layout_on_output`].
pub fn request_layout(&self) {
let output_name = OUTPUT.get().unwrap().get_focused().map(|op| op.name);
let output_name = self.api.output.get_focused().map(|op| op.name);
self.sender
.send(LayoutRequest {
body: Some(Body::Layout(ExplicitLayout { output_name })),

View file

@ -67,12 +67,7 @@
//! // `modules` is now available in the function body.
//! // You can deconstruct `ApiModules` to get all the config structs.
//! let ApiModules {
//! pinnacle,
//! process,
//! window,
//! input,
//! output,
//! tag,
//! ..
//! } = modules;
//! }
//! ```
@ -80,7 +75,7 @@
//! ## 5. Begin crafting your config!
//! You can peruse the documentation for things to configure.
use std::{sync::OnceLock, time::Duration};
use std::{sync::Arc, time::Duration};
use futures::{future::BoxFuture, Future, StreamExt};
use input::Input;
@ -115,18 +110,9 @@ pub use pinnacle_api_macros::config;
pub use tokio;
pub use xkbcommon;
static PINNACLE: OnceLock<Pinnacle> = OnceLock::new();
static PROCESS: OnceLock<Process> = OnceLock::new();
static WINDOW: OnceLock<Window> = OnceLock::new();
static INPUT: OnceLock<Input> = OnceLock::new();
static OUTPUT: OnceLock<Output> = OnceLock::new();
static TAG: OnceLock<Tag> = OnceLock::new();
static SIGNAL: OnceLock<RwLock<SignalState>> = OnceLock::new();
static LAYOUT: OnceLock<Layout> = OnceLock::new();
static RENDER: OnceLock<Render> = OnceLock::new();
/// A struct containing static references to all of the configuration structs.
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
#[derive(Clone)]
pub struct ApiModules {
/// The [`Pinnacle`] struct
pub pinnacle: &'static Pinnacle,
@ -144,6 +130,23 @@ pub struct ApiModules {
pub layout: &'static Layout,
/// The [`Render`] struct
pub render: &'static Render,
signal: Arc<RwLock<SignalState>>,
}
impl std::fmt::Debug for ApiModules {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApiModules")
.field("pinnacle", &self.pinnacle)
.field("process", &self.process)
.field("window", &self.window)
.field("input", &self.input)
.field("output", &self.output)
.field("tag", &self.tag)
.field("layout", &self.layout)
.field("render", &self.render)
.field("signal", &"...")
.finish()
}
}
/// Connects to Pinnacle and builds the configuration structs.
@ -164,21 +167,19 @@ pub async fn connect(
let (fut_sender, fut_recv) = unbounded_channel::<BoxFuture<'static, ()>>();
let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(channel.clone()));
let process = PROCESS.get_or_init(|| Process::new(channel.clone(), fut_sender.clone()));
let window = WINDOW.get_or_init(|| Window::new(channel.clone()));
let input = INPUT.get_or_init(|| Input::new(channel.clone(), fut_sender.clone()));
let tag = TAG.get_or_init(|| Tag::new(channel.clone()));
let output = OUTPUT.get_or_init(|| Output::new(channel.clone()));
let layout = LAYOUT.get_or_init(|| Layout::new(channel.clone()));
let render = RENDER.get_or_init(|| Render::new(channel.clone()));
let signal = Arc::new(RwLock::new(SignalState::new(
channel.clone(),
fut_sender.clone(),
)));
SIGNAL
.set(RwLock::new(SignalState::new(
channel.clone(),
fut_sender.clone(),
)))
.map_err(|_| "failed to create SIGNAL")?;
let pinnacle = Box::leak(Box::new(Pinnacle::new(channel.clone())));
let process = Box::leak(Box::new(Process::new(channel.clone(), fut_sender.clone())));
let window = Box::leak(Box::new(Window::new(channel.clone())));
let input = Box::leak(Box::new(Input::new(channel.clone(), fut_sender.clone())));
let output = Box::leak(Box::new(Output::new(channel.clone())));
let tag = Box::leak(Box::new(Tag::new(channel.clone())));
let render = Box::leak(Box::new(Render::new(channel.clone())));
let layout = Box::leak(Box::new(Layout::new(channel.clone())));
let modules = ApiModules {
pinnacle,
@ -189,8 +190,15 @@ pub async fn connect(
tag,
layout,
render,
signal: signal.clone(),
};
window.finish_init(modules.clone());
output.finish_init(modules.clone());
tag.finish_init(modules.clone());
layout.finish_init(modules.clone());
signal.read().await.finish_init(modules.clone());
Ok((modules, fut_recv))
}
@ -201,15 +209,13 @@ pub async fn connect(
///
/// This function is inserted at the end of your config through the [`config`] macro.
/// You should use the macro instead of this function directly.
pub async fn listen(fut_recv: UnboundedReceiver<BoxFuture<'static, ()>>) {
pub async fn listen(api: ApiModules, fut_recv: UnboundedReceiver<BoxFuture<'static, ()>>) {
let mut fut_recv = UnboundedReceiverStream::new(fut_recv);
let pinnacle = PINNACLE.get().unwrap();
let keepalive = async move {
loop {
tokio::time::sleep(Duration::from_secs(60)).await;
if let Err(err) = pinnacle.ping().await {
if let Err(err) = api.pinnacle.ping().await {
eprintln!("Failed to ping compositor: {err}");
std::process::exit(1);
}

View file

@ -9,12 +9,14 @@
//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different
//! connected monitors and set them up.
use std::{num::NonZeroU32, sync::OnceLock};
use futures::FutureExt;
use pinnacle_api_defs::pinnacle::output::{
self,
v0alpha1::{
output_service_client::OutputServiceClient, set_scale_request::AbsoluteOrRelative,
SetLocationRequest, SetModeRequest, SetScaleRequest,
SetLocationRequest, SetModeRequest, SetScaleRequest, SetTransformRequest,
},
};
use tonic::transport::Channel;
@ -22,9 +24,9 @@ use tonic::transport::Channel;
use crate::{
block_on_tokio,
signal::{OutputSignal, SignalHandle},
tag::TagHandle,
tag::{Tag, TagHandle},
util::Batch,
SIGNAL, TAG,
ApiModules,
};
/// A struct that allows you to get handles to connected outputs and set them up.
@ -33,19 +35,26 @@ use crate::{
#[derive(Debug, Clone)]
pub struct Output {
output_client: OutputServiceClient<Channel>,
api: OnceLock<ApiModules>,
}
impl Output {
pub(crate) fn new(channel: Channel) -> Self {
Self {
output_client: OutputServiceClient::new(channel.clone()),
api: OnceLock::new(),
}
}
pub(crate) fn finish_init(&self, api: ApiModules) {
self.api.set(api).unwrap();
}
pub(crate) fn new_handle(&self, name: impl Into<String>) -> OutputHandle {
OutputHandle {
name: name.into(),
output_client: self.output_client.clone(),
api: self.api.get().unwrap().clone(),
}
}
@ -145,7 +154,7 @@ impl Output {
for_all(&output);
}
let mut signal_state = block_on_tokio(SIGNAL.get().expect("SIGNAL doesn't exist").write());
let mut signal_state = block_on_tokio(self.api.get().unwrap().signal.write());
signal_state.output_connect.add_callback(Box::new(for_all));
}
@ -155,12 +164,396 @@ impl Output {
/// You can pass in an [`OutputSignal`] along with a callback and it will get run
/// with the necessary arguments every time a signal of that type is received.
pub fn connect_signal(&self, signal: OutputSignal) -> SignalHandle {
let mut signal_state = block_on_tokio(SIGNAL.get().expect("SIGNAL doesn't exist").write());
let mut signal_state = block_on_tokio(self.api.get().unwrap().signal.write());
match signal {
OutputSignal::Connect(f) => signal_state.output_connect.add_callback(f),
OutputSignal::Disconnect(f) => signal_state.output_disconnect.add_callback(f),
OutputSignal::Resize(f) => signal_state.output_resize.add_callback(f),
OutputSignal::Move(f) => signal_state.output_move.add_callback(f),
}
}
/// Declaratively setup outputs.
///
/// This method allows you to specify [`OutputSetup`]s that will be applied to outputs already
/// connected and that will be connected in the future. It handles the setting of modes,
/// scales, tags, and more.
///
/// Setups will be applied top to bottom.
///
/// See [`OutputSetup`] for more information.
///
/// # Examples
///
/// ```
/// use pinnacle_api::output::OutputSetup;
/// use pinnacle_api::output::OutputId;
///
/// output.setup([
/// // Give all outputs tags 1 through 5
/// OutputSetup::new_with_matcher(|_| true).with_tags(["1", "2", "3", "4", "5"]),
/// // Give outputs with a preferred mode of 4K a scale of 2.0
/// OutputSetup::new_with_matcher(|op| op.preferred_mode().unwrap().pixel_width == 2160)
/// .with_scale(2.0),
/// // Additionally give eDP-1 tags 6 and 7
/// OutputSetup::new(OutputId::name("eDP-1")).with_tags(["6", "7"]),
/// ]);
/// ```
pub fn setup(&self, setups: impl IntoIterator<Item = OutputSetup>) {
let setups = setups.into_iter().collect::<Vec<_>>();
let tag_mod = self.api.get().unwrap().tag.clone();
let apply_setups = move |output: &OutputHandle| {
for setup in setups.iter() {
if setup.output.matches(output) {
setup.apply(output, &tag_mod);
}
}
if let Some(tag) = output.tags().first() {
tag.set_active(true);
}
};
self.connect_for_all(move |output| {
apply_setups(output);
});
}
/// Specify locations for outputs and when they should be laid out.
///
/// This method allows you to specify locations for outputs, either as a specific point
/// or relative to another output.
///
/// This will relayout outputs according to the given [`UpdateLocsOn`] flags.
///
/// Layouts not specified in `setup` or that have cyclic relative-to outputs will be
/// laid out in a line to the right of the rightmost output.
///
/// # Examples
///
/// ```
/// use pinnacle_api::output::UpdateLocsOn;
/// use pinnacle_api::output::OutputLoc;
/// use pinnacle_api::output::OutputId;
///
/// output.setup_locs(
/// // Relayout all outputs when outputs are connected, disconnected, and resized
/// UpdateLocsOn::all(),
/// [
/// // Anchor eDP-1 to (0, 0) so other outputs can be placed relative to it
/// (OutputId::name("eDP-1"), OutputLoc::Point(0, 0)),
/// // Place HDMI-A-1 below it centered
/// (
/// OutputId::name("HDMI-A-1"),
/// OutputLoc::RelativeTo(OutputId::name("eDP-1"), Alignment::BottomAlignCenter),
/// ),
/// // Place HDMI-A-2 below HDMI-A-1.
/// (
/// OutputId::name("HDMI-A-2"),
/// OutputLoc::RelativeTo(OutputId::name("HDMI-A-1"), Alignment::BottomAlignCenter),
/// ),
/// // Additionally, if HDMI-A-1 isn't connected, place it below eDP-1 instead.
/// (
/// OutputId::name("HDMI-A-2"),
/// OutputLoc::RelativeTo(OutputId::name("eDP-1"), Alignment::BottomAlignCenter),
/// ),
/// ]
/// );
/// ```
pub fn setup_locs(
&self,
update_locs_on: UpdateLocsOn,
setup: impl IntoIterator<Item = (OutputId, OutputLoc)>,
) {
let setup: Vec<_> = setup.into_iter().collect();
let api = self.api.get().unwrap().clone();
let layout_outputs = move || {
let outputs = api.output.get_all();
let mut rightmost_output_and_x: Option<(OutputHandle, i32)> = None;
let mut placed_outputs = Vec::<OutputHandle>::new();
// Place outputs with OutputSetupLoc::Point
for output in outputs.iter() {
if let Some(&(_, OutputLoc::Point(x, y))) =
setup.iter().find(|(op_id, _)| op_id.matches(output))
{
output.set_location(x, y);
placed_outputs.push(output.clone());
let props = output.props();
let x = props.x.unwrap();
let width = props.logical_width.unwrap() as i32;
if rightmost_output_and_x.is_none()
|| rightmost_output_and_x
.as_ref()
.is_some_and(|(_, rm_x)| x + width > *rm_x)
{
rightmost_output_and_x = Some((output.clone(), x + width));
}
}
}
// Attempt to place relative outputs
//
// Because this code is hideous I'm gonna comment what it does
while let Some((output, relative_to, alignment)) =
setup.iter().find_map(|(setup_op_id, loc)| {
// For every location setup,
// find the first unplaced output it refers to that has a relative location
outputs
.iter()
.find(|setup_op| {
!placed_outputs.contains(setup_op) && setup_op_id.matches(setup_op)
})
.and_then(|setup_op| match loc {
OutputLoc::RelativeTo(rel_id, alignment) => {
placed_outputs.iter().find_map(|placed_op| {
(rel_id.matches(placed_op))
.then_some((setup_op, placed_op, alignment))
})
}
_ => None,
})
})
{
output.set_loc_adj_to(relative_to, *alignment);
placed_outputs.push(output.clone());
let props = output.props();
let x = props.x.unwrap();
let width = props.logical_width.unwrap() as i32;
if rightmost_output_and_x.is_none()
|| rightmost_output_and_x
.as_ref()
.is_some_and(|(_, rm_x)| x + width > *rm_x)
{
rightmost_output_and_x = Some((output.clone(), x + width));
}
}
// Place all remaining outputs right of the rightmost one
for output in outputs
.iter()
.filter(|op| !placed_outputs.contains(op))
.collect::<Vec<_>>()
{
if let Some((rm_op, _)) = rightmost_output_and_x.as_ref() {
output.set_loc_adj_to(rm_op, Alignment::RightAlignTop);
} else {
output.set_location(0, 0);
}
placed_outputs.push(output.clone());
let props = output.props();
let x = props.x.unwrap();
let width = props.logical_width.unwrap() as i32;
if rightmost_output_and_x.is_none()
|| rightmost_output_and_x
.as_ref()
.is_some_and(|(_, rm_x)| x + width > *rm_x)
{
rightmost_output_and_x = Some((output.clone(), x + width));
}
}
};
layout_outputs();
let layout_outputs_clone1 = layout_outputs.clone();
let layout_outputs_clone2 = layout_outputs.clone();
if update_locs_on.contains(UpdateLocsOn::CONNECT) {
self.connect_signal(OutputSignal::Connect(Box::new(move |_| {
layout_outputs_clone2();
})));
}
if update_locs_on.contains(UpdateLocsOn::DISCONNECT) {
self.connect_signal(OutputSignal::Disconnect(Box::new(move |_| {
layout_outputs_clone1();
})));
}
if update_locs_on.contains(UpdateLocsOn::RESIZE) {
self.connect_signal(OutputSignal::Resize(Box::new(move |_, _, _| {
layout_outputs();
})));
}
}
}
/// A matcher for outputs.
enum OutputMatcher {
/// Match outputs by unique id.
Id(OutputId),
/// Match outputs using a function that returns a bool.
Fn(Box<dyn Fn(&OutputHandle) -> bool + Send + Sync>),
}
impl OutputMatcher {
/// Returns whether this matcher matches the given output.
fn matches(&self, output: &OutputHandle) -> bool {
match self {
OutputMatcher::Id(id) => id.matches(output),
OutputMatcher::Fn(matcher) => matcher(output),
}
}
}
impl std::fmt::Debug for OutputMatcher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Id(name) => f.debug_tuple("Name").field(name).finish(),
Self::Fn(_) => f
.debug_tuple("Fn")
.field(&"<Box<dyn Fn(&OutputHandle)> -> bool>")
.finish(),
}
}
}
/// An output setup for use in [`Output::setup`].
pub struct OutputSetup {
output: OutputMatcher,
mode: Option<Mode>,
scale: Option<f32>,
tag_names: Option<Vec<String>>,
transform: Option<Transform>,
}
impl OutputSetup {
/// Creates a new `OutputSetup` that applies to the output with the given name.
pub fn new(id: OutputId) -> Self {
Self {
output: OutputMatcher::Id(id),
mode: None,
scale: None,
tag_names: None,
transform: None,
}
}
/// Creates a new `OutputSetup` that matches outputs according to the given function.
pub fn new_with_matcher(
matcher: impl Fn(&OutputHandle) -> bool + Send + Sync + 'static,
) -> Self {
Self {
output: OutputMatcher::Fn(Box::new(matcher)),
mode: None,
scale: None,
tag_names: None,
transform: None,
}
}
/// Makes this setup apply the given [`Mode`] to its outputs.
pub fn with_mode(self, mode: Mode) -> Self {
Self {
mode: Some(mode),
..self
}
}
/// Makes this setup apply the given scale to its outputs.
pub fn with_scale(self, scale: f32) -> Self {
Self {
scale: Some(scale),
..self
}
}
/// Makes this setup add tags with the given names to its outputs.
pub fn with_tags(self, tag_names: impl IntoIterator<Item = impl ToString>) -> Self {
Self {
tag_names: Some(tag_names.into_iter().map(|s| s.to_string()).collect()),
..self
}
}
/// Makes this setup apply the given transform to its outputs.
pub fn with_transform(self, transform: Transform) -> Self {
Self {
transform: Some(transform),
..self
}
}
fn apply(&self, output: &OutputHandle, tag: &Tag) {
if let Some(mode) = &self.mode {
output.set_mode(
mode.pixel_width,
mode.pixel_height,
Some(mode.refresh_rate_millihertz),
);
}
if let Some(scale) = self.scale {
output.set_scale(scale);
}
if let Some(tag_names) = &self.tag_names {
tag.add(output, tag_names);
}
if let Some(transform) = self.transform {
output.set_transform(transform);
}
}
}
/// A location for an output.
#[derive(Clone, Debug)]
pub enum OutputLoc {
/// A specific point in the global space of the form (x, y).
Point(i32, i32),
/// A location relative to another output with an [`Alignment`].
RelativeTo(OutputId, Alignment),
}
/// An identifier for an output.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum OutputId {
/// Identify using the output's name.
Name(String),
/// Identify using the output's EDID serial number.
///
/// Note: some displays (like laptop screens) don't have a serial number, in which case this won't match it.
/// Additionally the Rust API assumes monitor serial numbers are unique.
/// If you're unlucky enough to have two monitors with the same serial number,
/// use [`OutputId::Name`] instead.
Serial(NonZeroU32),
}
impl OutputId {
/// Creates an [`OutputId::Name`].
///
/// This is a convenience function so you don't have to call `.into()`
/// or `.to_string()`.
pub fn name(name: impl ToString) -> Self {
Self::Name(name.to_string())
}
/// Returns whether `output` is identified by this `OutputId`.
pub fn matches(&self, output: &OutputHandle) -> bool {
match self {
OutputId::Name(name) => name == output.name(),
OutputId::Serial(serial) => Some(serial.get()) == output.serial(),
}
}
}
bitflags::bitflags! {
/// Flags for when [`Output::setup_locs`] should relayout outputs.
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
pub struct UpdateLocsOn: u8 {
/// Relayout when an output is connected.
const CONNECT = 1;
/// Relayout when an output is disconnected.
const DISCONNECT = 1 << 1;
/// Relayout when an output is resized, either through a scale or mode change.
const RESIZE = 1 << 2;
}
}
/// A handle to an output.
@ -170,6 +563,7 @@ impl Output {
pub struct OutputHandle {
pub(crate) name: String,
output_client: OutputServiceClient<Channel>,
api: ApiModules,
}
impl PartialEq for OutputHandle {
@ -215,6 +609,31 @@ pub enum Alignment {
RightAlignBottom,
}
/// An output transform.
///
/// This determines what orientation outputs will render at.
#[derive(num_enum::TryFromPrimitive, Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum Transform {
/// No transform.
#[default]
Normal = 1,
/// 90 degrees counter-clockwise.
_90,
/// 180 degrees counter-clockwise.
_180,
/// 270 degrees counter-clockwise.
_270,
/// Flipped vertically (across the horizontal axis).
Flipped,
/// Flipped vertically and rotated 90 degrees counter-clockwise
Flipped90,
/// Flipped vertically and rotated 180 degrees counter-clockwise
Flipped180,
/// Flipped vertically and rotated 270 degrees counter-clockwise
Flipped270,
}
impl OutputHandle {
/// Set the location of this output in the global space.
///
@ -436,6 +855,25 @@ impl OutputHandle {
self.increase_scale(-decrease_by);
}
/// Set this output's transform.
///
/// # Examples
///
/// ```
/// use pinnacle_api::output::Transform;
///
/// // Rotate 90 degrees counter-clockwise
/// output.set_transform(Transform::_90);
/// ```
pub fn set_transform(&self, transform: Transform) {
let mut client = self.output_client.clone();
block_on_tokio(client.set_transform(SetTransformRequest {
output_name: Some(self.name.clone()),
transform: Some(transform as i32),
}))
.unwrap();
}
/// Get all properties of this output.
///
/// # Examples
@ -462,8 +900,6 @@ impl OutputHandle {
.unwrap()
.into_inner();
let tag = TAG.get().expect("TAG doesn't exist");
OutputProperties {
make: response.make,
model: response.model,
@ -502,9 +938,11 @@ impl OutputHandle {
tags: response
.tag_ids
.into_iter()
.map(|id| tag.new_handle(id))
.map(|id| self.api.tag.new_handle(id))
.collect(),
scale: response.scale,
transform: response.transform.and_then(|tf| tf.try_into().ok()),
serial: response.serial,
}
}
@ -680,6 +1118,30 @@ impl OutputHandle {
self.props_async().await.scale
}
/// Get this output's transform.
///
/// Shorthand for `self.props().transform`
pub fn transform(&self) -> Option<Transform> {
self.props().transform
}
/// The async version of [`OutputHandle::transform`].
pub async fn transform_async(&self) -> Option<Transform> {
self.props_async().await.transform
}
/// Get this output's EDID serial number.
///
/// Shorthand for `self.props().serial`
pub fn serial(&self) -> Option<u32> {
self.props().serial
}
/// The async version of [`OutputHandle::serial`].
pub async fn serial_async(&self) -> Option<u32> {
self.props_async().await.serial
}
/// Get this output's unique name (the name of its connector).
pub fn name(&self) -> &str {
&self.name
@ -700,6 +1162,7 @@ pub struct Mode {
}
/// The properties of an output.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct OutputProperties {
/// The make of the output.
@ -737,4 +1200,8 @@ pub struct OutputProperties {
pub tags: Vec<TagHandle>,
/// This output's scaling factor.
pub scale: Option<f32>,
/// This output's transform.
pub transform: Option<Transform>,
/// This output's EDID serial number.
pub serial: Option<u32>,
}

View file

@ -9,7 +9,7 @@
use std::time::Duration;
use pinnacle_api_defs::pinnacle::v0alpha1::{
pinnacle_service_client::PinnacleServiceClient, PingRequest, QuitRequest,
pinnacle_service_client::PinnacleServiceClient, PingRequest, QuitRequest, ReloadConfigRequest,
};
use rand::RngCore;
use tonic::{transport::Channel, Request};
@ -42,6 +42,12 @@ impl Pinnacle {
block_on_tokio(client.quit(QuitRequest {})).unwrap();
}
/// Reload the currently active config.
pub fn reload_config(&self) {
let mut client = self.client.clone();
block_on_tokio(client.reload_config(ReloadConfigRequest {})).unwrap();
}
pub(super) async fn ping(&self) -> Result<(), String> {
let mut client = self.client.clone();
let mut payload = [0u8; 8];

View file

@ -6,11 +6,13 @@
//! Some of the other modules have a `connect_signal` method that will allow you to pass in
//! callbacks to run on each signal. Use them to connect to the signals defined here.
#![allow(clippy::type_complexity)]
use std::{
collections::{btree_map, BTreeMap},
sync::{
atomic::{AtomicU32, Ordering},
Arc,
Arc, OnceLock,
},
};
@ -25,7 +27,9 @@ use tokio::sync::{
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
use tonic::{transport::Channel, Streaming};
use crate::{block_on_tokio, output::OutputHandle, window::WindowHandle, OUTPUT, WINDOW};
use crate::{
block_on_tokio, output::OutputHandle, tag::TagHandle, window::WindowHandle, ApiModules,
};
pub(crate) trait Signal {
type Callback;
@ -97,6 +101,7 @@ macro_rules! signals {
.into_inner()
},
$on_resp,
self.api.get().unwrap().clone(),
);
self.callback_sender.replace(channels.callback_sender);
@ -127,10 +132,9 @@ signals! {
enum_name = Connect,
callback_type = SingleOutputFn,
client_request = output_connect,
on_response = |response, callbacks| {
on_response = |response, callbacks, api| {
if let Some(output_name) = response.output_name {
let output = OUTPUT.get().expect("OUTPUT doesn't exist");
let handle = output.new_handle(output_name);
let handle = api.output.new_handle(output_name);
for callback in callbacks {
callback(&handle);
@ -138,6 +142,57 @@ signals! {
}
},
}
/// An output was connected.
///
/// Callbacks receive the disconnected output.
OutputDisconnect = {
enum_name = Disconnect,
callback_type = SingleOutputFn,
client_request = output_disconnect,
on_response = |response, callbacks, api| {
if let Some(output_name) = response.output_name {
let handle = api.output.new_handle(output_name);
for callback in callbacks {
callback(&handle);
}
}
},
}
/// An output's logical size changed.
///
/// Callbacks receive the output and new width and height.
OutputResize = {
enum_name = Resize,
callback_type = Box<dyn FnMut(&OutputHandle, u32, u32) + Send + 'static>,
client_request = output_resize,
on_response = |response, callbacks, api| {
if let Some(output_name) = &response.output_name {
let handle = api.output.new_handle(output_name);
for callback in callbacks {
callback(&handle, response.logical_width(), response.logical_height())
}
}
},
}
/// An output's location in the global space changed.
///
/// Callbacks receive the output and new x and y.
OutputMove = {
enum_name = Move,
callback_type = Box<dyn FnMut(&OutputHandle, i32, i32) + Send + 'static>,
client_request = output_move,
on_response = |response, callbacks, api| {
if let Some(output_name) = &response.output_name {
let handle = api.output.new_handle(output_name);
for callback in callbacks {
callback(&handle, response.x(), response.y())
}
}
},
}
}
/// Signals relating to window events.
WindowSignal => {
@ -148,10 +203,9 @@ signals! {
enum_name = PointerEnter,
callback_type = SingleWindowFn,
client_request = window_pointer_enter,
on_response = |response, callbacks| {
on_response = |response, callbacks, api| {
if let Some(window_id) = response.window_id {
let window = WINDOW.get().expect("WINDOW doesn't exist");
let handle = window.new_handle(window_id);
let handle = api.window.new_handle(window_id);
for callback in callbacks {
callback(&handle);
@ -166,10 +220,9 @@ signals! {
enum_name = PointerLeave,
callback_type = SingleWindowFn,
client_request = window_pointer_leave,
on_response = |response, callbacks| {
on_response = |response, callbacks, api| {
if let Some(window_id) = response.window_id {
let window = WINDOW.get().expect("WINDOW doesn't exist");
let handle = window.new_handle(window_id);
let handle = api.window.new_handle(window_id);
for callback in callbacks {
callback(&handle);
@ -178,6 +231,24 @@ signals! {
},
}
}
/// Signals relating to tag events.
TagSignal => {
/// A tag was set to active or not active.
TagActive = {
enum_name = Active,
callback_type = Box<dyn FnMut(&TagHandle, bool) + Send + 'static>,
client_request = tag_active,
on_response = |response, callbacks, api| {
if let Some(tag_id) = response.tag_id {
let handle = api.tag.new_handle(tag_id);
for callback in callbacks {
callback(&handle, response.active.unwrap());
}
}
},
}
}
}
pub(crate) type SingleOutputFn = Box<dyn FnMut(&OutputHandle) + Send + 'static>;
@ -185,8 +256,20 @@ pub(crate) type SingleWindowFn = Box<dyn FnMut(&WindowHandle) + Send + 'static>;
pub(crate) struct SignalState {
pub(crate) output_connect: SignalData<OutputConnect>,
pub(crate) output_disconnect: SignalData<OutputDisconnect>,
pub(crate) output_resize: SignalData<OutputResize>,
pub(crate) output_move: SignalData<OutputMove>,
pub(crate) window_pointer_enter: SignalData<WindowPointerEnter>,
pub(crate) window_pointer_leave: SignalData<WindowPointerLeave>,
pub(crate) tag_active: SignalData<TagActive>,
}
impl std::fmt::Debug for SignalState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SignalState").finish()
}
}
impl SignalState {
@ -197,10 +280,24 @@ impl SignalState {
let client = SignalServiceClient::new(channel);
Self {
output_connect: SignalData::new(client.clone(), fut_sender.clone()),
output_disconnect: SignalData::new(client.clone(), fut_sender.clone()),
output_resize: SignalData::new(client.clone(), fut_sender.clone()),
output_move: SignalData::new(client.clone(), fut_sender.clone()),
window_pointer_enter: SignalData::new(client.clone(), fut_sender.clone()),
window_pointer_leave: SignalData::new(client.clone(), fut_sender.clone()),
tag_active: SignalData::new(client.clone(), fut_sender.clone()),
}
}
pub(crate) fn finish_init(&self, api: ApiModules) {
self.output_connect.api.set(api.clone()).unwrap();
self.output_disconnect.api.set(api.clone()).unwrap();
self.output_resize.api.set(api.clone()).unwrap();
self.output_move.api.set(api.clone()).unwrap();
self.window_pointer_enter.api.set(api.clone()).unwrap();
self.window_pointer_leave.api.set(api.clone()).unwrap();
self.tag_active.api.set(api.clone()).unwrap();
}
}
#[derive(Default, Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
@ -208,6 +305,7 @@ pub(crate) struct SignalConnId(pub(crate) u32);
pub(crate) struct SignalData<S: Signal> {
client: SignalServiceClient<Channel>,
api: OnceLock<ApiModules>,
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
callback_sender: Option<UnboundedSender<(SignalConnId, S::Callback)>>,
remove_callback_sender: Option<UnboundedSender<SignalConnId>>,
@ -223,6 +321,7 @@ impl<S: Signal> SignalData<S> {
) -> Self {
Self {
client,
api: OnceLock::new(),
fut_sender,
callback_sender: Default::default(),
remove_callback_sender: Default::default(),
@ -244,13 +343,14 @@ fn connect_signal<Req, Resp, F, T, O>(
callback_count: Arc<AtomicU32>,
to_in_stream: T,
mut on_response: O,
api: ApiModules,
) -> ConnectSignalChannels<F>
where
Req: SignalRequest + Send + 'static,
Resp: Send + 'static,
F: Send + 'static,
T: FnOnce(UnboundedReceiverStream<Req>) -> Streaming<Resp>,
O: FnMut(Resp, btree_map::ValuesMut<'_, SignalConnId, F>) + Send + 'static,
O: FnMut(Resp, btree_map::ValuesMut<'_, SignalConnId, F>, &ApiModules) + Send + 'static,
{
let (control_sender, recv) = unbounded_channel::<Req>();
let out_stream = UnboundedReceiverStream::new(recv);
@ -283,7 +383,7 @@ where
match response {
Ok(response) => {
on_response(response, callbacks.values_mut());
on_response(response, callbacks.values_mut(), &api);
control_sender
.send(Req::from_control(StreamControl::Ready))

View file

@ -29,8 +29,9 @@
//!
//! These [`TagHandle`]s allow you to manipulate individual tags and get their properties.
use std::sync::OnceLock;
use futures::FutureExt;
use num_enum::TryFromPrimitive;
use pinnacle_api_defs::pinnacle::{
tag::{
self,
@ -43,25 +44,39 @@ use pinnacle_api_defs::pinnacle::{
};
use tonic::transport::Channel;
use crate::{block_on_tokio, output::OutputHandle, util::Batch, OUTPUT};
use crate::{
block_on_tokio,
output::OutputHandle,
signal::{SignalHandle, TagSignal},
util::Batch,
window::WindowHandle,
ApiModules,
};
/// A struct that allows you to add and remove tags and get [`TagHandle`]s.
#[derive(Clone, Debug)]
pub struct Tag {
tag_client: TagServiceClient<Channel>,
api: OnceLock<ApiModules>,
}
impl Tag {
pub(crate) fn new(channel: Channel) -> Self {
Self {
tag_client: TagServiceClient::new(channel.clone()),
api: OnceLock::new(),
}
}
pub(crate) fn finish_init(&self, api: ApiModules) {
self.api.set(api).unwrap();
}
pub(crate) fn new_handle(&self, id: u32) -> TagHandle {
TagHandle {
id,
tag_client: self.tag_client.clone(),
api: self.api.get().unwrap().clone(),
}
}
@ -157,8 +172,7 @@ impl Tag {
/// The async version of [`Tag::get`].
pub async fn get_async(&self, name: impl Into<String>) -> Option<TagHandle> {
let name = name.into();
let output_module = OUTPUT.get().expect("OUTPUT doesn't exist");
let focused_output = output_module.get_focused();
let focused_output = self.api.get().unwrap().output.get_focused();
if let Some(output) = focused_output {
self.get_on_output_async(name, &output).await
@ -220,6 +234,19 @@ impl Tag {
block_on_tokio(client.remove(RemoveRequest { tag_ids })).unwrap();
}
/// Connect to a tag signal.
///
/// The compositor will fire off signals that your config can listen for and act upon.
/// You can pass in a [`TagSignal`] along with a callback and it will get run
/// with the necessary arguments every time a signal of that type is received.
pub fn connect_signal(&self, signal: TagSignal) -> SignalHandle {
let mut signal_state = block_on_tokio(self.api.get().unwrap().signal.write());
match signal {
TagSignal::Active(f) => signal_state.tag_active.add_callback(f),
}
}
}
/// A handle to a tag.
@ -229,6 +256,7 @@ impl Tag {
pub struct TagHandle {
pub(crate) id: u32,
tag_client: TagServiceClient<Channel>,
api: ApiModules,
}
impl PartialEq for TagHandle {
@ -245,26 +273,6 @@ impl std::hash::Hash for TagHandle {
}
}
/// Various static layouts.
#[repr(i32)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)]
pub enum Layout {
/// One master window on the left with all other windows stacked to the right
MasterStack = 1,
/// Windows split in half towards the bottom right corner
Dwindle,
/// Windows split in half in a spiral
Spiral,
/// One main corner window in the top left with a column of windows on the right and a row on the bottom
CornerTopLeft,
/// One main corner window in the top right with a column of windows on the left and a row on the bottom
CornerTopRight,
/// One main corner window in the bottom left with a column of windows on the right and a row on the top.
CornerBottomLeft,
/// One main corner window in the bottom right with a column of windows on the left and a row on the top.
CornerBottomRight,
}
impl TagHandle {
/// Activate this tag and deactivate all other ones on the same output.
///
@ -396,12 +404,18 @@ impl TagHandle {
.unwrap()
.into_inner();
let output = OUTPUT.get().expect("OUTPUT doesn't exist");
let output = self.api.output;
let window = self.api.window;
TagProperties {
active: response.active,
name: response.name,
output: response.output_name.map(|name| output.new_handle(name)),
windows: response
.window_ids
.into_iter()
.map(|id| window.new_handle(id))
.collect(),
}
}
@ -440,6 +454,18 @@ impl TagHandle {
pub async fn output_async(&self) -> Option<OutputHandle> {
self.props_async().await.output
}
/// Get all windows with this tag.
///
/// Shorthand for `self.props().windows`.
pub fn windows(&self) -> Vec<WindowHandle> {
self.props().windows
}
/// The async version of [`TagHandle::windows`].
pub async fn windows_async(&self) -> Vec<WindowHandle> {
self.props_async().await.windows
}
}
/// Properties of a tag.
@ -451,4 +477,6 @@ pub struct TagProperties {
pub name: Option<String>,
/// The output the tag is on
pub output: Option<OutputHandle>,
/// The windows that have this tag
pub windows: Vec<WindowHandle>,
}

View file

@ -12,6 +12,8 @@
//!
//! This module also allows you to set window rules; see the [rules] module for more information.
use std::sync::OnceLock;
use futures::FutureExt;
use num_enum::TryFromPrimitive;
use pinnacle_api_defs::pinnacle::{
@ -34,7 +36,7 @@ use crate::{
signal::{SignalHandle, WindowSignal},
tag::TagHandle,
util::{Batch, Geometry},
SIGNAL, TAG,
ApiModules,
};
use self::rules::{WindowRule, WindowRuleCondition};
@ -47,19 +49,26 @@ pub mod rules;
#[derive(Debug, Clone)]
pub struct Window {
window_client: WindowServiceClient<Channel>,
api: OnceLock<ApiModules>,
}
impl Window {
pub(crate) fn new(channel: Channel) -> Self {
Self {
window_client: WindowServiceClient::new(channel.clone()),
api: OnceLock::new(),
}
}
pub(crate) fn finish_init(&self, api: ApiModules) {
self.api.set(api).unwrap();
}
pub(crate) fn new_handle(&self, id: u32) -> WindowHandle {
WindowHandle {
id,
window_client: self.window_client.clone(),
api: self.api.get().unwrap().clone(),
}
}
@ -180,7 +189,7 @@ impl Window {
/// You can pass in a [`WindowSignal`] along with a callback and it will get run
/// with the necessary arguments every time a signal of that type is received.
pub fn connect_signal(&self, signal: WindowSignal) -> SignalHandle {
let mut signal_state = block_on_tokio(SIGNAL.get().expect("SIGNAL doesn't exist").write());
let mut signal_state = block_on_tokio(self.api.get().unwrap().signal.write());
match signal {
WindowSignal::PointerEnter(f) => signal_state.window_pointer_enter.add_callback(f),
@ -196,6 +205,7 @@ impl Window {
pub struct WindowHandle {
id: u32,
window_client: WindowServiceClient<Channel>,
api: ApiModules,
}
impl PartialEq for WindowHandle {
@ -572,8 +582,6 @@ impl WindowHandle {
height: geo.height() as u32,
});
let tag = TAG.get().expect("TAG doesn't exist");
WindowProperties {
geometry,
class: response.class,
@ -584,7 +592,7 @@ impl WindowHandle {
tags: response
.tag_ids
.into_iter()
.map(|id| tag.new_handle(id))
.map(|id| self.api.tag.new_handle(id))
.collect(),
}
}

View file

@ -63,8 +63,12 @@ pub mod pinnacle {
impl_signal_request!(
OutputConnectRequest,
OutputDisconnectRequest,
OutputResizeRequest,
OutputMoveRequest,
WindowPointerEnterRequest,
WindowPointerLeaveRequest
WindowPointerLeaveRequest,
TagActiveRequest
);
}
}

View file

@ -16,7 +16,7 @@ use pinnacle_api_defs::pinnacle::{
self,
v0alpha1::{
output_service_server, set_scale_request::AbsoluteOrRelative, SetLocationRequest,
SetModeRequest, SetScaleRequest,
SetModeRequest, SetScaleRequest, SetTransformRequest,
},
},
process::v0alpha1::{process_service_server, SetEnvRequest, SpawnRequest, SpawnResponse},
@ -30,11 +30,13 @@ use pinnacle_api_defs::pinnacle::{
SwitchToRequest,
},
},
v0alpha1::{pinnacle_service_server, PingRequest, PingResponse, QuitRequest, SetOrToggle},
v0alpha1::{
pinnacle_service_server, PingRequest, PingResponse, QuitRequest, ReloadConfigRequest,
SetOrToggle,
},
};
use smithay::{
backend::renderer::TextureFilter,
desktop::layer_map_for_output,
input::keyboard::XkbConfig,
output::Scale,
reexports::{calloop, input as libinput},
@ -193,6 +195,18 @@ impl pinnacle_service_server::PinnacleService for PinnacleService {
.await
}
async fn reload_config(
&self,
_request: Request<ReloadConfigRequest>,
) -> Result<Response<()>, Status> {
run_unary_no_response(&self.sender, |state| {
state
.start_config(state.config.dir(&state.xdg_base_dirs))
.expect("failed to restart config");
})
.await
}
async fn ping(&self, request: Request<PingRequest>) -> Result<Response<PingResponse>, Status> {
let payload = request.into_inner().payload;
Ok(Response::new(PingResponse { payload }))
@ -707,9 +721,9 @@ impl tag_service_server::TagService for TagService {
};
match set_or_toggle {
SetOrToggle::Set => tag.set_active(true),
SetOrToggle::Unset => tag.set_active(false),
SetOrToggle::Toggle => tag.set_active(!tag.active()),
SetOrToggle::Set => tag.set_active(true, state),
SetOrToggle::Unset => tag.set_active(false, state),
SetOrToggle::Toggle => tag.set_active(!tag.active(), state),
SetOrToggle::Unspecified => unreachable!(),
}
@ -737,11 +751,11 @@ impl tag_service_server::TagService for TagService {
let Some(tag) = tag_id.tag(state) else { return };
let Some(output) = tag.output(state) else { return };
output.with_state_mut(|state| {
for op_tag in state.tags.iter_mut() {
op_tag.set_active(false);
output.with_state_mut(|op_state| {
for op_tag in op_state.tags.iter_mut() {
op_tag.set_active(false, state);
}
tag.set_active(true);
tag.set_active(true, state);
});
state.request_layout(&output);
@ -874,11 +888,26 @@ impl tag_service_server::TagService for TagService {
.map(|output| output.name());
let active = tag.as_ref().map(|tag| tag.active());
let name = tag.as_ref().map(|tag| tag.name());
let window_ids = tag
.as_ref()
.map(|tag| {
state
.windows
.iter()
.filter_map(|win| {
win.with_state(|win_state| {
win_state.tags.contains(tag).then_some(win_state.id.0)
})
})
.collect()
})
.unwrap_or_default();
tag::v0alpha1::GetPropertiesResponse {
active,
name,
output_name,
window_ids,
}
})
.await
@ -940,8 +969,7 @@ impl output_service_server::OutputService for OutputService {
if let Some(y) = y {
loc.y = y;
}
output.change_current_state(None, None, None, Some(loc));
state.space.map_output(&output, loc);
state.change_output_state(&output, None, None, None, Some(loc));
debug!("Mapping output {} to {loc:?}", output.name());
state.request_layout(&output);
})
@ -1001,8 +1029,49 @@ impl output_service_server::OutputService for OutputService {
current_scale = f64::max(current_scale, 0.25);
output.change_current_state(None, None, Some(Scale::Fractional(current_scale)), None);
layer_map_for_output(&output).arrange();
state.change_output_state(
&output,
None,
None,
Some(Scale::Fractional(current_scale)),
None,
);
state.request_layout(&output);
state.schedule_render(&output);
})
.await
}
async fn set_transform(
&self,
request: Request<SetTransformRequest>,
) -> Result<Response<()>, Status> {
let request = request.into_inner();
let smithay_transform = match request.transform() {
output::v0alpha1::Transform::Unspecified => {
return Err(Status::invalid_argument("transform was unspecified"));
}
output::v0alpha1::Transform::Normal => smithay::utils::Transform::Normal,
output::v0alpha1::Transform::Transform90 => smithay::utils::Transform::_90,
output::v0alpha1::Transform::Transform180 => smithay::utils::Transform::_180,
output::v0alpha1::Transform::Transform270 => smithay::utils::Transform::_270,
output::v0alpha1::Transform::Flipped => smithay::utils::Transform::Flipped,
output::v0alpha1::Transform::Flipped90 => smithay::utils::Transform::Flipped90,
output::v0alpha1::Transform::Flipped180 => smithay::utils::Transform::Flipped180,
output::v0alpha1::Transform::Flipped270 => smithay::utils::Transform::Flipped270,
};
let Some(output_name) = request.output_name else {
return Err(Status::invalid_argument("output_name was null"));
};
run_unary_no_response(&self.sender, move |state| {
let Some(output) = OutputName(output_name).output(state) else {
return;
};
state.change_output_state(&output, None, Some(smithay_transform), None, None);
state.request_layout(&output);
state.schedule_render(&output);
})
@ -1109,6 +1178,27 @@ impl output_service_server::OutputService for OutputService {
.as_ref()
.map(|output| output.current_scale().fractional_scale() as f32);
let transform = output.as_ref().map(|output| {
(match output.current_transform() {
smithay::utils::Transform::Normal => output::v0alpha1::Transform::Normal,
smithay::utils::Transform::_90 => output::v0alpha1::Transform::Transform90,
smithay::utils::Transform::_180 => output::v0alpha1::Transform::Transform180,
smithay::utils::Transform::_270 => output::v0alpha1::Transform::Transform270,
smithay::utils::Transform::Flipped => output::v0alpha1::Transform::Flipped,
smithay::utils::Transform::Flipped90 => output::v0alpha1::Transform::Flipped90,
smithay::utils::Transform::Flipped180 => {
output::v0alpha1::Transform::Flipped180
}
smithay::utils::Transform::Flipped270 => {
output::v0alpha1::Transform::Flipped270
}
}) as i32
});
let serial = output.as_ref().and_then(|output| {
output.with_state(|state| state.serial.map(|serial| serial.get()))
});
output::v0alpha1::GetPropertiesResponse {
make,
model,
@ -1124,6 +1214,8 @@ impl output_service_server::OutputService for OutputService {
focused,
tag_ids,
scale,
transform,
serial,
}
})
.await

View file

@ -1,9 +1,11 @@
use std::collections::VecDeque;
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
signal_service_server, OutputConnectRequest, OutputConnectResponse, SignalRequest,
StreamControl, WindowPointerEnterRequest, WindowPointerEnterResponse,
WindowPointerLeaveRequest, WindowPointerLeaveResponse,
signal_service_server, OutputConnectRequest, OutputConnectResponse, OutputDisconnectRequest,
OutputDisconnectResponse, OutputMoveRequest, OutputMoveResponse, OutputResizeRequest,
OutputResizeResponse, SignalRequest, StreamControl, TagActiveRequest, TagActiveResponse,
WindowPointerEnterRequest, WindowPointerEnterResponse, WindowPointerLeaveRequest,
WindowPointerLeaveResponse,
};
use tokio::{sync::mpsc::UnboundedSender, task::JoinHandle};
use tonic::{Request, Response, Status, Streaming};
@ -15,16 +17,28 @@ use super::{run_bidirectional_streaming, ResponseStream, StateFnSender};
#[derive(Debug, Default)]
pub struct SignalState {
// Output
pub output_connect: SignalData<OutputConnectResponse, VecDeque<OutputConnectResponse>>,
pub output_disconnect: SignalData<OutputDisconnectResponse, VecDeque<OutputDisconnectResponse>>,
pub output_resize: SignalData<OutputResizeResponse, VecDeque<OutputResizeResponse>>,
pub output_move: SignalData<OutputMoveResponse, VecDeque<OutputMoveResponse>>,
// Window
pub window_pointer_enter:
SignalData<WindowPointerEnterResponse, VecDeque<WindowPointerEnterResponse>>,
pub window_pointer_leave:
SignalData<WindowPointerLeaveResponse, VecDeque<WindowPointerLeaveResponse>>,
// Tag
pub tag_active: SignalData<TagActiveResponse, VecDeque<TagActiveResponse>>,
}
impl SignalState {
pub fn clear(&mut self) {
self.output_connect.disconnect();
self.output_disconnect.disconnect();
self.output_resize.disconnect();
self.output_move.disconnect();
self.window_pointer_enter.disconnect();
self.window_pointer_leave.disconnect();
}
@ -171,9 +185,15 @@ impl SignalService {
#[tonic::async_trait]
impl signal_service_server::SignalService for SignalService {
type OutputConnectStream = ResponseStream<OutputConnectResponse>;
type OutputDisconnectStream = ResponseStream<OutputDisconnectResponse>;
type OutputResizeStream = ResponseStream<OutputResizeResponse>;
type OutputMoveStream = ResponseStream<OutputMoveResponse>;
type WindowPointerEnterStream = ResponseStream<WindowPointerEnterResponse>;
type WindowPointerLeaveStream = ResponseStream<WindowPointerLeaveResponse>;
type TagActiveStream = ResponseStream<TagActiveResponse>;
async fn output_connect(
&self,
request: Request<Streaming<OutputConnectRequest>>,
@ -185,6 +205,39 @@ impl signal_service_server::SignalService for SignalService {
})
}
async fn output_disconnect(
&self,
request: Request<Streaming<OutputDisconnectRequest>>,
) -> Result<Response<Self::OutputDisconnectStream>, Status> {
let in_stream = request.into_inner();
start_signal_stream(self.sender.clone(), in_stream, |state| {
&mut state.signal_state.output_disconnect
})
}
async fn output_resize(
&self,
request: Request<Streaming<OutputResizeRequest>>,
) -> Result<Response<Self::OutputResizeStream>, Status> {
let in_stream = request.into_inner();
start_signal_stream(self.sender.clone(), in_stream, |state| {
&mut state.signal_state.output_resize
})
}
async fn output_move(
&self,
request: Request<Streaming<OutputMoveRequest>>,
) -> Result<Response<Self::OutputMoveStream>, Status> {
let in_stream = request.into_inner();
start_signal_stream(self.sender.clone(), in_stream, |state| {
&mut state.signal_state.output_move
})
}
async fn window_pointer_enter(
&self,
request: Request<Streaming<WindowPointerEnterRequest>>,
@ -206,4 +259,15 @@ impl signal_service_server::SignalService for SignalService {
&mut state.signal_state.window_pointer_leave
})
}
async fn tag_active(
&self,
request: Request<Streaming<TagActiveRequest>>,
) -> Result<Response<Self::TagActiveStream>, Status> {
let in_stream = request.into_inner();
start_signal_stream(self.sender.clone(), in_stream, |state| {
&mut state.signal_state.tag_active
})
}
}

View file

@ -1,6 +1,10 @@
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
OutputConnectResponse, OutputDisconnectResponse,
};
use smithay::backend::renderer::test::DummyRenderer;
use smithay::backend::renderer::ImportMemWl;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Physical, Size};
use std::ffi::OsString;
use std::path::PathBuf;
@ -116,3 +120,45 @@ pub fn setup_dummy(
Ok((state, event_loop))
}
impl State {
pub fn new_output(&mut self, name: impl std::fmt::Display, size: Size<i32, Physical>) {
let mode = smithay::output::Mode {
size,
refresh: 144_000,
};
let physical_properties = smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "Pinnacle".to_string(),
model: "Dummy Output".to_string(),
};
let output = Output::new(name.to_string(), physical_properties);
output.change_current_state(Some(mode), None, None, Some((0, 0).into()));
output.set_preferred(mode);
output.create_global::<State>(&self.display_handle);
self.space.map_output(&output, (0, 0));
self.signal_state.output_connect.signal(|buf| {
buf.push_back(OutputConnectResponse {
output_name: Some(output.name()),
});
});
}
pub fn remove_output(&mut self, output: &Output) {
self.space.unmap_output(output);
self.signal_state.output_disconnect.signal(|buffer| {
buffer.push_back(OutputDisconnectResponse {
output_name: Some(output.name()),
})
});
}
}

View file

@ -11,7 +11,9 @@ use std::{
};
use anyhow::{anyhow, ensure, Context};
use pinnacle_api_defs::pinnacle::signal::v0alpha1::OutputConnectResponse;
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
OutputConnectResponse, OutputDisconnectResponse,
};
use smithay::{
backend::{
allocator::{
@ -49,10 +51,7 @@ use smithay::{
vulkan::{self, version::Version, PhysicalDevice},
SwapBuffersError,
},
desktop::{
layer_map_for_output,
utils::{send_frames_surface_tree, OutputPresentationFeedback},
},
desktop::utils::{send_frames_surface_tree, OutputPresentationFeedback},
input::pointer::CursorImageStatus,
output::{Output, PhysicalProperties, Subpixel},
reexports::{
@ -272,16 +271,14 @@ impl State {
{
match render_surface.compositor.use_mode(drm_mode) {
Ok(()) => {
output.change_current_state(Some(mode), None, None, None);
layer_map_for_output(output).arrange();
self.change_output_state(output, Some(mode), None, None, None);
}
Err(err) => error!("Failed to resize output: {err}"),
}
}
}
} else {
output.change_current_state(Some(mode), None, None, None);
layer_map_for_output(output).arrange();
self.change_output_state(output, Some(mode), None, None, None);
}
self.schedule_render(output);
@ -1010,11 +1007,11 @@ impl State {
connector.interface_id()
);
let (make, model) = EdidInfo::try_from_connector(&device.drm, connector.handle())
.map(|info| (info.manufacturer, info.model))
let (make, model, serial) = EdidInfo::try_from_connector(&device.drm, connector.handle())
.map(|info| (info.manufacturer, info.model, info.serial))
.unwrap_or_else(|err| {
warn!("Failed to parse EDID info: {err}");
("Unknown".into(), "Unknown".into())
("Unknown".into(), "Unknown".into(), None)
});
let (phys_w, phys_h) = connector.size().unwrap_or((0, 0));
@ -1038,10 +1035,14 @@ impl State {
);
let global = output.create_global::<State>(&udev.display_handle);
output.with_state_mut(|state| state.serial = serial);
for mode in modes {
output.add_mode(mode);
}
output.set_preferred(wl_mode);
self.output_focus_stack.set_focus(output.clone());
let x = self.space.outputs().fold(0, |acc, o| {
@ -1052,10 +1053,6 @@ impl State {
});
let position = (x, 0).into();
output.set_preferred(wl_mode);
output.change_current_state(Some(wl_mode), None, None, Some(position));
self.space.map_output(&output, position);
output.user_data().insert_if_missing(|| UdevOutputData {
crtc,
device_id: node,
@ -1122,6 +1119,8 @@ impl State {
device.surfaces.insert(crtc, surface);
self.change_output_state(&output, Some(wl_mode), None, None, Some(position));
// If there is saved connector state, the connector was previously plugged in.
// In this case, restore its tags and location.
// TODO: instead of checking the connector, check the monitor's edid info instead
@ -1131,11 +1130,8 @@ impl State {
.get(&OutputName(output.name()))
{
let ConnectorSavedState { loc, tags, scale } = saved_state;
output.change_current_state(None, None, *scale, Some(*loc));
self.space.map_output(&output, *loc);
output.with_state_mut(|state| state.tags = tags.clone());
self.change_output_state(&output, None, None, *scale, Some(*loc));
} else {
self.signal_state.output_connect.signal(|buffer| {
buffer.push_back(OutputConnectResponse {
@ -1187,6 +1183,12 @@ impl State {
);
self.space.unmap_output(&output);
self.gamma_control_manager_state.output_removed(&output);
self.signal_state.output_disconnect.signal(|buffer| {
buffer.push_back(OutputDisconnectResponse {
output_name: Some(output.name()),
})
});
}
}

View file

@ -1,3 +1,5 @@
use std::num::NonZeroU32;
use smithay::reexports::drm::control::{connector, property, Device, ResourceHandle};
// A bunch of this stuff is from cosmic-comp
@ -6,6 +8,7 @@ use smithay::reexports::drm::control::{connector, property, Device, ResourceHand
pub struct EdidInfo {
pub model: String,
pub manufacturer: String,
pub serial: Option<NonZeroU32>,
}
impl EdidInfo {
@ -55,6 +58,9 @@ fn parse_edid(buffer: &[u8]) -> anyhow::Result<EdidInfo> {
let manufacturer = get_manufacturer([char1 as char, char2 as char, char3 as char]);
// INFO: This probably *isn't* completely unique between all monitors
let serial = u32::from_le_bytes(buffer[12..=15].try_into()?);
// Monitor names are inside of these display/monitor descriptors at bytes 72..=125.
// Each descriptor is 18 bytes long.
let descriptor1 = &buffer[72..=89];
@ -99,6 +105,7 @@ fn parse_edid(buffer: &[u8]) -> anyhow::Result<EdidInfo> {
Ok(EdidInfo {
model,
manufacturer,
serial: NonZeroU32::new(serial),
})
}

View file

@ -15,7 +15,6 @@ use smithay::{
},
winit::{self, WinitEvent, WinitGraphicsBackend},
},
desktop::layer_map_for_output,
input::pointer::CursorImageStatus,
output::{Output, Scale, Subpixel},
reexports::{
@ -109,7 +108,7 @@ pub fn setup_winit(
model: "Winit Window".to_string(),
};
let output = Output::new("Pinnacle window".to_string(), physical_properties);
let output = Output::new("Pinnacle Window".to_string(), physical_properties);
output.change_current_state(
Some(mode),
@ -221,13 +220,13 @@ pub fn setup_winit(
size,
refresh: 144_000,
};
output.change_current_state(
state.change_output_state(
&output,
Some(mode),
None,
Some(Scale::Fractional(scale_factor)),
None,
);
layer_map_for_output(&output).arrange();
state.request_layout(&output);
}
WinitEvent::Focus(focused) => {

View file

@ -1,8 +1,14 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use std::cell::RefCell;
use std::{cell::RefCell, num::NonZeroU32};
use smithay::output::Output;
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{OutputMoveResponse, OutputResizeResponse};
use smithay::{
desktop::layer_map_for_output,
output::{Mode, Output, Scale},
utils::{Logical, Point, Transform},
};
use tracing::info;
use crate::{
focus::WindowKeyboardFocusStack,
@ -35,6 +41,7 @@ pub struct OutputState {
pub tags: Vec<Tag>,
pub focus_stack: WindowKeyboardFocusStack,
pub screencopy: Option<Screencopy>,
pub serial: Option<NonZeroU32>,
}
impl WithState for Output {
@ -68,3 +75,40 @@ impl OutputState {
self.tags.iter().filter(|tag| tag.active())
}
}
impl State {
/// A wrapper around [`Output::change_current_state`] that additionally sends an output
/// geometry signal.
pub fn change_output_state(
&mut self,
output: &Output,
mode: Option<Mode>,
transform: Option<Transform>,
scale: Option<Scale>,
location: Option<Point<i32, Logical>>,
) {
output.change_current_state(mode, transform, scale, location);
if let Some(location) = location {
info!(?location);
self.space.map_output(output, location);
self.signal_state.output_move.signal(|buf| {
buf.push_back(OutputMoveResponse {
output_name: Some(output.name()),
x: Some(location.x),
y: Some(location.y),
});
});
}
if mode.is_some() || transform.is_some() || scale.is_some() {
layer_map_for_output(output).arrange();
self.signal_state.output_resize.signal(|buf| {
let geo = self.space.output_geometry(output);
buf.push_back(OutputResizeResponse {
output_name: Some(output.name()),
logical_width: geo.map(|geo| geo.size.w as u32),
logical_height: geo.map(|geo| geo.size.h as u32),
});
});
}
}
}

View file

@ -1,10 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use std::{
cell::RefCell,
hash::Hash,
rc::Rc,
sync::atomic::{AtomicU32, Ordering},
sync::{
atomic::{AtomicU32, Ordering},
Arc, Mutex,
},
};
use smithay::output::Output;
@ -63,31 +64,46 @@ impl Eq for TagInner {}
///
/// A window may have 0 or more tags, and you can display 0 or more tags
/// on each output at a time.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tag(Rc<RefCell<TagInner>>);
#[derive(Debug, Clone)]
pub struct Tag(Arc<Mutex<TagInner>>);
impl PartialEq for Tag {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
// RefCell Safety: These methods should never panic because they are all self-contained or Copy.
impl Tag {
pub fn id(&self) -> TagId {
self.0.borrow().id
self.0.lock().expect("tag already locked").id
}
pub fn name(&self) -> String {
self.0.borrow().name.clone()
self.0.lock().expect("tag already locked").name.clone()
}
pub fn active(&self) -> bool {
self.0.borrow().active
self.0.lock().expect("tag already locked").active
}
pub fn set_active(&self, active: bool) {
self.0.borrow_mut().active = active;
pub fn set_active(&self, active: bool, state: &mut State) {
self.0.lock().expect("tag already locked").active = active;
state.signal_state.tag_active.signal(|buf| {
buf.push_back(
pinnacle_api_defs::pinnacle::signal::v0alpha1::TagActiveResponse {
tag_id: Some(self.id().0),
active: Some(self.active()),
},
);
})
}
}
impl Tag {
pub fn new(name: String) -> Self {
Self(Rc::new(RefCell::new(TagInner {
Self(Arc::new(Mutex::new(TagInner {
id: TagId::next(),
name,
active: false,

88
tests/common/mod.rs Normal file
View file

@ -0,0 +1,88 @@
use std::{panic::UnwindSafe, time::Duration};
use pinnacle::{backend::dummy::setup_dummy, state::State};
use smithay::{
output::Output,
reexports::calloop::{
self,
channel::{Event, Sender},
},
};
#[allow(clippy::type_complexity)]
pub fn with_state(
sender: &Sender<Box<dyn FnOnce(&mut State) + Send>>,
with_state: impl FnOnce(&mut State) + Send + 'static,
) {
sender.send(Box::new(with_state)).unwrap();
}
pub fn sleep_secs(secs: u64) {
std::thread::sleep(Duration::from_secs(secs));
}
pub fn test_api(
test: impl FnOnce(Sender<Box<dyn FnOnce(&mut State) + Send>>) + Send + UnwindSafe + 'static,
) -> anyhow::Result<()> {
let (mut state, mut event_loop) = setup_dummy(true, None)?;
let (sender, recv) = calloop::channel::channel::<Box<dyn FnOnce(&mut State) + Send>>();
event_loop
.handle()
.insert_source(recv, |event, _, state| match event {
Event::Msg(f) => f(state),
Event::Closed => (),
})
.map_err(|_| anyhow::anyhow!("failed to insert source"))?;
let tempdir = tempfile::tempdir()?;
state.start_grpc_server(tempdir.path())?;
let loop_signal = event_loop.get_signal();
let join_handle = std::thread::spawn(move || {
let res = std::panic::catch_unwind(|| {
test(sender);
});
loop_signal.stop();
if let Err(err) = res {
std::panic::resume_unwind(err);
}
});
event_loop.run(None, &mut state, |state| {
state.fixup_z_layering();
state.space.refresh();
state.popup_manager.cleanup();
state
.display_handle
.flush_clients()
.expect("failed to flush client buffers");
// TODO: couple these or something, this is really error-prone
assert_eq!(
state.windows.len(),
state.z_index_stack.len(),
"Length of `windows` and `z_index_stack` are different. \
If you see this, report it to the developer."
);
})?;
if let Err(err) = join_handle.join() {
panic!("{err:?}");
}
Ok(())
}
pub fn output_for_name(state: &State, name: &str) -> Output {
state
.space
.outputs()
.find(|op| op.name() == name)
.unwrap()
.clone()
}

File diff suppressed because it is too large Load diff

354
tests/rust_api.rs Normal file
View file

@ -0,0 +1,354 @@
mod common;
use std::thread::JoinHandle;
use pinnacle_api::ApiModules;
use test_log::test;
use crate::common::output_for_name;
use crate::common::{sleep_secs, test_api, with_state};
#[tokio::main]
async fn run_rust_inner(run: impl FnOnce(ApiModules) + Send + 'static) {
let (api, _recv) = pinnacle_api::connect().await.unwrap();
run(api.clone());
}
fn run_rust(run: impl FnOnce(ApiModules) + Send + 'static) {
std::thread::spawn(|| {
run_rust_inner(run);
})
.join()
.unwrap();
}
#[tokio::main]
async fn setup_rust_inner(run: impl FnOnce(ApiModules) + Send + 'static) {
let (api, recv) = pinnacle_api::connect().await.unwrap();
run(api.clone());
pinnacle_api::listen(api, recv).await;
}
fn setup_rust(run: impl FnOnce(ApiModules) + Send + 'static) -> JoinHandle<()> {
std::thread::spawn(|| {
setup_rust_inner(run);
})
}
mod output {
use pinnacle::state::WithState;
use pinnacle_api::output::{Alignment, OutputId, OutputLoc, OutputSetup, UpdateLocsOn};
use smithay::{output::Output, utils::Rectangle};
use super::*;
#[tokio::main]
#[self::test]
async fn setup() -> anyhow::Result<()> {
test_api(|sender| {
setup_rust(|api| {
api.output.setup([
OutputSetup::new_with_matcher(|_| true).with_tags(["1", "2", "3"]),
OutputSetup::new_with_matcher(|op| op.name().contains("Test"))
.with_tags(["Test 4", "Test 5"]),
OutputSetup::new(OutputId::name("Second"))
.with_scale(2.0)
.with_mode(pinnacle_api::output::Mode {
pixel_width: 6900,
pixel_height: 420,
refresh_rate_millihertz: 69420,
})
.with_transform(pinnacle_api::output::Transform::_90),
]);
});
sleep_secs(1);
with_state(&sender, |state| {
state.new_output("First", (300, 200).into());
state.new_output("Second", (300, 200).into());
state.new_output("Test Third", (300, 200).into());
});
sleep_secs(1);
with_state(&sender, |state| {
let original_op = output_for_name(state, "Pinnacle Window");
let first_op = output_for_name(state, "First");
let second_op = output_for_name(state, "Second");
let test_third_op = output_for_name(state, "Test Third");
let tags_for = |output: &Output| {
output
.with_state(|state| state.tags.iter().map(|t| t.name()).collect::<Vec<_>>())
};
let focused_tags_for = |output: &Output| {
output.with_state(|state| {
state.focused_tags().map(|t| t.name()).collect::<Vec<_>>()
})
};
assert_eq!(tags_for(&original_op), vec!["1", "2", "3"]);
assert_eq!(tags_for(&first_op), vec!["1", "2", "3"]);
assert_eq!(tags_for(&second_op), vec!["1", "2", "3"]);
assert_eq!(
tags_for(&test_third_op),
vec!["1", "2", "3", "Test 4", "Test 5"]
);
assert_eq!(focused_tags_for(&original_op), vec!["1"]);
assert_eq!(focused_tags_for(&test_third_op), vec!["1"]);
assert_eq!(second_op.current_scale().fractional_scale(), 2.0);
let second_mode = second_op.current_mode().unwrap();
assert_eq!(second_mode.size.w, 6900);
assert_eq!(second_mode.size.h, 420);
assert_eq!(second_mode.refresh, 69420);
assert_eq!(
second_op.current_transform(),
smithay::utils::Transform::_90
);
});
})
}
#[tokio::main]
#[self::test]
async fn setup_loc_with_cyclic_relative_locs_works() -> anyhow::Result<()> {
test_api(|sender| {
setup_rust(|api| {
api.output.setup_locs(
UpdateLocsOn::all(),
[
(OutputId::name("Pinnacle Window"), OutputLoc::Point(0, 0)),
(
OutputId::name("First"),
OutputLoc::RelativeTo(
OutputId::name("Second"),
Alignment::LeftAlignTop,
),
),
(
OutputId::name("Second"),
OutputLoc::RelativeTo(
OutputId::name("First"),
Alignment::RightAlignTop,
),
),
],
);
});
sleep_secs(1);
with_state(&sender, |state| {
state.new_output("First", (300, 200).into());
});
sleep_secs(1);
with_state(&sender, |state| {
let original_op = output_for_name(state, "Pinnacle Window");
let first_op = output_for_name(state, "First");
let original_geo = state.space.output_geometry(&original_op).unwrap();
let first_geo = state.space.output_geometry(&first_op).unwrap();
assert_eq!(
original_geo,
Rectangle::from_loc_and_size((0, 0), (1920, 1080))
);
assert_eq!(
first_geo,
Rectangle::from_loc_and_size((1920, 0), (300, 200))
);
state.new_output("Second", (500, 500).into());
});
sleep_secs(1);
with_state(&sender, |state| {
let original_op = output_for_name(state, "Pinnacle Window");
let first_op = output_for_name(state, "First");
let second_op = output_for_name(state, "Second");
let original_geo = state.space.output_geometry(&original_op).unwrap();
let first_geo = state.space.output_geometry(&first_op).unwrap();
let second_geo = state.space.output_geometry(&second_op).unwrap();
assert_eq!(
original_geo,
Rectangle::from_loc_and_size((0, 0), (1920, 1080))
);
assert_eq!(
first_geo,
Rectangle::from_loc_and_size((1920, 0), (300, 200))
);
assert_eq!(
second_geo,
Rectangle::from_loc_and_size((1920 + 300, 0), (500, 500))
);
});
})
}
#[tokio::main]
#[self::test]
async fn setup_loc_with_relative_locs_with_more_than_one_relative_works() -> anyhow::Result<()>
{
test_api(|sender| {
setup_rust(|api| {
api.output.setup_locs(
UpdateLocsOn::all(),
[
(OutputId::name("Pinnacle Window"), OutputLoc::Point(0, 0)),
(
OutputId::name("First"),
OutputLoc::RelativeTo(
OutputId::name("Pinnacle Window"),
Alignment::BottomAlignLeft,
),
),
(
OutputId::name("Second"),
OutputLoc::RelativeTo(
OutputId::name("First"),
Alignment::BottomAlignLeft,
),
),
(
OutputId::name("Third"),
OutputLoc::RelativeTo(
OutputId::name("Second"),
Alignment::BottomAlignLeft,
),
),
(
OutputId::name("Third"),
OutputLoc::RelativeTo(
OutputId::name("First"),
Alignment::BottomAlignLeft,
),
),
],
);
});
sleep_secs(1);
with_state(&sender, |state| {
state.new_output("First", (300, 200).into());
state.new_output("Second", (300, 700).into());
state.new_output("Third", (300, 400).into());
});
sleep_secs(1);
with_state(&sender, |state| {
let original_op = output_for_name(state, "Pinnacle Window");
let first_op = output_for_name(state, "First");
let second_op = output_for_name(state, "Second");
let third_op = output_for_name(state, "Third");
let original_geo = state.space.output_geometry(&original_op).unwrap();
let first_geo = state.space.output_geometry(&first_op).unwrap();
let second_geo = state.space.output_geometry(&second_op).unwrap();
let third_geo = state.space.output_geometry(&third_op).unwrap();
assert_eq!(
original_geo,
Rectangle::from_loc_and_size((0, 0), (1920, 1080))
);
assert_eq!(
first_geo,
Rectangle::from_loc_and_size((0, 1080), (300, 200))
);
assert_eq!(
second_geo,
Rectangle::from_loc_and_size((0, 1080 + 200), (300, 700))
);
assert_eq!(
third_geo,
Rectangle::from_loc_and_size((0, 1080 + 200 + 700), (300, 400))
);
state.remove_output(&second_op);
});
sleep_secs(1);
with_state(&sender, |state| {
let original_op = output_for_name(state, "Pinnacle Window");
let first_op = output_for_name(state, "First");
let third_op = output_for_name(state, "Third");
let original_geo = state.space.output_geometry(&original_op).unwrap();
let first_geo = state.space.output_geometry(&first_op).unwrap();
let third_geo = state.space.output_geometry(&third_op).unwrap();
assert_eq!(
original_geo,
Rectangle::from_loc_and_size((0, 0), (1920, 1080))
);
assert_eq!(
first_geo,
Rectangle::from_loc_and_size((0, 1080), (300, 200))
);
assert_eq!(
third_geo,
Rectangle::from_loc_and_size((0, 1080 + 200), (300, 400))
);
});
})
}
mod handle {
use pinnacle_api::output::Transform;
use super::*;
#[tokio::main]
#[self::test]
async fn set_transform() -> anyhow::Result<()> {
test_api(|sender| {
run_rust(|api| {
api.output
.get_focused()
.unwrap()
.set_transform(Transform::Flipped270);
});
sleep_secs(1);
with_state(&sender, |state| {
let op = state.focused_output().unwrap();
assert_eq!(
op.current_transform(),
smithay::utils::Transform::Flipped270
);
});
run_rust(|api| {
api.output
.get_focused()
.unwrap()
.set_transform(Transform::_180);
});
sleep_secs(1);
with_state(&sender, |state| {
let op = state.focused_output().unwrap();
assert_eq!(op.current_transform(), smithay::utils::Transform::_180);
});
})
}
}
}