pinnacle/api/lua/pinnacle.lua
2023-07-18 12:37:40 -05:00

142 lines
4.2 KiB
Lua

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
--
-- SPDX-License-Identifier: MPL-2.0
local socket = require("posix.sys.socket")
local msgpack = require("msgpack")
local SOCKET_PATH = "/tmp/pinnacle_socket"
---Read the specified number of bytes.
---@param socket_fd integer The socket file descriptor
---@param count integer The amount of bytes to read
---@return string|nil data
---@return string|nil err_msg
---@return integer|nil err_num
local function read_exact(socket_fd, count)
local len_to_read = count
local data = ""
while len_to_read > 0 do
-- print("need to read " .. tostring(len_to_read) .. " bytes")
local bytes, err_msg, errnum = socket.recv(socket_fd, len_to_read)
if bytes == nil then
-- TODO: handle errors
print("bytes was nil")
return bytes, err_msg, errnum
end
---@type integer
local recv_len = bytes:len()
if recv_len == 0 then
print("stream closed")
break
end
len_to_read = len_to_read - recv_len
assert(len_to_read >= 0, "Overread message boundary")
data = data .. bytes
end
return data
end
---@class Pinnacle
---The main Pinnacle table, where all of the config options come from.
---
---While you *can* import the fields directly, all config must be in the `setup` function, so you might as well just use the provided table. The ability to directly `require` fields may be dropped in the future.
local pinnacle = {
---Key and mouse binds
input = require("input"),
---Window management
window = require("window"),
---Process spawning
process = require("process"),
---Tag management
tag = require("tag"),
---Output management
output = require("output"),
}
---Quit Pinnacle.
function pinnacle.quit()
SendMsg("Quit")
end
---Configure Pinnacle. You should put mostly eveything into the config_func to avoid invalid state.
---The function takes one argument: the Pinnacle table, which is how you'll access all of the available config options.
---@param config_func fun(pinnacle: Pinnacle)
function pinnacle.setup(config_func)
---@type integer
local socket_fd = assert(socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0), "Failed to create socket")
print("created socket at fd " .. socket_fd)
assert(0 == socket.connect(socket_fd, {
family = socket.AF_UNIX,
path = SOCKET_PATH,
}), "Failed to connect to Pinnacle socket")
---@type fun(args: table?)[]
CallbackTable = {}
---@param data Msg
function SendMsg(data)
local encoded = msgpack.encode(data)
assert(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,
})
end
function ReadMsg()
local msg_len_bytes, err_msg, err_num = read_exact(socket_fd, 4)
assert(msg_len_bytes)
-- TODO: break here if error in read_exact
---@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 tb = msgpack.decode(msg_bytes)
-- print(msg_bytes)
return tb
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
end
-- if tb.RequestResponse then
-- local req_id = tb.RequestResponse.request_id
-- Requests[req_id] = tb.RequestResponse.response
-- end
end
end
return pinnacle