Files
planar/planar.lua
2026-05-07 08:09:30 +02:00

548 lines
12 KiB
Lua
Executable File

#! /usr/bin/env lua
-- SPDX-FileCopyrightText: © 2026 FireFly
-- SPDX-License-Identifier: 0BSD
local wau = require("wau")
local xkbcommon = require("planar.xkbcommon")
local posix = require("posix")
wau:require("planar.protocol.river-window-management-v1")
wau:require("planar.protocol.river-xkb-bindings-v1")
local globals = {}
local required_globals = {
["river_window_manager_v1"] = 4,
["river_xkb_bindings_v1"] = 1,
}
local Mods = wau.river_seat_v1.Modifiers
local term = "foot"
local launcher = "rofi -show drun" -- broken on my end. pls check.
local xkb_bindings = {
{"t", Mods.MOD1, {"spawn", term}},
{"f", Mods.MOD1, {"spawn", launcher}}, -- broken on my end. pls check.
{"q", Mods.MOD1, "close"},
{"e", Mods.MOD1, "exit"},
{"r", Mods.MOD1, {"goto", 0, 0}},
{"F1", Mods.MOD1, {"goto", -2000, -2000}},
{"F2", Mods.MOD1, {"goto", 2000, -2000}},
{"F3", Mods.MOD1, {"goto", -2000, 2000}},
{"F4", Mods.MOD1, {"goto", 2000, 2000}}
}
local pointer_bindings = {
{"left", Mods.MOD1, "move"},
{"right", Mods.MOD1, "resize"},
}
local wm = {
outputs = {},
seats = {},
-- Windows are kept in rendering order; last window is topmost
windows = {},
cam = {x = 0, y = 0, dirty = false},
}
local function table_index_of(tbl, sought)
for i, v in ipairs(tbl) do
if v == sought then return i end
end
return 0
end
local function table_filter_inplace(tbl, pred)
local removed = 0
for i=1,#tbl do
if pred(tbl[i]) then
tbl[i - removed] = tbl[i]
else
removed = removed + 1
end
if removed > 0 then tbl[i] = nil end
end
return tbl
end
---- Output ---------------------------
local Output = { mt = {}, listener = {} }
Output.mt.__index = Output
function Output.create(obj)
local output = { obj = obj }
setmetatable(output, Output.mt)
obj:set_user_data(output)
obj:add_listener(Output.listener)
return output
end
function Output:maybe_destroy()
if self.removed then
self.obj:destroy()
else
return self
end
end
function Output.listener:removed()
self:get_user_data().removed = true
end
---- Window ---------------------------
local Window = { mt = {}, listener = {} }
Window.mt.__index = Window
function Window.create(obj)
local window = {
obj = obj,
node = obj:get_node(),
new = true,
}
setmetatable(window, Window.mt)
obj:set_user_data(window)
obj:add_listener(Window.listener)
return window
end
function Window:maybe_destroy()
if self.closed then
self.obj:destroy()
self.node:destroy()
else
return self
end
end
function Window:manage()
if self.new then
for i = 1,#wm.outputs do
local output = wm.outputs[i]
for k,v in pairs(output) do print(k,v) end
end
self.new = nil
self:set_position(wm.cam.x, wm.cam.y)
self.obj:propose_dimensions(0, 0)
end
local move = self.pointer_move_requested
if move ~= nil then
self.pointer_move_requested = nil
move.seat:pointer_move(self)
end
local resize = self.pointer_resize_requested
if resize ~= nil then
self.pointer_resize_requested = nil
resize.seat:pointer_resize(self, resize.edges)
end
end
function Window:set_position(x, y)
self.node:set_position(x - wm.cam.x, y - wm.cam.y)
self.x = x
self.y = y
end
function Window.listener:closed()
self:get_user_data().closed = true
end
function Window.listener:dimensions(width, height)
local window = self:get_user_data()
window.width = width
window.height = height
end
function Window.listener:pointer_move_requested(seat)
self:get_user_data().pointer_move_requested = {
seat = seat:get_user_data(),
}
end
function Window.listener:pointer_resize_requested(seat, edges)
local Edges = wau.river_window_v1.Edges
self:get_user_data().pointer_resize_requested = {
seat = seat:get_user_data(),
edges = {
left = (edges & Edges.LEFT) ~= 0,
right = (edges & Edges.RIGHT) ~= 0,
top = (edges & Edges.TOP) ~= 0,
bottom = (edges & Edges.BOTTOM) ~= 0,
},
}
end
---- Seat -----------------------------
local Seat = { mt = {}, listener = {} }
Seat.mt.__index = Seat
function Seat.create(obj)
local seat = {
obj = obj,
new = true,
xkb_bindings = {},
pointer_bindings = {},
}
setmetatable(seat, Seat.mt)
obj:set_user_data(seat)
obj:add_listener(Seat.listener)
return seat
end
function Seat:focus(window)
if window == nil and #wm.windows > 0 then
-- Fall back to topmost window
window = wm.windows[#wm.windows]
end
if window then
if self.focused ~= window then
self.obj:focus_window(window.obj)
self.focused = window
-- Move to top
local i = table_index_of(wm.windows, window)
table.remove(wm.windows, i)
table.insert(wm.windows, window)
window.node:place_top()
end
else
self.obj:clear_focus()
self.focused = nil
end
end
function Seat:pointer_move(window)
if window then
if self.op == nil then
self:focus(window)
self.obj:op_start_pointer()
self.op = {
type = "move",
window = window,
start = { x = window.x, y = window.y },
dx = 0,
dy = 0,
}
end
else
if self.op == nil then
self:focus(window)
self.obj:op_start_pointer()
self.op = {
type = "world_move",
dx = 0,
dy = 0,
pdx = 0,
pdy = 0
}
end
end
end
function Seat:pointer_resize(window, edges)
if self.op == nil then
self:focus(window)
window.obj:inform_resize_start()
self.obj:op_start_pointer()
self.op = {
type = "resize",
window = window,
edges = edges,
start = {
x = window.x,
y = window.y,
width = window.width,
height = window.height,
},
dx = 0,
dy = 0,
}
end
end
function Seat:action(action)
if type(action) == "table" then
if action[1] == "spawn" then
if posix.unistd.fork() == 0 then
posix.unistd.execp(action[2], {})
end
elseif action[1] == "goto" then
wm.cam.x = action[2] or 0
wm.cam.y = action[3] or 0
wm.cam.dirty = true;
end
elseif action == "close" then
if self.focused ~= nil then
self.focused.obj:close()
end
elseif action == "focus-next" then
self:focus(wm.windows[1])
elseif action == "move" then
-- if self.hovered ~= nil then
self:pointer_move(self.hovered)
-- end
elseif action == "resize" then
if self.hovered ~= nil then
self:pointer_resize(self.hovered, { bottom = true, right = true })
end
elseif action == "exit" then
globals["river_window_manager_v1"]:exit_session()
else
print("Seat:action: unimplemented", action)
end
end
function Seat:add_pointer_binding(button, mods, action)
-- From /usr/include/linux/input-event-codes.h
local button_code = ({ left = 0x110, right = 0x111 })[button]
local obj = self.obj:get_pointer_binding(button_code, mods)
local binding = { obj = obj }
obj:add_listener {
["pressed"] = function (_)
self.pending_action = action
end,
}
obj:enable()
table.insert(self.pointer_bindings, binding)
end
function Seat:add_xkb_binding(key, mods, action)
local keysym = xkbcommon.keysym(key)
local obj = globals["river_xkb_bindings_v1"]:get_xkb_binding(
self.obj, keysym, mods)
local binding = { obj = obj }
obj:add_listener {
["pressed"] = function (_)
self.pending_action = action
end,
}
obj:enable()
table.insert(self.xkb_bindings, binding)
end
function Seat:manage()
if self.new then
self.new = nil
for _, tbl in ipairs(xkb_bindings) do
self:add_xkb_binding(table.unpack(tbl))
end
for _, tbl in ipairs(pointer_bindings) do
self:add_pointer_binding(table.unpack(tbl))
end
end
if self.focused and self.focused.closed then
self.focused = nil
end
self:focus(self.interacted)
self.interacted = nil
if self.pending_action ~= nil then
self:action(self.pending_action)
self.pending_action = nil
end
if self.op then
local op, window = self.op, self.op.window
local window = self.op.window
if window and window.closed then
self.obj:op_end()
self.op = nil
elseif self.op_release then
if window and op.type == "resize" then
window.obj:inform_resize_end()
end
self.obj:op_end()
self.op = nil
elseif window and op.type == "resize" then
local width = math.max(
1,
op.edges.left and (op.start.width - op.dx) or
op.edges.right and (op.start.width + op.dx) or
op.start.width
)
local height = math.max(
1,
op.edges.top and (op.start.height - op.dy) or
op.edges.bottom and (op.start.height + op.dy) or
op.start.height
)
window.obj:propose_dimensions(width, height)
end
end
self.op_release = nil
end
function Seat:render()
if self.op then
local op, window = self.op, self.op.window
if window and self.op.type == "move" then
window:set_position(
op.start.x + op.dx,
op.start.y + op.dy
)
elseif window and self.op.type == "resize" then
local x = op.edges.left
and (op.start.x + (op.start.width - window.width))
or op.start.x
local y = op.edges.top
and (op.start.y + (op.start.height - window.height))
or op.start.y
window:set_position(x, y)
elseif self.op.type == "world_move" then
local adx, ady = op.dx - op.pdx, op.dy - op.pdy
wm.cam.x = wm.cam.x - adx -- TODO: make sure should be minus
wm.cam.y = wm.cam.y - ady
wm.cam.dirty = true;
op.pdx, op.pdy = op.dx, op.dy
end
end
end
function Seat:maybe_destroy()
if self.removed then
for _, binding in ipairs(self.xkb_bindings) do
binding.obj:destroy()
end
for _, binding in ipairs(self.pointer_bindings) do
binding.obj:destroy()
end
self.obj:destroy()
else
return self
end
end
function Seat.listener.removed(self)
self:get_user_data().removed = true
end
function Seat.listener.pointer_enter(self, window)
self:get_user_data().hovered = window:get_user_data()
end
function Seat.listener.pointer_leave(self)
self:get_user_data().hovered = nil
end
function Seat.listener.window_interaction(self, window)
self:get_user_data().interacted = window:get_user_data()
end
function Seat.listener.op_delta(self, dx, dy)
local seat = self:get_user_data()
seat.op.dx = dx
seat.op.dy = dy
end
function Seat.listener.op_release(self)
self:get_user_data().op_release = true
end
---- wm -------------------------------
local function wm_manage()
table_filter_inplace(wm.outputs, Output.maybe_destroy)
table_filter_inplace(wm.windows, Window.maybe_destroy)
table_filter_inplace(wm.seats, Seat.maybe_destroy)
for _, window in ipairs(wm.windows) do
window:manage()
end
for _, seat in ipairs(wm.seats) do
seat:manage()
end
globals["river_window_manager_v1"]:manage_finish()
end
local function wm_render()
if wm.cam.dirty then
for i = 1,#wm.windows do
local window = wm.windows[i]
window.node:set_position(window.x - wm.cam.x, window.y - wm.cam.y);
end
wm.cam.dirty = false;
end
for _, seat in ipairs(wm.seats) do
seat:render()
end
globals["river_window_manager_v1"]:render_finish()
end
local wm_handlers = {
["unavailable"] = function (self)
io.stderr:write("another window manager is already running\n")
os.exit(1)
end,
["finished"] = function (self)
os.exit(0)
end,
["manage_start"] = wm_manage,
["render_start"] = wm_render,
["output"] = function (self, obj)
table.insert(wm.outputs, Output.create(obj))
end,
["seat"] = function (self, obj)
table.insert(wm.seats, Seat.create(obj))
end,
["window"] = function (self, obj)
table.insert(wm.windows, Window.create(obj))
end,
}
---- Entry point ----------------------
display = wau.wl_display.connect()
assert(display, "Failed to connect to wayland compositor")
-- Ensure we exit nonzero if an event handler errors
local function handle_callback_error(proxy, name, func, err)
io.stderr:write(("-- Error calling event handler for %s %q:")
:format(tostring(proxy), name))
io.stderr:write(("%s\n"):format(tostring(err)))
os.exit(1)
end
wau.wl_proxy.set_error_callback(handle_callback_error)
-- Avoid passing WAYLAND_DEBUG to our children
posix.stdlib.setenv("WAYLAND_DEBUG", nil)
-- Ensure children are automatically reaped
posix.signal.signal(posix.signal.SIGCHLD, posix.signal.SIG_IGN)
local registry = display:get_registry()
registry:add_listener {
["global"] = function (self, name, iface, version)
local required_version = required_globals[iface]
if required_version ~= nil then
assert(required_version <= version,
("wayland compositor supported %s version too old (need %d, got %d)")
:format(iface, required_version, version))
globals[iface] = self:bind(name, wau[iface], required_version)
end
end,
}
display:roundtrip()
for k in pairs(required_globals) do
assert(globals[k] ~= nil, ("wayland compositor does not support %s"):format(k))
end
globals["river_window_manager_v1"]:add_listener(wm_handlers)
while display:dispatch() do end