From 86177a190edacc4cffcf357b21bb505bbd1bb5e9 Mon Sep 17 00:00:00 2001 From: Squibid Date: Sun, 30 Nov 2025 22:30:46 -0500 Subject: [PATCH] initial commit --- .editorconfig | 12 +++ .gitignore | 2 + README.md | 6 ++ build.zig | 53 +++++++++++ build.zig.zon | 29 ++++++ protocol/mez-remote-lua-unstable-v1.xml | 89 +++++++++++++++++ src/main.zig | 94 ++++++++++++++++++ src/remote.zig | 121 ++++++++++++++++++++++++ src/util.zig | 14 +++ 9 files changed, 420 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 protocol/mez-remote-lua-unstable-v1.xml create mode 100644 src/main.zig create mode 100644 src/remote.zig create mode 100644 src/util.zig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..afe398a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://editorconfig.org +# top-most EditorConfig file +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.zig] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3389c86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.zig-cache/ +zig-out/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..9786d1b --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Whetstone +Sharpen your Mezzaluna via quick iterations in the remote REPL. + +Whetstone provides a way to interact with your currently running Mezzaluna +instance by sending lua code to execute, and receiving lua log information to +show you. diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..b1b9a5f --- /dev/null +++ b/build.zig @@ -0,0 +1,53 @@ +const std = @import("std"); + +const Scanner = @import("wayland").Scanner; + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const scanner = Scanner.create(b, .{}); + scanner.addCustomProtocol(b.path("protocol/mez-remote-lua-unstable-v1.xml")); + scanner.generate("wl_compositor", 1); + scanner.generate("zmez_remote_lua_manager_v1", 1); + + const wayland = b.createModule(.{ .root_source_file = scanner.result }); + const wlroots = b.dependency("wlroots", .{}).module("wlroots"); + const zlua = b.dependency("zlua", .{}).module("zlua"); + const zargs = b.dependency("args", .{ .target = target, .optimize = optimize }).module("args"); + + wlroots.addImport("wayland", wayland); + wlroots.resolved_target = target; + wlroots.linkSystemLibrary("wlroots-0.19", .{}); + + const whet = b.addExecutable(.{ + .name = "whet", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + + whet.linkLibC(); + + whet.root_module.addImport("wayland", wayland); + whet.root_module.addImport("wlroots", wlroots); + whet.root_module.addImport("zlua", zlua); + whet.root_module.addImport("args", zargs); + + whet.root_module.linkSystemLibrary("wayland-client", .{}); + + b.installArtifact(whet); + + const run_step = b.step("run", "Run the app"); + + const run_cmd = b.addRunArtifact(whet); + run_step.dependOn(&run_cmd.step); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..6976916 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,29 @@ +.{ + .name = .whet, + .version = "0.0.1", + .fingerprint = 0xde8af7661a3498e4, + .minimum_zig_version = "0.15.2", + .dependencies = .{ + .wayland = .{ + .url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.4.0.tar.gz", + .hash = "wayland-0.4.0-lQa1khbMAQAsLS2eBR7M5lofyEGPIbu2iFDmoz8lPC27", + }, + .wlroots = .{ + .url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.19.3.tar.gz", + .hash = "wlroots-0.19.3-jmOlcuL_AwBHhLCwpFsXbTizE3q9BugFmGX-XIxqcPMc", + }, + .zlua = .{ + .url = "git+https://github.com/natecraddock/ziglua#39f8df588d0864070deffa308ef575bf492777a0", + .hash = "zlua-0.1.0-hGRpC6E9BQDBGKPqzmCRsI6Xd8jH9KohccmX69-L6HyS", + }, + .args = .{ + .url = "git+https://github.com/ikskuh/zig-args?ref=master#e060ac80c244e9675471b6d213b22ddc83cc8f98", + .hash = "args-0.0.0-CiLiqo_RAADz2TiHUzG5-0Mk7IZHR-h1SZgUrb_k4c7d", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/protocol/mez-remote-lua-unstable-v1.xml b/protocol/mez-remote-lua-unstable-v1.xml new file mode 100644 index 0000000..44605af --- /dev/null +++ b/protocol/mez-remote-lua-unstable-v1.xml @@ -0,0 +1,89 @@ + + + + Copyright © 2025 mezzaluna team + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + This protocol allows clients to receive lua errors from mez and execute lua + on a running mez server with the lua mez api. + + Warning! The protocol described in this file is experimental and backward + incompatible changes may be made. Backward compatible changes may be added + together with the corresponding interface version bump. Backward + incompatible changes are done by bumping the version number in the protocol + and interface names and resetting the interface version. Once the protocol + is to be declared stable, the 'z' prefix and the version number in the + protocol and interface names are removed and the interface version number + is reset. + + + + + A global factory for zmez_remote_lua_v1 objects. + + + + + This request indicates that the client will not use the + mez_remote_lua_manager object any more. Objects that have been created + through this instance are not affected. + + + + + + This creates a new mez_remote_lua_v1 object. + All lua related communication is done through this interface. + + + + + + + + This interface allows clients to receive lua logs from the compositor. + Additionally it allow for the client to send lua code for the compositor + to execute. + + + + + This request indicates that the client will not use the + mez_remote_lua_v1 object any more. + + + + + + The compositor sends this event to inform the client that it has a new + log entry for the client. + + The text contains the lua log information the server generates when + executing lua code. The server does not hold logs from before the client + connect, and therefore you will only recieve log information from the + point that you start listening for them and on. + + + + + + + This request sends stringified lua code for the server to run. + The output, if any, will be sent through the new_log_entry event. + + + + + diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..fcabb2f --- /dev/null +++ b/src/main.zig @@ -0,0 +1,94 @@ +const std = @import("std"); +const argsParser = @import("args"); + +const util = @import("util.zig"); +const Remote = @import("remote.zig"); + +const gpa = std.heap.c_allocator; + +var remote: Remote = undefined; + +fn loop(input: bool) !void { + var pollfds: [2]std.posix.pollfd = undefined; + + pollfds[0] = .{ // wayland fd + .fd = remote.display.getFd(), + .events = std.posix.POLL.IN, + .revents = 0, + }; + if (input) { + pollfds[1] = .{ // stdin + .fd = std.posix.STDIN_FILENO, + .events = std.posix.POLL.IN, + .revents = 0, + }; + } + + var buf: [2]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buf); + const stdout = &stdout_writer.interface; + while (true) { + if (input) { + try stdout.print("> ", .{}); + try stdout.flush(); + } + + _ = std.posix.poll(&pollfds, -1) catch |err| { + util.fatal("poll() failed: {s}", .{@errorName(err)}); + }; + + for (pollfds) |fd| { + if (fd.revents & std.posix.POLL.IN == 1) { + if (fd.fd == std.posix.STDIN_FILENO) { + var in_buf: [1024]u8 = undefined; + const len = std.posix.read(fd.fd, &in_buf) catch 0; + + if (len == 0) { + try stdout.print("\n", .{}); + continue; + } + + if (in_buf[len - 1] == '\n') in_buf[len - 1] = 0 else in_buf[len] = 0; + if (remote.remote_lua) |rl| rl.pushLua(@ptrCast(in_buf[0..len].ptr)); + } + + // FIXME: we really shouldn't be reading from the socket + if (fd.fd == remote.display.getFd()) { + var in_buf: [1024]u8 = undefined; + const len = std.posix.read(fd.fd, &in_buf) catch 0; + std.debug.print("\n{s}", .{in_buf[0..len]}); + } + } + } + + try remote.flush(); + } +} + +pub fn main() !void { + const options = argsParser.parseForCurrentProcess(struct { + // long options + code: ?[]const u8 = null, + @"follow-log": bool = false, + + // short-hand options + pub const shorthands = .{ + .c = "code", + .f = "follow-log", + }; + }, gpa, .print) catch return; + defer options.deinit(); + + // connect to the compositor + remote = Remote.init(); + defer remote.deinit(); + + // handle options + if (options.options.code) |c| { + remote.remote_lua.?.pushLua(@ptrCast(c[0..].ptr)); + } else if (options.options.@"follow-log") { + try loop(false); + } else { + try loop(true); + } +} diff --git a/src/remote.zig b/src/remote.zig new file mode 100644 index 0000000..8b281d5 --- /dev/null +++ b/src/remote.zig @@ -0,0 +1,121 @@ +const Remote = @This(); + +const std = @import("std"); +const posix = std.posix; +const wayland = @import("wayland"); +const wl = wayland.client.wl; +const mez = wayland.client.zmez; + +const util = @import("util.zig"); + +display: *wl.Display, +registry: *wl.Registry, +compositor: ?*wl.Compositor, +remote_lua_manager: ?*mez.RemoteLuaManagerV1, +remote_lua: ?*mez.RemoteLuaV1, + +pub fn init() Remote { + var self: Remote = .{ + .registry = undefined, + .compositor = null, + .remote_lua = null, + .remote_lua_manager = null, + .display = wl.Display.connect(null) catch |err| { + util.fatal("failed to connect to a wayland compositor: {s}", .{@errorName(err)}); + }, + }; + + self.registry = self.display.getRegistry() catch unreachable; + errdefer self.registry.destroy(); + self.registry.setListener(*Remote, registry_listener, &self); + + const errno = self.display.roundtrip(); + if (errno != .SUCCESS) { + util.fatal("initial roundtrip failed: {s}", .{@tagName(errno)}); + } + + if (self.compositor == null) util.not_advertised(wl.Compositor); + if (self.remote_lua_manager == null) util.not_advertised(mez.RemoteLuaManagerV1); + + self.remote_lua = self.remote_lua_manager.?.getRemote() catch util.oom(); + if (self.remote_lua) |rl| { + std.log.info("yayayy", .{}); + rl.setListener(?*anyopaque, handleRemote, null); + } else { + std.log.err("no luck", .{}); + } + + return self; +} + +pub fn flush(self: *Remote) !void { + while (true) { + while (!self.display.prepareRead()) { + const errno = self.display.dispatchPending(); + if (errno != .SUCCESS) { + util.fatal("failed to dispatch pending wayland events: E{s}", .{@tagName(errno)}); + } + } + + const errno = self.display.flush(); + switch (errno) { + .SUCCESS => return, + .PIPE => { + // libwayland uses this error to indicate that the wayland server + // closed its side of the wayland socket. We want to continue to + // read any buffered messages from the server though as there is + // likely a protocol error message we'd like libwayland to log. + _ = self.display.readEvents(); + util.fatal("connection to wayland server unexpectedly terminated", .{}); + }, + else => { + util.fatal("failed to flush wayland requests: E{s}", .{@tagName(errno)}); + }, + } + } +} + +pub fn deinit(self: *Remote) void { + self.registry.destroy(); +} + +fn registry_listener( +registry: *wl.Registry, +event: wl.Registry.Event, +remote: *Remote, +) void { + registry_event(registry, event, remote) catch |err| switch (err) { + error.OutOfMemory => util.oom(), + }; +} + +fn registry_event(registry: *wl.Registry, event: wl.Registry.Event, remote: *Remote) !void { + switch (event) { + .global => |ev| { + if (std.mem.orderZ(u8, ev.interface, wl.Compositor.interface.name) == .eq) { + const ver = 1; + if (ev.version < 1) { + util.fatal("advertised wl_compositor version too old, version {} required", .{ver}); + } + remote.compositor = try registry.bind(ev.name, wl.Compositor, ver); + } else if (std.mem.orderZ(u8, ev.interface, mez.RemoteLuaManagerV1.interface.name) == .eq) { + const ver = 1; + if (ev.version < ver) { + util.fatal("advertised remote_lua_manager version too old, version {} required", .{ver}); + } + remote.remote_lua_manager = try registry.bind(ev.name, mez.RemoteLuaManagerV1, ver); + } + }, + .global_remove => {}, + } +} + +// FIXME: this doesn't actually handle events for some reason and we currently +// just read from the socket directly +fn handleRemote(_: *mez.RemoteLuaV1, event: mez.RemoteLuaV1.Event, _: ?*anyopaque) void { + switch (event) { + .new_log_entry => |e| { + std.log.info("{s}", .{e.text}); + }, + } +} diff --git a/src/util.zig b/src/util.zig new file mode 100644 index 0000000..d5f58ae --- /dev/null +++ b/src/util.zig @@ -0,0 +1,14 @@ +const std = @import("std"); + +pub fn fatal(comptime str: []const u8, opts: anytype) noreturn { + std.log.err(str, opts); + std.process.exit(1); +} + +pub fn oom() noreturn { + fatal("out of memory", .{}); +} + +pub fn not_advertised(comptime Global: type) noreturn { + fatal("{s} not advertised", .{Global.interface.name}); +}