initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# SPDX-FileCopyrightText: © 2026 FireFly
|
||||||
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
tinyrwm/protocol/*.lua
|
||||||
37
README.md
Normal file
37
README.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: © 2026 FireFly
|
||||||
|
SPDX-License-Identifier: 0BSD
|
||||||
|
-->
|
||||||
|
|
||||||
|
# tinyrwm.lua
|
||||||
|
|
||||||
|
Tiny river window manager implemented in Lua.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
System dependencies:
|
||||||
|
- lua (5.4 tested)
|
||||||
|
- luarocks
|
||||||
|
- libwayland
|
||||||
|
- libxkbcommon
|
||||||
|
|
||||||
|
The lua-ecosystem dependencies should be handled by luarocks.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To fetch lua dependencies, build, and install to `~/.luarocks/bin`, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
eval $(luarocks --path bin)
|
||||||
|
luarocks --local make
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
Make sure libxkbcommon.so and libwayland-client.so are present in
|
||||||
|
`LD_LIBRARY_PATH` (and `river` and `foot` in your `PATH`). You should be able
|
||||||
|
to run river with the installed Lua tinyrwm with
|
||||||
|
|
||||||
|
```sh
|
||||||
|
river -c ~/.luarocks/bin/tinyrwm
|
||||||
|
```
|
||||||
44
planar-dev-1.rockspec
Normal file
44
planar-dev-1.rockspec
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
-- SPDX-FileCopyrightText: © 2026 FireFly
|
||||||
|
-- SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
|
package = "planar"
|
||||||
|
version = "dev-1"
|
||||||
|
rockspec_format = "3.0"
|
||||||
|
|
||||||
|
source = {
|
||||||
|
url = "",
|
||||||
|
}
|
||||||
|
|
||||||
|
description = {
|
||||||
|
summary = "stupid stupid",
|
||||||
|
homepage = "",
|
||||||
|
license = "0BSD",
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = {
|
||||||
|
"lua == 5.4",
|
||||||
|
"cffi-lua >= 0.2.4",
|
||||||
|
"firefly/wau",
|
||||||
|
"luaposix",
|
||||||
|
}
|
||||||
|
|
||||||
|
external_dependencies = {
|
||||||
|
-- runtime dependencies: ensure these are in your LD_LIBRARY_PATH
|
||||||
|
-- XKBCOMMON = { library = "libxkbcommon.so" },
|
||||||
|
-- WAYLAND = { library = "libwayland-client.so" },
|
||||||
|
}
|
||||||
|
|
||||||
|
build = {
|
||||||
|
type = "command",
|
||||||
|
build_command = [[
|
||||||
|
for f in planar/protocol/*.xml; do
|
||||||
|
wau-scanner <$f >${f%%.xml}.lua
|
||||||
|
done
|
||||||
|
]],
|
||||||
|
install_command = [[
|
||||||
|
# mimic build.type == "builtin" behaviour
|
||||||
|
install -Dm644 planar/xkbcommon.lua $(LUADIR)/planar/xkbcommon.lua
|
||||||
|
install -Dm644 -t $(LUADIR)/planar/protocol planar/protocol/*.lua
|
||||||
|
install -Dm755 planar.lua $(BINDIR)/planar
|
||||||
|
]],
|
||||||
|
}
|
||||||
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
|
||||||
|
|
||||||
2107
planar/protocol/river-window-management-v1.lua
Normal file
2107
planar/protocol/river-window-management-v1.lua
Normal file
File diff suppressed because it is too large
Load Diff
1854
planar/protocol/river-window-management-v1.xml
Normal file
1854
planar/protocol/river-window-management-v1.xml
Normal file
File diff suppressed because it is too large
Load Diff
341
planar/protocol/river-xkb-bindings-v1.lua
Normal file
341
planar/protocol/river-xkb-bindings-v1.lua
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
-- Auto generated by the wau-scanner v0
|
||||||
|
|
||||||
|
--- river_xkb_bindings_v1
|
||||||
|
-- @module river_xkb_bindings_v1
|
||||||
|
|
||||||
|
-- SPDX-FileCopyrightText: © 2025 Isaac Freund
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
-- Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
-- of this software and associated documentation files (the "Software"), to
|
||||||
|
-- deal in the Software without restriction, including without limitation the
|
||||||
|
-- rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
-- sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
-- furnished to do so, subject to the following conditions:
|
||||||
|
-- The above copyright notice and this permission notice shall be included in
|
||||||
|
-- all copies or substantial portions of the Software.
|
||||||
|
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
-- IN THE SOFTWARE.
|
||||||
|
return function(wau)
|
||||||
|
|
||||||
|
local interfaces = {
|
||||||
|
"river_xkb_bindings_v1",
|
||||||
|
"river_xkb_binding_v1",
|
||||||
|
"river_xkb_bindings_seat_v1",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, iface in ipairs(interfaces) do
|
||||||
|
wau[iface] = wau.wl_interface.new()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- xkbcommon bindings global interface
|
||||||
|
--
|
||||||
|
-- This global interface should only be advertised to the client if the
|
||||||
|
-- river_window_manager_v1 global is also advertised.
|
||||||
|
-- @type river_xkb_bindings_v1
|
||||||
|
wau.river_xkb_bindings_v1:init {
|
||||||
|
name = "river_xkb_bindings_v1",
|
||||||
|
version = 2,
|
||||||
|
methods = {
|
||||||
|
--- destroy the river_xkb_bindings_v1 object
|
||||||
|
--
|
||||||
|
-- This request indicates that the client will no longer use the
|
||||||
|
-- river_xkb_bindings_v1 object.
|
||||||
|
-- @function river_xkb_bindings_v1:destroy
|
||||||
|
-- @treturn river_xkb_bindings_v1 self
|
||||||
|
{
|
||||||
|
name = "destroy",
|
||||||
|
signature = "",
|
||||||
|
types = { },
|
||||||
|
type = "destructor"
|
||||||
|
},
|
||||||
|
--- define a new xkbcommon key binding
|
||||||
|
--
|
||||||
|
-- Define a key binding for the given seat in terms of an xkbcommon keysym
|
||||||
|
-- and other configurable properties.
|
||||||
|
-- The new key binding is not enabled until initial configuration is
|
||||||
|
-- completed and the enable request is made during a manage sequence.
|
||||||
|
-- @function river_xkb_bindings_v1:get_xkb_binding
|
||||||
|
-- @tparam river_seat_v1 seat
|
||||||
|
-- @treturn river_xkb_binding_v1
|
||||||
|
-- @tparam uint keysym an xkbcommon keysym
|
||||||
|
-- @tparam uint modifiers
|
||||||
|
{
|
||||||
|
name = "get_xkb_binding",
|
||||||
|
signature = "onuu",
|
||||||
|
types = { wau.river_seat_v1, wau.river_xkb_binding_v1, 0, 0, },
|
||||||
|
},
|
||||||
|
--- manage seat-specific state
|
||||||
|
--
|
||||||
|
-- Create an object to manage seat-specific xkb bindings state.
|
||||||
|
-- It is a protocol error to make this request more than once for a given
|
||||||
|
-- river_seat_v1 object.
|
||||||
|
-- @function river_xkb_bindings_v1:get_seat
|
||||||
|
-- @treturn river_xkb_bindings_seat_v1
|
||||||
|
-- @tparam river_seat_v1 seat
|
||||||
|
{
|
||||||
|
name = "get_seat",
|
||||||
|
signature = "2no",
|
||||||
|
types = { wau.river_xkb_bindings_seat_v1, wau.river_seat_v1, },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
events = {
|
||||||
|
},
|
||||||
|
enums = {
|
||||||
|
--- error
|
||||||
|
-- @enum river_xkb_bindings_v1.Error
|
||||||
|
-- @param OBJECT_ALREADY_CREATED 0
|
||||||
|
["error"] = {
|
||||||
|
["object_already_created"] = 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods_opcode = {
|
||||||
|
["destroy"] = 0,
|
||||||
|
["get_xkb_binding"] = 1,
|
||||||
|
["get_seat"] = 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
--- configure a xkb key binding, receive trigger events
|
||||||
|
--
|
||||||
|
-- This object allows the window manager to configure a xkbcommon key binding
|
||||||
|
-- and receive events when the key binding is triggered.
|
||||||
|
-- The new key binding is not enabled until the enable request is made during
|
||||||
|
-- a manage sequence.
|
||||||
|
-- Normally, all key events are sent to the surface with keyboard focus by
|
||||||
|
-- the compositor. Key events that trigger a key binding are not sent to the
|
||||||
|
-- surface with keyboard focus.
|
||||||
|
-- If multiple key bindings would be triggered by a single physical key event
|
||||||
|
-- on the compositor side, it is compositor policy which key binding(s) will
|
||||||
|
-- receive press/release events or if all of the matched key bindings receive
|
||||||
|
-- press/release events.
|
||||||
|
-- Key bindings might be matched by the same physical key event due to shared
|
||||||
|
-- keysym and modifiers. The layout override feature may also cause the same
|
||||||
|
-- physical key event to trigger two key bindings with different keysyms and
|
||||||
|
-- different layout overrides configured.
|
||||||
|
-- @type river_xkb_binding_v1
|
||||||
|
wau.river_xkb_binding_v1:init {
|
||||||
|
name = "river_xkb_binding_v1",
|
||||||
|
version = 2,
|
||||||
|
methods = {
|
||||||
|
--- destroy the xkb binding object
|
||||||
|
--
|
||||||
|
-- This request indicates that the client will no longer use the xkb key
|
||||||
|
-- binding object and that it may be safely destroyed.
|
||||||
|
-- @function river_xkb_binding_v1:destroy
|
||||||
|
-- @treturn river_xkb_binding_v1 self
|
||||||
|
{
|
||||||
|
name = "destroy",
|
||||||
|
signature = "",
|
||||||
|
types = { },
|
||||||
|
type = "destructor"
|
||||||
|
},
|
||||||
|
--- override currently active xkb layout
|
||||||
|
--
|
||||||
|
-- Specify an xkb layout that should be used to translate key events for
|
||||||
|
-- the purpose of triggering this key binding irrespective of the currently
|
||||||
|
-- active xkb layout.
|
||||||
|
-- The layout argument is a 0-indexed xkbcommon layout number for the
|
||||||
|
-- keyboard that generated the key event.
|
||||||
|
-- If this request is never made, the currently active xkb layout of the
|
||||||
|
-- keyboard that generated the key event will be used.
|
||||||
|
-- This request modifies window management state and may only be made as
|
||||||
|
-- part of a manage sequence, see the river_window_manager_v1 description.
|
||||||
|
-- @function river_xkb_binding_v1:set_layout_override
|
||||||
|
-- @tparam uint layout 0-indexed xkbcommon layout
|
||||||
|
-- @treturn river_xkb_binding_v1 self
|
||||||
|
{
|
||||||
|
name = "set_layout_override",
|
||||||
|
signature = "u",
|
||||||
|
types = { 0, },
|
||||||
|
},
|
||||||
|
--- enable the key binding
|
||||||
|
--
|
||||||
|
-- This request should be made after all initial configuration has been
|
||||||
|
-- completed and the window manager wishes the key binding to be able to be
|
||||||
|
-- triggered.
|
||||||
|
-- This request modifies window management state and may only be made as
|
||||||
|
-- part of a manage sequence, see the river_window_manager_v1 description.
|
||||||
|
-- @function river_xkb_binding_v1:enable
|
||||||
|
-- @treturn river_xkb_binding_v1 self
|
||||||
|
{
|
||||||
|
name = "enable",
|
||||||
|
signature = "",
|
||||||
|
types = { },
|
||||||
|
},
|
||||||
|
--- disable the key binding
|
||||||
|
--
|
||||||
|
-- This request may be used to temporarily disable the key binding. It may
|
||||||
|
-- be later re-enabled with the enable request.
|
||||||
|
-- This request modifies window management state and may only be made as
|
||||||
|
-- part of a manage sequence, see the river_window_manager_v1 description.
|
||||||
|
-- @function river_xkb_binding_v1:disable
|
||||||
|
-- @treturn river_xkb_binding_v1 self
|
||||||
|
{
|
||||||
|
name = "disable",
|
||||||
|
signature = "",
|
||||||
|
types = { },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
events = {
|
||||||
|
--- the key triggering the binding has been pressed
|
||||||
|
--
|
||||||
|
-- This event indicates that the physical key triggering the binding has
|
||||||
|
-- been pressed.
|
||||||
|
-- This event will be followed by a manage_start event after all other new
|
||||||
|
-- state has been sent by the server.
|
||||||
|
-- The compositor should wait for the manage sequence to complete before
|
||||||
|
-- processing further input events. This allows the window manager client
|
||||||
|
-- to, for example, modify key bindings and keyboard focus without racing
|
||||||
|
-- against future input events. The window manager should of course respond
|
||||||
|
-- as soon as possible as the capacity of the compositor to buffer incoming
|
||||||
|
-- input events is finite.
|
||||||
|
-- @event river_xkb_binding_v1:pressed
|
||||||
|
{
|
||||||
|
name = "pressed",
|
||||||
|
signature = "",
|
||||||
|
types = { },
|
||||||
|
},
|
||||||
|
--- the key triggering the binding has been released
|
||||||
|
--
|
||||||
|
-- This event indicates that the physical key triggering the binding has
|
||||||
|
-- been released.
|
||||||
|
-- Releasing the modifiers for the binding without releasing the "main"
|
||||||
|
-- physical key that produces the bound keysym does not trigger the release
|
||||||
|
-- event. This event is sent when the "main" key is released, even if the
|
||||||
|
-- modifiers have changed since the pressed event.
|
||||||
|
-- This event will be followed by a manage_start event after all other new
|
||||||
|
-- state has been sent by the server.
|
||||||
|
-- The compositor should wait for the manage sequence to complete before
|
||||||
|
-- processing further input events. This allows the window manager client
|
||||||
|
-- to, for example, modify key bindings and keyboard focus without racing
|
||||||
|
-- against future input events. The window manager should of course respond
|
||||||
|
-- as soon as possible as the capacity of the compositor to buffer incoming
|
||||||
|
-- input events is finite.
|
||||||
|
-- @event river_xkb_binding_v1:released
|
||||||
|
{
|
||||||
|
name = "released",
|
||||||
|
signature = "",
|
||||||
|
types = { },
|
||||||
|
},
|
||||||
|
--- repeating should be stopped
|
||||||
|
--
|
||||||
|
-- This event indicates that repeating should be stopped for the binding if
|
||||||
|
-- the window manager has been repeating some action since the pressed
|
||||||
|
-- event.
|
||||||
|
-- This event is generally sent when some other (possible unbound) key is
|
||||||
|
-- pressed after the pressed event is sent and before the released event
|
||||||
|
-- is sent for this binding.
|
||||||
|
-- This event will be followed by a manage_start event after all other new
|
||||||
|
-- state has been sent by the server.
|
||||||
|
-- @event river_xkb_binding_v1:stop_repeat
|
||||||
|
{
|
||||||
|
name = "stop_repeat",
|
||||||
|
signature = "2",
|
||||||
|
types = { },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enums = {
|
||||||
|
},
|
||||||
|
methods_opcode = {
|
||||||
|
["destroy"] = 0,
|
||||||
|
["set_layout_override"] = 1,
|
||||||
|
["enable"] = 2,
|
||||||
|
["disable"] = 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
--- xkb bindings seat
|
||||||
|
--
|
||||||
|
-- This object manages xkb bindings state associated with a specific seat.
|
||||||
|
-- @type river_xkb_bindings_seat_v1
|
||||||
|
wau.river_xkb_bindings_seat_v1:init {
|
||||||
|
name = "river_xkb_bindings_seat_v1",
|
||||||
|
version = 2,
|
||||||
|
methods = {
|
||||||
|
--- destroy the object
|
||||||
|
--
|
||||||
|
-- This request indicates that the client will no longer use the object and
|
||||||
|
-- that it may be safely destroyed.
|
||||||
|
-- @function river_xkb_bindings_seat_v1:destroy
|
||||||
|
-- @treturn river_xkb_bindings_seat_v1 self
|
||||||
|
{
|
||||||
|
name = "destroy",
|
||||||
|
signature = "2",
|
||||||
|
types = { },
|
||||||
|
type = "destructor"
|
||||||
|
},
|
||||||
|
--- ensure the next key press event is eaten
|
||||||
|
--
|
||||||
|
-- Ensure that the next non-modifier key press and corresponding release
|
||||||
|
-- events for this seat are not sent to the currently focused surface.
|
||||||
|
-- If the next non-modifier key press triggers a binding, the
|
||||||
|
-- pressed/released events are sent to the river_xkb_binding_v1 object as
|
||||||
|
-- usual.
|
||||||
|
-- If the next non-modifier key press does not trigger a binding, the
|
||||||
|
-- ate_unbound_key event is sent instead.
|
||||||
|
-- Rationale: the window manager may wish to implement "chorded"
|
||||||
|
-- keybindings where triggering a binding activates a "submap" with a
|
||||||
|
-- different set of keybindings. Without a way to eat the next key
|
||||||
|
-- press event, there is no good way for the window manager to know that it
|
||||||
|
-- should error out and exit the submap when a key not bound in the submap
|
||||||
|
-- is pressed.
|
||||||
|
-- This request modifies window management state and may only be made as
|
||||||
|
-- part of a manage sequence, see the river_window_manager_v1 description.
|
||||||
|
-- @function river_xkb_bindings_seat_v1:ensure_next_key_eaten
|
||||||
|
-- @treturn river_xkb_bindings_seat_v1 self
|
||||||
|
{
|
||||||
|
name = "ensure_next_key_eaten",
|
||||||
|
signature = "2",
|
||||||
|
types = { },
|
||||||
|
},
|
||||||
|
--- cancel an ensure_next_key_eaten request
|
||||||
|
--
|
||||||
|
-- This requests cancels the effect of the latest ensure_next_key_eaten
|
||||||
|
-- request if no key has been eaten due to the request yet. This request
|
||||||
|
-- has no effect if a key has already been eaten or no
|
||||||
|
-- ensure_next_key_eaten was made.
|
||||||
|
-- Rationale: the window manager may wish cancel an uncompleted "chorded"
|
||||||
|
-- keybinding after a timeout of a few seconds. Note that since this
|
||||||
|
-- timeout use-case requires the window manager to trigger a manage sequence
|
||||||
|
-- with the river_window_manager_v1.manage_dirty request it is possible that
|
||||||
|
-- the ate_unbound_key key event may be sent before the window manager has
|
||||||
|
-- a chance to make the cancel_ensure_next_key_eaten request.
|
||||||
|
-- This request modifies window management state and may only be made as
|
||||||
|
-- part of a manage sequence, see the river_window_manager_v1 description.
|
||||||
|
-- @function river_xkb_bindings_seat_v1:cancel_ensure_next_key_eaten
|
||||||
|
-- @treturn river_xkb_bindings_seat_v1 self
|
||||||
|
{
|
||||||
|
name = "cancel_ensure_next_key_eaten",
|
||||||
|
signature = "2",
|
||||||
|
types = { },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
events = {
|
||||||
|
--- an unbound key press event was eaten
|
||||||
|
--
|
||||||
|
-- An unbound key press event was eaten due to the ensure_next_key_eaten
|
||||||
|
-- request.
|
||||||
|
-- This event will be followed by a manage_start event after all other new
|
||||||
|
-- state has been sent by the server.
|
||||||
|
-- @event river_xkb_bindings_seat_v1:ate_unbound_key
|
||||||
|
{
|
||||||
|
name = "ate_unbound_key",
|
||||||
|
signature = "2",
|
||||||
|
types = { },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enums = {
|
||||||
|
},
|
||||||
|
methods_opcode = {
|
||||||
|
["destroy"] = 0,
|
||||||
|
["ensure_next_key_eaten"] = 1,
|
||||||
|
["cancel_ensure_next_key_eaten"] = 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
268
planar/protocol/river-xkb-bindings-v1.xml
Normal file
268
planar/protocol/river-xkb-bindings-v1.xml
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="river_xkb_bindings_v1">
|
||||||
|
<copyright>
|
||||||
|
SPDX-FileCopyrightText: © 2025 Isaac Freund
|
||||||
|
SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to
|
||||||
|
deal in the Software without restriction, including without limitation the
|
||||||
|
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
IN THE SOFTWARE.
|
||||||
|
</copyright>
|
||||||
|
|
||||||
|
<description summary="xkbcommon-based key bindings">
|
||||||
|
This protocol allows the river-window-management-v1 window manager to
|
||||||
|
define key bindings in terms of xkbcommon keysyms and other configurable
|
||||||
|
properties.
|
||||||
|
|
||||||
|
The key words "must", "must not", "required", "shall", "shall not",
|
||||||
|
"should", "should not", "recommended", "may", and "optional" in this
|
||||||
|
document are to be interpreted as described in IETF RFC 2119.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<interface name="river_xkb_bindings_v1" version="2">
|
||||||
|
<description summary="xkbcommon bindings global interface">
|
||||||
|
This global interface should only be advertised to the client if the
|
||||||
|
river_window_manager_v1 global is also advertised.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<enum name="error" since="2">
|
||||||
|
<entry name="object_already_created" value="0" since="2"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the river_xkb_bindings_v1 object">
|
||||||
|
This request indicates that the client will no longer use the
|
||||||
|
river_xkb_bindings_v1 object.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="get_xkb_binding">
|
||||||
|
<description summary="define a new xkbcommon key binding">
|
||||||
|
Define a key binding for the given seat in terms of an xkbcommon keysym
|
||||||
|
and other configurable properties.
|
||||||
|
|
||||||
|
The new key binding is not enabled until initial configuration is
|
||||||
|
completed and the enable request is made during a manage sequence.
|
||||||
|
</description>
|
||||||
|
<arg name="seat" type="object" interface="river_seat_v1"/>
|
||||||
|
<arg name="id" type="new_id" interface="river_xkb_binding_v1"/>
|
||||||
|
<arg name="keysym" type="uint" summary="an xkbcommon keysym"/>
|
||||||
|
<arg name="modifiers" type="uint" enum="river_seat_v1.modifiers"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="get_seat" since="2">
|
||||||
|
<description summary="manage seat-specific state">
|
||||||
|
Create an object to manage seat-specific xkb bindings state.
|
||||||
|
|
||||||
|
It is a protocol error to make this request more than once for a given
|
||||||
|
river_seat_v1 object.
|
||||||
|
</description>
|
||||||
|
<arg name="id" type="new_id" interface="river_xkb_bindings_seat_v1"/>
|
||||||
|
<arg name="seat" type="object" interface="river_seat_v1"/>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="river_xkb_binding_v1" version="2">
|
||||||
|
<description summary="configure a xkb key binding, receive trigger events">
|
||||||
|
This object allows the window manager to configure a xkbcommon key binding
|
||||||
|
and receive events when the key binding is triggered.
|
||||||
|
|
||||||
|
The new key binding is not enabled until the enable request is made during
|
||||||
|
a manage sequence.
|
||||||
|
|
||||||
|
Normally, all key events are sent to the surface with keyboard focus by
|
||||||
|
the compositor. Key events that trigger a key binding are not sent to the
|
||||||
|
surface with keyboard focus.
|
||||||
|
|
||||||
|
If multiple key bindings would be triggered by a single physical key event
|
||||||
|
on the compositor side, it is compositor policy which key binding(s) will
|
||||||
|
receive press/release events or if all of the matched key bindings receive
|
||||||
|
press/release events.
|
||||||
|
|
||||||
|
Key bindings might be matched by the same physical key event due to shared
|
||||||
|
keysym and modifiers. The layout override feature may also cause the same
|
||||||
|
physical key event to trigger two key bindings with different keysyms and
|
||||||
|
different layout overrides configured.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the xkb binding object">
|
||||||
|
This request indicates that the client will no longer use the xkb key
|
||||||
|
binding object and that it may be safely destroyed.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="set_layout_override">
|
||||||
|
<description summary="override currently active xkb layout">
|
||||||
|
Specify an xkb layout that should be used to translate key events for
|
||||||
|
the purpose of triggering this key binding irrespective of the currently
|
||||||
|
active xkb layout.
|
||||||
|
|
||||||
|
The layout argument is a 0-indexed xkbcommon layout number for the
|
||||||
|
keyboard that generated the key event.
|
||||||
|
|
||||||
|
If this request is never made, the currently active xkb layout of the
|
||||||
|
keyboard that generated the key event will be used.
|
||||||
|
|
||||||
|
This request modifies window management state and may only be made as
|
||||||
|
part of a manage sequence, see the river_window_manager_v1 description.
|
||||||
|
</description>
|
||||||
|
<arg name="layout" type="uint" summary="0-indexed xkbcommon layout"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="enable">
|
||||||
|
<description summary="enable the key binding">
|
||||||
|
This request should be made after all initial configuration has been
|
||||||
|
completed and the window manager wishes the key binding to be able to be
|
||||||
|
triggered.
|
||||||
|
|
||||||
|
This request modifies window management state and may only be made as
|
||||||
|
part of a manage sequence, see the river_window_manager_v1 description.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="disable">
|
||||||
|
<description summary="disable the key binding">
|
||||||
|
This request may be used to temporarily disable the key binding. It may
|
||||||
|
be later re-enabled with the enable request.
|
||||||
|
|
||||||
|
This request modifies window management state and may only be made as
|
||||||
|
part of a manage sequence, see the river_window_manager_v1 description.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<event name="pressed">
|
||||||
|
<description summary="the key triggering the binding has been pressed">
|
||||||
|
This event indicates that the physical key triggering the binding has
|
||||||
|
been pressed.
|
||||||
|
|
||||||
|
This event will be followed by a manage_start event after all other new
|
||||||
|
state has been sent by the server.
|
||||||
|
|
||||||
|
The compositor should wait for the manage sequence to complete before
|
||||||
|
processing further input events. This allows the window manager client
|
||||||
|
to, for example, modify key bindings and keyboard focus without racing
|
||||||
|
against future input events. The window manager should of course respond
|
||||||
|
as soon as possible as the capacity of the compositor to buffer incoming
|
||||||
|
input events is finite.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="released">
|
||||||
|
<description summary="the key triggering the binding has been released">
|
||||||
|
This event indicates that the physical key triggering the binding has
|
||||||
|
been released.
|
||||||
|
|
||||||
|
Releasing the modifiers for the binding without releasing the "main"
|
||||||
|
physical key that produces the bound keysym does not trigger the release
|
||||||
|
event. This event is sent when the "main" key is released, even if the
|
||||||
|
modifiers have changed since the pressed event.
|
||||||
|
|
||||||
|
This event will be followed by a manage_start event after all other new
|
||||||
|
state has been sent by the server.
|
||||||
|
|
||||||
|
The compositor should wait for the manage sequence to complete before
|
||||||
|
processing further input events. This allows the window manager client
|
||||||
|
to, for example, modify key bindings and keyboard focus without racing
|
||||||
|
against future input events. The window manager should of course respond
|
||||||
|
as soon as possible as the capacity of the compositor to buffer incoming
|
||||||
|
input events is finite.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="stop_repeat" since="2">
|
||||||
|
<description summary="repeating should be stopped">
|
||||||
|
This event indicates that repeating should be stopped for the binding if
|
||||||
|
the window manager has been repeating some action since the pressed
|
||||||
|
event.
|
||||||
|
|
||||||
|
This event is generally sent when some other (possible unbound) key is
|
||||||
|
pressed after the pressed event is sent and before the released event
|
||||||
|
is sent for this binding.
|
||||||
|
|
||||||
|
This event will be followed by a manage_start event after all other new
|
||||||
|
state has been sent by the server.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="river_xkb_bindings_seat_v1" version="2">
|
||||||
|
<description summary="xkb bindings seat">
|
||||||
|
This object manages xkb bindings state associated with a specific seat.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor" since="2">
|
||||||
|
<description summary="destroy the object">
|
||||||
|
This request indicates that the client will no longer use the object and
|
||||||
|
that it may be safely destroyed.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="ensure_next_key_eaten" since="2">
|
||||||
|
<description summary="ensure the next key press event is eaten">
|
||||||
|
Ensure that the next non-modifier key press and corresponding release
|
||||||
|
events for this seat are not sent to the currently focused surface.
|
||||||
|
|
||||||
|
If the next non-modifier key press triggers a binding, the
|
||||||
|
pressed/released events are sent to the river_xkb_binding_v1 object as
|
||||||
|
usual.
|
||||||
|
|
||||||
|
If the next non-modifier key press does not trigger a binding, the
|
||||||
|
ate_unbound_key event is sent instead.
|
||||||
|
|
||||||
|
Rationale: the window manager may wish to implement "chorded"
|
||||||
|
keybindings where triggering a binding activates a "submap" with a
|
||||||
|
different set of keybindings. Without a way to eat the next key
|
||||||
|
press event, there is no good way for the window manager to know that it
|
||||||
|
should error out and exit the submap when a key not bound in the submap
|
||||||
|
is pressed.
|
||||||
|
|
||||||
|
This request modifies window management state and may only be made as
|
||||||
|
part of a manage sequence, see the river_window_manager_v1 description.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="cancel_ensure_next_key_eaten" since="2">
|
||||||
|
<description summary="cancel an ensure_next_key_eaten request">
|
||||||
|
This requests cancels the effect of the latest ensure_next_key_eaten
|
||||||
|
request if no key has been eaten due to the request yet. This request
|
||||||
|
has no effect if a key has already been eaten or no
|
||||||
|
ensure_next_key_eaten was made.
|
||||||
|
|
||||||
|
Rationale: the window manager may wish cancel an uncompleted "chorded"
|
||||||
|
keybinding after a timeout of a few seconds. Note that since this
|
||||||
|
timeout use-case requires the window manager to trigger a manage sequence
|
||||||
|
with the river_window_manager_v1.manage_dirty request it is possible that
|
||||||
|
the ate_unbound_key key event may be sent before the window manager has
|
||||||
|
a chance to make the cancel_ensure_next_key_eaten request.
|
||||||
|
|
||||||
|
This request modifies window management state and may only be made as
|
||||||
|
part of a manage sequence, see the river_window_manager_v1 description.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<event name="ate_unbound_key" since="2">
|
||||||
|
<description summary="an unbound key press event was eaten">
|
||||||
|
An unbound key press event was eaten due to the ensure_next_key_eaten
|
||||||
|
request.
|
||||||
|
|
||||||
|
This event will be followed by a manage_start event after all other new
|
||||||
|
state has been sent by the server.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
</interface>
|
||||||
|
</protocol>
|
||||||
26
planar/xkbcommon.lua
Normal file
26
planar/xkbcommon.lua
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-- SPDX-FileCopyrightText: © 2026 FireFly
|
||||||
|
-- SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
|
local ffi = require("cffi")
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local raw = ffi.load("xkbcommon")
|
||||||
|
|
||||||
|
ffi.cdef [[
|
||||||
|
typedef uint32_t xkb_keysym_t;
|
||||||
|
|
||||||
|
enum xkb_keysym_flags {
|
||||||
|
XKB_KEYSYM_NO_FLAGS = 0,
|
||||||
|
XKB_KEYSYM_CASE_INSENSITIVE = (1 << 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
xkb_keysym_t
|
||||||
|
xkb_keysym_from_name(const char *name, enum xkb_keysym_flags flags);
|
||||||
|
]]
|
||||||
|
|
||||||
|
function M.keysym(name, flags)
|
||||||
|
return raw.xkb_keysym_from_name(name, flags or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
Reference in New Issue
Block a user