mirror of
https://github.com/MezzalunaWM/Whetstone.git
synced 2026-03-07 19:49:53 -05:00
initial commit
This commit is contained in:
commit
86177a190e
9 changed files with 420 additions and 0 deletions
12
.editorconfig
Normal file
12
.editorconfig
Normal file
|
|
@ -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
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.zig-cache/
|
||||
zig-out/
|
||||
6
README.md
Normal file
6
README.md
Normal file
|
|
@ -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.
|
||||
53
build.zig
Normal file
53
build.zig
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
29
build.zig.zon
Normal file
29
build.zig.zon
Normal file
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
89
protocol/mez-remote-lua-unstable-v1.xml
Normal file
89
protocol/mez-remote-lua-unstable-v1.xml
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<protocol name="zmez_remote_lua_v1">
|
||||
<copyright>
|
||||
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.
|
||||
</copyright>
|
||||
<description summary="Remote lua interface for Mezzaluna">
|
||||
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.
|
||||
</description>
|
||||
|
||||
<interface name="zmez_remote_lua_manager_v1" version="2">
|
||||
<description summary="manage mez layout objects">
|
||||
A global factory for zmez_remote_lua_v1 objects.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the mez_remote_lua_manager object">
|
||||
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.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="get_remote">
|
||||
<description summary="create a mez_remote_lua_v1 object">
|
||||
This creates a new mez_remote_lua_v1 object.
|
||||
All lua related communication is done through this interface.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="zmez_remote_lua_v1"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="zmez_remote_lua_v1" version="2">
|
||||
<description summary="receive lua logs and send lua code to execute">
|
||||
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.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the mez_remote_lua_v1 object">
|
||||
This request indicates that the client will not use the
|
||||
mez_remote_lua_v1 object any more.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="new_log_entry">
|
||||
<description summary="the compositor has a new log entry">
|
||||
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.
|
||||
</description>
|
||||
<arg name="text" type="string" summary="the log text"/>
|
||||
</event>
|
||||
|
||||
<request name="push_lua">
|
||||
<description summary="propose dimensions of the next view">
|
||||
This request sends stringified lua code for the server to run.
|
||||
The output, if any, will be sent through the new_log_entry event.
|
||||
</description>
|
||||
<arg name="lua_chunk" type="string" summary="stringified lua code"/>
|
||||
</request>
|
||||
</interface>
|
||||
</protocol>
|
||||
94
src/main.zig
Normal file
94
src/main.zig
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
121
src/remote.zig
Normal file
121
src/remote.zig
Normal file
|
|
@ -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});
|
||||
},
|
||||
}
|
||||
}
|
||||
14
src/util.zig
Normal file
14
src/util.zig
Normal file
|
|
@ -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});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue