This commit is contained in:
Jeffrey C. Ollie 2025-01-03 13:37:31 -06:00
parent a1d15f184c
commit d6d42bbec1
Signed by: jeff
GPG key ID: 6F86035A6D97044E
4 changed files with 244 additions and 39 deletions

View file

@ -1,15 +1,41 @@
const std = @import("std");
const Scanner = @import("wayland").Scanner;
const path = @import("src/path.zig");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const gdbus = b.option([]const u8, "gdbus", "path to gdbus binary") orelse {
return;
const rivertile = b.option([]const u8, "rivertile", "path to rivertile binary") orelse rivertile: {
const p = path.expandPath(b.allocator, "rivertile") catch {
break :rivertile "rivertile";
} orelse {
break :rivertile "rivertile";
};
break :rivertile p;
};
const fuzzel = b.option([]const u8, "fuzzel", "path to fuzzel binary") orelse fuzzel: {
const p = path.expandPath(b.allocator, "fuzzel") catch {
break :fuzzel "fuzzel";
} orelse {
break :fuzzel "fuzzel";
};
break :fuzzel p;
};
const gdbus = b.option([]const u8, "gdbus", "path to gdbus binary") orelse gdbus: {
const p = path.expandPath(b.allocator, "gdbus") catch {
break :gdbus "gdbus";
} orelse {
break :gdbus "gdbus";
};
break :gdbus p;
};
const build_options = b.addOptions();
build_options.addOption([]const u8, "rivertile", rivertile);
build_options.addOption([]const u8, "fuzzel", fuzzel);
build_options.addOption([]const u8, "gdbus", gdbus);
const scanner = Scanner.create(b, .{});
@ -30,10 +56,12 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
exe.root_module.addOptions("build_options", build_options);
exe.root_module.addImport("wayland", wayland);
exe.linkSystemLibrary2("wayland-client", .{});
exe.linkLibC();
scanner.addCSource(exe);
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);

View file

