Display a single window!

This commit is contained in:
Ben Buhse 2025-05-14 21:32:37 -05:00
commit bad5007670
No known key found for this signature in database
GPG key ID: 7916ACFCD38FD0B4
12 changed files with 1084 additions and 554 deletions

View file

@ -1,15 +1,6 @@
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: EUPL-1.2
const std = @import("std");
const mem = std.mem;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const log = std.log.scoped(.Backend);
// SPDX-License-Identifier: GPL-3.0-or-later
const Backend = @This();
@ -22,9 +13,7 @@ registry: *wl.Registry,
compositor: ?*wl.Compositor = null,
shm: ?*wl.Shm = null,
window_manager: ?*river.WindowManagerV1 = null,
// outputs: std.SinglyLinkedList(Output) = .{},
wm: ?WindowManager = null,
/// Return a new Backend
pub fn init(allocator: mem.Allocator) !Backend {
@ -39,7 +28,7 @@ pub fn init(allocator: mem.Allocator) !Backend {
.display = wl_display,
.registry = try wl_display.getRegistry(),
};
backend.registry.setListener(*Backend, registry_listener, &backend);
backend.registry.setListener(*Backend, registryListener, &backend);
// Do an initial roundtrip so the registry globals fire
const errno = backend.display.roundtrip();
@ -50,63 +39,62 @@ pub fn init(allocator: mem.Allocator) !Backend {
// These are all required by beansprout.
// If we are missing any of them, then let's exit.
if (backend.compositor == null) interface_not_advertised(wl.Compositor);
if (backend.shm == null) interface_not_advertised(wl.Shm);
if (backend.window_manager == null) interface_not_advertised(river.WindowManagerV1);
if (backend.compositor == null) interfaceNotAdvertised(wl.Compositor);
if (backend.shm == null) interfaceNotAdvertised(wl.Shm);
if (backend.wm == null) interfaceNotAdvertised(river.WindowManagerV1);
return backend;
}
/// Deinitialize a Backend
pub fn deinit(backend: *Backend) void {
_ = backend;
defer backend.display.disconnect();
if (backend.compositor) |c| {
c.destroy();
}
if (backend.shm) |s| {
s.destroy();
}
}
fn registry_listener(registry: *wl.Registry, event: wl.Registry.Event, backend: *Backend) void {
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, backend: *Backend) void {
// Since we can't return errors from the listener, we use a helper function so that
// we can easily log any errors the same way.
registry_listener_helper(registry, event, backend) catch |err| {
log.err("{any}", .{err});
return;
};
}
fn registry_listener_helper(registry: *wl.Registry, event: wl.Registry.Event, backend: *Backend) !void {
// Use a helper function to log errors; the actual listener can't return an error.
switch (event) {
.global => |ev| {
if (mem.orderZ(u8, ev.interface, wl.Compositor.interface.name) == .eq) {
if (ev.version < 4) version_not_supported(wl.Compositor, ev.version, 4);
if (ev.version < 4) versionNotSupported(wl.Compositor, ev.version, 4);
backend.compositor = try registry.bind(ev.name, wl.Compositor, 4);
} else if (mem.orderZ(u8, ev.interface, wl.Shm.interface.name) == .eq) {
backend.shm = try registry.bind(ev.name, wl.Shm, 1);
} else if (mem.orderZ(u8, ev.interface, river.WindowManagerV1.interface.name) == .eq) {
backend.window_manager = try registry.bind(ev.name, river.WindowManagerV1, 1);
backend.wm = .{ .window_manager = try WindowManager.init(try registry.bind(ev.name, river.WindowManagerV1, 1)) catch {} };
backend.wm.?.setListener();
}
},
.global_remove => |ev| {
log.debug("TODO...", .{});
_ = ev;
},
// We don't need .global_remove
.global_remove => {},
}
}
fn window_manager_listener(window_manager: *river.WindowManagerV1, event: river.WindowManagerV1.Event, backend: *Backend) !void {
_ = backend;
_ = window_manager;
switch (event) {
.update_windowing_start => log.debug("RAAAA"),
.update_windowing_end => log.debug("REEE"),
else => log.debug("WOOOO"),
}
}
fn interface_not_advertised(comptime WaylandGlobal: type) noreturn {
fn interfaceNotAdvertised(comptime WaylandGlobal: type) noreturn {
log.err("{s} not advertised. Exiting", .{WaylandGlobal.interface.name});
std.posix.exit(1);
}
fn version_not_supported(comptime WaylandGlobal: type, have_version: u32, need_version: u32) noreturn {
fn versionNotSupported(comptime WaylandGlobal: type, have_version: u32, need_version: u32) noreturn {
log.err("The compositor only advertised {s} version {d} but version {d} is required. Exiting", .{ WaylandGlobal.interface.name, have_version, need_version });
std.posix.exit(1);
}
const std = @import("std");
const mem = std.mem;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const WindowManager = @import("WindowManager.zig");
const log = std.log.scoped(.Backend);

57
src/Output.zig Normal file
View file

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
const Output = @This();
context: *Context,
output_v1: *river.OutputV1,
width: i32 = 0,
height: i32 = 0,
x: i32 = 0,
y: i32 = 0,
link: wl.list.Link,
pub fn init(output: *Output, context: *Context, river_output_v1: *river.OutputV1) void {
output.* = .{
.context = context,
.output_v1 = river_output_v1,
.link = undefined, // Handled by the wl.list
};
output.output_v1.setListener(*Output, outputListener, output);
}
fn outputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void {
assert(output.output_v1 == river_output_v1);
switch (event) {
.wl_output => |ev| {
log.debug("initializing new river_ouput_v1 corresponding to wl_output: {d}", .{ev.name});
},
.dimensions => |ev| {
output.width = ev.width;
output.height = ev.height;
},
.position => |ev| {
output.x = ev.x;
output.y = ev.y;
},
else => |ev| {
log.debug("unhandled event: {s}", .{@tagName(ev)});
},
}
}
const std = @import("std");
const assert = std.debug.assert;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const Context = @import("main.zig").Context;
const log = std.log.scoped(.Output);

44
src/Seat.zig Normal file
View file

@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
const Seat = @This();
context: *Context,
seat_v1: *river.SeatV1,
link: wl.list.Link,
pub fn init(seat: *Seat, context: *Context, river_seat_v1: *river.SeatV1) void {
seat.* = .{
.context = context,
.seat_v1 = river_seat_v1,
.link = undefined, // Handled by the wl.list
};
seat.seat_v1.setListener(*Seat, seatListener, seat);
}
fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *Seat) void {
assert(seat.seat_v1 == river_seat_v1);
switch (event) {
.wl_seat => |ev| {
log.debug("initializing new river_ouput_v1 corresponding to wl_seat: {d}", .{ev.name});
},
else => |ev| {
log.debug("unhandled event: {s}", .{@tagName(ev)});
},
}
}
const std = @import("std");
const assert = std.debug.assert;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const Context = @import("main.zig").Context;
const log = std.log.scoped(.Seat);

61
src/Window.zig Normal file
View file

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
const Window = @This();
context: *Context,
window_v1: *river.WindowV1,
width: i32 = 0,
height: i32 = 0,
x: i32 = 0,
y: i32 = 0,
link: wl.list.Link,
pub fn init(window: *Window, context: *Context, river_window_v1: *river.WindowV1) void {
window.* = .{
.context = context,
.window_v1 = river_window_v1,
.link = undefined, // Handled by the wl.list
};
window.window_v1.setListener(*Window, windowListener, window);
}
fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event, window: *Window) void {
assert(window.window_v1 == river_window_v1);
switch (event) {
else => |ev| {
log.debug("unhandled event: {s}", .{@tagName(ev)});
},
}
}
pub fn manage(window: *Window) void {
if (window.width != window.context.wm.outputs.first().?.width or
window.height != window.context.wm.outputs.first().?.height)
{
window.width = window.context.wm.outputs.first().?.width;
window.height = window.context.wm.outputs.first().?.height;
log.debug("setting window width={d} and height={d}", .{ window.width, window.height });
}
window.window_v1.proposeDimensions(window.width, window.height);
}
pub fn render(window: *Window) void {
_ = window;
}
const std = @import("std");
const assert = std.debug.assert;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const Context = @import("main.zig").Context;
const log = std.log.scoped(.Window);

93
src/WindowManager.zig Normal file
View file

@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
const WindowManager = @This();
context: *Context,
window_manager_v1: *river.WindowManagerV1,
seats: wl.list.Head(Seat, .link),
outputs: wl.list.Head(Output, .link),
windows: wl.list.Head(Window, .link),
pub fn init(wm: *WindowManager, context: *Context, window_manager_v1: *river.WindowManagerV1) void {
assert(wm == &context.wm);
wm.* = .{
.context = context,
.window_manager_v1 = window_manager_v1,
.seats = undefined,
.outputs = undefined,
.windows = undefined,
};
wm.seats.init();
wm.outputs.init();
wm.windows.init();
wm.window_manager_v1.setListener(*WindowManager, windowManagerV1Listener, wm);
}
fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: river.WindowManagerV1.Event, wm: *WindowManager) void {
assert(wm.window_manager_v1 == window_manager_v1);
const context = wm.context;
switch (event) {
.unavailable => {
log.err("Window manager unavailable (some other wm instance is running). Exiting", .{});
std.posix.exit(1);
},
.manage_start => {
log.debug("1", .{});
var it = wm.windows.iterator(.forward);
while (it.next()) |window| {
window.manage();
}
window_manager_v1.manageFinish();
},
.render_start => {
log.debug("2", .{});
var it = wm.windows.iterator(.forward);
while (it.next()) |window| {
window.render();
}
window_manager_v1.renderFinish();
},
.output => |ev| {
log.debug("3", .{});
var output = context.allocator.create(Output) catch @panic("out-of-memory; exiting.");
output.init(context, ev.id);
wm.outputs.append(output);
},
.seat => |ev| {
log.debug("4", .{});
var seat = context.allocator.create(Seat) catch @panic("out-of-memory; exiting.");
seat.init(context, ev.id);
wm.seats.append(seat);
},
.window => |ev| {
log.debug("5", .{});
var window = context.allocator.create(Window) catch @panic("out-of-memory; exiting.");
window.init(context, ev.id);
wm.windows.append(window);
},
else => |ev| {
log.debug("unhandled event: {s}", .{@tagName(ev)});
},
}
}
const std = @import("std");
const assert = std.debug.assert;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const Context = @import("main.zig").Context;
const Output = @import("Output.zig");
const Window = @import("Window.zig");
const log = std.log.scoped(.WindowManager);
const Seat = @import("Seat.zig");

