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