@ -23,7 +23,9 @@
default = pkgs.mkShell {
nativeBuildInputs = [
pkgs.zig_0_13
pkgs.glib
pkgs.pkg-config
pkgs.river
pkgs.wayland-protocols
pkgs.wayland-scanner
];
@ -36,21 +38,17 @@
};
};
packages = {
zmodem = let
river-init = let
cache = src:
pkgs.stdenv.mkDerivation {
inherit src;
name = "river-init-cache";
nativeBuildInputs = [
pkgs.git
pkgs.pkg-config
pkgs.wayland-protocols
pkgs.wayland-scanner
pkgs.zig_0_13.hook
];
buildInputs = [
pkgs.wayland
];
dontConfigure = true;
dontUseZigBuild = true;
@ -78,8 +76,23 @@
version = "0.0.0";
src = ./.;
nativeBuildInputs = [
pkgs.pkg-config
pkgs.wayland-protocols
pkgs.wayland-scanner
pkgs.zig_0_13.hook
];
buildInputs = [
pkgs.glib
pkgs.river
pkgs.wayland
];
zigBuildFlags = [
"-Drivertile=${pkgs.river}/bin/rivertile"
"-Dgdbus=${pkgs.glib}/bin/gdbus"
];
preBuild = ''
rm -rf $ZIG_GLOBAL_CACHE_DIR
cp -r --reflink=auto ${cache final.src} $ZIG_GLOBAL_CACHE_DIR

View file

@ -1,9 +1,20 @@
const std = @import("std");
const build_options = @import("build_options");
const wayland = @import("wayland");
const wl = wayland.client.wl;
const zriver = wayland.client.zriver;
const log = std.log.scoped(.init);
const Error = error{
RiverControlNotAdvertised,
RiverStatusManagerNotAdvertised,
SeatNotAdverstised,
OutputNotAdverstised,
ConnectFailed,
};
pub fn main() !void {
_main() catch |err| {
switch (err) {
@ -37,26 +48,82 @@ pub fn main() !void {
pub const Globals = struct {
control: ?*zriver.ControlV1 = null,
status_manager: ?*zriver.StatusManagerV1 = null,
output: ?*wl.Output = null,
// output: std.ArrayListUnmanaged(*wl.Output) = .{},
seat: ?*wl.Seat = null,
};
fn _main() !void {
fn _main() (Error || std.mem.Allocator.Error)!void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const gpa_alloc = gpa.allocator();
const display = try wl.Display.connect(null);
const registry = try display.getRegistry();
var globals = Globals{};
registry.setListener(*Globals, registryListener, &globals);
if (display.roundtrip() != .SUCCESS) fatal("initial roundtrip failed", .{});
const control = globals.control orelse return error.RiverControlNotAdvertised;
_ = globals.status_manager orelse return error.RiverStatusManagerNotAdvertised;
_ = globals.seat orelse return error.SeatNotAdverstised;
_ = globals.output orelse return error.OutputNotAdverstised;
control.addArgument("map");
control.addArgument("Super");
control.addArgument("G");
control.addArgument("spawn");
control.addArgument("gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new_window [] []'");
if (display.roundtrip() != .SUCCESS)
fatal("initial roundtrip failed", .{});
const control = globals.control orelse return error.RiverControlNotAdvertised;
defer control.destroy();
const status_manager = globals.status_manager orelse return error.RiverStatusManagerNotAdvertised;
defer status_manager.destroy();
const seat = globals.seat orelse return error.SeatNotAdverstised;
defer seat.destroy();
log.info("rivertile: {s}", .{build_options.rivertile});
log.info("gdbus: {s}", .{build_options.gdbus});
var arena = std.heap.ArenaAllocator.init(gpa_alloc);
defer arena.deinit();
const alloc = arena.allocator();
try riverControl(display, control, seat, &.{
"background-color",
"0x002b36",
});
try riverControl(display, control, seat, &.{
"border-color-focused",
"0x93a1a1",
});
try riverControl(display, control, seat, &.{
"border-color-unfocused",
"0x586e75",
});
try riverControl(display, control, seat, &.{
"default-layout",
"rivertile",
});
try riverControl(display, control, seat, &.{
"map",
"normal",
"Super",
"G",
"spawn",
try std.fmt.allocPrint(
alloc,
"{[gdbus]s} call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new_window [] []",
.{ .gdbus = build_options.gdbus },
),
});
try riverControl(display, control, seat, &.{
"map",
"normal",
"Super",
"D",
"spawn",
try std.fmt.allocPrint(
alloc,
"{[fuzzel]s} --show-actions",
.{ .fuzzel = build_options.fuzzel },
),
});
}
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: *Globals) void {
@ -64,9 +131,11 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: *
.global => |global| {
if (std.mem.orderZ(u8, global.interface, wl.Seat.getInterface().name) == .eq) {
std.debug.assert(globals.seat == null); // TODO: support multiple seats
log.info("seat: {d}", .{global.name});
globals.seat = registry.bind(global.name, wl.Seat, global.version) catch @panic("out of memory");
} else if (std.mem.orderZ(u8, global.interface, wl.Output.getInterface().name) == .eq) {
globals.output = registry.bind(global.name, wl.Output, global.version) catch @panic("out of memory");
log.info("output: {d}", .{global.name});
// globals.output = registry.bind(global.name, wl.Output, global.version) catch @panic("out of memory");
} else if (std.mem.orderZ(u8, global.interface, zriver.ControlV1.getInterface().name) == .eq) {
globals.control = registry.bind(global.name, zriver.ControlV1, global.version) catch @panic("out of memory");
} else if (std.mem.orderZ(u8, global.interface, zriver.StatusManagerV1.getInterface().name) == .eq) {
@ -77,28 +146,42 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: *
}
}
fn callbackListener(_: *zriver.CommandCallbackV1, event: zriver.CommandCallbackV1.Event, _: ?*anyopaque) void {
switch (event) {
.success => |success| {
if (std.mem.len(success.output) > 0) {
const stdout = std.io.getStdOut().writer();
stdout.print("{s}\n", .{success.output}) catch @panic("failed to write to stdout");
}
std.posix.exit(0);
fn riverControl(display: *wl.Display, control: *zriver.ControlV1, seat: *wl.Seat, args: []const [:0]const u8) !void {
for (args) |arg|
control.addArgument(arg);
const callback = try control.runCommand(seat);
var done: std.Thread.Semaphore = .{};
callback.setListener(*std.Thread.Semaphore, riverControlCallback, &done);
log.info("dispatch", .{});
switch (display.dispatch()) {
.SUCCESS => {
log.info("SUCCESS", .{});
done.wait();
},
.failure => |failure| {
// A small hack to provide usage text when river reports an unknown command.
if (std.mem.orderZ(u8, failure.failure_message, "unknown command") == .eq) {
std.log.err("unknown command", .{});
std.io.getStdErr().writeAll("blah blah blah") catch {};
std.posix.exit(1);
}
fatal("{s}", .{failure.failure_message});
else => |err| {
log.err("problem! {}", .{err});
},
}
}
fn riverControlCallback(_: *zriver.CommandCallbackV1, event: zriver.CommandCallbackV1.Event, done: *std.Thread.Semaphore) void {
switch (event) {
.success => |success| {
if (std.mem.len(success.output) > 0) {
log.info("{s}", .{success.output});
}
},
.failure => |failure| {
log.err("{s}", .{failure.failure_message});
},
}
done.post();
}
fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.log.err(format, args);
log.err(format, args);
std.posix.exit(1);
}

81
src/path.zig Normal file
View file

@ -0,0 +1,81 @@
const std = @import("std");
const builtin = @import("builtin");
const log = std.log.scoped(.path);
pub fn expandPath(alloc: std.mem.Allocator, cmd: []const u8) !?[]u8 {
const PATH = std.posix.getenv("PATH") orelse return null;
var path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
var seen_eaccess = false;
var it = std.mem.tokenizeScalar(u8, PATH, std.fs.path.delimiter);
search: while (it.next()) |search_path| {
const path_len = search_path.len + cmd.len + 1;
if (path_buf.len < path_len) return error.PathTooLong;
@memcpy(path_buf[0..search_path.len], search_path);
path_buf[search_path.len] = std.fs.path.sep;
@memcpy(path_buf[search_path.len + 1 ..][0..cmd.len], cmd);
path_buf[path_len] = 0;
var path = path_buf[0..path_len :0];
follow: while (true) {
var stat: std.os.linux.Stat = undefined;
{
const rc = std.os.linux.lstat(path, &stat);
switch (std.posix.errno(rc)) {
.SUCCESS => {},
.NOENT => continue :search,
.PERM => {
seen_eaccess = true;
continue :search;
},
else => |err| {
log.err("error: {}", .{err});
return error.Unknown;
},
}
}
if (stat.mode & std.os.linux.S.IFMT == std.os.linux.S.IFLNK) {
log.info("symlink {s}", .{path});
const rc = std.os.linux.readlink(path, &path_buf, path_buf.len);
switch (std.posix.errno(rc)) {
.SUCCESS => {
path_buf[rc] = 0;
path = path_buf[0..rc :0];
continue :follow;
},
.NOENT => continue :search,
.PERM => {
seen_eaccess = true;
continue :search;
},
else => |err| {
log.err("{}", .{err});
return error.Unknown;
},
}
}
if (stat.mode & std.os.linux.S.IFMT == std.os.linux.S.IFREG) {
log.info("file {s}", .{path});
if (isExecutable(stat.mode))
return try alloc.dupe(u8, path);
}
continue :search;
}
}
if (seen_eaccess) return error.AccessDenied;
return null;
}
pub fn isExecutable(mode: std.os.linux.mode_t) bool {
return mode & 0o0111 != 0;
}