View file

@ -1,9 +1,86 @@
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-License-Identifier: GPL-3.0-or-later
pub fn run(backend: Backend) !void {
var mask = posix.empty_sigset;
// TODO: Try to make this work on freebsd
os.linux.sigaddset(&mask, posix.SIG.INT);
os.linux.sigaddset(&mask, posix.SIG.QUIT);
posix.sigprocmask(posix.SIG.BLOCK, &mask, null);
const sig_fd = try posix.signalfd(-1, &mask, 0);
const poll_wayland = 0;
const poll_sig = 1;
var pollfds: [2]posix.pollfd = undefined;
pollfds[poll_wayland] = .{
.fd = backend.display.getFd(),
.events = posix.POLL.IN,
.revents = 0,
};
pollfds[poll_sig] = .{
.fd = sig_fd,
.events = posix.POLL.IN,
.revents = 0,
};
while (backend.initialized) {
{
const errno = backend.display.flush();
if (errno != .SUCCESS) {
log.err("wl_display_dispatch failed: {}", .{errno});
posix.exit(1);
}
}
if (pollfds[poll_wayland].revents & posix.POLL.HUP != 0) {
log.warn("disconnected by compositor", .{});
break;
}
if (pollfds[poll_wayland].revents & posix.POLL.IN != 0) {
const errno = backend.display.dispatch();
if (errno != .SUCCESS) {
log.err("failed to dispatch Wayland events", .{});
break;
}
}
if (pollfds[poll_sig].revents & posix.POLL.HUP != 0) {
log.err("received SIGHUP", .{});
posix.exit(1);
}
if (pollfds[poll_sig].revents & posix.POLL.IN != 0) {
var buf: [128]u8 = [_]u8{0} ** 128;
_ = posix.read(sig_fd, &buf) catch |err| {
log.err("failed to read from the signalfd {s}", .{@errorName(err)});
break;
};
const info: std.os.linux.signalfd_siginfo = @bitCast(buf);
// the mask should only catch these
std.debug.assert(info.signo == posix.SIG.INT or info.signo == posix.SIG.QUIT);
log.info("beanclock is exiting", .{});
break;
}
_ = backend.display.flush();
}
}
const std = @import("std");
const os = std.os;
const posix = std.posix;
pub fn run() void {
while (true) {}
}
const wayland = @import("wayland");
const wl = wayland.client.wl;
const Backend = @import("Backend.zig");
const log = std.log.scoped(.event_loop);

