Merge pull request #28 from Ottatop/dev

Improve API
This commit is contained in:
Ottatop 2023-07-21 21:52:53 -05:00 committed by GitHub
commit d52feb8401
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 3322 additions and 742 deletions

View file

@ -11,6 +11,9 @@
A very, VERY WIP Smithay-based wayland compositor
</div>
## API Documentation
There is now *finally* [some form of documentation](https://github.com/Ottatop/pinnacle/wiki/API-Documentation) so you don't have to dig around in the code. It isn't great and automating it seems like a pain, but hey it's something! This may become out of date real quick though, and I'm probably going to need to move to LDoc in the future.
## Features
- [x] Winit backend
- [x] Udev backend

View file

@ -2,7 +2,7 @@ MIT License
Copyright (c) 2023 Ottatop
This license applies to the example_config.lua file only.
This license applies to the *_config.lua files.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

1395
api/lua/doc.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -41,7 +41,12 @@ require("pinnacle").setup(function(pinnacle)
end
end)
input.keybind({ mod_key, "Alt" }, keys.space, window.toggle_floating)
input.keybind({ mod_key, "Alt" }, keys.space, function()
local win = window.get_focused()
if win ~= nil then
win:toggle_floating()
end
end)
input.keybind({ mod_key }, keys.Return, function()
process.spawn(terminal, function(stdout, stderr, exit_code, exit_msg)
@ -86,6 +91,9 @@ require("pinnacle").setup(function(pinnacle)
for _, tg in pairs(tags) do
if tg:active() then
local name = tg:name()
if name == nil then
return
end
tg:set_layout(layouts[indices[name] or 1])
if indices[name] == nil then
indices[name] = 2
@ -105,6 +113,9 @@ require("pinnacle").setup(function(pinnacle)
for _, tg in pairs(tags) do
if tg:active() then
local name = tg:name()
if name == nil then
return
end
tg:set_layout(layouts[indices[name] or #layouts])
if indices[name] == nil then
indices[name] = #layouts - 1

View file

@ -4,7 +4,8 @@
--
-- SPDX-License-Identifier: MPL-2.0
local input = {
---@class InputModule
local input_module = {
keys = require("keys"),
}
@ -13,8 +14,7 @@ local input = {
---### Example
---
---```lua
----- The following sets Super + Return to open Alacritty
---
----- Set `Super + Return` to open Alacritty
---input.keybind({ "Super" }, input.keys.Return, function()
--- process.spawn("Alacritty")
---end)
@ -22,7 +22,7 @@ local input = {
---@param key Keys The key for the keybind.
---@param modifiers (Modifier)[] Which modifiers need to be pressed for the keybind to trigger.
---@param action fun() What to do.
function input.keybind(modifiers, key, action)
function input_module.keybind(modifiers, key, action)
table.insert(CallbackTable, action)
SendMsg({
SetKeybind = {
@ -33,4 +33,4 @@ function input.keybind(modifiers, key, action)
})
end
return input
return input_module

View file

@ -4,7 +4,11 @@
--
-- SPDX-License-Identifier: MPL-2.0
---@alias Modifier "Alt" | "Ctrl" | "Shift" | "Super"
---@alias Modifier
---| "Alt" # The "Alt" key
---| "Ctrl" # The "Control" key
---| "Shift" # The "Shift" key
---| "Super" # The "Super" key, aka "Meta", "Mod4" in X11, the Windows key, etc.
---@enum Keys
local keys = {

View file

@ -10,20 +10,20 @@
---@field SetKeybind { key: Keys, modifiers: Modifier[], callback_id: integer }
---@field SetMousebind { button: integer }
--Windows
---@field CloseWindow { window_id: integer }
---@field ToggleFloating { window_id: integer }
---@field SetWindowSize { window_id: integer, size: integer[] }
---@field MoveWindowToTag { window_id: integer, tag_id: string }
---@field ToggleTagOnWindow { window_id: integer, tag_id: string }
---@field CloseWindow { window_id: WindowId }
---@field ToggleFloating { window_id: WindowId }
---@field SetWindowSize { window_id: WindowId, width: integer?, height: integer? }
---@field MoveWindowToTag { window_id: WindowId, tag_id: TagId }
---@field ToggleTagOnWindow { window_id: WindowId, tag_id: TagId }
--
---@field Spawn { command: string[], callback_id: integer? }
---@field Request Request
--Tags
---@field ToggleTag { output_name: string, tag_name: string }
---@field SwitchToTag { output_name: string, tag_name: string }
---@field ToggleTag { tag_id: TagId }
---@field SwitchToTag { tag_id: TagId }
---@field AddTags { output_name: string, tag_names: string[] }
---@field RemoveTags { output_name: string, tag_names: string[] }
---@field SetLayout { output_name: string, tag_name: string, layout: Layout }
---@field RemoveTags { tag_ids: TagId[] }
---@field SetLayout { tag_id: TagId, layout: Layout }
--Outputs
---@field ConnectForAllOutputs { callback_id: integer }
@ -31,23 +31,20 @@
--------------------------------------------------------------------------------------------
---@class _Request
---@class __Request
--Windows
---@field GetWindowByAppId { app_id: string }
---@field GetWindowByTitle { title: string }
---@field GetWindowProps { window_id: WindowId }
--Outputs
---@field GetOutputByName { output_name: OutputName }
---@field GetOutputsByModel { model: string }
---@field GetOutputsByRes { res: integer[] }
---@field GetTagsByOutput { output_name: string }
---@field GetTagActive { tag_id: TagId }
---@field GetTagName { tag_id: TagId }
---@field GetOutputProps { output_name: string }
--Tags
---@field GetTagProps { tag_id: TagId }
---@alias Request _Request | "GetWindowByFocus" | "GetAllWindows" | "GetOutputByFocus"
---@alias _Request __Request | "GetWindows" | "GetOutputs" | "GetTags"
---@alias Request { request_id: integer, request: _Request }
---@class IncomingMsg
---@field CallCallback { callback_id: integer, args: Args }
---@field RequestResponse { response: RequestResponse }
---@field CallCallback { callback_id: integer, args: Args? }
---@field RequestResponse { request_id: integer, response: RequestResponse }
---@class Args
---@field Spawn { stdout: string?, stderr: string?, exit_code: integer?, exit_msg: string? }
@ -55,12 +52,18 @@
---@alias WindowId integer
---@alias TagId integer
---@alias RequestId integer
---@alias OutputName string
---@class RequestResponse
--Windows
---@field Window { window_id: WindowId|nil }
---@field Windows { window_ids: WindowId[] }
---@field WindowProps { size: integer[]?, loc: integer[]?, class: string?, title: string?, floating: boolean?, focused: boolean? }
--Outputs
---@field Output { output_name: OutputName? }
---@field Outputs { output_names: OutputName[] }
---@field OutputProps { make: string?, model: string?, loc: integer[]?, res: integer[]?, refresh_rate: integer?, physical_size: integer[]?, focused: boolean?, tag_ids: integer[]? }
--Tags
---@field Tags { tag_ids: TagId[] }
---@field TagActive { active: boolean }
---@field TagName { name: string }
---@field TagProps { active: boolean?, name: string?, output_name: string? }

View file

@ -4,44 +4,101 @@
--
-- SPDX-License-Identifier: MPL-2.0
---@class OutputModule
local output_module = {}
---@class Output A display.
---@field name string The name of this output (or rather, of its connector).
local op = {}
---@field private _name string The name of this output (or rather, of its connector).
local output = {}
---Get all tags on this output. See `tag.get_on_output`.
---@return Tag[]
function op:tags()
return require("tag").get_on_output(self)
end
---Add tags to this output. See `tag.add`.
---@param ... string The names of the tags you want to add.
function op:add_tags(...)
require("tag").add(self, ...)
end
---Add tags to this output as a table. See `tag.add_table`.
---@param names string[] The names of the tags you want to add, as a table.
function op:add_tags_table(names)
require("tag").add_table(self, names)
end
---Add methods to this output.
---@param props Output
---Create a new output object from a name.
---The name is the unique identifier for each output.
---@param name string
---@return Output
local function new_output(props)
local function create_output(name)
---@type Output
local o = { _name = name }
-- Copy functions over
for k, v in pairs(op) do
props[k] = v
for k, v in pairs(output) do
o[k] = v
end
return props
return o
end
---Get this output's name. This is something like "eDP-1" or "HDMI-A-0".
---@return string
function output:name()
return self._name
end
---Get all tags on this output.
---@return Tag[]
---@see OutputModule.tags — The corresponding module function
function output:tags()
return output_module.tags(self)
end
---Add tags to this output.
---@param ... string The names of the tags you want to add. You can also pass in a table.
---@overload fun(self: self, tag_names: string[])
---@see OutputModule.add_tags — The corresponding module function
function output:add_tags(...)
output_module.add_tags(self, ...)
end
---Get this output's make.
---@return string|nil
---@see OutputModule.make — The corresponding module function
function output:make()
return output_module.make(self)
end
---Get this output's model.
---@return string|nil
---@see OutputModule.model — The corresponding module function
function output:model()
return output_module.model(self)
end
---Get this output's location in the global space, in pixels.
---@return { x: integer, y: integer }|nil
---@see OutputModule.loc — The corresponding module function
function output:loc()
return output_module.loc(self)
end
---Get this output's resolution in pixels.
---@return { w: integer, h: integer }|nil
---@see OutputModule.res — The corresponding module function
function output:res()
return output_module.res(self)
end
---Get this output's refresh rate in millihertz.
---For example, 60Hz will be returned as 60000.
---@return integer|nil
---@see OutputModule.refresh_rate — The corresponding module function
function output:refresh_rate()
return output_module.refresh_rate(self)
end
---Get this output's physical size in millimeters.
---@return { w: integer, h: integer }|nil
---@see OutputModule.physical_size — The corresponding module function
function output:physical_size()
return output_module.physical_size(self)
end
---Get whether or not this output is focused. This is currently defined as having the cursor on it.
---@return boolean|nil
---@see OutputModule.focused — The corresponding module function
function output:focused()
return output_module.focused(self)
end
------------------------------------------------------
local output = {}
---Get an output by its name.
---
---"Name" in this sense does not mean its model or manufacturer;
@ -54,23 +111,18 @@ local output = {}
---print(monitor.name) -- should print `DP-1`
---```
---@param name string The name of the output.
---@return Output|nil
function output.get_by_name(name)
SendRequest({
GetOutputByName = {
output_name = name,
},
})
local response = ReadMsg()
---@return Output|nil output The output, or nil if none have the provided name.
function output_module.get_by_name(name)
local response = Request("GetOutputs")
local output_names = response.RequestResponse.response.Outputs.output_names
if output_names[1] ~= nil then
return new_output({ name = output_names[1] })
else
return nil
for _, output_name in pairs(output_names) do
if output_name == name then
return create_output(output_name)
end
end
return nil
end
---Note: This may or may not be what is reported by other monitor listing utilities. Pinnacle currently fails to pick up one of my monitors' models when it is correctly picked up by tools like wlr-randr. I'll fix this in the future.
@ -78,22 +130,18 @@ end
---Get outputs by their model.
---This is something like "DELL E2416H" or whatever gibberish monitor manufacturers call their displays.
---@param model string The model of the output(s).
---@return Output[] outputs All outputs with this model. If there are none, the returned table will be empty.
function output.get_by_model(model)
SendRequest({
GetOutputsByModel = {
model = model,
},
})
local response = ReadMsg()
---@return Output[] outputs All outputs with this model.
function output_module.get_by_model(model)
local response = Request("GetOutputs")
local output_names = response.RequestResponse.response.Outputs.output_names
---@type Output
---@type Output[]
local outputs = {}
for _, v in pairs(output_names) do
table.insert(outputs, new_output({ name = v }))
for _, output_name in pairs(output_names) do
local o = create_output(output_name)
if o:model() == model then
table.insert(outputs, o)
end
end
return outputs
@ -103,22 +151,19 @@ end
---
---@param width integer The width of the outputs, in pixels.
---@param height integer The height of the outputs, in pixels.
---@return Output[] outputs All outputs with this resolution. If there are none, the returned table will be empty.
function output.get_by_res(width, height)
SendRequest({
GetOutputsByRes = {
res = { width, height },
},
})
local response = ReadMsg()
---@return Output[] outputs All outputs with this resolution.
function output_module.get_by_res(width, height)
local response = Request("GetOutputs")
local output_names = response.RequestResponse.response.Outputs.output_names
---@type Output
local outputs = {}
for _, output_name in pairs(output_names) do
table.insert(outputs, new_output({ name = output_name }))
local o = create_output(output_name)
if o:res() and o:res().w == width and o:res().h == height then
table.insert(outputs, o)
end
end
return outputs
@ -144,18 +189,18 @@ end
---local tags = output.get_focused():tags() -- will NOT warn for nil
---```
---@return Output|nil output The output, or nil if none are focused.
function output.get_focused()
SendRequest("GetOutputByFocus")
local response = ReadMsg()
function output_module.get_focused()
local response = Request("GetOutputs")
local output_names = response.RequestResponse.response.Outputs.output_names
if output_names[1] ~= nil then
return new_output({ name = output_names[1] })
else
return nil
for _, output_name in pairs(output_names) do
local o = create_output(output_name)
if o:focused() then
return o
end
end
return nil
end
---Connect a function to be run on all current and future outputs.
@ -166,11 +211,11 @@ end
---Please note: this function will be run *after* Pinnacle processes your entire config.
---For example, if you define tags in `func` but toggle them directly after `connect_for_all`, nothing will happen as the tags haven't been added yet.
---@param func fun(output: Output) The function that will be run.
function output.connect_for_all(func)
function output_module.connect_for_all(func)
---@param args Args
table.insert(CallbackTable, function(args)
local args = args.ConnectForAllOutputs
func(new_output({ name = args.output_name }))
func(create_output(args.output_name))
end)
SendMsg({
ConnectForAllOutputs = {
@ -179,4 +224,153 @@ function output.connect_for_all(func)
})
end
return output
---Get the output the specified tag is on.
---@param tag Tag
---@return Output|nil
---@see TagModule.output — A global method for fully qualified syntax (for you Rustaceans out there)
---@see Tag.output — The corresponding object method
function output_module.get_for_tag(tag)
local response = Request({
GetTagProps = {
tag_id = tag:id(),
},
})
local output_name = response.RequestResponse.response.TagProps.output_name
if output_name == nil then
return nil
else
return create_output(output_name)
end
end
---Get the specified output's make.
---@param op Output
---@return string|nil
---@see Output.make — The corresponding object method
function output_module.make(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
return props.make
end
---Get the specified output's model.
---@param op Output
---@return string|nil
---@see Output.model — The corresponding object method
function output_module.model(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
return props.model
end
---Get the specified output's location in the global space, in pixels.
---@param op Output
---@return { x: integer, y: integer }|nil
---@see Output.loc — The corresponding object method
function output_module.loc(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
if props.loc == nil then
return nil
else
return { x = props.loc[1], y = props.loc[2] }
end
end
---Get the specified output's resolution in pixels.
---@param op Output
---@return { w: integer, h: integer }|nil
---@see Output.res — The corresponding object method
function output_module.res(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
if props.res == nil then
return nil
else
return { w = props.res[1], h = props.res[2] }
end
end
---Get the specified output's refresh rate in millihertz.
---For example, 60Hz will be returned as 60000.
---@param op Output
---@return integer|nil
---@see Output.refresh_rate — The corresponding object method
function output_module.refresh_rate(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
return props.refresh_rate
end
---Get the specified output's physical size in millimeters.
---@param op Output
---@return { w: integer, h: integer }|nil
---@see Output.physical_size — The corresponding object method
function output_module.physical_size(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
if props.physical_size == nil then
return nil
else
return { w = props.physical_size[1], h = props.physical_size[2] }
end
end
---Get whether or not the specified output is focused. This is currently defined as having the cursor on it.
---@param op Output
---@return boolean|nil
---@see Output.focused — The corresponding object method
function output_module.focused(op)
local response = Request({
GetOutputProps = {
output_name = op:name(),
},
})
local props = response.RequestResponse.response.OutputProps
return props.focused
end
---Get the specified output's tags.
---@param op Output
---@see TagModule.get_on_output — The called function
---@see Output.tags — The corresponding object method
function output_module.tags(op)
return require("tag").get_on_output(op)
end
---Add tags to the specified output.
---@param op Output
---@param ... string The names of the tags you want to add. You can also pass in a table.
---@overload fun(op: Output, tag_names: string[])
---@see TagModule.add — The called function
---@see Output.add_tags — The corresponding object method
function output_module.add_tags(op, ...)
require("tag").add(op, ...)
end
return output_module

View file

@ -9,6 +9,37 @@ local msgpack = require("msgpack")
local SOCKET_PATH = "/tmp/pinnacle_socket"
---From https://gist.github.com/stuby/5445834#file-rprint-lua
---rPrint(struct, [limit], [indent]) Recursively print arbitrary data.
--- Set limit (default 100) to stanch infinite loops.
--- Indents tables as [KEY] VALUE, nested tables as [KEY] [KEY]...[KEY] VALUE
--- Set indent ("") to prefix each line: Mytable [KEY] [KEY]...[KEY] VALUE
---@param s table The table
---@param l integer? Recursion limit
---@param i string? The indent string
---@return integer l The remaining depth limit
function RPrint(s, l, i) -- recursive Print (structure, limit, indent)
l = l or 100
i = i or "" -- default item limit, indent string
if l < 1 then
print("ERROR: Item limit reached.")
return l - 1
end
local ts = type(s)
if ts ~= "table" then
print(i, ts, s)
return l - 1
end
print(i, ts) -- print "table"
for k, v in pairs(s) do -- print "[KEY] VALUE"
l = RPrint(v, l, i .. "\t[" .. tostring(k) .. "]")
if l < 0 then
break
end
end
return l
end
---Read the specified number of bytes.
---@param socket_fd integer The socket file descriptor
---@param count integer The amount of bytes to read
@ -82,60 +113,110 @@ function pinnacle.setup(config_func)
---@type fun(args: table?)[]
CallbackTable = {}
---This is an internal global function used to send serialized messages to the Pinnacle server.
---@param data Msg
function SendMsg(data)
-- RPrint(data)
local encoded = msgpack.encode(data)
assert(encoded)
-- print(encoded)
local len = encoded:len()
socket.send(socket_fd, string.pack("=I4", len))
socket.send(socket_fd, encoded)
end
---@param data Request
function SendRequest(data)
SendMsg({
Request = data,
})
local request_id = 1
---Get the next request id.
---@return integer
local function next_request_id()
local ret = request_id
request_id = request_id + 1
return ret
end
function ReadMsg()
local msg_len_bytes, err_msg, err_num = read_exact(socket_fd, 4)
assert(msg_len_bytes)
---@type table<integer, IncomingMsg>
local unread_req_msgs = {}
---@type table<integer, IncomingMsg>
local unread_cb_msgs = {}
-- TODO: break here if error in read_exact
---This is an internal global function used to send requests to the Pinnacle server for information.
---@param data _Request
---@return IncomingMsg
function Request(data)
local req_id = next_request_id()
SendMsg({
Request = {
request_id = req_id,
request = data,
},
})
return ReadMsg(req_id)
end
---@type integer
local msg_len = string.unpack("=I4", msg_len_bytes)
-- print(msg_len)
---This is an internal global function used to read messages sent from the server.
---These are used to call user-defined functions and provide requested information.
---@return IncomingMsg
---@param req_id integer? A request id if you're looking for that specific message.
function ReadMsg(req_id)
while true do
if req_id then
if unread_req_msgs[req_id] then
local msg = unread_req_msgs[req_id]
unread_req_msgs[req_id] = nil -- INFO: is this a reference?
return msg
end
end
local msg_bytes, err_msg2, err_num2 = read_exact(socket_fd, msg_len)
assert(msg_bytes)
-- print(msg_bytes)
local msg_len_bytes, err_msg, err_num = read_exact(socket_fd, 4)
assert(msg_len_bytes)
---@type IncomingMsg
local tb = msgpack.decode(msg_bytes)
-- print(msg_bytes)
-- TODO: break here if error in read_exact
return tb
---@type integer
local msg_len = string.unpack("=I4", msg_len_bytes)
-- print(msg_len)
local msg_bytes, err_msg2, err_num2 = read_exact(socket_fd, msg_len)
assert(msg_bytes)
-- print(msg_bytes)
---@type IncomingMsg
local inc_msg = msgpack.decode(msg_bytes)
-- print(msg_bytes)
if req_id then
if inc_msg.CallCallback then
unread_cb_msgs[inc_msg.CallCallback.callback_id] = inc_msg
elseif inc_msg.RequestResponse.request_id ~= req_id then
unread_req_msgs[inc_msg.RequestResponse.request_id] = inc_msg
else
return inc_msg
end
else
return inc_msg
end
end
end
config_func(pinnacle)
while true do
local tb = ReadMsg()
if tb.CallCallback and tb.CallCallback.callback_id then
if tb.CallCallback.args then -- TODO: can just inline
CallbackTable[tb.CallCallback.callback_id](tb.CallCallback.args)
else
CallbackTable[tb.CallCallback.callback_id](nil)
end
for cb_id, inc_msg in pairs(unread_cb_msgs) do
CallbackTable[inc_msg.CallCallback.callback_id](inc_msg.CallCallback.args)
unread_cb_msgs[cb_id] = nil -- INFO: does this shift the table and frick everything up?
end
-- if tb.RequestResponse then
-- local req_id = tb.RequestResponse.request_id
-- Requests[req_id] = tb.RequestResponse.response
-- end
local inc_msg = ReadMsg()
assert(inc_msg.CallCallback) -- INFO: is this gucci or no
if inc_msg.CallCallback and inc_msg.CallCallback.callback_id then
if inc_msg.CallCallback.args then -- TODO: can just inline
CallbackTable[inc_msg.CallCallback.callback_id](inc_msg.CallCallback.args)
else
CallbackTable[inc_msg.CallCallback.callback_id](nil)
end
end
end
end

View file

@ -6,7 +6,8 @@
---@diagnostic disable: redefined-local
local process = {}
---@class ProcessModule
local process_module = {}
---Spawn a process with an optional callback for its stdout, stderr, and exit information.
---
@ -17,7 +18,7 @@ local process = {}
--- - `exit_msg`: The process exited with this message.
---@param command string|string[] The command as one whole string or a table of each of its arguments
---@param callback fun(stdout: string|nil, stderr: string|nil, exit_code: integer|nil, exit_msg: string|nil)? A callback to do something whenever the process's stdout or stderr print a line, or when the process exits.
function process.spawn(command, callback)
function process_module.spawn(command, callback)
---@type integer|nil
local callback_id = nil
@ -58,7 +59,7 @@ end
---`spawn_once` checks for the process using `pgrep`. If your system doesn't have `pgrep`, this won't work properly.
---@param command string|string[] The command as one whole string or a table of each of its arguments
---@param callback fun(stdout: string|nil, stderr: string|nil, exit_code: integer|nil, exit_msg: string|nil)? A callback to do something whenever the process's stdout or stderr print a line, or when the process exits.
function process.spawn_once(command, callback)
function process_module.spawn_once(command, callback)
local proc = ""
if type(command) == "string" then
proc = command:match("%S+")
@ -71,7 +72,7 @@ function process.spawn_once(command, callback)
if procs:len() ~= 0 then -- if process exists, return
return
end
process.spawn(command, callback)
process_module.spawn(command, callback)
end
return process
return process_module

View file

@ -4,7 +4,8 @@
--
-- SPDX-License-Identifier: MPL-2.0
local tag = {}
---@class TagModule
local tag_module = {}
---@alias Layout
---| "MasterStack" # One master window on the left with all other windows stacked to the right.
@ -16,100 +17,113 @@ local tag = {}
---| "CornerBottomRight" # One main corner window in the bottom right with a column of windows on the left and a row on the top.
---@class Tag
---@field private id integer The internal id of this tag.
local tg = {}
---@field private _id integer The internal id of this tag.
local tag = {}
---@param props Tag
---Create a tag from an id.
---The id is the unique identifier for each tag.
---@param id TagId
---@return Tag
local function new_tag(props)
local function create_tag(id)
---@type Tag
local t = { _id = id }
-- Copy functions over
for k, v in pairs(tg) do
props[k] = v
for k, v in pairs(tag) do
t[k] = v
end
return props
return t
end
---Get this tag's internal id.
---***You probably won't need to use this.***
---@return integer
function tag:id()
return self._id
end
---Get this tag's active status.
---@return boolean active True if the tag is active, otherwise false.
function tg:active()
SendRequest({
GetTagActive = {
tag_id = self.id,
},
})
local response = ReadMsg()
local active = response.RequestResponse.response.TagActive.active
return active
---@return boolean|nil active `true` if the tag is active, `false` if not, and `nil` if the tag doesn't exist.
---@see TagModule.active — The corresponding module function
function tag:active()
return tag_module.active(self)
end
function tg:name()
SendRequest({
GetTagName = {
tag_id = self.id,
},
})
---Get this tag's name.
---@return string|nil name The name of this tag, or nil if it doesn't exist.
---@see TagModule.name — The corresponding module function
function tag:name()
return tag_module.name(self)
end
local response = ReadMsg()
local name = response.RequestResponse.response.TagName.name
return name
---Get this tag's output.
---@return Output|nil output The output this tag is on, or nil if the tag doesn't exist.
---@see TagModule.output — The corresponding module function
function tag:output()
return tag_module.output(self)
end
---Switch to this tag.
---@see TagModule.switch_to — The corresponding module function
function tag:switch_to()
tag_module.switch_to(self)
end
---Toggle this tag.
---@see TagModule.toggle — The corresponding module function
function tag:toggle()
tag_module.toggle(self)
end
---Set this tag's layout.
---@param layout Layout
function tg:set_layout(layout) -- TODO: output param
tag.set_layout(self:name(), layout)
---@see TagModule.set_layout — The corresponding module function
function tag:set_layout(layout)
tag_module.set_layout(self, layout)
end
-----------------------------------------------------------
---Add tags.
---
---If you need to add the names as a table, use `tag.add_table` instead.
---
---### Example
---Add tags to the specified output.
---
---### Examples
---```lua
---local output = output.get_by_name("DP-1")
---if output ~= nil then
--- tag.add(output, "1", "2", "3", "4", "5") -- Add tags with names 1-5
---local op = output.get_by_name("DP-1")
---if op ~= nil then
--- tag.add(op, "1", "2", "3", "4", "5") -- Add tags with names 1-5
---end
---```
---You can also pass in a table.
---```lua
---local tags = {"Terminal", "Browser", "Code", "Potato", "Email"}
---tag.add(op, tags) -- Add tags with those names
---```
---@param output Output The output you want these tags to be added to.
---@param ... string The names of the new tags you want to add.
function tag.add(output, ...)
local tag_names = table.pack(...)
tag_names["n"] = nil -- remove the length to make it a true array for serializing
---@overload fun(output: Output, tag_names: string[])
---@see Output.add_tags — The corresponding object method
function tag_module.add(output, ...)
local varargs = { ... }
if type(varargs[1]) == "string" then
local tag_names = varargs
tag_names["n"] = nil -- remove the length to make it a true array for serializing
SendMsg({
AddTags = {
output_name = output.name,
tag_names = tag_names,
},
})
end
SendMsg({
AddTags = {
output_name = output:name(),
tag_names = tag_names,
},
})
else
local tag_names = varargs[1] --[=[@as string[]]=]
---Like `tag.add`, but with a table of strings instead.
---
---### Example
---
---```lua
---local tags = { "Terminal", "Browser", "Mail", "Gaming", "Potato" }
---local output = output.get_by_name("DP-1")
---if output ~= nil then
--- tag.add(output, tags) -- Add tags with the names above
---end
---```
---@param output Output The output you want these tags to be added to.
---@param names string[] The names of the new tags you want to add, as a table.
function tag.add_table(output, names)
SendMsg({
AddTags = {
output_name = output.name,
tag_names = names,
},
})
SendMsg({
AddTags = {
output_name = output:name(),
tag_names = tag_names,
},
})
end
end
---Toggle a tag on the specified output. If `output` isn't specified, toggle it on the currently focused output instead.
@ -125,105 +139,207 @@ end
---```
---@param name string The name of the tag.
---@param output Output? The output.
function tag.toggle(name, output)
if output ~= nil then
---@overload fun(t: Tag)
---@see Tag.toggle — The corresponding object method
function tag_module.toggle(name, output)
if type(name) == "table" then
SendMsg({
ToggleTag = {
output_name = output.name,
tag_name = name,
tag_id = name--[[@as Tag]]:id(),
},
})
else
local op = require("output").get_focused()
if op ~= nil then
return
end
local output = output or require("output").get_focused()
if output == nil then
return
end
print("before tag_global.get_by_name")
local tags = tag_module.get_by_name(name)
print("after tag_global.get_by_name")
for _, t in pairs(tags) do
if t:output() and t:output():name() == output:name() then
SendMsg({
ToggleTag = {
output_name = op.name,
tag_name = name,
tag_id = t:id(),
},
})
return
end
end
end
---Switch to a tag on the specified output, deactivating any other active tags on it.
---If `output` is not specified, this uses the currently focused output instead.
---Alternatively, provide a tag object instead of a name and output.
---
---This is used to replicate what a traditional workspace is on some other Wayland compositors.
---
---### Example
---
---### Examples
---```lua
---tag.switch_to("3") -- Switches to and displays *only* windows on tag 3
----- Switches to and displays *only* windows on tag `3` on the focused output.
---tag.switch_to("3")
---
---local
---```
---@param name string The name of the tag.
---@param output Output? The output.
function tag.switch_to(name, output)
if output ~= nil then
---@overload fun(t: Tag)
---@see Tag.switch_to — The corresponding object method
function tag_module.switch_to(name, output)
if type(name) == "table" then
SendMsg({
SwitchToTag = {
output_name = output.name,
tag_name = name,
tag_id = name--[[@as Tag]]:id(),
},
})
else
local op = require("output").get_focused()
if op ~= nil then
return
end
local output = output or require("output").get_focused()
if output == nil then
return
end
local tags = tag_module.get_by_name(name)
for _, t in pairs(tags) do
if t:output() and t:output():name() == output:name() then
SendMsg({
SwitchToTag = {
output_name = op.name,
tag_name = name,
tag_id = t:id(),
},
})
return
end
end
end
---Set a layout for the tag on the specified output. If there is none, set it for the tag on the currently focused one.
---Set a layout for the tag on the specified output. If no output is provided, set it for the tag on the currently focused one.
---Alternatively, provide a tag object instead of a name and output.
---
---### Examples
---```lua
----- Set tag `1` on `DP-1` to the `Dwindle` layout
---tag.set_layout("1", "Dwindle", output.get_by_name("DP-1"))
---
----- Do the same as above. Note: if you have more than one tag named `1` then this picks the first one.
---local t = tag.get_by_name("1")[1]
---tag.set_layout(t, "Dwindle")
---```
---@param name string The name of the tag.
---@param layout Layout The layout.
---@param output Output? The output.
function tag.set_layout(name, layout, output)
if output ~= nil then
---@overload fun(t: Tag, layout: Layout)
---@see Tag.set_layout — The corresponding object method
function tag_module.set_layout(name, layout, output)
if type(name) == "table" then
SendMsg({
SetLayout = {
output_name = output.name,
tag_name = name,
tag_id = name--[[@as Tag]]:id(),
layout = layout,
},
})
else
local op = require("output").get_focused()
if op ~= nil then
return
end
local output = output or require("output").get_focused()
if output == nil then
return
end
local tags = tag_module.get_by_name(name)
for _, t in pairs(tags) do
if t:output() and t:output():name() == output:name() then
SendMsg({
SetLayout = {
output_name = op.name,
tag_name = name,
tag_id = t:id(),
layout = layout,
},
})
return
end
end
end
---Get all tags on the specified output.
---
---You can also use `output_obj:tags()`, which delegates to this function:
---### Example
---```lua
---local tags_on_output = output.get_focused():tags()
----- This is the same as
----- local tags_on_output = tag.get_on_output(output.get_focused())
---local op = output.get_focused()
---if op ~= nil then
--- local tags = tag.get_on_output(op) -- All tags on the focused output
---end
---```
---@param output Output
---@return Tag[]
function tag.get_on_output(output)
SendRequest({
GetTagsByOutput = {
output_name = output.name,
---
---@see Output.tags — The corresponding object method
function tag_module.get_on_output(output)
local response = Request({
GetOutputProps = {
output_name = output:name(),
},
})
local response = ReadMsg()
local tag_ids = response.RequestResponse.response.OutputProps.tag_ids
---@type Tag[]
local tags = {}
if tag_ids == nil then
return tags
end
for _, tag_id in pairs(tag_ids) do
table.insert(tags, create_tag(tag_id))
end
return tags
end
---Get all tags with this name across all outputs.
---
---### Example
---```lua
----- Given one monitor with the tags "OBS", "OBS", "VSCode", and "Spotify"...
---local tags = tag.get_by_name("OBS")
----- ...will have 2 tags in `tags`, while...
---local no_tags = tag.get_by_name("Firefox")
----- ...will have `no_tags` be empty.
---```
---@param name string The name of the tag(s) you want.
---@return Tag[]
function tag_module.get_by_name(name)
local t_s = tag_module.get_all()
---@type Tag[]
local tags = {}
for _, t in pairs(t_s) do
if t:name() == name then
table.insert(tags, t)
end
end
return tags
end
---Get all tags across all outputs.
---
---### Example
---```lua
----- With two monitors with the same tags: "1", "2", "3", "4", and "5"...
---local tags = tag.get_all()
----- ...`tags` should have 10 tags, with 5 pairs of those names across both outputs.
---```
---@return Tag[]
function tag_module.get_all()
local response = Request("GetTags")
local tag_ids = response.RequestResponse.response.Tags.tag_ids
@ -231,10 +347,54 @@ function tag.get_on_output(output)
local tags = {}
for _, tag_id in pairs(tag_ids) do
table.insert(tags, new_tag({ id = tag_id }))
table.insert(tags, create_tag(tag_id))
end
return tags
end
return tag
---Get the specified tag's name.
---
---### Example
---```lua
----- Assuming the tag `Terminal` exists...
---print(tag.name(tag.get_by_name("Terminal")[1]))
----- ...should print `Terminal`.
---```
---@param t Tag
---@return string|nil
---@see Tag.name — The corresponding object method
function tag_module.name(t)
local response = Request({
GetTagProps = {
tag_id = t:id(),
},
})
local name = response.RequestResponse.response.TagProps.name
return name
end
---Get whether or not the specified tag is active.
---@param t Tag
---@return boolean|nil
---@see Tag.active — The corresponding object method
function tag_module.active(t)
local response = Request({
GetTagProps = {
tag_id = t:id(),
},
})
local active = response.RequestResponse.response.TagProps.active
return active
end
---Get the output the specified tag is on.
---@param t Tag
---@return Output|nil
---@see OutputModule.get_for_tag — The called function
---@see Tag.output — The corresponding object method
function tag_module.output(t)
return require("output").get_for_tag(t)
end
return tag_module

356
api/lua/test_config.lua Normal file
View file

@ -0,0 +1,356 @@
-- SPDX-License-Identifier: MIT
-- Just like in Awesome, if you want access to Luarocks packages, this needs to be called.
-- NOTE: The loader doesn't load from the local Luarocks directory (probably in ~/.luarocks),
-- | so if you have any rocks installed with --local,
-- | you may need to add those paths to package.path and package.cpath.
-- Alternatively, you can add
-- eval $(luarocks path --bin)
-- to your shell's startup script to permanently have access to Luarocks in all your Lua files.
pcall(require, "luarocks.loader")
-- Neovim users be like:
require("pinnacle").setup(function(pinnacle)
local input = pinnacle.input -- Key and mouse binds
local window = pinnacle.window -- Window management
local process = pinnacle.process -- Process spawning
local tag = pinnacle.tag -- Tag management
local output = pinnacle.output -- Output management
-- Every key supported by xkbcommon.
-- Support for just putting in a string of a key is intended.
local keys = input.keys
---@type Modifier
local mod_key = "Ctrl" -- This is set to `Ctrl` instead of `Super` to not conflict with your WM/DE keybinds
-- ^ Add type annotations for that sweet, sweet autocomplete
local terminal = "alacritty"
-- Keybinds ----------------------------------------------------------------------
input.keybind({ mod_key, "Alt" }, keys.q, pinnacle.quit)
input.keybind({ mod_key, "Alt" }, keys.c, function()
-- The commented out line may crash the config process if you have no windows open.
-- There is no nil warning here due to limitations in Lua LS type checking, so check for nil as shown below.
-- window.get_focused():close()
local win = window.get_focused()
if win ~= nil then
win:close()
end
end)
input.keybind({ mod_key, "Alt" }, keys.space, function()
local win = window.get_focused()
if win ~= nil then
win:toggle_floating()
end
end)
input.keybind({ mod_key }, keys.Return, function()
process.spawn(terminal, function(stdout, stderr, exit_code, exit_msg)
-- do something with the output here
end)
end)
input.keybind({ mod_key }, keys.l, function()
process.spawn("kitty")
end)
input.keybind({ mod_key }, keys.k, function()
process.spawn("foot")
end)
input.keybind({ mod_key }, keys.j, function()
process.spawn("nautilus")
end)
-- Just testing stuff
input.keybind({ mod_key }, keys.h, function()
local wins = window.get_all()
for _, win in pairs(wins) do
print("loc: " .. (win:loc() and win:loc().x or "nil") .. ", " .. (win:loc() and win:loc().y or "nil"))
print("size: " .. (win:size() and win:size().w or "nil") .. ", " .. (win:size() and win:size().h or "nil"))
print("class: " .. (win:class() or "nil"))
print("title: " .. (win:title() or "nil"))
print("float: " .. tostring(win:floating()))
end
print("----------------------")
local op = output.get_focused() --[[@as Output]]
print("res: " .. (op:res() and (op:res().w .. ", " .. op:res().h) or "nil"))
print("loc: " .. (op:loc() and (op:loc().x .. ", " .. op:loc().y) or "nil"))
print("rr: " .. (op:refresh_rate() or "nil"))
print("make: " .. (op:make() or "nil"))
print("model: " .. (op:model() or "nil"))
print("focused: " .. (tostring(op:focused())))
print("----------------------")
local wins = window.get_by_class("Alacritty")
for _, win in pairs(wins) do
print("loc: " .. (win:loc() and win:loc().x or "nil") .. ", " .. (win:loc() and win:loc().y or "nil"))
print("size: " .. (win:size() and win:size().w or "nil") .. ", " .. (win:size() and win:size().h or "nil"))
print("class: " .. (win:class() or "nil"))
print("title: " .. (win:title() or "nil"))
print("float: " .. tostring(win:floating()))
end
print("----------------------")
local wins = window.get_by_title("~/p/pinnacle")
for _, win in pairs(wins) do
print("loc: " .. (win:loc() and win:loc().x or "nil") .. ", " .. (win:loc() and win:loc().y or "nil"))
print("size: " .. (win:size() and win:size().w or "nil") .. ", " .. (win:size() and win:size().h or "nil"))
print("class: " .. (win:class() or "nil"))
print("title: " .. (win:title() or "nil"))
print("float: " .. tostring(win:floating()))
end
print("----------------------")
local tags = tag.get_on_output(output.get_focused() --[[@as Output]])
for _, tg in pairs(tags) do
print(tg:name())
print((tg:output() and tg:output():name()) or "nil output")
print(tg:active())
end
print("----------------------")
local tags = tag.get_by_name("2")
for _, tg in pairs(tags) do
print(tg:name())
print((tg:output() and tg:output():name()) or "nil output")
print(tg:active())
end
print("----------------------")
local tags = tag.get_all()
for _, tg in pairs(tags) do
print(tg:name())
print((tg:output() and tg:output():name()) or "nil output")
print(tg:active())
end
end)
-- Tags ---------------------------------------------------------------------------
output.connect_for_all(function(op)
op:add_tags("1", "2", "3", "4", "5")
-- Same as tag.add(op, "1", "2", "3", "4", "5")
-- local tags_table = { "Terminal", "Browser", "Code", "Email", "Potato" }
-- op:add_tags(tags_table)
for _, t in pairs(tag.get_by_name("1")) do
if t:output() and t:output():focused() then
t:toggle()
end
end
end)
---@type Layout[]
local layouts = {
"MasterStack",
"Dwindle",
"Spiral",
"CornerTopLeft",
"CornerTopRight",
"CornerBottomLeft",
"CornerBottomRight",
}
local indices = {}
-- Layout cycling
-- Yes, this is overly complicated and yes, I'll cook up a way to make it less so.
input.keybind({ mod_key }, keys.space, function()
local tags = output.get_focused():tags()
for _, tg in pairs(tags) do
if tg:active() then
local name = tg:name()
if name == nil then
return
end
tg:set_layout(layouts[indices[name] or 1])
if indices[name] == nil then
indices[name] = 2
else
if indices[name] + 1 > #layouts then
indices[name] = 1
else
indices[name] = indices[name] + 1
end
end
break
end
end
end)
input.keybind({ mod_key, "Shift" }, keys.space, function()
local tags = output.get_focused():tags()
for _, tg in pairs(tags) do
if tg:active() then
local name = tg:name()
if name == nil then
return
end
tg:set_layout(layouts[indices[name] or #layouts])
if indices[name] == nil then
indices[name] = #layouts - 1
else
if indices[name] - 1 < 1 then
indices[name] = #layouts
else
indices[name] = indices[name] - 1
end
end
break
end
end
end)
input.keybind({ mod_key }, keys.KEY_1, function()
for _, t in pairs(tag.get_by_name("1")) do
if t:output() and t:output():focused() then
t:switch_to()
end
end
end)
input.keybind({ mod_key }, keys.KEY_2, function()
for _, t in pairs(tag.get_by_name("2")) do
if t:output() and t:output():focused() then
t:switch_to()
end
end
end)
input.keybind({ mod_key }, keys.KEY_3, function()
for _, t in pairs(tag.get_by_name("3")) do
if t:output() and t:output():focused() then
t:switch_to()
end
end
end)
input.keybind({ mod_key }, keys.KEY_4, function()
for _, t in pairs(tag.get_by_name("4")) do
if t:output() and t:output():focused() then
t:switch_to()
end
end
end)
input.keybind({ mod_key }, keys.KEY_5, function()
for _, t in pairs(tag.get_by_name("5")) do
if t:output() and t:output():focused() then
t:switch_to()
end
end
end)
input.keybind({ mod_key, "Shift" }, keys.KEY_1, function()
for _, t in pairs(tag.get_by_name("1")) do
if t:output() and t:output():focused() then
t:toggle()
end
end
end)
input.keybind({ mod_key, "Shift" }, keys.KEY_2, function()
for _, t in pairs(tag.get_by_name("2")) do
if t:output() and t:output():focused() then
t:toggle()
end
end
end)
input.keybind({ mod_key, "Shift" }, keys.KEY_3, function()
for _, t in pairs(tag.get_by_name("3")) do
if t:output() and t:output():focused() then
t:toggle()
end
end
end)
input.keybind({ mod_key, "Shift" }, keys.KEY_4, function()
for _, t in pairs(tag.get_by_name("4")) do
if t:output() and t:output():focused() then
t:toggle()
end
end
end)
input.keybind({ mod_key, "Shift" }, keys.KEY_5, function()
for _, t in pairs(tag.get_by_name("5")) do
if t:output() and t:output():focused() then
t:toggle()
end
end
end)
input.keybind({ mod_key, "Alt" }, keys.KEY_1, function()
for _, t in pairs(tag.get_by_name("1")) do
if t:output() and t:output():focused() then
window.get_focused():move_to_tag(t)
end
end
end)
input.keybind({ mod_key, "Alt" }, keys.KEY_2, function()
for _, t in pairs(tag.get_by_name("2")) do
if t:output() and t:output():focused() then
window.get_focused():move_to_tag(t)
end
end
end)
input.keybind({ mod_key, "Alt" }, keys.KEY_3, function()
for _, t in pairs(tag.get_by_name("3")) do
if t:output() and t:output():focused() then
window.get_focused():move_to_tag(t)
end
end
end)
input.keybind({ mod_key, "Alt" }, keys.KEY_4, function()
for _, t in pairs(tag.get_by_name("4")) do
if t:output() and t:output():focused() then
window.get_focused():move_to_tag(t)
end
end
end)
input.keybind({ mod_key, "Alt" }, keys.KEY_5, function()
for _, t in pairs(tag.get_by_name("5")) do
if t:output() and t:output():focused() then
window.get_focused():move_to_tag(t)
end
end
end)
input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_1, function()
for _, t in pairs(tag.get_by_name("1")) do
if t:output() and t:output():focused() then
window.get_focused():toggle_tag(t)
end
end
end)
input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_2, function()
for _, t in pairs(tag.get_by_name("2")) do
if t:output() and t:output():focused() then
window.get_focused():toggle_tag(t)
end
end
end)
input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_3, function()
for _, t in pairs(tag.get_by_name("3")) do
if t:output() and t:output():focused() then
window.get_focused():toggle_tag(t)
end
end
end)
input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_4, function()
for _, t in pairs(tag.get_by_name("4")) do
if t:output() and t:output():focused() then
window.get_focused():toggle_tag(t)
end
end
end)
input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_5, function()
for _, t in pairs(tag.get_by_name("5")) do
if t:output() and t:output():focused() then
window.get_focused():toggle_tag(t)
end
end
end)
end)

View file

@ -4,180 +4,541 @@
--
-- SPDX-License-Identifier: MPL-2.0
---@class Window
---@field private id integer The internal id of this window
---@field private app_id string? The equivalent of an X11 window's class
---@field private title string? The window's title
---@field private size { w: integer, h: integer } The size of the window
---@field private location { x: integer, y: integer } The location of the window
---@field private floating boolean Whether the window is floating or not (tiled)
local win = {}
---@class WindowModule
local window_module = {}
---@param props Window
---@class Window
---@field private _id integer The internal id of this window
local window = {}
---@param window_id WindowId
---@return Window
local function new_window(props)
local function create_window(window_id)
---@type Window
local w = { _id = window_id }
-- Copy functions over
for k, v in pairs(win) do
props[k] = v
for k, v in pairs(window) do
w[k] = v
end
return props
return w
end
---Set a window's size.
---Get this window's unique id.
---
---***You will probably not need to use this.***
---@return WindowId
function window:id()
return self._id
end
---Set this window's size.
---
---### Examples
---```lua
---window.get_focused():set_size({ w = 500, h = 500 }) -- make the window square and 500 pixels wide/tall
---window.get_focused():set_size({ h = 300 }) -- keep the window's width but make it 300 pixels tall
---window.get_focused():set_size({}) -- do absolutely nothing useful
---```
---@param size { w: integer?, h: integer? }
function win:set_size(size)
self.size = {
w = size.w or self.size.w,
h = size.h or self.size.h,
}
SendMsg({
SetWindowSize = {
window_id = self.id,
size = { self.size.w, self.size.h },
},
})
---@see WindowModule.set_size — The corresponding module function
function window:set_size(size)
window_module.set_size(self, size)
end
---Move a window to a tag, removing all other ones.
---@param name string The name of the tag.
function win:move_to_tag(name)
SendMsg({
MoveWindowToTag = {
window_id = self.id,
tag_id = name,
},
})
---Move this window to a tag, removing all other ones.
---
---### Example
---```lua
----- With the focused window on tags 1, 2, 3, and 4...
---window.get_focused():move_to_tag("5")
----- ...will make the window only appear on tag 5.
---```
---@param name string
---@param output Output?
---@overload fun(self: self, t: Tag)
---@see WindowModule.move_to_tag — The corresponding module function
function window:move_to_tag(name, output)
window_module.move_to_tag(self, name, output)
end
---Toggle the specified tag for this window.
---@param name string The name of the tag.
function win:toggle_tag(name)
SendMsg({
ToggleTagOnWindow = {
window_id = self.id,
tag_id = name,
},
})
---
---Note: toggling off all tags currently makes a window not response to layouting.
---
---### Example
---```lua
----- With the focused window only on tag 1...
---window.get_focused():toggle_tag("2")
----- ...will also make the window appear on tag 2.
---```
---@param name string
---@param output Output?
---@overload fun(self: self, t: Tag)
---@see WindowModule.toggle_tag — The corresponding module function
function window:toggle_tag(name, output)
window_module.toggle_tag(self, name, output)
end
---Close this window.
function win:close()
SendMsg({
CloseWindow = {
window_id = self.id,
},
})
---
---This only sends a close *event* to the window and is the same as just clicking the X button in the titlebar.
---This will trigger save prompts in applications like GIMP.
---
---### Example
---```lua
---window.get_focused():close() -- close the currently focused window
---```
---@see WindowModule.close — The corresponding module function
function window:close()
window_module.close(self)
end
---Toggle this window's floating status.
function win:toggle_floating()
SendMsg({
ToggleFloating = {
window_id = self.id,
},
})
---
---### Example
---```lua
---window.get_focused():toggle_floating() -- toggles the focused window between tiled and floating
---```
---@see WindowModule.toggle_floating — The corresponding module function
function window:toggle_floating()
window_module.toggle_floating(self)
end
---Get a window's size.
---@return { w: integer, h: integer }
function win:get_size()
return self.size
---Get this window's size.
---
---### Example
---```lua
----- With a 4K monitor, given a focused fullscreen window...
---local size = window.get_focused():size()
----- ...should have size equal to `{ w = 3840, h = 2160 }`.
---```
---@return { w: integer, h: integer }|nil size The size of the window, or nil if it doesn't exist.
---@see WindowModule.size — The corresponding module function
function window:size()
return window_module.size(self)
end
---Get this window's location in the global space.
---
---Think of your monitors as being laid out on a big sheet.
---The top left of the sheet if you trim it down is (0, 0).
---The location of this window is relative to that point.
---
---### Example
---```lua
----- With two 1080p monitors side by side and set up as such,
----- if a window is fullscreen on the right one...
---local loc = that_window:loc()
----- ...should have loc equal to `{ x = 1920, y = 0 }`.
---```
---@return { x: integer, y: integer }|nil loc The location of the window, or nil if it's not on-screen or alive.
---@see WindowModule.loc — The corresponding module function
function window:loc()
return window_module.loc(self)
end
---Get this window's class. This is usually the name of the application.
---
---### Example
---```lua
----- With Alacritty focused...
---print(window.get_focused():class())
----- ...should print "Alacritty".
---```
---@return string|nil class This window's class, or nil if it doesn't exist.
---@see WindowModule.class — The corresponding module function
function window:class()
return window_module.class(self)
end
---Get this window's title.
---
---### Example
---```lua
----- With Alacritty focused...
---print(window.get_focused():title())
----- ...should print the directory Alacritty is in or what it's running (what's in its title bar).
---```
---@return string|nil title This window's title, or nil if it doesn't exist.
---@see WindowModule.title — The corresponding module function
function window:title()
return window_module.title(self)
end
---Get this window's floating status.
---
---### Example
---```lua
----- With the focused window floating...
---print(window.get_focused():floating())
----- ...should print `true`.
---```
---@return boolean|nil floating `true` if it's floating, `false` if it's tiled, or nil if it doesn't exist.
---@see WindowModule.floating — The corresponding module function
function window:floating()
return window_module.floating(self)
end
---Get whether or not this window is focused.
---
---### Example
---```lua
---print(window.get_focused():focused()) -- should print `true`.
---```
---@return boolean|nil floating `true` if it's floating, `false` if it's tiled, or nil if it doesn't exist.
---@see WindowModule.focused — The corresponding module function
function window:focused()
return window_module.focused(self)
end
-------------------------------------------------------------------
local window = {}
---Get all windows with the specified class (usually the name of the application).
---@param class string The class. For example, Alacritty's class is "Alacritty".
---@return Window[]
function window_module.get_by_class(class)
local windows = window_module.get_all()
---TODO: This function is not implemented yet.
---
---Get a window by its app id (aka its X11 class).
---@param app_id string The window's app id. For example, Alacritty's app id is "Alacritty".
---@return Window|nil
function window.get_by_app_id(app_id)
SendRequest({
GetWindowByAppId = {
app_id = app_id,
},
})
local response = ReadMsg()
local window_id = response.RequestResponse.response.Window.window_id
if window_id == nil then
return nil
---@type Window[]
local windows_ret = {}
for _, w in pairs(windows) do
if w:class() == class then
table.insert(windows_ret, w)
end
end
---@type Window
local wind = {
id = window_id,
}
return new_window(wind)
return windows_ret
end
---TODO: This function is not implemented yet.
---
---Get a window by its title.
---@param title string The window's title.
---@return Window|nil
function window.get_by_title(title)
SendRequest({
GetWindowByTitle = {
title = title,
},
})
---Get all windows with the specified title.
---@param title string The title.
---@return Window[]
function window_module.get_by_title(title)
local windows = window_module.get_all()
local response = ReadMsg()
local window_id = response.RequestResponse.response.Window.window_id
if window_id == nil then
return nil
---@type Window[]
local windows_ret = {}
for _, w in pairs(windows) do
if w:title() == title then
table.insert(windows_ret, w)
end
end
---@type Window
local wind = {
id = window_id,
}
return new_window(wind)
return windows_ret
end
---Get the currently focused window.
---@return Window|nil
function window.get_focused()
SendRequest("GetWindowByFocus")
function window_module.get_focused()
local windows = window_module.get_all()
local response = ReadMsg()
local window_id = response.RequestResponse.response.Window.window_id
if window_id == nil then
return nil
for _, w in pairs(windows) do
if w:focused() then
return w
end
end
---@type Window
local wind = {
id = window_id,
}
return new_window(wind)
return nil
end
---Get all windows.
---@return Window[]
function window.get_all()
SendRequest("GetAllWindows")
local window_ids = ReadMsg().RequestResponse.response.Windows.window_ids
function window_module.get_all()
local window_ids = Request("GetWindows").RequestResponse.response.Windows.window_ids
---@type Window[]
local windows = {}
for i, window_id in ipairs(window_ids) do
windows[i] = new_window({ id = window_id })
for _, window_id in pairs(window_ids) do
table.insert(windows, create_window(window_id))
end
return windows
end
return window
---Toggle the tag with the given name and (optional) output for the specified window.
---You can also provide a tag object instead of a name and output.
---@param w Window
---@param name string
---@param output Output?
---@overload fun(w: Window, t: Tag)
---@see Window.toggle_tag — The corresponding object method
function window_module.toggle_tag(w, name, output)
if type(name) == "table" then
SendMsg({
ToggleTagOnWindow = {
window_id = w:id(),
tag_id = name--[[@as Tag]]:id(),
},
})
return
end
local output = output or require("output").get_focused()
if output == nil then
return
end
local tags = require("tag").get_by_name(name)
for _, t in pairs(tags) do
if t:output() and t:output():name() == output:name() then
SendMsg({
ToggleTagOnWindow = {
window_id = w:id(),
tag_id = t:id(),
},
})
return
end
end
end
---Move the specified window to the tag with the given name and (optional) output.
---You can also provide a tag object instead of a name and output.
---@param w Window
---@param name string
---@param output Output?
---@overload fun(w: Window, t: Tag)
---@see Window.move_to_tag — The corresponding object method
function window_module.move_to_tag(w, name, output)
if type(name) == "table" then
SendMsg({
MoveWindowToTag = {
window_id = w:id(),
tag_id = name--[[@as Tag]]:id(),
},
})
return
end
local output = output or require("output").get_focused()
if output == nil then
return
end
local tags = require("tag").get_by_name(name)
for _, t in pairs(tags) do
if t:output() and t:output():name() == output:name() then
SendMsg({
MoveWindowToTag = {
window_id = w:id(),
tag_id = t:id(),
},
})
return
end
end
end
---Set the specified window's size.
---
---### Examples
---```lua
---local win = window.get_focused()
---if win ~= nil then
--- window.set_size(win, { w = 500, h = 500 }) -- make the window square and 500 pixels wide/tall
--- window.set_size(win, { h = 300 }) -- keep the window's width but make it 300 pixels tall
--- window.set_size(win, {}) -- do absolutely nothing useful
---end
---```
---@param win Window
---@param size { w: integer?, h: integer? }
---@see Window.set_size — The corresponding object method
function window_module.set_size(win, size)
SendMsg({
SetWindowSize = {
window_id = win:id(),
width = size.w,
height = size.h,
},
})
end
---Close the specified window.
---
---This only sends a close *event* to the window and is the same as just clicking the X button in the titlebar.
---This will trigger save prompts in applications like GIMP.
---
---### Example
---```lua
---local win = window.get_focused()
---if win ~= nil then
--- window.close(win) -- close the currently focused window
---end
---```
---@param win Window
---@see Window.close — The corresponding object method
function window_module.close(win)
SendMsg({
CloseWindow = {
window_id = win:id(),
},
})
end
---Toggle the specified window between tiled and floating.
---@param win Window
---@see Window.toggle_floating — The corresponding object method
function window_module.toggle_floating(win)
SendMsg({
ToggleFloating = {
window_id = win:id(),
},
})
end
---Get the specified window's size.
---
---### Example
---```lua
----- With a 4K monitor, given a focused fullscreen window `win`...
---local size = window.size(win)
----- ...should have size equal to `{ w = 3840, h = 2160 }`.
---```
---@param win Window
---@return { w: integer, h: integer }|nil size The size of the window, or nil if it doesn't exist.
---@see Window.size — The corresponding object method
function window_module.size(win)
local response = Request({
GetWindowProps = {
window_id = win:id(),
},
})
local size = response.RequestResponse.response.WindowProps.size
if size == nil then
return nil
else
return {
w = size[1],
h = size[2],
}
end
end
---Get the specified window's location in the global space.
---
---Think of your monitors as being laid out on a big sheet.
---The top left of the sheet if you trim it down is (0, 0).
---The location of this window is relative to that point.
---
---### Example
---```lua
----- With two 1080p monitors side by side and set up as such,
----- if a window `win` is fullscreen on the right one...
---local loc = window.loc(win)
----- ...should have loc equal to `{ x = 1920, y = 0 }`.
---```
---@param win Window
---@return { x: integer, y: integer }|nil loc The location of the window, or nil if it's not on-screen or alive.
---@see Window.loc — The corresponding object method
function window_module.loc(win)
local response = Request({
GetWindowProps = {
window_id = win:id(),
},
})
local loc = response.RequestResponse.response.WindowProps.loc
if loc == nil then
return nil
else
return {
x = loc[1],
y = loc[2],
}
end
end
---Get the specified window's class. This is usually the name of the application.
---
---### Example
---```lua
----- With Alacritty focused...
---local win = window.get_focused()
---if win ~= nil then
--- print(window.class(win))
---end
----- ...should print "Alacritty".
---```
---@param win Window
---@return string|nil class This window's class, or nil if it doesn't exist.
---@see Window.class — The corresponding object method
function window_module.class(win)
local response = Request({
GetWindowProps = {
window_id = win:id(),
},
})
local class = response.RequestResponse.response.WindowProps.class
return class
end
---Get the specified window's title.
---
---### Example
---```lua
----- With Alacritty focused...
---local win = window.get_focused()
---if win ~= nil then
--- print(window.title(win))
---end
----- ...should print the directory Alacritty is in or what it's running (what's in its title bar).
---```
---@param win Window
---@return string|nil title This window's title, or nil if it doesn't exist.
---@see Window.title — The corresponding object method
function window_module.title(win)
local response = Request({
GetWindowProps = {
window_id = win:id(),
},
})
local title = response.RequestResponse.response.WindowProps.title
return title
end
---Get this window's floating status.
---
---### Example
---```lua
----- With the focused window floating...
---local win = window.get_focused()
---if win ~= nil then
--- print(window.floating(win))
---end
----- ...should print `true`.
---```
---@param win Window
---@return boolean|nil floating `true` if it's floating, `false` if it's tiled, or nil if it doesn't exist.
---@see Window.floating — The corresponding object method
function window_module.floating(win)
local response = Request({
GetWindowProps = {
window_id = win:id(),
},
})
local floating = response.RequestResponse.response.WindowProps.floating
return floating
end
---Get whether or not this window is focused.
---
---### Example
---```lua
---local win = window.get_focused()
---if win ~= nil then
--- print(window.focused(win)) -- Should print `true`
---end
---```
---@param win Window
---@return boolean|nil floating `true` if it's floating, `false` if it's tiled, or nil if it doesn't exist.
---@see Window.focused — The corresponding object method
function window_module.focused(win)
local response = Request({
GetWindowProps = {
window_id = win:id(),
},
})
local focused = response.RequestResponse.response.WindowProps.focused
return focused
end
return window_module

View file

@ -33,25 +33,26 @@ pub enum Msg {
},
SetWindowSize {
window_id: WindowId,
size: (i32, i32),
#[serde(default)]
width: Option<i32>,
#[serde(default)]
height: Option<i32>,
},
MoveWindowToTag {
window_id: WindowId,
tag_id: String,
tag_id: TagId,
},
ToggleTagOnWindow {
window_id: WindowId,
tag_id: String,
tag_id: TagId,
},
// Tag management
ToggleTag {
output_name: String,
tag_name: String,
tag_id: TagId,
},
SwitchToTag {
output_name: String,
tag_name: String,
tag_id: TagId,
},
AddTags {
/// The name of the output you want these tags on.
@ -60,12 +61,10 @@ pub enum Msg {
},
RemoveTags {
/// The name of the output you want these tags removed from.
output_name: String,
tag_names: Vec<String>,
tag_ids: Vec<TagId>,
},
SetLayout {
output_name: String,
tag_name: String,
tag_id: TagId,
layout: Layout,
},
@ -86,27 +85,28 @@ pub enum Msg {
/// Quit the compositor.
Quit,
Request(Request),
Request {
request_id: RequestId,
request: Request,
},
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct RequestId(pub u32);
pub struct RequestId(u32);
#[allow(clippy::enum_variant_names)]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
/// Messages that require a server response, usually to provide some data.
pub enum Request {
GetWindowByAppId { app_id: String },
GetWindowByTitle { title: String },
GetWindowByFocus,
GetAllWindows,
GetOutputByName { output_name: String },
GetOutputsByModel { model: String },
GetOutputsByRes { res: (u32, u32) },
GetOutputByFocus,
GetTagsByOutput { output_name: String },
GetTagActive { tag_id: TagId },
GetTagName { tag_id: TagId },
// Windows
GetWindows,
GetWindowProps { window_id: WindowId },
// Outputs
GetOutputs,
GetOutputProps { output_name: String },
// Tags
GetTags,
GetTagProps { tag_id: TagId },
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)]
@ -161,6 +161,7 @@ pub enum OutgoingMsg {
args: Option<Args>,
},
RequestResponse {
request_id: RequestId,
response: RequestResponse,
},
}
@ -185,10 +186,49 @@ pub enum Args {
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum RequestResponse {
Window { window_id: Option<WindowId> },
Windows { window_ids: Vec<WindowId> },
Outputs { output_names: Vec<String> },
Tags { tag_ids: Vec<TagId> },
TagActive { active: bool },
TagName { name: String },
Window {
window_id: Option<WindowId>,
},
Windows {
window_ids: Vec<WindowId>,
},
WindowProps {
size: Option<(i32, i32)>,
loc: Option<(i32, i32)>,
class: Option<String>,
title: Option<String>,
floating: Option<bool>,
focused: Option<bool>,
},
Output {
output_name: Option<String>,
},
Outputs {
output_names: Vec<String>,
},
OutputProps {
/// The make of the output.
make: Option<String>,
/// The model of the output.
model: Option<String>,
/// The location of the output in the space.
loc: Option<(i32, i32)>,
/// The resolution of the output.
res: Option<(i32, i32)>,
/// The refresh rate of the output.
refresh_rate: Option<i32>,
/// The size of the output, in millimeters.
physical_size: Option<(i32, i32)>,
/// Whether the output is focused or not.
focused: Option<bool>,
tag_ids: Option<Vec<TagId>>,
},
Tags {
tag_ids: Vec<TagId>,
},
TagProps {
active: Option<bool>,
name: Option<String>,
output_name: Option<String>,
},
}

View file

@ -159,7 +159,8 @@ impl Layout {
state.size.expect("size should have been set")
});
let win1_loc = win1.with_state(|state| {
let WindowResizeState::Requested(_, loc) = state.resize_state else { unreachable!() };
let WindowResizeState::Requested(_, loc) =
state.resize_state else { unreachable!() };
loc
});
@ -271,7 +272,8 @@ impl Layout {
state.size.expect("size should have been set")
});
let win1_loc = win1.with_state(|state| {
let WindowResizeState::Requested(_, loc) = state.resize_state else { unreachable!() };
let WindowResizeState::Requested(_, loc) =
state.resize_state else { unreachable!() };
loc
});

View file

@ -16,7 +16,7 @@ use std::{
use crate::{
api::{
msg::{Args, CallbackId, Msg, OutgoingMsg, Request, RequestResponse},
msg::{Args, CallbackId, Msg, OutgoingMsg, Request, RequestId, RequestResponse},
PinnacleSocketSource,
},
focus::FocusState,
@ -55,7 +55,7 @@ use smithay::{
dmabuf::DmabufFeedback,
fractional_scale::FractionalScaleManagerState,
output::OutputManagerState,
shell::xdg::XdgShellState,
shell::xdg::{XdgShellState, XdgToplevelSurfaceData},
shm::ShmState,
socket::ListeningSocketSource,
viewporter::ViewporterState,
@ -119,20 +119,12 @@ impl<B: Backend> State<B> {
}
Msg::SetMousebind { button: _ } => todo!(),
Msg::CloseWindow { window_id } => {
if let Some(window) = self
.windows
.iter()
.find(|win| win.with_state(|state| state.id == window_id))
{
if let Some(window) = window_id.window(self) {
window.toplevel().send_close();
}
}
Msg::ToggleFloating { window_id } => {
if let Some(window) = self
.windows
.iter()
.find(|win| win.with_state(|state| state.id == window_id)).cloned()
{
if let Some(window) = window_id.window(self) {
crate::window::toggle_floating(self, &window);
}
}
@ -144,150 +136,101 @@ impl<B: Backend> State<B> {
self.handle_spawn(command, callback_id);
}
Msg::SetWindowSize { window_id, size } => {
let Some(window) = self.space.elements().find(|&win| {
win.with_state( |state| state.id == window_id)
}) else { return; };
Msg::SetWindowSize {
window_id,
width,
height,
} => {
let Some(window) = window_id.window(self) else { return };
// TODO: tiled vs floating
let window_size = window.geometry().size;
window.toplevel().with_pending_state(|state| {
state.size = Some(size.into());
// INFO: calling window.geometry() in with_pending_state
// | will hang the compositor
state.size = Some(
(
width.unwrap_or(window_size.w),
height.unwrap_or(window_size.h),
)
.into(),
);
});
window.toplevel().send_pending_configure();
}
Msg::MoveWindowToTag { window_id, tag_id } => {
if let Some(window) = self
.windows
.iter()
.find(|&win| win.with_state(|state| state.id == window_id))
{
window.with_state(|state| {
self.focus_state
.focused_output
.as_ref()
.unwrap()
.with_state(|op_state| {
let tag = op_state.tags.iter().find(|tag| tag.name() == tag_id);
if let Some(tag) = tag {
state.tags = vec![tag.clone()];
}
});
});
}
let output = self.focus_state.focused_output.clone().unwrap();
let Some(window) = window_id.window(self) else { return };
let Some(tag) = tag_id.tag(self) else { return };
window.with_state(|state| {
state.tags = vec![tag.clone()];
});
let Some(output) = tag.output(self) else { return };
self.re_layout(&output);
}
Msg::ToggleTagOnWindow { window_id, tag_id } => {
if let Some(window) = self
.windows
.iter()
.find(|&win| win.with_state(|state| state.id == window_id))
{
window.with_state(|state| {
self.focus_state
.focused_output
.as_ref()
.unwrap()
.with_state(|op_state| {
let tag = op_state.tags.iter().find(|tag| tag.name() == tag_id);
if let Some(tag) = tag {
if state.tags.contains(tag) {
state.tags.retain(|tg| tg != tag);
} else {
state.tags.push(tag.clone());
}
}
});
});
let Some(window) = window_id.window(self) else { return };
let Some(tag) = tag_id.tag(self) else { return };
let output = self.focus_state.focused_output.clone().unwrap();
self.re_layout(&output);
}
window.with_state(|state| {
if state.tags.contains(&tag) {
state.tags.retain(|tg| tg != &tag);
} else {
state.tags.push(tag.clone());
}
});
let Some(output) = tag.output(self) else { return };
self.re_layout(&output);
}
Msg::ToggleTag { output_name, tag_name } => {
Msg::ToggleTag { tag_id } => {
tracing::debug!("ToggleTag");
let output = self.space.outputs().find(|op| op.name() == output_name).cloned();
if let Some(output) = output {
output.with_state(|state| {
if let Some(tag) = state.tags.iter_mut().find(|tag| tag.name() == tag_name) {
tracing::debug!("Setting tag {tag:?} to {}", !tag.active());
tag.set_active(!tag.active());
}
});
self.re_layout(&output);
if let Some(tag) = tag_id.tag(self) {
tag.set_active(!tag.active());
if let Some(output) = tag.output(self) {
self.re_layout(&output);
}
}
}
Msg::SwitchToTag { output_name, tag_name } => {
let output = self.space.outputs().find(|op| op.name() == output_name).cloned();
if let Some(output) = output {
output.with_state(|state| {
if !state.tags.iter().any(|tag| tag.name() == tag_name) {
// TODO: notify error
return;
}
for tag in state.tags.iter_mut() {
tag.set_active(false);
}
let Some(tag) = state.tags.iter_mut().find(|tag| tag.name() == tag_name) else {
unreachable!()
};
tag.set_active(true);
tracing::debug!(
"focused tags: {:?}",
state
.tags
.iter()
.filter(|tag| tag.active())
.map(|tag| tag.name())
.collect::<Vec<_>>()
);
});
self.re_layout(&output);
}
Msg::SwitchToTag { tag_id } => {
let Some(tag) = tag_id.tag(self) else { return };
let Some(output) = tag.output(self) else { return };
output.with_state(|state| {
for op_tag in state.tags.iter_mut() {
op_tag.set_active(false);
}
tag.set_active(true);
});
self.re_layout(&output);
}
// TODO: add output
Msg::AddTags { output_name, tag_names } => {
Msg::AddTags {
output_name,
tag_names,
} => {
if let Some(output) = self
.space
.outputs()
.find(|output| output.name() == output_name)
{
output.with_state(|state| {
state
.tags
.extend(tag_names.iter().cloned().map(Tag::new));
state.tags.extend(tag_names.iter().cloned().map(Tag::new));
tracing::debug!("tags added, are now {:?}", state.tags);
});
}
}
Msg::RemoveTags { output_name, tag_names } => {
if let Some(output) = self
.space
.outputs()
.find(|output| output.name() == output_name)
{
Msg::RemoveTags { tag_ids } => {
let tags = tag_ids.into_iter().filter_map(|tag_id| tag_id.tag(self));
for tag in tags {
let Some(output) = tag.output(self) else { continue };
output.with_state(|state| {
state.tags.retain(|tag| !tag_names.contains(&tag.name()));
state.tags.retain(|tg| tg != &tag);
});
}
}
Msg::SetLayout { output_name, tag_name, layout } => {
let output = self.space.outputs().find(|op| op.name() == output_name).cloned();
if let Some(output) = output {
output.with_state(|state| {
if let Some(tag) = state.tags.iter_mut().find(|tag| tag.name() == tag_name) {
tag.set_layout(layout);
}
});
self.re_layout(&output);
}
Msg::SetLayout { tag_id, layout } => {
let Some(tag) = tag_id.tag(self) else { return };
tag.set_layout(layout);
let Some(output) = tag.output(self) else { return };
self.re_layout(&output);
}
Msg::ConnectForAllOutputs { callback_id } => {
@ -316,195 +259,191 @@ impl<B: Backend> State<B> {
self.loop_signal.stop();
}
Msg::Request(request) => {
let stream = self
.api_state
.stream
.as_ref()
.expect("Stream doesn't exist");
let mut stream = stream.lock().expect("Couldn't lock stream");
match request {
Request::GetWindowByAppId { app_id: _ } => todo!(),
Request::GetWindowByTitle { title: _ } => todo!(),
Request::GetWindowByFocus => {
match self.focus_state.current_focus() {
Some(current_focus) => {
let window_id =
current_focus.with_state(|state| state.id);
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
response: RequestResponse::Window { window_id: Some(window_id) },
},
)
.expect("Send to client failed");
},
None => {
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
response: RequestResponse::Window { window_id: None },
},
)
.expect("Send to client failed");
},
}
}
Request::GetAllWindows => {
let window_ids = self
.windows
.iter()
.map(|win| {
win.with_state(|state| state.id)
})
.collect::<Vec<_>>();
Msg::Request {
request_id,
request,
} => {
self.handle_request(request_id, request);
}
}
}
// FIXME: figure out what to do if error
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
response: RequestResponse::Windows {
window_ids,
},
},
)
.expect("Couldn't send to client");
}
Request::GetOutputByName { output_name } => {
// TODO: name better
let names = self
.space
.outputs()
.find(|output| output.name() == output_name)
.map(|output| output.name());
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
response: RequestResponse::Outputs {
output_names: if let Some(name) = names {
vec![name]
} else {
vec![]
}
},
},
)
.unwrap();
}
Request::GetOutputsByModel { model } => {
let names = self
.space
.outputs()
.filter(|output| output.physical_properties().model == model)
.map(|output| output.name())
.collect::<Vec<_>>();
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
response: RequestResponse::Outputs { output_names: names },
},
)
.unwrap();
}
Request::GetOutputsByRes { res } => {
let names = self
.space
.outputs()
.filter_map(|output| {
if let Some(mode) = output.current_mode() {
if mode.size == (res.0 as i32, res.1 as i32).into() {
Some(output.name())
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
response: RequestResponse::Outputs { output_names: names },
},
)
.unwrap();
}
Request::GetOutputByFocus => {
let names = self
.focus_state
.focused_output
.as_ref()
.map(|output| output.name())
.into_iter()
.collect::<Vec<_>>();
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
response: RequestResponse::Outputs { output_names: names },
},
)
.unwrap();
}
Request::GetTagsByOutput { output_name } => {
let output = self
.space
.outputs()
.find(|op| op.name() == output_name);
if let Some(output) = output {
let tag_ids = output.with_state(|state| {
state.tags
.iter()
.map(|tag| tag.id())
.collect::<Vec<_>>()
});
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
response: RequestResponse::Tags { tag_ids }
}).unwrap();
}
}
Request::GetTagActive { tag_id } => {
let tag = self
.space
.outputs()
.flat_map(|op| {
op.with_state(|state| state.tags.clone())
})
.find(|tag| tag.id() == tag_id);
if let Some(tag) = tag {
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
response: RequestResponse::TagActive {
active: tag.active()
}
})
.unwrap();
}
}
Request::GetTagName { tag_id } => {
let tag = self
.space
.outputs()
.flat_map(|op| {
op.with_state(|state| state.tags.clone())
})
.find(|tag| tag.id() == tag_id);
if let Some(tag) = tag {
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
response: RequestResponse::TagName {
name: tag.name()
}
})
.unwrap();
}
}
}
},
fn handle_request(&mut self, request_id: RequestId, request: Request) {
let stream = self
.api_state
.stream
.as_ref()
.expect("Stream doesn't exist");
let mut stream = stream.lock().expect("Couldn't lock stream");
match request {
Request::GetWindows => {
let window_ids = self
.windows
.iter()
.map(|win| win.with_state(|state| state.id))
.collect::<Vec<_>>();
// FIXME: figure out what to do if error
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::Windows { window_ids },
},
)
.expect("Couldn't send to client");
}
Request::GetWindowProps { window_id } => {
let window = window_id.window(self);
let size = window
.as_ref()
.map(|win| (win.geometry().size.w, win.geometry().size.h));
let loc = window
.as_ref()
.and_then(|win| self.space.element_location(win))
.map(|loc| (loc.x, loc.y));
let (class, title) = window.as_ref().map_or((None, None), |win| {
compositor::with_states(win.toplevel().wl_surface(), |states| {
let lock = states
.data_map
.get::<XdgToplevelSurfaceData>()
.expect("XdgToplevelSurfaceData wasn't in surface's data map")
.lock()
.expect("failed to acquire lock");
(lock.app_id.clone(), lock.title.clone())
})
});
let floating = window
.as_ref()
.map(|win| win.with_state(|state| state.floating.is_floating()));
let focused = window.as_ref().and_then(|win| {
self.focus_state
.current_focus() // TODO: actual focus
.map(|foc_win| win == &foc_win)
});
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::WindowProps {
size,
loc,
class,
title,
floating,
focused,
},
},
)
.expect("failed to send to client");
}
Request::GetOutputs => {
let output_names = self
.space
.outputs()
.map(|output| output.name())
.collect::<Vec<_>>();
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::Outputs { output_names },
},
)
.expect("failed to send to client");
}
Request::GetOutputProps { output_name } => {
let output = self
.space
.outputs()
.find(|output| output.name() == output_name);
let res = output.as_ref().and_then(|output| {
output.current_mode().map(|mode| (mode.size.w, mode.size.h))
});
let refresh_rate = output
.as_ref()
.and_then(|output| output.current_mode().map(|mode| mode.refresh));
let model = output
.as_ref()
.map(|output| output.physical_properties().model);
let physical_size = output.as_ref().map(|output| {
(
output.physical_properties().size.w,
output.physical_properties().size.h,
)
});
let make = output
.as_ref()
.map(|output| output.physical_properties().make);
let loc = output
.as_ref()
.map(|output| (output.current_location().x, output.current_location().y));
let focused = self
.focus_state
.focused_output
.as_ref()
.and_then(|foc_op| output.map(|op| op == foc_op));
let tag_ids = output.as_ref().map(|output| {
output.with_state(|state| {
state.tags.iter().map(|tag| tag.id()).collect::<Vec<_>>()
})
});
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::OutputProps {
make,
model,
loc,
res,
refresh_rate,
physical_size,
focused,
tag_ids,
},
},
)
.expect("failed to send to client");
}
Request::GetTags => {
let tag_ids = self
.space
.outputs()
.flat_map(|op| op.with_state(|state| state.tags.clone()))
.map(|tag| tag.id())
.collect::<Vec<_>>();
tracing::debug!("GetTags: {:?}", tag_ids);
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::Tags { tag_ids },
},
)
.expect("failed to send to client");
}
Request::GetTagProps { tag_id } => {
let tag = tag_id.tag(self);
let output_name = tag
.as_ref()
.and_then(|tag| tag.output(self))
.map(|output| output.name());
let active = tag.as_ref().map(|tag| tag.active());
let name = tag.as_ref().map(|tag| tag.name());
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id,
response: RequestResponse::TagProps {
active,
name,
output_name,
},
},
)
.expect("failed to send to client");
}
}
}
@ -657,9 +596,19 @@ impl<B: Backend> State<B> {
}
pub fn re_layout(&mut self, output: &Output) {
let windows = self.windows.iter().filter(|win| {
win.with_state(|state| state.tags.iter().any(|tag| self.output_for_tag(tag).is_some_and(|op| &op == output)))
}).cloned().collect::<Vec<_>>();
let windows = self
.windows
.iter()
.filter(|win| {
win.with_state(|state| {
state
.tags
.iter()
.any(|tag| tag.output(self).is_some_and(|op| &op == output))
})
})
.cloned()
.collect::<Vec<_>>();
let (render, do_not_render) = output.with_state(|state| {
let first_tag = state.focused_tags().next();
if let Some(first_tag) = first_tag {
@ -716,13 +665,15 @@ impl<B: Backend> State<B> {
}
/// Schedule something to be done when windows have finished committing and have become
/// idle.
pub fn schedule_on_commit<F, B: Backend>(data: &mut CalloopData<B>, windows: Vec<Window>, on_commit: F)
where
pub fn schedule_on_commit<F, B: Backend>(
data: &mut CalloopData<B>,
windows: Vec<Window>,
on_commit: F,
) where
F: FnOnce(&mut CalloopData<B>) + 'static,
{
for window in windows.iter() {
if window.with_state(|state| !matches!(state.resize_state, WindowResizeState::Idle))
{
if window.with_state(|state| !matches!(state.resize_state, WindowResizeState::Idle)) {
data.state.loop_handle.insert_idle(|data| {
schedule_on_commit(data, windows, on_commit);
});

View file

@ -28,6 +28,14 @@ impl TagId {
fn next() -> Self {
Self(TAG_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
}
pub fn tag<B: Backend>(&self, state: &State<B>) -> Option<Tag> {
state
.space
.outputs()
.flat_map(|op| op.with_state(|state| state.tags.clone()))
.find(|tag| &tag.id() == self)
}
}
#[derive(Debug)]
@ -88,13 +96,11 @@ impl Tag {
layout: Layout::MasterStack, // TODO: get from config
})))
}
}
impl<B: Backend> State<B> {
pub fn output_for_tag(&self, tag: &Tag) -> Option<Output> {
self.space
pub fn output<B: Backend>(&self, state: &State<B>) -> Option<Output> {
state
.space
.outputs()
.find(|output| output.with_state(|state| state.tags.iter().any(|tg| tg == tag)))
.find(|output| output.with_state(|state| state.tags.iter().any(|tg| tg == self)))
.cloned()
}
}

View file

@ -15,18 +15,30 @@ use smithay::{
utils::{Logical, Point, Serial, Size},
};
use crate::{state::WithState, tag::Tag};
use crate::{
backend::Backend,
state::{State, WithState},
tag::Tag,
};
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct WindowId(u32);
// TODO: this probably doesn't need to be atomic
static WINDOW_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
impl WindowId {
pub fn next() -> Self {
Self(WINDOW_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
}
/// Get the window that has this WindowId.
pub fn window<B: Backend>(&self, state: &State<B>) -> Option<Window> {
state
.windows
.iter()
.find(|win| win.with_state(|state| &state.id == self))
.cloned()
}
}
pub struct WindowState {