initial commit
This commit is contained in:
538
planar.lua
Executable file
538
planar.lua
Executable file
@@ -0,0 +1,538 @@
|
||||
#! /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 xkb_bindings = {
|
||||
{"space", Mods.MOD1, "spawn-foot"},
|
||||
{"q", Mods.MOD1, "close"},
|
||||
{"r", Mods.MOD1, "reset-view"},
|
||||
{"Escape", Mods.MOD1, "exit"},
|
||||
}
|
||||
|
||||
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 action == "spawn-foot" then
|
||||
if posix.unistd.fork() == 0 then
|
||||
posix.unistd.execp("foot", {})
|
||||
end
|
||||
elseif action == "reset-view" then
|
||||
wm.cam.x = 0
|
||||
wm.cam.y = 0
|
||||
wm.cam.dirty = true;
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user