View file

@ -1,13 +1,48 @@
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-License-Identifier: GPL-3.0-or-later
const std = @import("std");
/// Context to pass some info around.
pub const Context = struct {
allocator: mem.Allocator,
const event_loop = @import("event_loop.zig");
const Backend = @import("Backend.zig");
initialized: bool,
const log = std.log.scoped(.main);
display: *wl.Display,
registry: *wl.Registry,
compositor: ?*wl.Compositor = null,
shm: ?*wl.Shm = null,
wm: WindowManager,
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, context: *Context) void {
switch (event) {
.global => |ev| {
if (mem.orderZ(u8, ev.interface, wl.Compositor.interface.name) == .eq) {
if (ev.version < 4) versionNotSupported(wl.Compositor, ev.version, 4);
context.compositor = registry.bind(ev.name, wl.Compositor, 4) catch |e| {
log.err("Failed to bind to compositor: {any}", .{@errorName(e)});
std.posix.exit(1);
};
} else if (mem.orderZ(u8, ev.interface, wl.Shm.interface.name) == .eq) {
context.shm = registry.bind(ev.name, wl.Shm, 1) catch |e| {
log.err("Failed to bind to shm: {any}", .{@errorName(e)});
std.posix.exit(1);
};
} else if (mem.orderZ(u8, ev.interface, river.WindowManagerV1.interface.name) == .eq) {
const window_manager_v1 = registry.bind(ev.name, river.WindowManagerV1, 1) catch |e| {
log.err("Failed to bind to window_manager_v: {any}", .{@errorName(e)});
std.posix.exit(1);
};
context.wm.init(context, window_manager_v1);
}
},
// We don't need .global_remove
.global_remove => {},
}
}
};
const usage: []const u8 =
\\usage: beansprout [options]
@ -21,12 +56,58 @@ const usage: []const u8 =
pub fn main() !void {
const allocator = std.heap.c_allocator;
// Set up Wayland stuff
var backend = try Backend.init(allocator);
defer backend.deinit();
const wl_display = wl.Display.connect(null) catch {
log.err("Error connecting to Wayland server. Exiting", .{});
std.posix.exit(1);
};
event_loop.run();
const registry = try wl_display.getRegistry();
// TODO: REMOVEME
log.debug("Exiting...", .{});
var context: Context = .{
.allocator = allocator,
.initialized = false,
.display = wl_display,
.registry = registry,
.wm = undefined,
};
registry.setListener(*Context, Context.registryListener, &context);
const errno = context.display.roundtrip();
if (errno != .SUCCESS) {
log.err("Initial roundtrip failed: E{s}", .{@tagName(errno)});
std.posix.exit(1);
}
while (true) {
if (wl_display.dispatch() != .SUCCESS) {
log.err("wayland display dispatch failed", .{});
std.posix.exit(1);
}
}
log.info("Exiting beansprout", .{});
}
fn interfaceNotAdvertised(comptime WaylandGlobal: type) noreturn {
log.err("{s} not advertised. Exiting", .{WaylandGlobal.interface.name});
std.posix.exit(1);
}
fn versionNotSupported(comptime WaylandGlobal: type, have_version: u32, need_version: u32) noreturn {
log.err("The compositor only advertised {s} version {d} but version {d} is required. Exiting", .{ WaylandGlobal.interface.name, have_version, need_version });
std.posix.exit(1);
}
const std = @import("std");
const mem = std.mem;
const wayland = @import("wayland");
const river = wayland.client.river;
const wl = wayland.client.wl;
const event_loop = @import("event_loop.zig");
const Backend = @import("Backend.zig");
const WindowManager = @import("WindowManager.zig");
const log = std.log.scoped(.main);