Initial Commit!

This commit is contained in:
LordMZTE 2022-06-11 23:52:21 +02:00
commit 105eb205e6
Signed by: LordMZTE
GPG Key ID: B64802DC33A64FF6
17 changed files with 988 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
# Zig
zig-*
# Packages
deps.zig
# Gyro
.gyro/
gyro.lock

33
README.md Normal file
View File

@ -0,0 +1,33 @@
# zellzig
A zig framework for writing [zellij](https://zellij.dev/) plugins.
# usage
For an example of how to use it, see the example directory.
Here's a quick overview:
```zig
const std = @import("std");
const zz = @import("zellzig");
comptime {
// register plugin
zz.createPlugin(@This());
}
pub fn init() void {
// do initialization stuff
}
pub fn update(ev: zz.Event) void {
// handle events
}
pub fn render(rows: i32, cols: i32) void {
// draw UI
}
```
# development
PRs are always welcome if you feel that something needs improvement/fixing! Make sure to follow [Conventional Commits](https://www.conventionalcommits.org/) and to run tests first, though.
Run tests using `gyro build test`. Note that tests are run on the native target, not WASM.

29
build.zig Normal file
View File

@ -0,0 +1,29 @@
const std = @import("std");
const pkgs = @import("deps.zig").pkgs;
pub fn build(b: *std.build.Builder) void {
const docs = b.option(bool, "docs", "emit docs") orelse false;
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const lib = b.addStaticLibrary("zellzig", "src/main.zig");
lib.setBuildMode(mode);
lib.target.cpu_arch = .wasm32;
lib.target.os_tag = .wasi;
pkgs.addAllTo(lib);
if (docs) {
lib.emit_docs = .{ .emit_to = "zig-out/docs" };
}
lib.install();
const main_tests = b.addTest("src/main.zig");
main_tests.setBuildMode(mode);
pkgs.addAllTo(main_tests);
const test_step = b.step("test", "Run library tests");
test_step.dependOn(&main_tests.step);
}

9
example/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
# Zig
zig-*
# Packages
deps.zig
# Gyro
.gyro/
gyro.lock

12
example/README.md Normal file
View File

@ -0,0 +1,12 @@
# example
This is an example of a simple zellij plugin in zig.
It's a super simple status bar.
If you plan on making your own plugin, be sure to pay special attention to `build.zig` and `gyro.zzz` to configure dependancies and WASM compilation.
# building & running
```bash
gyro build -Drelease-fast
zellij --layout-path plugin.yaml
```

22
example/build.zig Normal file
View File

@ -0,0 +1,22 @@
const std = @import("std");
const deps = @import("deps.zig");
pub fn build(b: *std.build.Builder) void {
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const lib = b.addSharedLibrary("example", "src/main.zig", .{ .unversioned = {} });
lib.setBuildMode(mode);
lib.target.cpu_arch = .wasm32;
lib.target.os_tag = .wasi;
deps.pkgs.addAllTo(lib);
lib.install();
const main_tests = b.addTest("src/main.zig");
main_tests.setBuildMode(mode);
deps.pkgs.addAllTo(main_tests);
const test_step = b.step("test", "Run library tests");
test_step.dependOn(&main_tests.step);
}

10
example/gyro.zzz Normal file
View File

@ -0,0 +1,10 @@
pkgs:
zellzig-example:
version: 0.0.0
description: A basic zellzig plugin
license: GPL-3.0
root: src/main.zig
deps:
zellzig:
local: ..
root: src/main.zig

22
example/plugin.yaml Normal file
View File

@ -0,0 +1,22 @@
---
template:
direction: Horizontal
parts:
- direction: Vertical
borderless: true
split_size:
Fixed: 1
run:
plugin:
location: "zellij:tab-bar"
- direction: Vertical
plugin: "zig-out/lib/example.wasm"
- direction: Vertical
borderless: true
split_size:
Fixed: 1
run:
plugin:
location: "file:zig-out/lib/example.wasm"

49
example/src/main.zig Normal file
View File

@ -0,0 +1,49 @@
const std = @import("std");
const zz = @import("zellzig");
comptime {
// This function creates all the exports needed by zellij plugins.
// As a paremeter, you should pass the struct that contains the
// `init`, `update` and `render` functions. It is recommended
// to put these functions in your root file, and pass the root
// struct by using `@This()`.
zz.createPlugin(@This());
}
// assign out allocator to make sure it doesn't get free'd once init returns
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
var mode: ?zz.types.InputMode = null;
// called on startup
pub fn init() void {
// set zellzig's allocator
// This is required to receive events.
zz.allocator = gpa.allocator();
// This is required to make zellij close once everything but our plugin is gone.
zz.api.setSelectable(false);
// Make sure we get events.
zz.api.subscribe(&[_]zz.types.EventType{.ModeUpdate}) catch unreachable;
}
// called on every event
pub fn update(ev: zz.Event) void {
switch (ev) {
.ModeUpdate => |mode_info| mode = mode_info.mode,
else => {},
}
}
// called to draw the UI
pub fn render(rows: i32, cols: i32) void {
_ = rows;
_ = cols;
if (mode) |m| {
var out = std.io.getStdOut();
var writer = out.writer();
writer.writeAll("Super sophisticated status bar: ") catch {};
writer.writeAll(@tagName(m)) catch {};
}
}

18
gyro.zzz Normal file
View File

@ -0,0 +1,18 @@
pkgs:
zellzig:
version: 0.0.0
description: Build zellij plugins in zig
license: GPL-3.0
source_url: "https://mzte.de/git/LordMZTE/zellzig"
root: src/main.zig
deps:
getty:
git:
url: "https://github.com/getty-zig/getty.git"
ref: 69b4df59511203f91d38660794d8cb7a073eb815
root: src/lib.zig
json:
git:
url: "https://github.com/lordmzte/json.git"
ref: fix/math-changes
root: src/lib.zig

64
src/api.zig Normal file
View File

@ -0,0 +1,64 @@
const std = @import("std");
const zz = @import("main.zig");
const json = @import("json");
const zapi = @import("zellij_api.zig");
const types = @import("types.zig");
pub fn sendObj(data: anytype) !void {
var stdout = std.io.getStdOut();
try json.toWriter(data, stdout.writer());
try stdout.writeAll("\n");
}
pub fn recvObj(comptime T: type) !types.OwnedDeserData(T) {
var buf: [4096]u8 = undefined;
var stdin = std.io.getStdIn();
const data = (try stdin.reader().readUntilDelimiterOrEof(&buf, '\n')) orelse unreachable;
return types.OwnedDeserData(T).deserialize(zz.allocator.?, data);
}
// Subscription Handling
pub fn subscribe(event_types: []const types.EventType) !void {
try sendObj(event_types);
zapi.host_subscribe();
}
pub fn unsubscribe(event_types: []const types.EventType) !void {
try sendObj(event_types);
zapi.host_unsubscribe();
}
// Plugin Settings
pub fn setSelectable(selectable: bool) void {
zapi.host_set_selectable(@boolToInt(selectable));
}
// Query Functions
pub fn getPluginIds() !types.OwnedDeserData(types.PluginIds) {
zapi.host_get_plugin_ids();
return try recvObj(types.PluginIds);
}
pub fn getZellijVersion() !types.OwnedDeserData([]const u8) {
zapi.host_get_zellij_version();
return try recvObj([]const u8);
}
pub fn openFile(path: []const u8) !void {
try sendObj(path);
zapi.host_open_file();
}
pub fn switchTabTo(tab_idx: u32) void {
zapi.host_switch_tab_to(tab_idx);
}
pub fn setTimeout(secs: f64) void {
zapi.host_set_timeout(secs);
}
pub fn execCmd(cmd: []const []const u8) !void {
try sendObj(cmd);
zapi.host_exec_cmd();
}

View File

@ -0,0 +1,53 @@
const std = @import("std");
const getty = @import("getty");
const types = @import("../types.zig");
const Vis = struct {
pub usingnamespace getty.de.Visitor(
@This(),
types.CharOrArrow,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
visitString,
undefined,
undefined,
);
pub fn visitString(
_: @This(),
_: ?std.mem.Allocator,
comptime Deserializer: type,
input: anytype,
) Deserializer.Error!types.CharOrArrow {
if (input.len == 1) {
return types.CharOrArrow{ .Char = input[0] };
}
return types.CharOrArrow{
.Direction = std.meta.stringToEnum(types.Direction, input) orelse
return error.UnknownVariant,
};
}
};
pub fn is(comptime T: type) bool {
return T == types.CharOrArrow;
}
pub fn Visitor(comptime _: type) type {
return Vis;
}
pub fn deserialize(
alloc: ?std.mem.Allocator,
comptime _: type,
deserializer: anytype,
visitor: anytype,
) !@TypeOf(visitor).Value {
return try deserializer.deserializeString(alloc, visitor);
}

View File

@ -0,0 +1,54 @@
const std = @import("std");
const getty = @import("getty");
const types = @import("../types.zig");
const Vis = struct {
pub usingnamespace getty.de.Visitor(
@This(),
types.LineAndColumn,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
visitSeq,
undefined,
undefined,
undefined,
);
pub fn visitSeq(
_: @This(),
alloc: ?std.mem.Allocator,
comptime Deserializer: type,
input: anytype,
) Deserializer.Error!types.LineAndColumn {
const line = (try input.nextElement(alloc, isize)) orelse
return error.InvalidLength;
const column = (try input.nextElement(alloc, usize)) orelse
return error.InvalidLength;
return types.LineAndColumn{
.line = line,
.column = column,
};
}
};
pub fn is(comptime T: type) bool {
return T == types.LineAndColumn;
}
pub fn Visitor(comptime _: type) type {
return Vis;
}
pub fn deserialize(
alloc: ?std.mem.Allocator,
comptime _: type,
deserializer: anytype,
visitor: anytype,
) !@TypeOf(visitor).Value {
return try deserializer.deserializeSeq(alloc, visitor);
}

150
src/deser/union_db.zig Normal file
View File

@ -0,0 +1,150 @@
const std = @import("std");
const getty = @import("getty");
const types = @import("../types.zig");
pub fn is(comptime T: type) bool {
return switch (@typeInfo(T)) {
.Union => true,
else => false,
};
}
pub fn Visitor(comptime T: type) type {
return struct {
pub usingnamespace getty.de.Visitor(
@This(),
T,
undefined,
undefined,
undefined,
undefined,
visitMap,
undefined,
undefined,
visitString,
undefined,
undefined,
);
pub fn visitMap(
_: @This(),
alloc: ?std.mem.Allocator,
comptime Deserializer: type,
input: anytype,
) Deserializer.Error!T {
const tag = (try input.nextKey(alloc, []const u8)) orelse
return error.InvalidLength;
var val: ?T = null;
inline for (@typeInfo(T).Union.fields) |field| {
if (val == null and std.mem.eql(u8, field.name, tag)) {
if (field.field_type == void) {
return error.InvalidType;
}
const data = try input.nextValue(alloc, field.field_type);
val = @unionInit(T, field.name, data);
}
}
if (val) |v| {
while (try input.nextKey(alloc, []const u8)) |_| {}
return v;
}
return error.UnknownVariant;
}
pub fn visitString(
_: @This(),
_: ?std.mem.Allocator,
comptime Deserializer: type,
input: anytype,
) Deserializer.Error!T {
inline for (@typeInfo(T).Union.fields) |field| {
if (std.mem.eql(u8, field.name, input))
if (field.field_type == void)
return @unionInit(T, field.name, {})
else
return error.InvalidType;
}
return error.UnknownVariant;
}
};
}
pub fn deserialize(
alloc: ?std.mem.Allocator,
comptime _: type,
deserializer: anytype,
visitor: anytype,
) !@TypeOf(visitor).Value {
// This horrifying hack is needed since just trying deserializeString
// would consume tokens needed by deserializeMap
var tokens_copy = deserializer.context.tokens;
const token = (try tokens_copy.next()) orelse
return error.UnexpectedEndOfJson;
return switch (token) {
.String => try deserializer.deserializeString(alloc, visitor),
.ObjectBegin => try deserializer.deserializeMap(alloc, visitor),
else => error.InvalidType,
};
}
test "union_db deserialize" {
const TestUnion = union(enum) {
Foo: u8,
Bar: struct { a: u8, b: u8 },
Baz: void,
};
const json_src_a =
\\ {
\\ "Foo": 42
\\ }
;
const json_src_b =
\\ {
\\ "Bar": {
\\ "a": 42,
\\ "b": 69
\\ }
\\ }
;
const json_src_c =
\\"Baz"
;
var deser_a = try types.OwnedDeserData(TestUnion).deserialize(
std.testing.allocator,
json_src_a,
);
defer deser_a.deinit();
var deser_b = try types.OwnedDeserData(TestUnion).deserialize(
std.testing.allocator,
json_src_b,
);
defer deser_b.deinit();
var deser_c = try types.OwnedDeserData(TestUnion).deserialize(
std.testing.allocator,
json_src_c,
);
defer deser_c.deinit();
try std.testing.expectEqual(TestUnion{ .Foo = 42 }, deser_a.data);
try std.testing.expectEqual(TestUnion{
.Bar = .{
.a = 42,
.b = 69,
},
}, deser_b.data);
try std.testing.expectEqual(TestUnion{ .Baz = {} }, deser_c.data);
}

65
src/main.zig Normal file
View File

@ -0,0 +1,65 @@
const std = @import("std");
pub const types = @import("types.zig");
pub const Event = types.Event;
pub const api = @import("api.zig");
// This is the allocator that will be used by zellzig for communication.
// This must be set before events can be received.
pub var allocator: ?std.mem.Allocator = null;
comptime {
if (@import("builtin").is_test)
std.testing.refAllDecls(@This());
}
/// Creates a plugin that can be called into by zellij. This function should take a
/// struct type with the functions of your plugin. Usage of this function should
/// look like this:
/// ```zig
/// // main.zig
/// const zz = @import("zellzig");
/// comptime {
/// zz.createPlugin(@This());
/// }
///
/// pub fn init() void {
/// const alloc = std.heap.GeneralPurposeAllocator(.{}){};
/// zz.allocator = alloc.allocator();
/// }
/// pub fn update() void {}
/// pub fn render(rows: i32, cols: i32) void {}
/// ```
pub fn createPlugin(comptime Plugin: type) void {
if (@TypeOf(Plugin.init) != fn () void)
@compileError("Function 'init' has invalid signature!");
if (@TypeOf(Plugin.update) != fn (Event) void)
@compileError("Function 'update' has invalid signature!");
if (@TypeOf(Plugin.render) != fn (i32, i32) void)
@compileError("Function 'render' has invalid signature!");
_ = struct {
export fn _start() void {
Plugin.init();
}
export fn render(rows: i32, cols: i32) void {
Plugin.render(rows, cols);
}
export fn update() void {
if (allocator == null) {
@panic("Got event while allocator is null! Did you forget to set it?");
}
var ev = api.recvObj(types.Event) catch |err| {
std.log.err("Deserialize error: {}", .{err});
return;
};
defer ev.deinit();
Plugin.update(ev.data);
}
};
}

379
src/types.zig Normal file
View File

@ -0,0 +1,379 @@
const std = @import("std");
const getty = @import("getty");
const json = @import("json");
/// getty deserialization blocks required to properly deserialize messages
pub const dbs = .{
@import("deser/char_or_arrow_db.zig"),
@import("deser/line_and_column_db.zig"),
@import("deser/union_db.zig"),
};
// required because the only way to properly free deserilized
// json is using an arena alloc.
pub fn OwnedDeserData(comptime T: type) type {
return struct {
data: T,
arena: std.heap.ArenaAllocator,
const Self = @This();
pub fn deserialize(alloc: std.mem.Allocator, data: []const u8) !Self {
@setEvalBranchQuota(10000);
var arena = std.heap.ArenaAllocator.init(alloc);
errdefer arena.deinit();
const arena_alloc = arena.allocator();
var deser = json.Deserializer(dbs).withAllocator(arena_alloc, data);
const deser_data = try getty.deserialize(arena_alloc, T, deser.deserializer());
return Self{
.data = deser_data,
.arena = arena,
};
}
pub fn deinit(self: *Self) void {
self.arena.deinit();
}
};
}
pub const EventType = enum {
ModeUpdate,
TabUpdate,
Key,
Mouse,
Timer,
CopyToClipboard,
SystemClipboardFailure,
InputReceived,
Visible,
};
pub const Event = union(EventType) {
ModeUpdate: ModeInfo,
TabUpdate: []TabInfo,
Key: Key,
Mouse: Mouse,
Timer: f64,
CopyToClipboard: CopyDestination,
SystemClipboardFailure,
InputReceived,
Visible: bool,
};
test "deserialize Event" {
const json_src =
\\{
\\ "ModeUpdate": {
\\ "mode": "Normal",
\\ "keybinds": [],
\\ "style": {
\\ "colors": {
\\ "source": "Default",
\\ "theme_hue": "Dark",
\\ "fg": {
\\ "EightBit": 15
\\ },
\\ "bg": {
\\ "Rgb": [
\\ 40,
\\ 42,
\\ 54
\\ ]
\\ },
\\ "black": {
\\ "EightBit": 0
\\ },
\\ "red": {
\\ "EightBit": 1
\\ },
\\ "green": {
\\ "EightBit": 2
\\ },
\\ "yellow": {
\\ "EightBit": 3
\\ },
\\ "blue": {
\\ "EightBit": 6
\\ },
\\ "magenta": {
\\ "EightBit": 5
\\ },
\\ "cyan": {
\\ "EightBit": 14
\\ },
\\ "white": {
\\ "EightBit": 15
\\ },
\\ "orange": {
\\ "EightBit": 3
\\ },
\\ "gray": {
\\ "EightBit": 0
\\ },
\\ "purple": {
\\ "EightBit": 0
\\ },
\\ "gold": {
\\ "EightBit": 0
\\ },
\\ "silver": {
\\ "EightBit": 0
\\ },
\\ "pink": {
\\ "EightBit": 0
\\ },
\\ "brown": {
\\ "EightBit": 0
\\ }
\\ },
\\ "rounded_corners": false
\\ },
\\ "capabilities": {
\\ "arrow_fonts": false
\\ },
\\ "session_name": "tasteful-root"
\\ }
\\}
;
var deser = try OwnedDeserData(Event).deserialize(std.testing.allocator, json_src);
defer deser.deinit();
}
pub const ModeInfo = struct {
mode: InputMode,
keybinds: [][2][]const u8,
style: Style,
capabilities: PluginCapabilities,
session_name: ?[]const u8,
};
pub const InputMode = enum {
Normal,
Locked,
Resize,
Pane,
Tab,
Scroll,
RenameTab,
RenamePane,
Session,
Move,
Prompt,
Tmux,
};
pub const Style = struct {
colors: Palette,
rounded_corners: bool,
};
pub const Palette = struct {
source: PaletteSource,
theme_hue: ThemeHue,
fg: PaletteColor,
bg: PaletteColor,
black: PaletteColor,
red: PaletteColor,
green: PaletteColor,
yellow: PaletteColor,
blue: PaletteColor,
magenta: PaletteColor,
cyan: PaletteColor,
white: PaletteColor,
orange: PaletteColor,
gray: PaletteColor,
purple: PaletteColor,
gold: PaletteColor,
silver: PaletteColor,
pink: PaletteColor,
brown: PaletteColor,
};
test "deserialize Palette" {
const json_src =
\\ {
\\ "source": "Default",
\\ "theme_hue": "Dark",
\\ "fg": {
\\ "EightBit": 15
\\ },
\\ "bg": {
\\ "Rgb": [
\\ 40,
\\ 42,
\\ 54
\\ ]
\\ },
\\ "black": {
\\ "EightBit": 0
\\ },
\\ "red": {
\\ "EightBit": 1
\\ },
\\ "green": {
\\ "EightBit": 2
\\ },
\\ "yellow": {
\\ "EightBit": 3
\\ },
\\ "blue": {
\\ "EightBit": 6
\\ },
\\ "magenta": {
\\ "EightBit": 5
\\ },
\\ "cyan": {
\\ "EightBit": 14
\\ },
\\ "white": {
\\ "EightBit": 15
\\ },
\\ "orange": {
\\ "EightBit": 3
\\ },
\\ "gray": {
\\ "EightBit": 0
\\ },
\\ "purple": {
\\ "EightBit": 0
\\ },
\\ "gold": {
\\ "EightBit": 0
\\ },
\\ "silver": {
\\ "EightBit": 0
\\ },
\\ "pink": {
\\ "EightBit": 0
\\ },
\\ "brown": {
\\ "EightBit": 0
\\ }
\\ }
;
var deser = try OwnedDeserData(Palette).deserialize(std.testing.allocator, json_src);
defer deser.deinit();
}
pub const PaletteSource = enum {
Default,
Xresources,
};
pub const PaletteColor = union(enum) {
Rgb: [3]u8,
EightBit: u8,
};
pub const TabInfo = struct {
position: usize,
name: []u8,
active: bool,
panes_to_hide: usize,
is_fullscreen_active: bool,
is_sync_panes_active: bool,
are_floating_panes_visible: bool,
other_focused_clients: []u16,
};
pub const Key = union(enum) {
Backspace,
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
BackTab,
Delete,
Insert,
F: u8,
Char: []u8,
Alt: CharOrArrow,
Ctrl: []u8,
Null,
Esc,
};
pub const CharOrArrow = union(enum) {
Char: u8,
Direction: Direction,
};
test "deserialize CharOrArrow" {
var data_1 = try OwnedDeserData(CharOrArrow).deserialize(
std.testing.allocator,
"\"A\"",
);
defer data_1.deinit();
var data_2 = try OwnedDeserData(CharOrArrow).deserialize(
std.testing.allocator,
"\"Left\"",
);
defer data_2.deinit();
try std.testing.expectEqual(CharOrArrow{ .Char = 'A' }, data_1.data);
try std.testing.expectEqual(CharOrArrow{ .Direction = .Left }, data_2.data);
}
pub const Direction = enum {
Left,
Right,
Up,
Down,
};
pub const Mouse = union(enum) {
ScrollUp: usize,
ScrollDown: usize,
};
// TODO: implement deserialization block
pub const LineAndColumn = struct {
line: isize,
column: usize,
};
test "deserialize LineAndColumn" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
try std.testing.expectEqual(
LineAndColumn{ .line = 42, .column = 123 },
try json.fromSliceWith(alloc, LineAndColumn, "[42, 123]", dbs),
);
try std.testing.expectEqual(
LineAndColumn{ .line = -42, .column = 123 },
try json.fromSliceWith(alloc, LineAndColumn, "[-42, 123]", dbs),
);
}
pub const CopyDestination = enum {
Command,
Primary,
System,
};
pub const PluginCapabilities = struct {
arrow_fonts: bool,
};
pub const ThemeHue = enum {
Light,
Dark,
};
pub const PluginIds = struct {
plugin_id: u32,
zellij_pid: u32,
};

10
src/zellij_api.zig Normal file
View File

@ -0,0 +1,10 @@
pub extern "zellij" fn host_subscribe() void;
pub extern "zellij" fn host_unsubscribe() void;
pub extern "zellij" fn host_set_selectable(selectable: i32) void;
pub extern "zellij" fn host_get_plugin_ids() void;
pub extern "zellij" fn host_get_zellij_version() void;
pub extern "zellij" fn host_open_file() void;
pub extern "zellij" fn host_switch_tab_to(tab_idx: u32) void;
pub extern "zellij" fn host_set_timeout(secs: f64) void;
pub extern "zellij" fn host_exec_cmd() void;