initial commit

This commit is contained in:
2026-07-26 20:47:19 +02:00
commit fff3a5e734
37 changed files with 10781 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
//! Thin typed wrappers over the Linux syscalls att_wm needs.
//!
//! Zig 0.16 moved most of `std.posix` behind the new `std.Io` interface, which
//! is the wrong shape for a window manager: everything here is a raw fd driven
//! by a single `poll()` loop, with no allocator and no async runtime. Going
//! straight to `std.os.linux` is both simpler and closer to what the code
//! actually does.
const std = @import("std");
const linux = std.os.linux;
/// Must be the linux decoder, not `std.posix.errno`: with libc linked the
/// latter expects a libc-style -1 return and reports every raw syscall error
/// as success, which then overflows the casts below.
const errno = linux.errno;
pub const fd_t = linux.fd_t;
pub const pid_t = linux.pid_t;
pub const E = linux.E;
pub const Error = error{
Again,
Interrupted,
ConnectionReset,
AddressInUse,
NotFound,
PermissionDenied,
ConnectionRefused,
BrokenPipe,
NameTooLong,
OutOfMemory,
Unexpected,
};
fn check(rc: usize) Error!usize {
return switch (errno(rc)) {
.SUCCESS => rc,
.AGAIN => error.Again,
.INTR => error.Interrupted,
.ADDRINUSE => error.AddressInUse,
.NOENT => error.NotFound,
.ACCES, .PERM => error.PermissionDenied,
.CONNREFUSED => error.ConnectionRefused,
.CONNRESET => error.ConnectionReset,
.PIPE => error.BrokenPipe,
.NAMETOOLONG => error.NameTooLong,
.NOMEM => error.OutOfMemory,
else => error.Unexpected,
};
}
pub fn read(fd: fd_t, buf: []u8) Error!usize {
if (buf.len == 0) return 0;
while (true) {
return check(linux.read(fd, buf.ptr, buf.len)) catch |err| switch (err) {
error.Interrupted => continue,
else => err,
};
}
}
pub fn write(fd: fd_t, bytes: []const u8) Error!usize {
if (bytes.len == 0) return 0;
return check(linux.write(fd, bytes.ptr, bytes.len));
}
/// Write everything, retrying short writes. Best effort: errors are swallowed
/// because every caller is emitting diagnostics or usage text.
pub fn writeAllBestEffort(fd: fd_t, bytes: []const u8) void {
var off: usize = 0;
while (off < bytes.len) {
off += write(fd, bytes[off..]) catch return;
}
}
pub fn close(fd: fd_t) void {
_ = linux.close(fd);
}
pub fn socket(domain: u32, socket_type: u32, protocol: u32) Error!fd_t {
return @intCast(try check(linux.socket(domain, socket_type, protocol)));
}
pub fn bind(fd: fd_t, addr: *const linux.sockaddr, len: linux.socklen_t) Error!void {
_ = try check(linux.bind(fd, addr, len));
}
pub fn listen(fd: fd_t, backlog: u31) Error!void {
_ = try check(linux.listen(fd, backlog));
}
pub fn accept4(fd: fd_t, flags: u32) Error!fd_t {
return @intCast(try check(linux.accept4(fd, null, null, flags)));
}
pub fn connect(fd: fd_t, addr: *const linux.sockaddr, len: linux.socklen_t) Error!void {
_ = try check(linux.connect(fd, addr, len));
}
pub fn ftruncate(fd: fd_t, length: u64) Error!void {
_ = try check(linux.ftruncate(fd, @intCast(length)));
}
/// Make a memfd immutable, so a compositor mapping it cannot have the bytes
/// changed underneath it.
pub fn addSeals(fd: fd_t, seals: usize) Error!void {
_ = try check(linux.fcntl(fd, linux.F.ADD_SEALS, seals));
}
pub fn timerfdCreate(flags: linux.TFD) Error!fd_t {
return @intCast(try check(linux.timerfd_create(.MONOTONIC, flags)));
}
pub fn timerfdSetTime(fd: fd_t, spec: *const linux.itimerspec) Error!void {
_ = try check(linux.timerfd_settime(fd, .{}, spec, null));
}
pub fn fork() Error!pid_t {
return @intCast(try check(linux.fork()));
}
pub fn setsid() void {
_ = linux.setsid();
}
pub fn exit(code: u8) noreturn {
linux.exit(code);
}
pub fn waitpid(pid: pid_t) void {
var status: u32 = undefined;
while (true) {
const rc = linux.wait4(pid, &status, 0, null);
switch (errno(rc)) {
.INTR => continue,
else => return,
}
}
}
/// Remove a path. Best effort: the only caller is clearing a stale socket.
pub fn unlink(path: []const u8) void {
var buf: [std.fs.max_path_bytes]u8 = undefined;
if (path.len >= buf.len) return;
@memcpy(buf[0..path.len], path);
buf[path.len] = 0;
_ = linux.unlink(@ptrCast(&buf));
}
/// Build a unix socket address. Paths must fit in sun_path with room for the
/// terminating NUL.
pub fn sockaddrUn(path: []const u8) Error!linux.sockaddr.un {
var addr: linux.sockaddr.un = .{ .family = linux.AF.UNIX, .path = undefined };
if (path.len >= addr.path.len) return error.NameTooLong;
@memset(&addr.path, 0);
@memcpy(addr.path[0..path.len], path);
return addr;
}
pub fn sockaddrUnLen(addr: *const linux.sockaddr.un) linux.socklen_t {
_ = addr;
return @sizeOf(linux.sockaddr.un);
}
/// execvp: run `argv[0]`, searching PATH when it contains no slash.
///
/// Only ever called between fork() and exec in the child, so it must not
/// allocate; the candidate path is assembled in a stack buffer.
pub fn execvpe(
argv: [*:null]const ?[*:0]const u8,
envp: [*:null]const ?[*:0]const u8,
path_env: ?[]const u8,
) Error {
const file = std.mem.span(argv[0].?);
if (std.mem.indexOfScalar(u8, file, '/') != null) {
return execErr(linux.execve(argv[0].?, argv, envp));
}
const search = path_env orelse "/usr/local/bin:/usr/bin:/bin";
var buf: [std.fs.max_path_bytes]u8 = undefined;
var last: Error = error.NotFound;
var it = std.mem.tokenizeScalar(u8, search, ':');
while (it.next()) |dir| {
if (dir.len + 1 + file.len + 1 > buf.len) continue;
@memcpy(buf[0..dir.len], dir);
buf[dir.len] = '/';
@memcpy(buf[dir.len + 1 ..][0..file.len], file);
buf[dir.len + 1 + file.len] = 0;
const candidate: [*:0]const u8 = @ptrCast(&buf);
last = execErr(linux.execve(candidate, argv, envp));
// ENOENT just means "not in this directory"; keep looking.
if (last != error.NotFound) return last;
}
return last;
}
/// execve only returns on failure, so its result is always an error.
fn execErr(rc: usize) Error {
_ = check(rc) catch |err| return err;
return error.Unexpected;
}