mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-27 19:58:08 +01:00
Merge pull request #201 from pinnacle-comp/api_enhancements
Add various API enhancements
This commit is contained in:
commit
9af11d7b6a
34 changed files with 2920 additions and 463 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1767,6 +1767,7 @@ dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"nix",
|
"nix",
|
||||||
"pinnacle",
|
"pinnacle",
|
||||||
|
"pinnacle-api",
|
||||||
"pinnacle-api-defs",
|
"pinnacle-api-defs",
|
||||||
"prost",
|
"prost",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -1796,6 +1797,7 @@ dependencies = [
|
||||||
name = "pinnacle-api"
|
name = "pinnacle-api"
|
||||||
version = "0.0.2"
|
version = "0.0.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.5.0",
|
||||||
"futures",
|
"futures",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"pinnacle-api-defs",
|
"pinnacle-api-defs",
|
||||||
|
|
|
@ -20,6 +20,7 @@ pinnacle-api-defs = { path = "./pinnacle-api-defs" }
|
||||||
# Misc.
|
# Misc.
|
||||||
xkbcommon = "0.7.0"
|
xkbcommon = "0.7.0"
|
||||||
xdg = "2.5.2"
|
xdg = "2.5.2"
|
||||||
|
bitflags = "2.5.0"
|
||||||
|
|
||||||
########################################################################yo😎###########
|
########################################################################yo😎###########
|
||||||
|
|
||||||
|
@ -59,7 +60,7 @@ tokio-stream = { workspace = true }
|
||||||
clap = { version = "4.5.4", features = ["derive"] }
|
clap = { version = "4.5.4", features = ["derive"] }
|
||||||
cliclack = "0.2.5"
|
cliclack = "0.2.5"
|
||||||
# Misc.
|
# Misc.
|
||||||
bitflags = "2.5.0"
|
bitflags = { workspace = true }
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
toml = "0.8.12"
|
toml = "0.8.12"
|
||||||
shellexpand = { version = "3.1.0", features = ["path"] }
|
shellexpand = { version = "3.1.0", features = ["path"] }
|
||||||
|
@ -109,6 +110,7 @@ temp-env = "0.3.6"
|
||||||
tempfile = "3.10.1"
|
tempfile = "3.10.1"
|
||||||
test-log = { version = "0.2.15", default-features = false, features = ["trace"] }
|
test-log = { version = "0.2.15", default-features = false, features = ["trace"] }
|
||||||
pinnacle = { path = ".", features = ["testing"] }
|
pinnacle = { path = ".", features = ["testing"] }
|
||||||
|
pinnacle-api = { path = "./api/rust" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
testing = [
|
testing = [
|
||||||
|
|
|
@ -36,6 +36,11 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
Pinnacle.quit()
|
Pinnacle.quit()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
-- mod_key + alt + r = Reload config
|
||||||
|
Input.keybind({ mod_key, "alt" }, "r", function()
|
||||||
|
Pinnacle.reload_config()
|
||||||
|
end)
|
||||||
|
|
||||||
-- mod_key + alt + c = Close window
|
-- mod_key + alt + c = Close window
|
||||||
Input.keybind({ mod_key, "alt" }, "c", function()
|
Input.keybind({ mod_key, "alt" }, "c", function()
|
||||||
local focused = Window.get_focused()
|
local focused = Window.get_focused()
|
||||||
|
@ -76,18 +81,26 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
--------------------
|
----------------------
|
||||||
-- Tags --
|
-- Tags and Outputs --
|
||||||
--------------------
|
----------------------
|
||||||
|
|
||||||
local tag_names = { "1", "2", "3", "4", "5" }
|
local tag_names = { "1", "2", "3", "4", "5" }
|
||||||
|
|
||||||
-- `connect_for_all` is useful for performing setup on every monitor you have.
|
-- Setup outputs.
|
||||||
-- Here, we add tags with names 1-5 and set tag 1 as active.
|
--
|
||||||
Output.connect_for_all(function(op)
|
-- `Output.setup` allows you to declare things like mode, scale, and tags for outputs.
|
||||||
local tags = Tag.add(op, tag_names)
|
-- Here we give all outputs tags 1 through 5.
|
||||||
tags[1]:set_active(true)
|
Output.setup({
|
||||||
end)
|
-- "*" 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
|
-- Tag keybinds
|
||||||
for _, tag_name in ipairs(tag_names) do
|
for _, tag_name in ipairs(tag_names) do
|
||||||
|
@ -246,6 +259,10 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
Input.set_libinput_settings({
|
||||||
|
tap = true,
|
||||||
|
})
|
||||||
|
|
||||||
-- Enable sloppy focus
|
-- Enable sloppy focus
|
||||||
Window.connect_signal({
|
Window.connect_signal({
|
||||||
pointer_enter = function(window)
|
pointer_enter = function(window)
|
||||||
|
|
|
@ -37,6 +37,16 @@ function pinnacle.quit()
|
||||||
})
|
})
|
||||||
end
|
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.
|
---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.
|
---You must pass in a function that takes in the `Pinnacle` table. This table is how you'll access the other config modules.
|
||||||
|
|
|
@ -14,6 +14,7 @@ local rpc_types = {
|
||||||
SetLocation = {},
|
SetLocation = {},
|
||||||
SetMode = {},
|
SetMode = {},
|
||||||
SetScale = {},
|
SetScale = {},
|
||||||
|
SetTransform = {},
|
||||||
ConnectForAll = {
|
ConnectForAll = {
|
||||||
response_type = "ConnectForAllResponse",
|
response_type = "ConnectForAllResponse",
|
||||||
},
|
},
|
||||||
|
@ -166,12 +167,465 @@ function output.connect_for_all(callback)
|
||||||
})
|
})
|
||||||
end
|
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 = {
|
local signal_name_to_SignalName = {
|
||||||
connect = "OutputConnect",
|
connect = "OutputConnect",
|
||||||
|
disconnect = "OutputDisconnect",
|
||||||
|
resize = "OutputResize",
|
||||||
|
move = "OutputMove",
|
||||||
}
|
}
|
||||||
|
|
||||||
---@class OutputSignal Signals related to output events.
|
---@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 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.
|
---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 }))
|
client.unary_request(build_grpc_request_params("SetScale", { output_name = self.name, relative = -decrease_by }))
|
||||||
end
|
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
|
---@class Mode
|
||||||
---@field pixel_width integer
|
---@field pixel_width integer
|
||||||
---@field pixel_height integer
|
---@field pixel_height integer
|
||||||
|
@ -431,6 +920,8 @@ end
|
||||||
---@field focused boolean?
|
---@field focused boolean?
|
||||||
---@field tags TagHandle[]
|
---@field tags TagHandle[]
|
||||||
---@field scale number?
|
---@field scale number?
|
||||||
|
---@field transform Transform?
|
||||||
|
---@field serial integer?
|
||||||
|
|
||||||
---Get all properties of this output.
|
---Get all properties of this output.
|
||||||
---
|
---
|
||||||
|
@ -444,6 +935,7 @@ function OutputHandle:props()
|
||||||
response.tags = handles
|
response.tags = handles
|
||||||
response.tag_ids = nil
|
response.tag_ids = nil
|
||||||
response.modes = response.modes or {}
|
response.modes = response.modes or {}
|
||||||
|
response.transform = transform_code_to_name[response.transform]
|
||||||
|
|
||||||
return response
|
return response
|
||||||
end
|
end
|
||||||
|
@ -580,6 +1072,24 @@ function OutputHandle:scale()
|
||||||
return self:props().scale
|
return self:props().scale
|
||||||
end
|
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
|
---@nodoc
|
||||||
---Create a new `OutputHandle` from its raw name.
|
---Create a new `OutputHandle` from its raw name.
|
||||||
---@param output_name string
|
---@param output_name string
|
||||||
|
|
|
@ -14,12 +14,26 @@ local rpc_types = {
|
||||||
OutputConnect = {
|
OutputConnect = {
|
||||||
response_type = "OutputConnectResponse",
|
response_type = "OutputConnectResponse",
|
||||||
},
|
},
|
||||||
|
OutputDisconnect = {
|
||||||
|
response_type = "OutputDisconnectResponse",
|
||||||
|
},
|
||||||
|
OutputResize = {
|
||||||
|
response_type = "OutputResizeResponse",
|
||||||
|
},
|
||||||
|
OutputMove = {
|
||||||
|
response_type = "OutputMoveResponse",
|
||||||
|
},
|
||||||
|
|
||||||
WindowPointerEnter = {
|
WindowPointerEnter = {
|
||||||
response_type = "WindowPointerEnterResponse",
|
response_type = "WindowPointerEnterResponse",
|
||||||
},
|
},
|
||||||
WindowPointerLeave = {
|
WindowPointerLeave = {
|
||||||
response_type = "WindowPointerLeaveResponse",
|
response_type = "WindowPointerLeaveResponse",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
TagActive = {
|
||||||
|
response_type = "TagActiveResponse",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
---Build GrpcRequestParams
|
---Build GrpcRequestParams
|
||||||
|
@ -62,6 +76,39 @@ local signals = {
|
||||||
---@type fun(response: table)
|
---@type fun(response: table)
|
||||||
on_response = nil,
|
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 = {
|
WindowPointerEnter = {
|
||||||
---@nodoc
|
---@nodoc
|
||||||
---@type H2Stream?
|
---@type H2Stream?
|
||||||
|
@ -84,6 +131,17 @@ local signals = {
|
||||||
---@type fun(response: table)
|
---@type fun(response: table)
|
||||||
on_response = nil,
|
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)
|
signals.OutputConnect.on_response = function(response)
|
||||||
|
@ -94,6 +152,30 @@ signals.OutputConnect.on_response = function(response)
|
||||||
end
|
end
|
||||||
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)
|
signals.WindowPointerEnter.on_response = function(response)
|
||||||
---@diagnostic disable-next-line: invisible
|
---@diagnostic disable-next-line: invisible
|
||||||
local window_handle = require("pinnacle.window").handle.new(response.window_id)
|
local window_handle = require("pinnacle.window").handle.new(response.window_id)
|
||||||
|
@ -112,6 +194,15 @@ signals.WindowPointerLeave.on_response = function(response)
|
||||||
end
|
end
|
||||||
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
|
---@nodoc
|
||||||
|
|
|
@ -210,6 +210,52 @@ function tag.remove(tags)
|
||||||
client.unary_request(build_grpc_request_params("Remove", { tag_ids = ids }))
|
client.unary_request(build_grpc_request_params("Remove", { tag_ids = ids }))
|
||||||
end
|
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.
|
---Remove this tag.
|
||||||
---
|
---
|
||||||
---### Example
|
---### Example
|
||||||
|
@ -280,6 +326,7 @@ end
|
||||||
---@field active boolean? Whether or not the tag is currently being displayed
|
---@field active boolean? Whether or not the tag is currently being displayed
|
||||||
---@field name string? The name of the tag
|
---@field name string? The name of the tag
|
||||||
---@field output OutputHandle? The output the tag is on
|
---@field output OutputHandle? The output the tag is on
|
||||||
|
---@field windows WindowHandle[] The windows that have this tag
|
||||||
|
|
||||||
---Get all properties of this tag.
|
---Get all properties of this tag.
|
||||||
---
|
---
|
||||||
|
@ -292,6 +339,8 @@ function TagHandle:props()
|
||||||
name = response.name,
|
name = response.name,
|
||||||
---@diagnostic disable-next-line: invisible
|
---@diagnostic disable-next-line: invisible
|
||||||
output = response.output_name and require("pinnacle.output").handle.new(response.output_name),
|
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
|
end
|
||||||
|
|
||||||
|
@ -322,6 +371,15 @@ function TagHandle:output()
|
||||||
return self:props().output
|
return self:props().output
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Get the windows that have this tag.
|
||||||
|
---
|
||||||
|
---Shorthand for `handle:props().windows`.
|
||||||
|
---
|
||||||
|
---@return WindowHandle[]
|
||||||
|
function TagHandle:windows()
|
||||||
|
return self:props().windows
|
||||||
|
end
|
||||||
|
|
||||||
---@nodoc
|
---@nodoc
|
||||||
---Create a new `TagHandle` from an id.
|
---Create a new `TagHandle` from an id.
|
||||||
---@param tag_id integer
|
---@param tag_id integer
|
||||||
|
|
|
@ -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)
|
|
|
@ -10,6 +10,18 @@ message Mode {
|
||||||
optional uint32 refresh_rate_millihz = 3;
|
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 {
|
message SetLocationRequest {
|
||||||
optional string output_name = 1;
|
optional string output_name = 1;
|
||||||
optional int32 x = 2;
|
optional int32 x = 2;
|
||||||
|
@ -32,6 +44,11 @@ message SetScaleRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SetTransformRequest {
|
||||||
|
optional string output_name = 1;
|
||||||
|
optional Transform transform = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message GetRequest {}
|
message GetRequest {}
|
||||||
message GetResponse {
|
message GetResponse {
|
||||||
repeated string output_names = 1;
|
repeated string output_names = 1;
|
||||||
|
@ -72,12 +89,18 @@ message GetPropertiesResponse {
|
||||||
optional bool focused = 10;
|
optional bool focused = 10;
|
||||||
repeated uint32 tag_ids = 11;
|
repeated uint32 tag_ids = 11;
|
||||||
optional float scale = 12;
|
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 {
|
service OutputService {
|
||||||
rpc SetLocation(SetLocationRequest) returns (google.protobuf.Empty);
|
rpc SetLocation(SetLocationRequest) returns (google.protobuf.Empty);
|
||||||
rpc SetMode(SetModeRequest) returns (google.protobuf.Empty);
|
rpc SetMode(SetModeRequest) returns (google.protobuf.Empty);
|
||||||
rpc SetScale(SetScaleRequest) returns (google.protobuf.Empty);
|
rpc SetScale(SetScaleRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc SetTransform(SetTransformRequest) returns (google.protobuf.Empty);
|
||||||
rpc Get(GetRequest) returns (GetResponse);
|
rpc Get(GetRequest) returns (GetResponse);
|
||||||
rpc GetProperties(GetPropertiesRequest) returns (GetPropertiesResponse);
|
rpc GetProperties(GetPropertiesRequest) returns (GetPropertiesResponse);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,34 @@ message OutputConnectRequest {
|
||||||
message OutputConnectResponse {
|
message OutputConnectResponse {
|
||||||
optional string output_name = 1;
|
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 {
|
message WindowPointerEnterRequest {
|
||||||
optional StreamControl control = 1;
|
optional StreamControl control = 1;
|
||||||
|
@ -33,8 +61,23 @@ message WindowPointerLeaveResponse {
|
||||||
optional uint32 window_id = 1;
|
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 {
|
service SignalService {
|
||||||
rpc OutputConnect(stream OutputConnectRequest) returns (stream OutputConnectResponse);
|
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 WindowPointerEnter(stream WindowPointerEnterRequest) returns (stream WindowPointerEnterResponse);
|
||||||
rpc WindowPointerLeave(stream WindowPointerLeaveRequest) returns (stream WindowPointerLeaveResponse);
|
rpc WindowPointerLeave(stream WindowPointerLeaveRequest) returns (stream WindowPointerLeaveResponse);
|
||||||
|
|
||||||
|
rpc TagActive(stream TagActiveRequest) returns (stream TagActiveResponse);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,9 +35,14 @@ message GetPropertiesRequest {
|
||||||
optional uint32 tag_id = 1;
|
optional uint32 tag_id = 1;
|
||||||
}
|
}
|
||||||
message GetPropertiesResponse {
|
message GetPropertiesResponse {
|
||||||
|
// Whether or not this tag is active
|
||||||
optional bool active = 1;
|
optional bool active = 1;
|
||||||
|
// The name of this tag
|
||||||
optional string name = 2;
|
optional string name = 2;
|
||||||
|
// The output this tag is on
|
||||||
optional string output_name = 3;
|
optional string output_name = 3;
|
||||||
|
// All windows that have this tag
|
||||||
|
repeated uint32 window_ids = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
service TagService {
|
service TagService {
|
||||||
|
|
|
@ -21,6 +21,8 @@ enum SetOrToggle {
|
||||||
|
|
||||||
message QuitRequest {}
|
message QuitRequest {}
|
||||||
|
|
||||||
|
message ReloadConfigRequest {}
|
||||||
|
|
||||||
// A manual ping request independent of any HTTP keepalive.
|
// A manual ping request independent of any HTTP keepalive.
|
||||||
//
|
//
|
||||||
// Tonic does not seems to give you the means to run something
|
// Tonic does not seems to give you the means to run something
|
||||||
|
@ -36,5 +38,6 @@ message PingResponse {
|
||||||
|
|
||||||
service PinnacleService {
|
service PinnacleService {
|
||||||
rpc Quit(QuitRequest) returns (google.protobuf.Empty);
|
rpc Quit(QuitRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc ReloadConfig(ReloadConfigRequest) returns (google.protobuf.Empty);
|
||||||
rpc Ping(PingRequest) returns (PingResponse);
|
rpc Ping(PingRequest) returns (PingResponse);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,3 +20,4 @@ futures = "0.3.30"
|
||||||
num_enum = "0.7.2"
|
num_enum = "0.7.2"
|
||||||
xkbcommon = { workspace = true }
|
xkbcommon = { workspace = true }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
bitflags = { workspace = true }
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
use pinnacle_api::input::libinput::LibinputSetting;
|
||||||
use pinnacle_api::layout::{
|
use pinnacle_api::layout::{
|
||||||
CornerLayout, CornerLocation, CyclingLayoutManager, DwindleLayout, FairLayout, MasterSide,
|
CornerLayout, CornerLocation, CyclingLayoutManager, DwindleLayout, FairLayout, MasterSide,
|
||||||
MasterStackLayout, SpiralLayout,
|
MasterStackLayout, SpiralLayout,
|
||||||
};
|
};
|
||||||
|
use pinnacle_api::output::OutputSetup;
|
||||||
use pinnacle_api::signal::WindowSignal;
|
use pinnacle_api::signal::WindowSignal;
|
||||||
use pinnacle_api::util::{Axis, Batch};
|
use pinnacle_api::util::{Axis, Batch};
|
||||||
use pinnacle_api::xkbcommon::xkb::Keysym;
|
use pinnacle_api::xkbcommon::xkb::Keysym;
|
||||||
|
@ -26,6 +28,7 @@ async fn main() {
|
||||||
tag,
|
tag,
|
||||||
layout,
|
layout,
|
||||||
render,
|
render,
|
||||||
|
..
|
||||||
} = modules;
|
} = modules;
|
||||||
|
|
||||||
let mod_key = Mod::Ctrl;
|
let mod_key = Mod::Ctrl;
|
||||||
|
@ -55,6 +58,11 @@ async fn main() {
|
||||||
pinnacle.quit();
|
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
|
// `mod_key + alt + c` closes the focused window
|
||||||
input.keybind([mod_key, Mod::Alt], 'c', || {
|
input.keybind([mod_key, Mod::Alt], 'c', || {
|
||||||
if let Some(window) = window.get_focused() {
|
if let Some(window) = window.get_focused() {
|
||||||
|
@ -206,12 +214,7 @@ async fn main() {
|
||||||
let tag_names = ["1", "2", "3", "4", "5"];
|
let tag_names = ["1", "2", "3", "4", "5"];
|
||||||
|
|
||||||
// Setup all monitors with tags "1" through "5"
|
// Setup all monitors with tags "1" through "5"
|
||||||
output.connect_for_all(move |op| {
|
output.setup([OutputSetup::new_with_matcher(|_| true).with_tags(tag_names)]);
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
for tag_name in tag_names {
|
for tag_name in tag_names {
|
||||||
// `mod_key + 1-5` switches to tag "1" to "5"
|
// `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
|
// Enable sloppy focus
|
||||||
window.connect_signal(WindowSignal::PointerEnter(Box::new(|win| {
|
window.connect_signal(WindowSignal::PointerEnter(Box::new(|win| {
|
||||||
win.set_focused(true);
|
win.set_focused(true);
|
||||||
|
|
|
@ -131,11 +131,13 @@ pub fn config(
|
||||||
#(#attrs)*
|
#(#attrs)*
|
||||||
#tokio_attr
|
#tokio_attr
|
||||||
#vis #sig {
|
#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)*
|
#(#stmts)*
|
||||||
|
|
||||||
::pinnacle_api::listen(__fut_receiver).await;
|
::pinnacle_api::listen(__api, __fut_receiver).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex, OnceLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
use pinnacle_api_defs::pinnacle::layout::v0alpha1::{
|
use pinnacle_api_defs::pinnacle::layout::v0alpha1::{
|
||||||
|
@ -26,22 +26,28 @@ use crate::{
|
||||||
tag::TagHandle,
|
tag::TagHandle,
|
||||||
util::{Axis, Geometry},
|
util::{Axis, Geometry},
|
||||||
window::WindowHandle,
|
window::WindowHandle,
|
||||||
OUTPUT, TAG, WINDOW,
|
ApiModules,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A struct that allows you to manage layouts.
|
/// A struct that allows you to manage layouts.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Layout {
|
pub struct Layout {
|
||||||
|
api: OnceLock<ApiModules>,
|
||||||
layout_client: LayoutServiceClient<Channel>,
|
layout_client: LayoutServiceClient<Channel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout {
|
impl Layout {
|
||||||
pub(crate) fn new(channel: Channel) -> Self {
|
pub(crate) fn new(channel: Channel) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
api: OnceLock::new(),
|
||||||
layout_client: LayoutServiceClient::new(channel.clone()),
|
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.
|
/// Consume the given [`LayoutManager`] and set it as the global layout handler.
|
||||||
///
|
///
|
||||||
/// This returns a [`LayoutRequester`] that allows you to manually request layouts from
|
/// 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 manager = Arc::new(Mutex::new(manager));
|
||||||
|
|
||||||
|
let api = self.api.get().unwrap().clone();
|
||||||
|
|
||||||
let requester = LayoutRequester {
|
let requester = LayoutRequester {
|
||||||
|
api: api.clone(),
|
||||||
sender: from_client_clone,
|
sender: from_client_clone,
|
||||||
manager: manager.clone(),
|
manager: manager.clone(),
|
||||||
};
|
};
|
||||||
|
@ -69,16 +78,16 @@ impl Layout {
|
||||||
let thing = async move {
|
let thing = async move {
|
||||||
while let Some(Ok(response)) = from_server.next().await {
|
while let Some(Ok(response)) = from_server.next().await {
|
||||||
let args = LayoutArgs {
|
let args = LayoutArgs {
|
||||||
output: OUTPUT.get().unwrap().new_handle(response.output_name()),
|
output: api.output.new_handle(response.output_name()),
|
||||||
windows: response
|
windows: response
|
||||||
.window_ids
|
.window_ids
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|id| WINDOW.get().unwrap().new_handle(id))
|
.map(|id| api.window.new_handle(id))
|
||||||
.collect(),
|
.collect(),
|
||||||
tags: response
|
tags: response
|
||||||
.tag_ids
|
.tag_ids
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|id| TAG.get().unwrap().new_handle(id))
|
.map(|id| api.tag.new_handle(id))
|
||||||
.collect(),
|
.collect(),
|
||||||
output_width: response.output_width.unwrap_or_default(),
|
output_width: response.output_width.unwrap_or_default(),
|
||||||
output_height: response.output_height.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`].
|
/// A struct that can request layouts and provides access to a consumed [`LayoutManager`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct LayoutRequester<T> {
|
pub struct LayoutRequester<T> {
|
||||||
|
api: ApiModules,
|
||||||
sender: UnboundedSender<LayoutRequest>,
|
sender: UnboundedSender<LayoutRequest>,
|
||||||
/// The manager that was consumed, wrapped in an `Arc<Mutex>`.
|
/// The manager that was consumed, wrapped in an `Arc<Mutex>`.
|
||||||
pub manager: Arc<Mutex<T>>,
|
pub manager: Arc<Mutex<T>>,
|
||||||
|
@ -233,6 +243,7 @@ pub struct LayoutRequester<T> {
|
||||||
impl<T> Clone for LayoutRequester<T> {
|
impl<T> Clone for LayoutRequester<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
api: self.api.clone(),
|
||||||
sender: self.sender.clone(),
|
sender: self.sender.clone(),
|
||||||
manager: self.manager.clone(),
|
manager: self.manager.clone(),
|
||||||
}
|
}
|
||||||
|
@ -245,7 +256,7 @@ impl<T> LayoutRequester<T> {
|
||||||
/// This uses the focused output for the request.
|
/// This uses the focused output for the request.
|
||||||
/// If you want to layout a specific output, see [`LayoutRequester::request_layout_on_output`].
|
/// If you want to layout a specific output, see [`LayoutRequester::request_layout_on_output`].
|
||||||
pub fn request_layout(&self) {
|
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
|
self.sender
|
||||||
.send(LayoutRequest {
|
.send(LayoutRequest {
|
||||||
body: Some(Body::Layout(ExplicitLayout { output_name })),
|
body: Some(Body::Layout(ExplicitLayout { output_name })),
|
||||||
|
|
|
@ -67,12 +67,7 @@
|
||||||
//! // `modules` is now available in the function body.
|
//! // `modules` is now available in the function body.
|
||||||
//! // You can deconstruct `ApiModules` to get all the config structs.
|
//! // You can deconstruct `ApiModules` to get all the config structs.
|
||||||
//! let ApiModules {
|
//! let ApiModules {
|
||||||
//! pinnacle,
|
//! ..
|
||||||
//! process,
|
|
||||||
//! window,
|
|
||||||
//! input,
|
|
||||||
//! output,
|
|
||||||
//! tag,
|
|
||||||
//! } = modules;
|
//! } = modules;
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
|
@ -80,7 +75,7 @@
|
||||||
//! ## 5. Begin crafting your config!
|
//! ## 5. Begin crafting your config!
|
||||||
//! You can peruse the documentation for things to configure.
|
//! 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 futures::{future::BoxFuture, Future, StreamExt};
|
||||||
use input::Input;
|
use input::Input;
|
||||||
|
@ -115,18 +110,9 @@ pub use pinnacle_api_macros::config;
|
||||||
pub use tokio;
|
pub use tokio;
|
||||||
pub use xkbcommon;
|
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.
|
/// A struct containing static references to all of the configuration structs.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[non_exhaustive]
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct ApiModules {
|
pub struct ApiModules {
|
||||||
/// The [`Pinnacle`] struct
|
/// The [`Pinnacle`] struct
|
||||||
pub pinnacle: &'static Pinnacle,
|
pub pinnacle: &'static Pinnacle,
|
||||||
|
@ -144,6 +130,23 @@ pub struct ApiModules {
|
||||||
pub layout: &'static Layout,
|
pub layout: &'static Layout,
|
||||||
/// The [`Render`] struct
|
/// The [`Render`] struct
|
||||||
pub render: &'static Render,
|
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.
|
/// 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 (fut_sender, fut_recv) = unbounded_channel::<BoxFuture<'static, ()>>();
|
||||||
|
|
||||||
let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(channel.clone()));
|
let signal = Arc::new(RwLock::new(SignalState::new(
|
||||||
let process = PROCESS.get_or_init(|| Process::new(channel.clone(), fut_sender.clone()));
|
channel.clone(),
|
||||||
let window = WINDOW.get_or_init(|| Window::new(channel.clone()));
|
fut_sender.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()));
|
|
||||||
|
|
||||||
SIGNAL
|
let pinnacle = Box::leak(Box::new(Pinnacle::new(channel.clone())));
|
||||||
.set(RwLock::new(SignalState::new(
|
let process = Box::leak(Box::new(Process::new(channel.clone(), fut_sender.clone())));
|
||||||
channel.clone(),
|
let window = Box::leak(Box::new(Window::new(channel.clone())));
|
||||||
fut_sender.clone(),
|
let input = Box::leak(Box::new(Input::new(channel.clone(), fut_sender.clone())));
|
||||||
)))
|
let output = Box::leak(Box::new(Output::new(channel.clone())));
|
||||||
.map_err(|_| "failed to create SIGNAL")?;
|
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 {
|
let modules = ApiModules {
|
||||||
pinnacle,
|
pinnacle,
|
||||||
|
@ -189,8 +190,15 @@ pub async fn connect(
|
||||||
tag,
|
tag,
|
||||||
layout,
|
layout,
|
||||||
render,
|
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))
|
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.
|
/// This function is inserted at the end of your config through the [`config`] macro.
|
||||||
/// You should use the macro instead of this function directly.
|
/// 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 mut fut_recv = UnboundedReceiverStream::new(fut_recv);
|
||||||
|
|
||||||
let pinnacle = PINNACLE.get().unwrap();
|
|
||||||
|
|
||||||
let keepalive = async move {
|
let keepalive = async move {
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
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}");
|
eprintln!("Failed to ping compositor: {err}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,14 @@
|
||||||
//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different
|
//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different
|
||||||
//! connected monitors and set them up.
|
//! connected monitors and set them up.
|
||||||
|
|
||||||
|
use std::{num::NonZeroU32, sync::OnceLock};
|
||||||
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use pinnacle_api_defs::pinnacle::output::{
|
use pinnacle_api_defs::pinnacle::output::{
|
||||||
self,
|
self,
|
||||||
v0alpha1::{
|
v0alpha1::{
|
||||||
output_service_client::OutputServiceClient, set_scale_request::AbsoluteOrRelative,
|
output_service_client::OutputServiceClient, set_scale_request::AbsoluteOrRelative,
|
||||||
SetLocationRequest, SetModeRequest, SetScaleRequest,
|
SetLocationRequest, SetModeRequest, SetScaleRequest, SetTransformRequest,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tonic::transport::Channel;
|
use tonic::transport::Channel;
|
||||||
|
@ -22,9 +24,9 @@ use tonic::transport::Channel;
|
||||||
use crate::{
|
use crate::{
|
||||||
block_on_tokio,
|
block_on_tokio,
|
||||||
signal::{OutputSignal, SignalHandle},
|
signal::{OutputSignal, SignalHandle},
|
||||||
tag::TagHandle,
|
tag::{Tag, TagHandle},
|
||||||
util::Batch,
|
util::Batch,
|
||||||
SIGNAL, TAG,
|
ApiModules,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A struct that allows you to get handles to connected outputs and set them up.
|
/// A struct that allows you to get handles to connected outputs and set them up.
|
||||||
|
@ -33,19 +35,26 @@ use crate::{
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Output {
|
pub struct Output {
|
||||||
output_client: OutputServiceClient<Channel>,
|
output_client: OutputServiceClient<Channel>,
|
||||||
|
api: OnceLock<ApiModules>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Output {
|
impl Output {
|
||||||
pub(crate) fn new(channel: Channel) -> Self {
|
pub(crate) fn new(channel: Channel) -> Self {
|
||||||
Self {
|
Self {
|
||||||
output_client: OutputServiceClient::new(channel.clone()),
|
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 {
|
pub(crate) fn new_handle(&self, name: impl Into<String>) -> OutputHandle {
|
||||||
OutputHandle {
|
OutputHandle {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
output_client: self.output_client.clone(),
|
output_client: self.output_client.clone(),
|
||||||
|
api: self.api.get().unwrap().clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +154,7 @@ impl Output {
|
||||||
for_all(&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));
|
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
|
/// 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.
|
/// with the necessary arguments every time a signal of that type is received.
|
||||||
pub fn connect_signal(&self, signal: OutputSignal) -> SignalHandle {
|
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 {
|
match signal {
|
||||||
OutputSignal::Connect(f) => signal_state.output_connect.add_callback(f),
|
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.
|
/// A handle to an output.
|
||||||
|
@ -170,6 +563,7 @@ impl Output {
|
||||||
pub struct OutputHandle {
|
pub struct OutputHandle {
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
output_client: OutputServiceClient<Channel>,
|
output_client: OutputServiceClient<Channel>,
|
||||||
|
api: ApiModules,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for OutputHandle {
|
impl PartialEq for OutputHandle {
|
||||||
|
@ -215,6 +609,31 @@ pub enum Alignment {
|
||||||
RightAlignBottom,
|
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 {
|
impl OutputHandle {
|
||||||
/// Set the location of this output in the global space.
|
/// Set the location of this output in the global space.
|
||||||
///
|
///
|
||||||
|
@ -436,6 +855,25 @@ impl OutputHandle {
|
||||||
self.increase_scale(-decrease_by);
|
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.
|
/// Get all properties of this output.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
@ -462,8 +900,6 @@ impl OutputHandle {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_inner();
|
.into_inner();
|
||||||
|
|
||||||
let tag = TAG.get().expect("TAG doesn't exist");
|
|
||||||
|
|
||||||
OutputProperties {
|
OutputProperties {
|
||||||
make: response.make,
|
make: response.make,
|
||||||
model: response.model,
|
model: response.model,
|
||||||
|
@ -502,9 +938,11 @@ impl OutputHandle {
|
||||||
tags: response
|
tags: response
|
||||||
.tag_ids
|
.tag_ids
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|id| tag.new_handle(id))
|
.map(|id| self.api.tag.new_handle(id))
|
||||||
.collect(),
|
.collect(),
|
||||||
scale: response.scale,
|
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
|
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).
|
/// Get this output's unique name (the name of its connector).
|
||||||
pub fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
&self.name
|
&self.name
|
||||||
|
@ -700,6 +1162,7 @@ pub struct Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The properties of an output.
|
/// The properties of an output.
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone, PartialEq, Default)]
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
pub struct OutputProperties {
|
pub struct OutputProperties {
|
||||||
/// The make of the output.
|
/// The make of the output.
|
||||||
|
@ -737,4 +1200,8 @@ pub struct OutputProperties {
|
||||||
pub tags: Vec<TagHandle>,
|
pub tags: Vec<TagHandle>,
|
||||||
/// This output's scaling factor.
|
/// This output's scaling factor.
|
||||||
pub scale: Option<f32>,
|
pub scale: Option<f32>,
|
||||||
|
/// This output's transform.
|
||||||
|
pub transform: Option<Transform>,
|
||||||
|
/// This output's EDID serial number.
|
||||||
|
pub serial: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use pinnacle_api_defs::pinnacle::v0alpha1::{
|
use pinnacle_api_defs::pinnacle::v0alpha1::{
|
||||||
pinnacle_service_client::PinnacleServiceClient, PingRequest, QuitRequest,
|
pinnacle_service_client::PinnacleServiceClient, PingRequest, QuitRequest, ReloadConfigRequest,
|
||||||
};
|
};
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use tonic::{transport::Channel, Request};
|
use tonic::{transport::Channel, Request};
|
||||||
|
@ -42,6 +42,12 @@ impl Pinnacle {
|
||||||
block_on_tokio(client.quit(QuitRequest {})).unwrap();
|
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> {
|
pub(super) async fn ping(&self) -> Result<(), String> {
|
||||||
let mut client = self.client.clone();
|
let mut client = self.client.clone();
|
||||||
let mut payload = [0u8; 8];
|
let mut payload = [0u8; 8];
|
||||||
|
|
|
@ -6,11 +6,13 @@
|
||||||
//! Some of the other modules have a `connect_signal` method that will allow you to pass in
|
//! 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.
|
//! callbacks to run on each signal. Use them to connect to the signals defined here.
|
||||||
|
|
||||||
|
#![allow(clippy::type_complexity)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{btree_map, BTreeMap},
|
collections::{btree_map, BTreeMap},
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicU32, Ordering},
|
atomic::{AtomicU32, Ordering},
|
||||||
Arc,
|
Arc, OnceLock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -25,7 +27,9 @@ use tokio::sync::{
|
||||||
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
||||||
use tonic::{transport::Channel, Streaming};
|
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 {
|
pub(crate) trait Signal {
|
||||||
type Callback;
|
type Callback;
|
||||||
|
@ -97,6 +101,7 @@ macro_rules! signals {
|
||||||
.into_inner()
|
.into_inner()
|
||||||
},
|
},
|
||||||
$on_resp,
|
$on_resp,
|
||||||
|
self.api.get().unwrap().clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.callback_sender.replace(channels.callback_sender);
|
self.callback_sender.replace(channels.callback_sender);
|
||||||
|
@ -127,10 +132,9 @@ signals! {
|
||||||
enum_name = Connect,
|
enum_name = Connect,
|
||||||
callback_type = SingleOutputFn,
|
callback_type = SingleOutputFn,
|
||||||
client_request = output_connect,
|
client_request = output_connect,
|
||||||
on_response = |response, callbacks| {
|
on_response = |response, callbacks, api| {
|
||||||
if let Some(output_name) = response.output_name {
|
if let Some(output_name) = response.output_name {
|
||||||
let output = OUTPUT.get().expect("OUTPUT doesn't exist");
|
let handle = api.output.new_handle(output_name);
|
||||||
let handle = output.new_handle(output_name);
|
|
||||||
|
|
||||||
for callback in callbacks {
|
for callback in callbacks {
|
||||||
callback(&handle);
|
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.
|
/// Signals relating to window events.
|
||||||
WindowSignal => {
|
WindowSignal => {
|
||||||
|
@ -148,10 +203,9 @@ signals! {
|
||||||
enum_name = PointerEnter,
|
enum_name = PointerEnter,
|
||||||
callback_type = SingleWindowFn,
|
callback_type = SingleWindowFn,
|
||||||
client_request = window_pointer_enter,
|
client_request = window_pointer_enter,
|
||||||
on_response = |response, callbacks| {
|
on_response = |response, callbacks, api| {
|
||||||
if let Some(window_id) = response.window_id {
|
if let Some(window_id) = response.window_id {
|
||||||
let window = WINDOW.get().expect("WINDOW doesn't exist");
|
let handle = api.window.new_handle(window_id);
|
||||||
let handle = window.new_handle(window_id);
|
|
||||||
|
|
||||||
for callback in callbacks {
|
for callback in callbacks {
|
||||||
callback(&handle);
|
callback(&handle);
|
||||||
|
@ -166,10 +220,9 @@ signals! {
|
||||||
enum_name = PointerLeave,
|
enum_name = PointerLeave,
|
||||||
callback_type = SingleWindowFn,
|
callback_type = SingleWindowFn,
|
||||||
client_request = window_pointer_leave,
|
client_request = window_pointer_leave,
|
||||||
on_response = |response, callbacks| {
|
on_response = |response, callbacks, api| {
|
||||||
if let Some(window_id) = response.window_id {
|
if let Some(window_id) = response.window_id {
|
||||||
let window = WINDOW.get().expect("WINDOW doesn't exist");
|
let handle = api.window.new_handle(window_id);
|
||||||
let handle = window.new_handle(window_id);
|
|
||||||
|
|
||||||
for callback in callbacks {
|
for callback in callbacks {
|
||||||
callback(&handle);
|
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>;
|
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) struct SignalState {
|
||||||
pub(crate) output_connect: SignalData<OutputConnect>,
|
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_enter: SignalData<WindowPointerEnter>,
|
||||||
pub(crate) window_pointer_leave: SignalData<WindowPointerLeave>,
|
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 {
|
impl SignalState {
|
||||||
|
@ -197,10 +280,24 @@ impl SignalState {
|
||||||
let client = SignalServiceClient::new(channel);
|
let client = SignalServiceClient::new(channel);
|
||||||
Self {
|
Self {
|
||||||
output_connect: SignalData::new(client.clone(), fut_sender.clone()),
|
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_enter: SignalData::new(client.clone(), fut_sender.clone()),
|
||||||
window_pointer_leave: 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)]
|
#[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> {
|
pub(crate) struct SignalData<S: Signal> {
|
||||||
client: SignalServiceClient<Channel>,
|
client: SignalServiceClient<Channel>,
|
||||||
|
api: OnceLock<ApiModules>,
|
||||||
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
|
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
|
||||||
callback_sender: Option<UnboundedSender<(SignalConnId, S::Callback)>>,
|
callback_sender: Option<UnboundedSender<(SignalConnId, S::Callback)>>,
|
||||||
remove_callback_sender: Option<UnboundedSender<SignalConnId>>,
|
remove_callback_sender: Option<UnboundedSender<SignalConnId>>,
|
||||||
|
@ -223,6 +321,7 @@ impl<S: Signal> SignalData<S> {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
client,
|
client,
|
||||||
|
api: OnceLock::new(),
|
||||||
fut_sender,
|
fut_sender,
|
||||||
callback_sender: Default::default(),
|
callback_sender: Default::default(),
|
||||||
remove_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>,
|
callback_count: Arc<AtomicU32>,
|
||||||
to_in_stream: T,
|
to_in_stream: T,
|
||||||
mut on_response: O,
|
mut on_response: O,
|
||||||
|
api: ApiModules,
|
||||||
) -> ConnectSignalChannels<F>
|
) -> ConnectSignalChannels<F>
|
||||||
where
|
where
|
||||||
Req: SignalRequest + Send + 'static,
|
Req: SignalRequest + Send + 'static,
|
||||||
Resp: Send + 'static,
|
Resp: Send + 'static,
|
||||||
F: Send + 'static,
|
F: Send + 'static,
|
||||||
T: FnOnce(UnboundedReceiverStream<Req>) -> Streaming<Resp>,
|
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 (control_sender, recv) = unbounded_channel::<Req>();
|
||||||
let out_stream = UnboundedReceiverStream::new(recv);
|
let out_stream = UnboundedReceiverStream::new(recv);
|
||||||
|
@ -283,7 +383,7 @@ where
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
on_response(response, callbacks.values_mut());
|
on_response(response, callbacks.values_mut(), &api);
|
||||||
|
|
||||||
control_sender
|
control_sender
|
||||||
.send(Req::from_control(StreamControl::Ready))
|
.send(Req::from_control(StreamControl::Ready))
|
||||||
|
|
|
@ -29,8 +29,9 @@
|
||||||
//!
|
//!
|
||||||
//! These [`TagHandle`]s allow you to manipulate individual tags and get their properties.
|
//! These [`TagHandle`]s allow you to manipulate individual tags and get their properties.
|
||||||
|
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use num_enum::TryFromPrimitive;
|
|
||||||
use pinnacle_api_defs::pinnacle::{
|
use pinnacle_api_defs::pinnacle::{
|
||||||
tag::{
|
tag::{
|
||||||
self,
|
self,
|
||||||
|
@ -43,25 +44,39 @@ use pinnacle_api_defs::pinnacle::{
|
||||||
};
|
};
|
||||||
use tonic::transport::Channel;
|
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.
|
/// A struct that allows you to add and remove tags and get [`TagHandle`]s.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Tag {
|
pub struct Tag {
|
||||||
tag_client: TagServiceClient<Channel>,
|
tag_client: TagServiceClient<Channel>,
|
||||||
|
api: OnceLock<ApiModules>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tag {
|
impl Tag {
|
||||||
pub(crate) fn new(channel: Channel) -> Self {
|
pub(crate) fn new(channel: Channel) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tag_client: TagServiceClient::new(channel.clone()),
|
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 {
|
pub(crate) fn new_handle(&self, id: u32) -> TagHandle {
|
||||||
TagHandle {
|
TagHandle {
|
||||||
id,
|
id,
|
||||||
tag_client: self.tag_client.clone(),
|
tag_client: self.tag_client.clone(),
|
||||||
|
api: self.api.get().unwrap().clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,8 +172,7 @@ impl Tag {
|
||||||
/// The async version of [`Tag::get`].
|
/// The async version of [`Tag::get`].
|
||||||
pub async fn get_async(&self, name: impl Into<String>) -> Option<TagHandle> {
|
pub async fn get_async(&self, name: impl Into<String>) -> Option<TagHandle> {
|
||||||
let name = name.into();
|
let name = name.into();
|
||||||
let output_module = OUTPUT.get().expect("OUTPUT doesn't exist");
|
let focused_output = self.api.get().unwrap().output.get_focused();
|
||||||
let focused_output = output_module.get_focused();
|
|
||||||
|
|
||||||
if let Some(output) = focused_output {
|
if let Some(output) = focused_output {
|
||||||
self.get_on_output_async(name, &output).await
|
self.get_on_output_async(name, &output).await
|
||||||
|
@ -220,6 +234,19 @@ impl Tag {
|
||||||
|
|
||||||
block_on_tokio(client.remove(RemoveRequest { tag_ids })).unwrap();
|
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.
|
/// A handle to a tag.
|
||||||
|
@ -229,6 +256,7 @@ impl Tag {
|
||||||
pub struct TagHandle {
|
pub struct TagHandle {
|
||||||
pub(crate) id: u32,
|
pub(crate) id: u32,
|
||||||
tag_client: TagServiceClient<Channel>,
|
tag_client: TagServiceClient<Channel>,
|
||||||
|
api: ApiModules,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for TagHandle {
|
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 {
|
impl TagHandle {
|
||||||
/// Activate this tag and deactivate all other ones on the same output.
|
/// Activate this tag and deactivate all other ones on the same output.
|
||||||
///
|
///
|
||||||
|
@ -396,12 +404,18 @@ impl TagHandle {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_inner();
|
.into_inner();
|
||||||
|
|
||||||
let output = OUTPUT.get().expect("OUTPUT doesn't exist");
|
let output = self.api.output;
|
||||||
|
let window = self.api.window;
|
||||||
|
|
||||||
TagProperties {
|
TagProperties {
|
||||||
active: response.active,
|
active: response.active,
|
||||||
name: response.name,
|
name: response.name,
|
||||||
output: response.output_name.map(|name| output.new_handle(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> {
|
pub async fn output_async(&self) -> Option<OutputHandle> {
|
||||||
self.props_async().await.output
|
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.
|
/// Properties of a tag.
|
||||||
|
@ -451,4 +477,6 @@ pub struct TagProperties {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
/// The output the tag is on
|
/// The output the tag is on
|
||||||
pub output: Option<OutputHandle>,
|
pub output: Option<OutputHandle>,
|
||||||
|
/// The windows that have this tag
|
||||||
|
pub windows: Vec<WindowHandle>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
//!
|
//!
|
||||||
//! This module also allows you to set window rules; see the [rules] module for more information.
|
//! This module also allows you to set window rules; see the [rules] module for more information.
|
||||||
|
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
use pinnacle_api_defs::pinnacle::{
|
use pinnacle_api_defs::pinnacle::{
|
||||||
|
@ -34,7 +36,7 @@ use crate::{
|
||||||
signal::{SignalHandle, WindowSignal},
|
signal::{SignalHandle, WindowSignal},
|
||||||
tag::TagHandle,
|
tag::TagHandle,
|
||||||
util::{Batch, Geometry},
|
util::{Batch, Geometry},
|
||||||
SIGNAL, TAG,
|
ApiModules,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::rules::{WindowRule, WindowRuleCondition};
|
use self::rules::{WindowRule, WindowRuleCondition};
|
||||||
|
@ -47,19 +49,26 @@ pub mod rules;
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
window_client: WindowServiceClient<Channel>,
|
window_client: WindowServiceClient<Channel>,
|
||||||
|
api: OnceLock<ApiModules>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
pub(crate) fn new(channel: Channel) -> Self {
|
pub(crate) fn new(channel: Channel) -> Self {
|
||||||
Self {
|
Self {
|
||||||
window_client: WindowServiceClient::new(channel.clone()),
|
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 {
|
pub(crate) fn new_handle(&self, id: u32) -> WindowHandle {
|
||||||
WindowHandle {
|
WindowHandle {
|
||||||
id,
|
id,
|
||||||
window_client: self.window_client.clone(),
|
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
|
/// 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.
|
/// with the necessary arguments every time a signal of that type is received.
|
||||||
pub fn connect_signal(&self, signal: WindowSignal) -> SignalHandle {
|
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 {
|
match signal {
|
||||||
WindowSignal::PointerEnter(f) => signal_state.window_pointer_enter.add_callback(f),
|
WindowSignal::PointerEnter(f) => signal_state.window_pointer_enter.add_callback(f),
|
||||||
|
@ -196,6 +205,7 @@ impl Window {
|
||||||
pub struct WindowHandle {
|
pub struct WindowHandle {
|
||||||
id: u32,
|
id: u32,
|
||||||
window_client: WindowServiceClient<Channel>,
|
window_client: WindowServiceClient<Channel>,
|
||||||
|
api: ApiModules,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for WindowHandle {
|
impl PartialEq for WindowHandle {
|
||||||
|
@ -572,8 +582,6 @@ impl WindowHandle {
|
||||||
height: geo.height() as u32,
|
height: geo.height() as u32,
|
||||||
});
|
});
|
||||||
|
|
||||||
let tag = TAG.get().expect("TAG doesn't exist");
|
|
||||||
|
|
||||||
WindowProperties {
|
WindowProperties {
|
||||||
geometry,
|
geometry,
|
||||||
class: response.class,
|
class: response.class,
|
||||||
|
@ -584,7 +592,7 @@ impl WindowHandle {
|
||||||
tags: response
|
tags: response
|
||||||
.tag_ids
|
.tag_ids
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|id| tag.new_handle(id))
|
.map(|id| self.api.tag.new_handle(id))
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,8 +63,12 @@ pub mod pinnacle {
|
||||||
|
|
||||||
impl_signal_request!(
|
impl_signal_request!(
|
||||||
OutputConnectRequest,
|
OutputConnectRequest,
|
||||||
|
OutputDisconnectRequest,
|
||||||
|
OutputResizeRequest,
|
||||||
|
OutputMoveRequest,
|
||||||
WindowPointerEnterRequest,
|
WindowPointerEnterRequest,
|
||||||
WindowPointerLeaveRequest
|
WindowPointerLeaveRequest,
|
||||||
|
TagActiveRequest
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
120
src/api.rs
120
src/api.rs
|
@ -16,7 +16,7 @@ use pinnacle_api_defs::pinnacle::{
|
||||||
self,
|
self,
|
||||||
v0alpha1::{
|
v0alpha1::{
|
||||||
output_service_server, set_scale_request::AbsoluteOrRelative, SetLocationRequest,
|
output_service_server, set_scale_request::AbsoluteOrRelative, SetLocationRequest,
|
||||||
SetModeRequest, SetScaleRequest,
|
SetModeRequest, SetScaleRequest, SetTransformRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
process::v0alpha1::{process_service_server, SetEnvRequest, SpawnRequest, SpawnResponse},
|
process::v0alpha1::{process_service_server, SetEnvRequest, SpawnRequest, SpawnResponse},
|
||||||
|
@ -30,11 +30,13 @@ use pinnacle_api_defs::pinnacle::{
|
||||||
SwitchToRequest,
|
SwitchToRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
v0alpha1::{pinnacle_service_server, PingRequest, PingResponse, QuitRequest, SetOrToggle},
|
v0alpha1::{
|
||||||
|
pinnacle_service_server, PingRequest, PingResponse, QuitRequest, ReloadConfigRequest,
|
||||||
|
SetOrToggle,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::renderer::TextureFilter,
|
backend::renderer::TextureFilter,
|
||||||
desktop::layer_map_for_output,
|
|
||||||
input::keyboard::XkbConfig,
|
input::keyboard::XkbConfig,
|
||||||
output::Scale,
|
output::Scale,
|
||||||
reexports::{calloop, input as libinput},
|
reexports::{calloop, input as libinput},
|
||||||
|
@ -193,6 +195,18 @@ impl pinnacle_service_server::PinnacleService for PinnacleService {
|
||||||
.await
|
.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> {
|
async fn ping(&self, request: Request<PingRequest>) -> Result<Response<PingResponse>, Status> {
|
||||||
let payload = request.into_inner().payload;
|
let payload = request.into_inner().payload;
|
||||||
Ok(Response::new(PingResponse { payload }))
|
Ok(Response::new(PingResponse { payload }))
|
||||||
|
@ -707,9 +721,9 @@ impl tag_service_server::TagService for TagService {
|
||||||
};
|
};
|
||||||
|
|
||||||
match set_or_toggle {
|
match set_or_toggle {
|
||||||
SetOrToggle::Set => tag.set_active(true),
|
SetOrToggle::Set => tag.set_active(true, state),
|
||||||
SetOrToggle::Unset => tag.set_active(false),
|
SetOrToggle::Unset => tag.set_active(false, state),
|
||||||
SetOrToggle::Toggle => tag.set_active(!tag.active()),
|
SetOrToggle::Toggle => tag.set_active(!tag.active(), state),
|
||||||
SetOrToggle::Unspecified => unreachable!(),
|
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(tag) = tag_id.tag(state) else { return };
|
||||||
let Some(output) = tag.output(state) else { return };
|
let Some(output) = tag.output(state) else { return };
|
||||||
|
|
||||||
output.with_state_mut(|state| {
|
output.with_state_mut(|op_state| {
|
||||||
for op_tag in state.tags.iter_mut() {
|
for op_tag in op_state.tags.iter_mut() {
|
||||||
op_tag.set_active(false);
|
op_tag.set_active(false, state);
|
||||||
}
|
}
|
||||||
tag.set_active(true);
|
tag.set_active(true, state);
|
||||||
});
|
});
|
||||||
|
|
||||||
state.request_layout(&output);
|
state.request_layout(&output);
|
||||||
|
@ -874,11 +888,26 @@ impl tag_service_server::TagService for TagService {
|
||||||
.map(|output| output.name());
|
.map(|output| output.name());
|
||||||
let active = tag.as_ref().map(|tag| tag.active());
|
let active = tag.as_ref().map(|tag| tag.active());
|
||||||
let name = tag.as_ref().map(|tag| tag.name());
|
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 {
|
tag::v0alpha1::GetPropertiesResponse {
|
||||||
active,
|
active,
|
||||||
name,
|
name,
|
||||||
output_name,
|
output_name,
|
||||||
|
window_ids,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
@ -940,8 +969,7 @@ impl output_service_server::OutputService for OutputService {
|
||||||
if let Some(y) = y {
|
if let Some(y) = y {
|
||||||
loc.y = y;
|
loc.y = y;
|
||||||
}
|
}
|
||||||
output.change_current_state(None, None, None, Some(loc));
|
state.change_output_state(&output, None, None, None, Some(loc));
|
||||||
state.space.map_output(&output, loc);
|
|
||||||
debug!("Mapping output {} to {loc:?}", output.name());
|
debug!("Mapping output {} to {loc:?}", output.name());
|
||||||
state.request_layout(&output);
|
state.request_layout(&output);
|
||||||
})
|
})
|
||||||
|
@ -1001,8 +1029,49 @@ impl output_service_server::OutputService for OutputService {
|
||||||
|
|
||||||
current_scale = f64::max(current_scale, 0.25);
|
current_scale = f64::max(current_scale, 0.25);
|
||||||
|
|
||||||
output.change_current_state(None, None, Some(Scale::Fractional(current_scale)), None);
|
state.change_output_state(
|
||||||
layer_map_for_output(&output).arrange();
|
&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.request_layout(&output);
|
||||||
state.schedule_render(&output);
|
state.schedule_render(&output);
|
||||||
})
|
})
|
||||||
|
@ -1109,6 +1178,27 @@ impl output_service_server::OutputService for OutputService {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|output| output.current_scale().fractional_scale() as f32);
|
.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 {
|
output::v0alpha1::GetPropertiesResponse {
|
||||||
make,
|
make,
|
||||||
model,
|
model,
|
||||||
|
@ -1124,6 +1214,8 @@ impl output_service_server::OutputService for OutputService {
|
||||||
focused,
|
focused,
|
||||||
tag_ids,
|
tag_ids,
|
||||||
scale,
|
scale,
|
||||||
|
transform,
|
||||||
|
serial,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||||
signal_service_server, OutputConnectRequest, OutputConnectResponse, SignalRequest,
|
signal_service_server, OutputConnectRequest, OutputConnectResponse, OutputDisconnectRequest,
|
||||||
StreamControl, WindowPointerEnterRequest, WindowPointerEnterResponse,
|
OutputDisconnectResponse, OutputMoveRequest, OutputMoveResponse, OutputResizeRequest,
|
||||||
WindowPointerLeaveRequest, WindowPointerLeaveResponse,
|
OutputResizeResponse, SignalRequest, StreamControl, TagActiveRequest, TagActiveResponse,
|
||||||
|
WindowPointerEnterRequest, WindowPointerEnterResponse, WindowPointerLeaveRequest,
|
||||||
|
WindowPointerLeaveResponse,
|
||||||
};
|
};
|
||||||
use tokio::{sync::mpsc::UnboundedSender, task::JoinHandle};
|
use tokio::{sync::mpsc::UnboundedSender, task::JoinHandle};
|
||||||
use tonic::{Request, Response, Status, Streaming};
|
use tonic::{Request, Response, Status, Streaming};
|
||||||
|
@ -15,16 +17,28 @@ use super::{run_bidirectional_streaming, ResponseStream, StateFnSender};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct SignalState {
|
pub struct SignalState {
|
||||||
|
// Output
|
||||||
pub output_connect: SignalData<OutputConnectResponse, VecDeque<OutputConnectResponse>>,
|
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:
|
pub window_pointer_enter:
|
||||||
SignalData<WindowPointerEnterResponse, VecDeque<WindowPointerEnterResponse>>,
|
SignalData<WindowPointerEnterResponse, VecDeque<WindowPointerEnterResponse>>,
|
||||||
pub window_pointer_leave:
|
pub window_pointer_leave:
|
||||||
SignalData<WindowPointerLeaveResponse, VecDeque<WindowPointerLeaveResponse>>,
|
SignalData<WindowPointerLeaveResponse, VecDeque<WindowPointerLeaveResponse>>,
|
||||||
|
|
||||||
|
// Tag
|
||||||
|
pub tag_active: SignalData<TagActiveResponse, VecDeque<TagActiveResponse>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignalState {
|
impl SignalState {
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.output_connect.disconnect();
|
self.output_connect.disconnect();
|
||||||
|
self.output_disconnect.disconnect();
|
||||||
|
self.output_resize.disconnect();
|
||||||
|
self.output_move.disconnect();
|
||||||
self.window_pointer_enter.disconnect();
|
self.window_pointer_enter.disconnect();
|
||||||
self.window_pointer_leave.disconnect();
|
self.window_pointer_leave.disconnect();
|
||||||
}
|
}
|
||||||
|
@ -171,9 +185,15 @@ impl SignalService {
|
||||||
#[tonic::async_trait]
|
#[tonic::async_trait]
|
||||||
impl signal_service_server::SignalService for SignalService {
|
impl signal_service_server::SignalService for SignalService {
|
||||||
type OutputConnectStream = ResponseStream<OutputConnectResponse>;
|
type OutputConnectStream = ResponseStream<OutputConnectResponse>;
|
||||||
|
type OutputDisconnectStream = ResponseStream<OutputDisconnectResponse>;
|
||||||
|
type OutputResizeStream = ResponseStream<OutputResizeResponse>;
|
||||||
|
type OutputMoveStream = ResponseStream<OutputMoveResponse>;
|
||||||
|
|
||||||
type WindowPointerEnterStream = ResponseStream<WindowPointerEnterResponse>;
|
type WindowPointerEnterStream = ResponseStream<WindowPointerEnterResponse>;
|
||||||
type WindowPointerLeaveStream = ResponseStream<WindowPointerLeaveResponse>;
|
type WindowPointerLeaveStream = ResponseStream<WindowPointerLeaveResponse>;
|
||||||
|
|
||||||
|
type TagActiveStream = ResponseStream<TagActiveResponse>;
|
||||||
|
|
||||||
async fn output_connect(
|
async fn output_connect(
|
||||||
&self,
|
&self,
|
||||||
request: Request<Streaming<OutputConnectRequest>>,
|
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(
|
async fn window_pointer_enter(
|
||||||
&self,
|
&self,
|
||||||
request: Request<Streaming<WindowPointerEnterRequest>>,
|
request: Request<Streaming<WindowPointerEnterRequest>>,
|
||||||
|
@ -206,4 +259,15 @@ impl signal_service_server::SignalService for SignalService {
|
||||||
&mut state.signal_state.window_pointer_leave
|
&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
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||||
|
OutputConnectResponse, OutputDisconnectResponse,
|
||||||
|
};
|
||||||
use smithay::backend::renderer::test::DummyRenderer;
|
use smithay::backend::renderer::test::DummyRenderer;
|
||||||
use smithay::backend::renderer::ImportMemWl;
|
use smithay::backend::renderer::ImportMemWl;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
|
use smithay::utils::{Physical, Size};
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
@ -116,3 +120,45 @@ pub fn setup_dummy(
|
||||||
|
|
||||||
Ok((state, event_loop))
|
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()),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,9 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, ensure, Context};
|
use anyhow::{anyhow, ensure, Context};
|
||||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::OutputConnectResponse;
|
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||||
|
OutputConnectResponse, OutputDisconnectResponse,
|
||||||
|
};
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::{
|
backend::{
|
||||||
allocator::{
|
allocator::{
|
||||||
|
@ -49,10 +51,7 @@ use smithay::{
|
||||||
vulkan::{self, version::Version, PhysicalDevice},
|
vulkan::{self, version::Version, PhysicalDevice},
|
||||||
SwapBuffersError,
|
SwapBuffersError,
|
||||||
},
|
},
|
||||||
desktop::{
|
desktop::utils::{send_frames_surface_tree, OutputPresentationFeedback},
|
||||||
layer_map_for_output,
|
|
||||||
utils::{send_frames_surface_tree, OutputPresentationFeedback},
|
|
||||||
},
|
|
||||||
input::pointer::CursorImageStatus,
|
input::pointer::CursorImageStatus,
|
||||||
output::{Output, PhysicalProperties, Subpixel},
|
output::{Output, PhysicalProperties, Subpixel},
|
||||||
reexports::{
|
reexports::{
|
||||||
|
@ -272,16 +271,14 @@ impl State {
|
||||||
{
|
{
|
||||||
match render_surface.compositor.use_mode(drm_mode) {
|
match render_surface.compositor.use_mode(drm_mode) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
output.change_current_state(Some(mode), None, None, None);
|
self.change_output_state(output, Some(mode), None, None, None);
|
||||||
layer_map_for_output(output).arrange();
|
|
||||||
}
|
}
|
||||||
Err(err) => error!("Failed to resize output: {err}"),
|
Err(err) => error!("Failed to resize output: {err}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
output.change_current_state(Some(mode), None, None, None);
|
self.change_output_state(output, Some(mode), None, None, None);
|
||||||
layer_map_for_output(output).arrange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.schedule_render(output);
|
self.schedule_render(output);
|
||||||
|
@ -1010,11 +1007,11 @@ impl State {
|
||||||
connector.interface_id()
|
connector.interface_id()
|
||||||
);
|
);
|
||||||
|
|
||||||
let (make, model) = EdidInfo::try_from_connector(&device.drm, connector.handle())
|
let (make, model, serial) = EdidInfo::try_from_connector(&device.drm, connector.handle())
|
||||||
.map(|info| (info.manufacturer, info.model))
|
.map(|info| (info.manufacturer, info.model, info.serial))
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
warn!("Failed to parse EDID info: {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));
|
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);
|
let global = output.create_global::<State>(&udev.display_handle);
|
||||||
|
|
||||||
|
output.with_state_mut(|state| state.serial = serial);
|
||||||
|
|
||||||
for mode in modes {
|
for mode in modes {
|
||||||
output.add_mode(mode);
|
output.add_mode(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output.set_preferred(wl_mode);
|
||||||
|
|
||||||
self.output_focus_stack.set_focus(output.clone());
|
self.output_focus_stack.set_focus(output.clone());
|
||||||
|
|
||||||
let x = self.space.outputs().fold(0, |acc, o| {
|
let x = self.space.outputs().fold(0, |acc, o| {
|
||||||
|
@ -1052,10 +1053,6 @@ impl State {
|
||||||
});
|
});
|
||||||
let position = (x, 0).into();
|
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 {
|
output.user_data().insert_if_missing(|| UdevOutputData {
|
||||||
crtc,
|
crtc,
|
||||||
device_id: node,
|
device_id: node,
|
||||||
|
@ -1122,6 +1119,8 @@ impl State {
|
||||||
|
|
||||||
device.surfaces.insert(crtc, surface);
|
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.
|
// If there is saved connector state, the connector was previously plugged in.
|
||||||
// In this case, restore its tags and location.
|
// In this case, restore its tags and location.
|
||||||
// TODO: instead of checking the connector, check the monitor's edid info instead
|
// TODO: instead of checking the connector, check the monitor's edid info instead
|
||||||
|
@ -1131,11 +1130,8 @@ impl State {
|
||||||
.get(&OutputName(output.name()))
|
.get(&OutputName(output.name()))
|
||||||
{
|
{
|
||||||
let ConnectorSavedState { loc, tags, scale } = saved_state;
|
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());
|
output.with_state_mut(|state| state.tags = tags.clone());
|
||||||
|
self.change_output_state(&output, None, None, *scale, Some(*loc));
|
||||||
} else {
|
} else {
|
||||||
self.signal_state.output_connect.signal(|buffer| {
|
self.signal_state.output_connect.signal(|buffer| {
|
||||||
buffer.push_back(OutputConnectResponse {
|
buffer.push_back(OutputConnectResponse {
|
||||||
|
@ -1187,6 +1183,12 @@ impl State {
|
||||||
);
|
);
|
||||||
self.space.unmap_output(&output);
|
self.space.unmap_output(&output);
|
||||||
self.gamma_control_manager_state.output_removed(&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()),
|
||||||
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::num::NonZeroU32;
|
||||||
|
|
||||||
use smithay::reexports::drm::control::{connector, property, Device, ResourceHandle};
|
use smithay::reexports::drm::control::{connector, property, Device, ResourceHandle};
|
||||||
|
|
||||||
// A bunch of this stuff is from cosmic-comp
|
// 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 struct EdidInfo {
|
||||||
pub model: String,
|
pub model: String,
|
||||||
pub manufacturer: String,
|
pub manufacturer: String,
|
||||||
|
pub serial: Option<NonZeroU32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EdidInfo {
|
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]);
|
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.
|
// Monitor names are inside of these display/monitor descriptors at bytes 72..=125.
|
||||||
// Each descriptor is 18 bytes long.
|
// Each descriptor is 18 bytes long.
|
||||||
let descriptor1 = &buffer[72..=89];
|
let descriptor1 = &buffer[72..=89];
|
||||||
|
@ -99,6 +105,7 @@ fn parse_edid(buffer: &[u8]) -> anyhow::Result<EdidInfo> {
|
||||||
Ok(EdidInfo {
|
Ok(EdidInfo {
|
||||||
model,
|
model,
|
||||||
manufacturer,
|
manufacturer,
|
||||||
|
serial: NonZeroU32::new(serial),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ use smithay::{
|
||||||
},
|
},
|
||||||
winit::{self, WinitEvent, WinitGraphicsBackend},
|
winit::{self, WinitEvent, WinitGraphicsBackend},
|
||||||
},
|
},
|
||||||
desktop::layer_map_for_output,
|
|
||||||
input::pointer::CursorImageStatus,
|
input::pointer::CursorImageStatus,
|
||||||
output::{Output, Scale, Subpixel},
|
output::{Output, Scale, Subpixel},
|
||||||
reexports::{
|
reexports::{
|
||||||
|
@ -109,7 +108,7 @@ pub fn setup_winit(
|
||||||
model: "Winit Window".to_string(),
|
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(
|
output.change_current_state(
|
||||||
Some(mode),
|
Some(mode),
|
||||||
|
@ -221,13 +220,13 @@ pub fn setup_winit(
|
||||||
size,
|
size,
|
||||||
refresh: 144_000,
|
refresh: 144_000,
|
||||||
};
|
};
|
||||||
output.change_current_state(
|
state.change_output_state(
|
||||||
|
&output,
|
||||||
Some(mode),
|
Some(mode),
|
||||||
None,
|
None,
|
||||||
Some(Scale::Fractional(scale_factor)),
|
Some(Scale::Fractional(scale_factor)),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
layer_map_for_output(&output).arrange();
|
|
||||||
state.request_layout(&output);
|
state.request_layout(&output);
|
||||||
}
|
}
|
||||||
WinitEvent::Focus(focused) => {
|
WinitEvent::Focus(focused) => {
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// 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::{
|
use crate::{
|
||||||
focus::WindowKeyboardFocusStack,
|
focus::WindowKeyboardFocusStack,
|
||||||
|
@ -35,6 +41,7 @@ pub struct OutputState {
|
||||||
pub tags: Vec<Tag>,
|
pub tags: Vec<Tag>,
|
||||||
pub focus_stack: WindowKeyboardFocusStack,
|
pub focus_stack: WindowKeyboardFocusStack,
|
||||||
pub screencopy: Option<Screencopy>,
|
pub screencopy: Option<Screencopy>,
|
||||||
|
pub serial: Option<NonZeroU32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WithState for Output {
|
impl WithState for Output {
|
||||||
|
@ -68,3 +75,40 @@ impl OutputState {
|
||||||
self.tags.iter().filter(|tag| tag.active())
|
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),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
38
src/tag.rs
38
src/tag.rs
|
@ -1,10 +1,11 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
rc::Rc,
|
sync::{
|
||||||
sync::atomic::{AtomicU32, Ordering},
|
atomic::{AtomicU32, Ordering},
|
||||||
|
Arc, Mutex,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use smithay::output::Output;
|
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
|
/// A window may have 0 or more tags, and you can display 0 or more tags
|
||||||
/// on each output at a time.
|
/// on each output at a time.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Tag(Rc<RefCell<TagInner>>);
|
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.
|
// RefCell Safety: These methods should never panic because they are all self-contained or Copy.
|
||||||
impl Tag {
|
impl Tag {
|
||||||
pub fn id(&self) -> TagId {
|
pub fn id(&self) -> TagId {
|
||||||
self.0.borrow().id
|
self.0.lock().expect("tag already locked").id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> String {
|
pub fn name(&self) -> String {
|
||||||
self.0.borrow().name.clone()
|
self.0.lock().expect("tag already locked").name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active(&self) -> bool {
|
pub fn active(&self) -> bool {
|
||||||
self.0.borrow().active
|
self.0.lock().expect("tag already locked").active
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_active(&self, active: bool) {
|
pub fn set_active(&self, active: bool, state: &mut State) {
|
||||||
self.0.borrow_mut().active = active;
|
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 {
|
impl Tag {
|
||||||
pub fn new(name: String) -> Self {
|
pub fn new(name: String) -> Self {
|
||||||
Self(Rc::new(RefCell::new(TagInner {
|
Self(Arc::new(Mutex::new(TagInner {
|
||||||
id: TagId::next(),
|
id: TagId::next(),
|
||||||
name,
|
name,
|
||||||
active: false,
|
active: false,
|
||||||
|
|
88
tests/common/mod.rs
Normal file
88
tests/common/mod.rs
Normal 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()
|
||||||
|
}
|
917
tests/lua_api.rs
917
tests/lua_api.rs
File diff suppressed because it is too large
Load diff
354
tests/rust_api.rs
Normal file
354
tests/rust_api.rs
Normal 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);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue