I will never get tired of vendoring dependencies. ha ha. It is possible
I am insane. I had to do a lot of pruning to get these not to be
ridiculous (especially the unicode data, which had nearly 1 million
lines of... stuff).
This commit is contained in:
torque 2024-08-09 17:32:06 -07:00
commit 7692cb4bc7
Signed by: torque
SSH Key Fingerprint: SHA256:nCrXefBNo6EbjNSQhv0nXmEg/VuNq3sMF5b8zETw3Tk
155 changed files with 206515 additions and 0 deletions

38
build.zig Normal file
View File

@ -0,0 +1,38 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "rotint",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const xev_dep = b.dependency(
"xev",
.{ .target = target, .optimize = optimize },
);
exe.root_module.addImport("xev", xev_dep.module("xev"));
b.installArtifact(exe);
const vaxis_dep = b.dependency(
"vaxis",
.{ .target = target, .optimize = optimize, .images = false },
);
exe.root_module.addImport("vaxis", vaxis_dep.module("vaxis"));
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}

13
build.zig.zon Normal file
View File

@ -0,0 +1,13 @@
.{
.name = "rotint",
.version = "0.0.0",
.dependencies = .{
.vaxis = .{ .path = "deps/libvaxis" },
.xev = .{ .path = "deps/libxev" },
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

21
deps/GapBuffer/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Rylee Alanza Lyman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

38
deps/GapBuffer/README.org vendored Normal file
View File

@ -0,0 +1,38 @@
* GapBuffer.zig
A gap buffer is like a =std.ArrayList=, except that rather than having one contiguous block of items, there are two, with a moveable “gap” between them. Although moving the gap requires a copy, insertions and deletions at either side of the gap become O(1) operations.
This repository implements managed, unmanaged and optionally aligned versions of =GapBuffer(T)=. The API is directly inspired by =std.ArrayList=. The main differences are “Before” and “After” versions of operations that operate or affect the gap—“Before” operations will add or remove elements before the gap. There are also convenience functions for translating a “logical” index into an offset, an element, or a pointer from the buffer,
allowing the user to be largely agnostic about the location of the gap.
Also implemented is a “struct of arrays” transform of =GapBuffer(T)= where =T= is a struct or tagged union type, =MultiGapBuffer(T)=.
To add this package to your project, pick a commit hash =<hash>= and run this:
#+begin_src bash
$ zig fetch --save https://github.com/ryleelyman/GapBuffer.zig/archive/<hash>.tar.gz
#+end_src
Then in your =build.zig= you can add this:
#+begin_src zig
const gap_buffer = b.dependency("gap_buffer", .{
.target = target,
.optimize = optimize,
});
const gb = gap_buffer.module("gap_buffer");
// For whatever you're building; in this case let's assume it's called exe.
exe.root_module.addImport("gap_buffer", gb);
#+end_src
and in your source code:
#+begin_src zig
// import
const gb = @import("gap_buffer");
// and use it something like this...
var buffer = gb.GapBuffer(u8).init(allocator);
#+end_src

20
deps/GapBuffer/build.zig vendored Normal file
View File

@ -0,0 +1,20 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
_ = b.addModule("gap_buffer", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/root.zig"),
});
const tests = b.addTest(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/root.zig"),
});
const run_tests = b.addRunArtifact(tests);
const tests_step = b.step("test", "run gap buffer tests");
tests_step.dependOn(&run_tests.step);
}

5
deps/GapBuffer/build.zig.zon vendored Normal file
View File

@ -0,0 +1,5 @@
.{
.name = "gap_buffer",
.version = "0.1.0-alpha",
.paths = .{ "build.zig", "build.zig.zon", "src" },
}

3072
deps/GapBuffer/src/gap_buffer.zig vendored Normal file

File diff suppressed because it is too large Load Diff

1132
deps/GapBuffer/src/multi_gap_buffer.zig vendored Normal file

File diff suppressed because it is too large Load Diff

12
deps/GapBuffer/src/root.zig vendored Normal file
View File

@ -0,0 +1,12 @@
const gb = @import("gap_buffer.zig");
pub const GapBuffer = gb.GapBuffer;
pub const GapBufferAligned = gb.GapBufferAligned;
pub const GapBufferUnmanaged = gb.GapBufferUnmanaged;
pub const GapBufferAlignedUnmanaged = gb.GapBufferAlignedUnmanaged;
const mgb = @import("multi_gap_buffer.zig");
pub const MultiGapBuffer = mgb.MultiGapBuffer;
test "refAllDecls" {
@import("std").testing.refAllDecls(@This());
}

21
deps/libvaxis/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Tim Culverhouse
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

211
deps/libvaxis/README.md vendored Normal file
View File

@ -0,0 +1,211 @@
# libvaxis
```
It begins with them, but ends with me. Their son, Vaxis
```
![vaxis demo gif](vaxis.gif)
Libvaxis _does not use terminfo_. Support for vt features is detected through
terminal queries.
Contributions are welcome.
Vaxis uses zig `0.13.0`.
## Features
libvaxis supports all major platforms: macOS, Windows, Linux/BSD/and other
Unix-likes.
| Feature | libvaxis |
| ------------------------------ | :------: |
| RGB | ✅ |
| Hyperlinks | ✅ |
| Bracketed Paste | ✅ |
| Kitty Keyboard | ✅ |
| Styled Underlines | ✅ |
| Mouse Shapes (OSC 22) | ✅ |
| System Clipboard (OSC 52) | ✅ |
| System Notifications (OSC 9) | ✅ |
| System Notifications (OSC 777) | ✅ |
| Synchronized Output (DEC 2026) | ✅ |
| Unicode Core (DEC 2027) | ✅ |
| Color Mode Updates (DEC 2031) | ✅ |
| [In-Band Resize Reports](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83) | ✅ |
| Images (kitty) | ✅ |
## Usage
[Documentation](https://rockorager.github.io/libvaxis/#vaxis.Vaxis)
[Starter repo](https://github.com/rockorager/libvaxis-starter)
Vaxis requires three basic primitives to operate:
1. A TTY instance
2. An instance of Vaxis
3. An event loop
The library provides a general purpose posix TTY implementation, as well as a
multi-threaded event loop implementation. Users of the library are encouraged to
use the event loop of their choice. The event loop is responsible for reading
the TTY, passing the read bytes to the vaxis parser, and handling events.
A core feature of Vaxis is it's ability to detect features via terminal queries
instead of relying on a terminfo database. This requires that the event loop
also handle these query responses and update the Vaxis.caps struct accordingly.
See the `Loop` implementation to see how this is done if writing your own event
loop.
## Example
```zig
const std = @import("std");
const vaxis = @import("vaxis");
const Cell = vaxis.Cell;
const TextInput = vaxis.widgets.TextInput;
const border = vaxis.widgets.border;
// This can contain internal events as well as Vaxis events.
// Internal events can be posted into the same queue as vaxis events to allow
// for a single event loop with exhaustive switching. Booya
const Event = union(enum) {
key_press: vaxis.Key,
winsize: vaxis.Winsize,
focus_in,
foo: u8,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const deinit_status = gpa.deinit();
//fail test; can't try in defer as defer is executed after we return
if (deinit_status == .leak) {
std.log.err("memory leak", .{});
}
}
const alloc = gpa.allocator();
// Initialize a tty
var tty = try vaxis.Tty.init();
defer tty.deinit();
// Initialize Vaxis
var vx = try vaxis.init(alloc, .{});
// deinit takes an optional allocator. If your program is exiting, you can
// choose to pass a null allocator to save some exit time.
defer vx.deinit(alloc, tty.anyWriter());
// The event loop requires an intrusive init. We create an instance with
// stable pointers to Vaxis and our TTY, then init the instance. Doing so
// installs a signal handler for SIGWINCH on posix TTYs
//
// This event loop is thread safe. It reads the tty in a separate thread
var loop: vaxis.Loop(Event) = .{
.tty = &tty,
.vaxis = &vx,
};
try loop.init();
// Start the read loop. This puts the terminal in raw mode and begins
// reading user input
try loop.start();
defer loop.stop();
// Optionally enter the alternate screen
try vx.enterAltScreen(tty.anyWriter());
// We'll adjust the color index every keypress for the border
var color_idx: u8 = 0;
// init our text input widget. The text input widget needs an allocator to
// store the contents of the input
var text_input = TextInput.init(alloc, &vx.unicode);
defer text_input.deinit();
// Sends queries to terminal to detect certain features. This should always
// be called after entering the alt screen, if you are using the alt screen
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
while (true) {
// nextEvent blocks until an event is in the queue
const event = loop.nextEvent();
// exhaustive switching ftw. Vaxis will send events if your Event enum
// has the fields for those events (ie "key_press", "winsize")
switch (event) {
.key_press => |key| {
color_idx = switch (color_idx) {
255 => 0,
else => color_idx + 1,
};
if (key.matches('c', .{ .ctrl = true })) {
break;
} else if (key.matches('l', .{ .ctrl = true })) {
vx.queueRefresh();
} else {
try text_input.update(.{ .key_press = key });
}
},
// winsize events are sent to the application to ensure that all
// resizes occur in the main thread. This lets us avoid expensive
// locks on the screen. All applications must handle this event
// unless they aren't using a screen (IE only detecting features)
//
// The allocations are because we keep a copy of each cell to
// optimize renders. When resize is called, we allocated two slices:
// one for the screen, and one for our buffered screen. Each cell in
// the buffered screen contains an ArrayList(u8) to be able to store
// the grapheme for that cell. Each cell is initialized with a size
// of 1, which is sufficient for all of ASCII. Anything requiring
// more than one byte will incur an allocation on the first render
// after it is drawn. Thereafter, it will not allocate unless the
// screen is resized
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
else => {},
}
// vx.window() returns the root window. This window is the size of the
// terminal and can spawn child windows as logical areas. Child windows
// cannot draw outside of their bounds
const win = vx.window();
// Clear the entire space because we are drawing in immediate mode.
// vaxis double buffers the screen. This new frame will be compared to
// the old and only updated cells will be drawn
win.clear();
// Create a style
const style: vaxis.Style = .{
.fg = .{ .index = color_idx },
};
// Create a bordered child window
const child = win.child(.{
.x_off = win.width / 2 - 20,
.y_off = win.height / 2 - 3,
.width = .{ .limit = 40 },
.height = .{ .limit = 3 },
.border = .{
.where = .all,
.style = style,
},
});
// Draw the text_input in the child window
text_input.draw(child);
// Render the screen. Using a buffered writer will offer much better
// performance, but is not required
try vx.render(tty.anyWriter());
}
}
```
## Contributing
Contributions are welcome. Please submit a PR on Github or a patch on the
[mailing list](mailto:~rockorager/libvaxis@lists.sr.ht)

135
deps/libvaxis/build.zig vendored Normal file
View File

@ -0,0 +1,135 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const include_libxev = b.option(bool, "libxev", "Enable support for libxev library (default: true)") orelse true;
const include_images = b.option(bool, "images", "Enable support for images (default: true)") orelse true;
const include_text_input = b.option(bool, "text_input", "Enable support for the TextInput widget (default: true)") orelse true;
const include_aio = b.option(bool, "aio", "Enable support for zig-aio library (default: false)") orelse false;
const options = b.addOptions();
options.addOption(bool, "libxev", include_libxev);
options.addOption(bool, "images", include_images);
options.addOption(bool, "text_input", include_text_input);
options.addOption(bool, "aio", include_aio);
const options_mod = options.createModule();
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const root_source_file = b.path("src/main.zig");
// Dependencies
const zg_dep = b.dependency("zg", .{
.optimize = optimize,
.target = target,
});
const zigimg_dep = if (include_images) b.lazyDependency("zigimg", .{
.optimize = optimize,
.target = target,
}) else null;
const gap_buffer_dep = if (include_text_input) b.lazyDependency("gap_buffer", .{
.optimize = optimize,
.target = target,
}) else null;
const xev_dep = if (include_libxev) b.lazyDependency("libxev", .{
.optimize = optimize,
.target = target,
}) else null;
const aio_dep = if (include_aio) b.lazyDependency("aio", .{
.optimize = optimize,
.target = target,
}) else null;
// Module
const vaxis_mod = b.addModule("vaxis", .{
.root_source_file = root_source_file,
.target = target,
.optimize = optimize,
});
vaxis_mod.addImport("code_point", zg_dep.module("code_point"));
vaxis_mod.addImport("grapheme", zg_dep.module("grapheme"));
vaxis_mod.addImport("DisplayWidth", zg_dep.module("DisplayWidth"));
if (zigimg_dep) |dep| vaxis_mod.addImport("zigimg", dep.module("zigimg"));
if (gap_buffer_dep) |dep| vaxis_mod.addImport("gap_buffer", dep.module("gap_buffer"));
if (xev_dep) |dep| vaxis_mod.addImport("xev", dep.module("xev"));
if (aio_dep) |dep| vaxis_mod.addImport("aio", dep.module("aio"));
if (aio_dep) |dep| vaxis_mod.addImport("coro", dep.module("coro"));
vaxis_mod.addImport("build_options", options_mod);
// Examples
const Example = enum {
cli,
image,
main,
nvim,
table,
text_input,
vaxis,
vt,
xev,
aio,
};
const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input;
const example_step = b.step("example", "Run example");
const example = b.addExecutable(.{
.name = "example",
// future versions should use b.path, see zig PR #19597
.root_source_file = b.path(
b.fmt("examples/{s}.zig", .{@tagName(example_option)}),
),
.target = target,
.optimize = optimize,
});
example.root_module.addImport("vaxis", vaxis_mod);
if (xev_dep) |dep| example.root_module.addImport("xev", dep.module("xev"));
if (aio_dep) |dep| example.root_module.addImport("aio", dep.module("aio"));
if (aio_dep) |dep| example.root_module.addImport("coro", dep.module("coro"));
const example_run = b.addRunArtifact(example);
example_step.dependOn(&example_run.step);
// Tests
const tests_step = b.step("test", "Run tests");
const tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
tests.root_module.addImport("code_point", zg_dep.module("code_point"));
tests.root_module.addImport("grapheme", zg_dep.module("grapheme"));
tests.root_module.addImport("DisplayWidth", zg_dep.module("DisplayWidth"));
if (zigimg_dep) |dep| tests.root_module.addImport("zigimg", dep.module("zigimg"));
if (gap_buffer_dep) |dep| tests.root_module.addImport("gap_buffer", dep.module("gap_buffer"));
tests.root_module.addImport("build_options", options_mod);
const tests_run = b.addRunArtifact(tests);
b.installArtifact(tests);
tests_step.dependOn(&tests_run.step);
// Lints
const lints_step = b.step("lint", "Run lints");
const lints = b.addFmt(.{
.paths = &.{ "src", "build.zig" },
.check = true,
});
lints_step.dependOn(&lints.step);
b.default_step.dependOn(lints_step);
// Docs
const docs_step = b.step("docs", "Build the vaxis library docs");
const docs_obj = b.addObject(.{
.name = "vaxis",
.root_source_file = root_source_file,
.target = target,
.optimize = optimize,
});
const docs = docs_obj.getEmittedDocs();
docs_step.dependOn(&b.addInstallDirectory(.{
.source_dir = docs,
.install_dir = .prefix,
.install_subdir = "docs",
}).step);
}

16
deps/libvaxis/build.zig.zon vendored Normal file
View File

@ -0,0 +1,16 @@
.{
.name = "vaxis",
.version = "0.1.0",
.minimum_zig_version = "0.13.0",
.dependencies = .{
.gap_buffer = .{ .path = "../GapBuffer" },
.zg = .{ .path = "../zg" },
.libxev = .{ .path = "../libxev" },
},
.paths = .{
"LICENSE",
"build.zig",
"build.zig.zon",
"src",
},
}

203
deps/libvaxis/src/Cell.zig vendored Normal file
View File

@ -0,0 +1,203 @@
const std = @import("std");
const Image = @import("Image.zig");
char: Character = .{},
style: Style = .{},
link: Hyperlink = .{},
image: ?Image.Placement = null,
default: bool = false,
/// Set to true if this cell is the last cell printed in a row before wrap. Vaxis will determine if
/// it should rely on the terminal's autowrap feature which can help with primary screen resizes
wrapped: bool = false,
/// Segment is a contiguous run of text that has a constant style
pub const Segment = struct {
text: []const u8,
style: Style = .{},
link: Hyperlink = .{},
};
pub const Character = struct {
grapheme: []const u8 = " ",
/// width should only be provided when the application is sure the terminal
/// will measure the same width. This can be ensure by using the gwidth method
/// included in libvaxis. If width is 0, libvaxis will measure the glyph at
/// render time
width: usize = 1,
};
pub const CursorShape = enum {
default,
block_blink,
block,
underline_blink,
underline,
beam_blink,
beam,
};
pub const Hyperlink = struct {
uri: []const u8 = "",
/// ie "id=app-1234"
params: []const u8 = "",
};
pub const Style = struct {
pub const Underline = enum {
off,
single,
double,
curly,
dotted,
dashed,
};
fg: Color = .default,
bg: Color = .default,
ul: Color = .default,
ul_style: Underline = .off,
bold: bool = false,
dim: bool = false,
italic: bool = false,
blink: bool = false,
reverse: bool = false,
invisible: bool = false,
strikethrough: bool = false,
pub fn eql(a: Style, b: Style) bool {
const SGRBits = packed struct {
bold: bool,
dim: bool,
italic: bool,
blink: bool,
reverse: bool,
invisible: bool,
strikethrough: bool,
};
const a_sgr: SGRBits = .{
.bold = a.bold,
.dim = a.dim,
.italic = a.italic,
.blink = a.blink,
.reverse = a.reverse,
.invisible = a.invisible,
.strikethrough = a.strikethrough,
};
const b_sgr: SGRBits = .{
.bold = b.bold,
.dim = b.dim,
.italic = b.italic,
.blink = b.blink,
.reverse = b.reverse,
.invisible = b.invisible,
.strikethrough = b.strikethrough,
};
const a_cast: u7 = @bitCast(a_sgr);
const b_cast: u7 = @bitCast(b_sgr);
return a_cast == b_cast and
Color.eql(a.fg, b.fg) and
Color.eql(a.bg, b.bg) and
Color.eql(a.ul, b.ul) and
a.ul_style == b.ul_style;
}
};
pub const Color = union(enum) {
default,
index: u8,
rgb: [3]u8,
pub const Kind = union(enum) {
fg,
bg,
cursor,
index: u8,
};
/// Returned when querying a color from the terminal
pub const Report = struct {
kind: Kind,
value: [3]u8,
};
pub const Scheme = enum {
dark,
light,
};
pub fn eql(a: Color, b: Color) bool {
switch (a) {
.default => return b == .default,
.index => |a_idx| {
switch (b) {
.index => |b_idx| return a_idx == b_idx,
else => return false,
}
},
.rgb => |a_rgb| {
switch (b) {
.rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and
a_rgb[1] == b_rgb[1] and
a_rgb[2] == b_rgb[2],
else => return false,
}
},
}
}
pub fn rgbFromUint(val: u24) Color {
const r_bits = val & 0b11111111_00000000_00000000;
const g_bits = val & 0b00000000_11111111_00000000;
const b_bits = val & 0b00000000_00000000_11111111;
const rgb = [_]u8{
@truncate(r_bits >> 16),
@truncate(g_bits >> 8),
@truncate(b_bits),
};
return .{ .rgb = rgb };
}
/// parse an XParseColor-style rgb specification into an rgb Color. The spec
/// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always
/// be the same as the low two bits.
pub fn rgbFromSpec(spec: []const u8) !Color {
var iter = std.mem.splitScalar(u8, spec, ':');
const prefix = iter.next() orelse return error.InvalidColorSpec;
if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec;
const spec_str = iter.next() orelse return error.InvalidColorSpec;
var spec_iter = std.mem.splitScalar(u8, spec_str, '/');
const r_raw = spec_iter.next() orelse return error.InvalidColorSpec;
if (r_raw.len != 4) return error.InvalidColorSpec;
const g_raw = spec_iter.next() orelse return error.InvalidColorSpec;
if (g_raw.len != 4) return error.InvalidColorSpec;
const b_raw = spec_iter.next() orelse return error.InvalidColorSpec;
if (b_raw.len != 4) return error.InvalidColorSpec;
const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16);
const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16);
const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16);
return .{
.rgb = [_]u8{ r, g, b },
};
}
test "rgbFromSpec" {
const spec = "rgb:aaaa/bbbb/cccc";
const actual = try rgbFromSpec(spec);
switch (actual) {
.rgb => |rgb| {
try std.testing.expectEqual(0xAA, rgb[0]);
try std.testing.expectEqual(0xBB, rgb[1]);
try std.testing.expectEqual(0xCC, rgb[2]);
},
else => try std.testing.expect(false),
}
}
};

20
deps/libvaxis/src/GraphemeCache.zig vendored Normal file
View File

@ -0,0 +1,20 @@
const std = @import("std");
const GraphemeCache = @This();
/// the underlying storage for graphemes. Right now 8kb
buf: [1024 * 8]u8 = undefined,
// the start index of the next grapheme
idx: usize = 0,
/// put a slice of bytes in the cache as a grapheme
pub fn put(self: *GraphemeCache, bytes: []const u8) []u8 {
// reset the idx to 0 if we would overflow
if (self.idx + bytes.len > self.buf.len) self.idx = 0;
defer self.idx += bytes.len;
// copy the grapheme to our storage
@memcpy(self.buf[self.idx .. self.idx + bytes.len], bytes);
// return the slice
return self.buf[self.idx .. self.idx + bytes.len];
}

188
deps/libvaxis/src/Image.zig vendored Normal file
View File

@ -0,0 +1,188 @@
const std = @import("std");
const fmt = std.fmt;
const math = std.math;
const base64 = std.base64.standard.Encoder;
const zigimg = @import("zigimg");
const Window = @import("Window.zig");
const Image = @This();
const transmit_opener = "\x1b_Gf=32,i={d},s={d},v={d},m={d};";
pub const Source = union(enum) {
path: []const u8,
mem: []const u8,
};
pub const TransmitFormat = enum {
rgb,
rgba,
png,
};
pub const TransmitMedium = enum {
file,
temp_file,
shared_mem,
};
pub const Placement = struct {
img_id: u32,
options: Image.DrawOptions,
};
pub const CellSize = struct {
rows: usize,
cols: usize,
};
pub const DrawOptions = struct {
/// an offset into the top left cell, in pixels, with where to place the
/// origin of the image. These must be less than the pixel size of a single
/// cell
pixel_offset: ?struct {
x: usize,
y: usize,
} = null,
/// the vertical stacking order
/// < 0: Drawn beneath text
/// < -1_073_741_824: Drawn beneath "default" background cells
z_index: ?i32 = null,
/// A clip region of the source image to draw.
clip_region: ?struct {
x: ?usize = null,
y: ?usize = null,
width: ?usize = null,
height: ?usize = null,
} = null,
/// Scaling to apply to the Image
scale: enum {
/// no scaling applied. the image may extend beyond the window
none,
/// Stretch / shrink the image to fill the window
fill,
/// Scale the image to fit the window, maintaining aspect ratio
fit,
/// Scale the image to fit the window, only if needed.
contain,
} = .none,
/// the size to render the image. Generally you will not need to use this
/// field, and should prefer to use scale. `draw` will fill in this field with
/// the correct values if a scale method is applied.
size: ?struct {
rows: ?usize = null,
cols: ?usize = null,
} = null,
};
/// unique identifier for this image. This will be managed by the screen.
id: u32,
/// width in pixels
width: usize,
/// height in pixels
height: usize,
pub fn draw(self: Image, win: Window, opts: DrawOptions) !void {
var p_opts = opts;
switch (opts.scale) {
.none => {},
.fill => {
p_opts.size = .{
.rows = win.height,
.cols = win.width,
};
},
.fit,
.contain,
=> contain: {
// cell geometry
const x_pix = win.screen.width_pix;
const y_pix = win.screen.height_pix;
const w = win.screen.width;
const h = win.screen.height;
const pix_per_col = try std.math.divCeil(usize, x_pix, w);
const pix_per_row = try std.math.divCeil(usize, y_pix, h);
const win_width_pix = pix_per_col * win.width;
const win_height_pix = pix_per_row * win.height;
const fit_x: bool = if (win_width_pix >= self.width) true else false;
const fit_y: bool = if (win_height_pix >= self.height) true else false;
// Does the image fit with no scaling?
if (opts.scale == .contain and fit_x and fit_y) break :contain;
// Does the image require vertical scaling?
if (fit_x and !fit_y)
p_opts.size = .{
.rows = win.height,
}
// Does the image require horizontal scaling?
else if (!fit_x and fit_y)
p_opts.size = .{
.cols = win.width,
}
else if (!fit_x and !fit_y) {
const diff_x = self.width - win_width_pix;
const diff_y = self.height - win_height_pix;
// The width difference is larger than the height difference.
// Scale by width
if (diff_x > diff_y)
p_opts.size = .{
.cols = win.width,
}
else
// The height difference is larger than the width difference.
// Scale by height
p_opts.size = .{
.rows = win.height,
};
} else {
std.debug.assert(opts.scale == .fit);
std.debug.assert(win_width_pix >= self.width);
std.debug.assert(win_height_pix >= self.height);
// Fits in both directions. Find the closer direction
const diff_x = win_width_pix - self.width;
const diff_y = win_height_pix - self.height;
// The width is closer in dimension. Scale by that
if (diff_x < diff_y)
p_opts.size = .{
.cols = win.width,
}
else
p_opts.size = .{
.rows = win.height,
};
}
},
}
const p = Placement{
.img_id = self.id,
.options = p_opts,
};
win.writeCell(0, 0, .{ .image = p });
}
/// the size of the image, in cells
pub fn cellSize(self: Image, win: Window) !CellSize {
// cell geometry
const x_pix = win.screen.width_pix;
const y_pix = win.screen.height_pix;
const w = win.screen.width;
const h = win.screen.height;
const pix_per_col = try std.math.divCeil(usize, x_pix, w);
const pix_per_row = try std.math.divCeil(usize, y_pix, h);
const cell_width = std.math.divCeil(usize, self.width, pix_per_col) catch 0;
const cell_height = std.math.divCeil(usize, self.height, pix_per_row) catch 0;
return .{
.rows = cell_height,
.cols = cell_width,
};
}

123
deps/libvaxis/src/InternalScreen.zig vendored Normal file
View File

@ -0,0 +1,123 @@
const std = @import("std");
const assert = std.debug.assert;
const Style = @import("Cell.zig").Style;
const Cell = @import("Cell.zig");
const MouseShape = @import("Mouse.zig").Shape;
const CursorShape = Cell.CursorShape;
const log = std.log.scoped(.vaxis);
const InternalScreen = @This();
pub const InternalCell = struct {
char: std.ArrayList(u8) = undefined,
style: Style = .{},
uri: std.ArrayList(u8) = undefined,
uri_id: std.ArrayList(u8) = undefined,
// if we got skipped because of a wide character
skipped: bool = false,
default: bool = true,
pub fn eql(self: InternalCell, cell: Cell) bool {
// fastpath when both cells are default
if (self.default and cell.default) return true;
// this is actually faster than std.meta.eql on the individual items.
// Our strings are always small, usually less than 4 bytes so the simd
// usage in std.mem.eql has too much overhead vs looping the bytes
if (!std.mem.eql(u8, self.char.items, cell.char.grapheme)) return false;
if (!Style.eql(self.style, cell.style)) return false;
if (!std.mem.eql(u8, self.uri.items, cell.link.uri)) return false;
if (!std.mem.eql(u8, self.uri_id.items, cell.link.params)) return false;
return true;
}
};
width: usize = 0,
height: usize = 0,
buf: []InternalCell = undefined,
cursor_row: usize = 0,
cursor_col: usize = 0,
cursor_vis: bool = false,
cursor_shape: CursorShape = .default,
mouse_shape: MouseShape = .default,
/// sets each cell to the default cell
pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !InternalScreen {
var screen = InternalScreen{
.buf = try alloc.alloc(InternalCell, w * h),
};
for (screen.buf, 0..) |_, i| {
screen.buf[i] = .{
.char = try std.ArrayList(u8).initCapacity(alloc, 1),
.uri = std.ArrayList(u8).init(alloc),
.uri_id = std.ArrayList(u8).init(alloc),
};
try screen.buf[i].char.append(' ');
}
screen.width = w;
screen.height = h;
return screen;
}
pub fn deinit(self: *InternalScreen, alloc: std.mem.Allocator) void {
for (self.buf, 0..) |_, i| {
self.buf[i].char.deinit();
self.buf[i].uri.deinit();
self.buf[i].uri_id.deinit();
}
alloc.free(self.buf);
}
/// writes a cell to a location. 0 indexed
pub fn writeCell(
self: *InternalScreen,
col: usize,
row: usize,
cell: Cell,
) void {
if (self.width < col) {
// column out of bounds
return;
}
if (self.height < row) {
// height out of bounds
return;
}
const i = (row * self.width) + col;
assert(i < self.buf.len);
self.buf[i].char.clearRetainingCapacity();
self.buf[i].char.appendSlice(cell.char.grapheme) catch {
log.warn("couldn't write grapheme", .{});
};
self.buf[i].uri.clearRetainingCapacity();
self.buf[i].uri.appendSlice(cell.link.uri) catch {
log.warn("couldn't write uri", .{});
};
self.buf[i].uri_id.clearRetainingCapacity();
self.buf[i].uri_id.appendSlice(cell.link.params) catch {
log.warn("couldn't write uri_id", .{});
};
self.buf[i].style = cell.style;
self.buf[i].default = cell.default;
}
pub fn readCell(self: *InternalScreen, col: usize, row: usize) ?Cell {
if (self.width < col) {
// column out of bounds
return null;
}
if (self.height < row) {
// height out of bounds
return null;
}
const i = (row * self.width) + col;
assert(i < self.buf.len);
return .{
.char = .{ .grapheme = self.buf[i].char.items },
.style = self.buf[i].style,
};
}

434
deps/libvaxis/src/Key.zig vendored Normal file
View File

@ -0,0 +1,434 @@
const std = @import("std");
const testing = std.testing;
const Key = @This();
/// Modifier Keys for a Key Match Event.
pub const Modifiers = packed struct(u8) {
shift: bool = false,
alt: bool = false,
ctrl: bool = false,
super: bool = false,
hyper: bool = false,
meta: bool = false,
caps_lock: bool = false,
num_lock: bool = false,
};
/// Flags for the Kitty Protocol.
pub const KittyFlags = packed struct(u5) {
disambiguate: bool = true,
report_events: bool = false,
report_alternate_keys: bool = true,
report_all_as_ctl_seqs: bool = true,
report_text: bool = true,
};
/// the unicode codepoint of the key event.
codepoint: u21,
/// the text generated from the key event. The underlying slice has a limited
/// lifetime. Vaxis maintains an internal ring buffer to temporarily store text.
/// If the application needs these values longer than the lifetime of the event
/// it must copy the data.
text: ?[]const u8 = null,
/// the shifted codepoint of this key event. This will only be present if the
/// Shift modifier was used to generate the event
shifted_codepoint: ?u21 = null,
/// the key that would have been pressed on a standard keyboard layout. This is
/// useful for shortcut matching
base_layout_codepoint: ?u21 = null,
mods: Modifiers = .{},
// matches follows a loose matching algorithm for key matches.
// 1. If the codepoint and modifiers are exact matches, after removing caps_lock
// and num_lock
// 2. If the utf8 encoding of the codepoint matches the text, after removing
// num_lock
// 3. If there is a shifted codepoint and it matches after removing the shift
// modifier from self, after removing caps_lock and num_lock
pub fn matches(self: Key, cp: u21, mods: Modifiers) bool {
// rule 1
if (self.matchExact(cp, mods)) return true;
// rule 2
if (self.matchText(cp, mods)) return true;
// rule 3
if (self.matchShiftedCodepoint(cp, mods)) return true;
return false;
}
/// matches against any of the provided codepoints.
pub fn matchesAny(self: Key, cps: []const u21, mods: Modifiers) bool {
for (cps) |cp| {
if (self.matches(cp, mods)) return true;
}
return false;
}
/// matches base layout codes, useful for shortcut matching when an alternate key
/// layout is used
pub fn matchShortcut(self: Key, cp: u21, mods: Modifiers) bool {
if (self.base_layout_codepoint == null) return false;
return cp == self.base_layout_codepoint.? and std.meta.eql(self.mods, mods);
}
/// matches keys that aren't upper case versions when shifted. For example, shift
/// + semicolon produces a colon. The key can be matched against shift +
/// semicolon or just colon...or shift + ctrl + ; or just ctrl + :
pub fn matchShiftedCodepoint(self: Key, cp: u21, mods: Modifiers) bool {
if (self.shifted_codepoint == null) return false;
if (!self.mods.shift) return false;
var self_mods = self.mods;
self_mods.shift = false;
self_mods.caps_lock = false;
self_mods.num_lock = false;
var tgt_mods = mods;
tgt_mods.caps_lock = false;
tgt_mods.num_lock = false;
return cp == self.shifted_codepoint.? and std.meta.eql(self_mods, mods);
}
/// matches when the utf8 encoding of the codepoint and relevant mods matches the
/// text of the key. This function will consume Shift and Caps Lock when matching
pub fn matchText(self: Key, cp: u21, mods: Modifiers) bool {
// return early if we have no text
if (self.text == null) return false;
var self_mods = self.mods;
self_mods.num_lock = false;
self_mods.shift = false;
self_mods.caps_lock = false;
var arg_mods = mods;
arg_mods.num_lock = false;
arg_mods.shift = false;
arg_mods.caps_lock = false;
var buf: [4]u8 = undefined;
const n = std.unicode.utf8Encode(cp, buf[0..]) catch return false;
return std.mem.eql(u8, self.text.?, buf[0..n]) and std.meta.eql(self_mods, arg_mods);
}
// The key must exactly match the codepoint and modifiers. caps_lock and
// num_lock are removed before matching
pub fn matchExact(self: Key, cp: u21, mods: Modifiers) bool {
var self_mods = self.mods;
self_mods.caps_lock = false;
self_mods.num_lock = false;
var tgt_mods = mods;
tgt_mods.caps_lock = false;
tgt_mods.num_lock = false;
return self.codepoint == cp and std.meta.eql(self_mods, tgt_mods);
}
/// True if the key is a single modifier (ie: left_shift)
pub fn isModifier(self: Key) bool {
return self.codepoint == left_shift or
self.codepoint == left_alt or
self.codepoint == left_super or
self.codepoint == left_hyper or
self.codepoint == left_control or
self.codepoint == left_meta or
self.codepoint == right_shift or
self.codepoint == right_alt or
self.codepoint == right_super or
self.codepoint == right_hyper or
self.codepoint == right_control or
self.codepoint == right_meta;
}
// a few special keys that we encode as their actual ascii value
pub const tab: u21 = 0x09;
pub const enter: u21 = 0x0D;
pub const escape: u21 = 0x1B;
pub const space: u21 = 0x20;
pub const backspace: u21 = 0x7F;
/// multicodepoint is a key which generated text but cannot be expressed as a
/// single codepoint. The value is the maximum unicode codepoint + 1
pub const multicodepoint: u21 = 1_114_112 + 1;
// kitty encodes these keys directly in the private use area. We reuse those
// mappings
pub const insert: u21 = 57348;
pub const delete: u21 = 57349;
pub const left: u21 = 57350;
pub const right: u21 = 57351;
pub const up: u21 = 57352;
pub const down: u21 = 57353;
pub const page_up: u21 = 57354;
pub const page_down: u21 = 57355;
pub const home: u21 = 57356;
pub const end: u21 = 57357;
pub const caps_lock: u21 = 57358;
pub const scroll_lock: u21 = 57359;
pub const num_lock: u21 = 57360;
pub const print_screen: u21 = 57361;
pub const pause: u21 = 57362;
pub const menu: u21 = 57363;
pub const f1: u21 = 57364;
pub const f2: u21 = 57365;
pub const f3: u21 = 57366;
pub const f4: u21 = 57367;
pub const f5: u21 = 57368;
pub const f6: u21 = 57369;
pub const f7: u21 = 57370;
pub const f8: u21 = 57371;
pub const f9: u21 = 57372;
pub const f10: u21 = 57373;
pub const f11: u21 = 57374;
pub const f12: u21 = 57375;
pub const f13: u21 = 57376;
pub const f14: u21 = 57377;
pub const f15: u21 = 57378;
pub const @"f16": u21 = 57379;
pub const f17: u21 = 57380;
pub const f18: u21 = 57381;
pub const f19: u21 = 57382;
pub const f20: u21 = 57383;
pub const f21: u21 = 57384;
pub const f22: u21 = 57385;
pub const f23: u21 = 57386;
pub const f24: u21 = 57387;
pub const f25: u21 = 57388;
pub const f26: u21 = 57389;
pub const f27: u21 = 57390;
pub const f28: u21 = 57391;
pub const f29: u21 = 57392;
pub const f30: u21 = 57393;
pub const f31: u21 = 57394;
pub const @"f32": u21 = 57395;
pub const f33: u21 = 57396;
pub const f34: u21 = 57397;
pub const f35: u21 = 57398;
pub const kp_0: u21 = 57399;
pub const kp_1: u21 = 57400;
pub const kp_2: u21 = 57401;
pub const kp_3: u21 = 57402;
pub const kp_4: u21 = 57403;
pub const kp_5: u21 = 57404;
pub const kp_6: u21 = 57405;
pub const kp_7: u21 = 57406;
pub const kp_8: u21 = 57407;
pub const kp_9: u21 = 57408;
pub const kp_decimal: u21 = 57409;
pub const kp_divide: u21 = 57410;
pub const kp_multiply: u21 = 57411;
pub const kp_subtract: u21 = 57412;
pub const kp_add: u21 = 57413;
pub const kp_enter: u21 = 57414;
pub const kp_equal: u21 = 57415;
pub const kp_separator: u21 = 57416;
pub const kp_left: u21 = 57417;
pub const kp_right: u21 = 57418;
pub const kp_up: u21 = 57419;
pub const kp_down: u21 = 57420;
pub const kp_page_up: u21 = 57421;
pub const kp_page_down: u21 = 57422;
pub const kp_home: u21 = 57423;
pub const kp_end: u21 = 57424;
pub const kp_insert: u21 = 57425;
pub const kp_delete: u21 = 57426;
pub const kp_begin: u21 = 57427;
pub const media_play: u21 = 57428;
pub const media_pause: u21 = 57429;
pub const media_play_pause: u21 = 57430;
pub const media_reverse: u21 = 57431;
pub const media_stop: u21 = 57432;
pub const media_fast_forward: u21 = 57433;
pub const media_rewind: u21 = 57434;
pub const media_track_next: u21 = 57435;
pub const media_track_previous: u21 = 57436;
pub const media_record: u21 = 57437;
pub const lower_volume: u21 = 57438;
pub const raise_volume: u21 = 57439;
pub const mute_volume: u21 = 57440;
pub const left_shift: u21 = 57441;
pub const left_control: u21 = 57442;
pub const left_alt: u21 = 57443;
pub const left_super: u21 = 57444;
pub const left_hyper: u21 = 57445;
pub const left_meta: u21 = 57446;
pub const right_shift: u21 = 57447;
pub const right_control: u21 = 57448;
pub const right_alt: u21 = 57449;
pub const right_super: u21 = 57450;
pub const right_hyper: u21 = 57451;
pub const right_meta: u21 = 57452;
pub const iso_level_3_shift: u21 = 57453;
pub const iso_level_5_shift: u21 = 57454;
pub const name_map = blk: {
@setEvalBranchQuota(2000);
break :blk std.StaticStringMap(u21).initComptime(.{
// common names
.{ "plus", '+' },
.{ "minus", '-' },
.{ "colon", ':' },
.{ "semicolon", ';' },
.{ "comma", ',' },
// special keys
.{ "insert", insert },
.{ "delete", delete },
.{ "left", left },
.{ "right", right },
.{ "up", up },
.{ "down", down },
.{ "page_up", page_up },
.{ "page_down", page_down },
.{ "home", home },
.{ "end", end },
.{ "caps_lock", caps_lock },
.{ "scroll_lock", scroll_lock },
.{ "num_lock", num_lock },
.{ "print_screen", print_screen },
.{ "pause", pause },
.{ "menu", menu },
.{ "f1", f1 },
.{ "f2", f2 },
.{ "f3", f3 },
.{ "f4", f4 },
.{ "f5", f5 },
.{ "f6", f6 },
.{ "f7", f7 },
.{ "f8", f8 },
.{ "f9", f9 },
.{ "f10", f10 },
.{ "f11", f11 },
.{ "f12", f12 },
.{ "f13", f13 },
.{ "f14", f14 },
.{ "f15", f15 },
.{ "f16", @"f16" },
.{ "f17", f17 },
.{ "f18", f18 },
.{ "f19", f19 },
.{ "f20", f20 },
.{ "f21", f21 },
.{ "f22", f22 },
.{ "f23", f23 },
.{ "f24", f24 },
.{ "f25", f25 },
.{ "f26", f26 },
.{ "f27", f27 },
.{ "f28", f28 },
.{ "f29", f29 },
.{ "f30", f30 },
.{ "f31", f31 },
.{ "f32", @"f32" },
.{ "f33", f33 },
.{ "f34", f34 },
.{ "f35", f35 },
.{ "kp_0", kp_0 },
.{ "kp_1", kp_1 },
.{ "kp_2", kp_2 },
.{ "kp_3", kp_3 },
.{ "kp_4", kp_4 },
.{ "kp_5", kp_5 },
.{ "kp_6", kp_6 },
.{ "kp_7", kp_7 },
.{ "kp_8", kp_8 },
.{ "kp_9", kp_9 },
.{ "kp_decimal", kp_decimal },
.{ "kp_divide", kp_divide },
.{ "kp_multiply", kp_multiply },
.{ "kp_subtract", kp_subtract },
.{ "kp_add", kp_add },
.{ "kp_enter", kp_enter },
.{ "kp_equal", kp_equal },
.{ "kp_separator", kp_separator },
.{ "kp_left", kp_left },
.{ "kp_right", kp_right },
.{ "kp_up", kp_up },
.{ "kp_down", kp_down },
.{ "kp_page_up", kp_page_up },
.{ "kp_page_down", kp_page_down },
.{ "kp_home", kp_home },
.{ "kp_end", kp_end },
.{ "kp_insert", kp_insert },
.{ "kp_delete", kp_delete },
.{ "kp_begin", kp_begin },
.{ "media_play", media_play },
.{ "media_pause", media_pause },
.{ "media_play_pause", media_play_pause },
.{ "media_reverse", media_reverse },
.{ "media_stop", media_stop },
.{ "media_fast_forward", media_fast_forward },
.{ "media_rewind", media_rewind },
.{ "media_track_next", media_track_next },
.{ "media_track_previous", media_track_previous },
.{ "media_record", media_record },
.{ "lower_volume", lower_volume },
.{ "raise_volume", raise_volume },
.{ "mute_volume", mute_volume },
.{ "left_shift", left_shift },
.{ "left_control", left_control },
.{ "left_alt", left_alt },
.{ "left_super", left_super },
.{ "left_hyper", left_hyper },
.{ "left_meta", left_meta },
.{ "right_shift", right_shift },
.{ "right_control", right_control },
.{ "right_alt", right_alt },
.{ "right_super", right_super },
.{ "right_hyper", right_hyper },
.{ "right_meta", right_meta },
.{ "iso_level_3_shift", iso_level_3_shift },
.{ "iso_level_5_shift", iso_level_5_shift },
});
};
test "matches 'a'" {
const key: Key = .{
.codepoint = 'a',
.mods = .{ .num_lock = true },
};
try testing.expect(key.matches('a', .{}));
}
test "matches 'shift+a'" {
const key: Key = .{
.codepoint = 'a',
.mods = .{ .shift = true },
.text = "A",
};
try testing.expect(key.matches('a', .{ .shift = true }));
try testing.expect(key.matches('A', .{}));
try testing.expect(!key.matches('A', .{ .ctrl = true }));
}
test "matches 'shift+tab'" {
const key: Key = .{
.codepoint = Key.tab,
.mods = .{ .shift = true, .num_lock = true },
};
try testing.expect(key.matches(Key.tab, .{ .shift = true }));
try testing.expect(!key.matches(Key.tab, .{}));
}
test "matches 'shift+;'" {
const key: Key = .{
.codepoint = ';',
.shifted_codepoint = ':',
.mods = .{ .shift = true },
.text = ":",
};
try testing.expect(key.matches(';', .{ .shift = true }));
try testing.expect(key.matches(':', .{}));
const colon: Key = .{
.codepoint = ':',
.mods = .{},
};
try testing.expect(colon.matches(':', .{}));
}
test "name_map" {
try testing.expectEqual(insert, name_map.get("insert"));
}

308
deps/libvaxis/src/Loop.zig vendored Normal file
View File

@ -0,0 +1,308 @@
const std = @import("std");
const builtin = @import("builtin");
const grapheme = @import("grapheme");
const GraphemeCache = @import("GraphemeCache.zig");
const Parser = @import("Parser.zig");
const Queue = @import("queue.zig").Queue;
const vaxis = @import("main.zig");
const Tty = vaxis.Tty;
const Vaxis = @import("Vaxis.zig");
const log = std.log.scoped(.vaxis);
pub fn Loop(comptime T: type) type {
return struct {
const Self = @This();
const Event = T;
tty: *Tty,
vaxis: *Vaxis,
queue: Queue(T, 512) = .{},
thread: ?std.Thread = null,
should_quit: bool = false,
/// Initialize the event loop. This is an intrusive init so that we have
/// a stable pointer to register signal callbacks with posix TTYs
pub fn init(self: *Self) !void {
switch (builtin.os.tag) {
.windows => {},
else => {
const handler: Tty.SignalHandler = .{
.context = self,
.callback = Self.winsizeCallback,
};
try Tty.notifyWinsize(handler);
},
}
}
/// spawns the input thread to read input from the tty
pub fn start(self: *Self) !void {
if (self.thread) |_| return;
self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{
self,
&self.vaxis.unicode.grapheme_data,
self.vaxis.opts.system_clipboard_allocator,
});
}
/// stops reading from the tty.
pub fn stop(self: *Self) void {
// If we don't have a thread, we have nothing to stop
if (self.thread == null) return;
self.should_quit = true;
// trigger a read
self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {};
if (self.thread) |thread| {
thread.join();
self.thread = null;
self.should_quit = false;
}
}
/// returns the next available event, blocking until one is available
pub fn nextEvent(self: *Self) T {
return self.queue.pop();
}
/// blocks until an event is available. Useful when your application is
/// operating on a poll + drain architecture (see tryEvent)
pub fn pollEvent(self: *Self) void {
self.queue.poll();
}
/// returns an event if one is available, otherwise null. Non-blocking.
pub fn tryEvent(self: *Self) ?T {
return self.queue.tryPop();
}
/// posts an event into the event queue. Will block if there is not
/// capacity for the event
pub fn postEvent(self: *Self, event: T) void {
self.queue.push(event);
}
pub fn tryPostEvent(self: *Self, event: T) bool {
return self.queue.tryPush(event);
}
pub fn winsizeCallback(ptr: *anyopaque) void {
const self: *Self = @ptrCast(@alignCast(ptr));
// We will be receiving winsize updates in-band
if (self.vaxis.state.in_band_resize) return;
const winsize = Tty.getWinsize(self.tty.fd) catch return;
if (@hasField(Event, "winsize")) {
self.postEvent(.{ .winsize = winsize });
}
}
/// read input from the tty. This is run in a separate thread
fn ttyRun(
self: *Self,
grapheme_data: *const grapheme.GraphemeData,
paste_allocator: ?std.mem.Allocator,
) !void {
// initialize a grapheme cache
var cache: GraphemeCache = .{};
switch (builtin.os.tag) {
.windows => {
var parser: Parser = .{
.grapheme_data = grapheme_data,
};
while (!self.should_quit) {
const event = try self.tty.nextEvent(&parser, paste_allocator);
try handleEventGeneric(self, self.vaxis, &cache, Event, event, null);
}
},
else => {
// get our initial winsize
const winsize = try Tty.getWinsize(self.tty.fd);
if (@hasField(Event, "winsize")) {
self.postEvent(.{ .winsize = winsize });
}
var parser: Parser = .{
.grapheme_data = grapheme_data,
};
// initialize the read buffer
var buf: [1024]u8 = undefined;
var read_start: usize = 0;
// read loop
read_loop: while (!self.should_quit) {
const n = try self.tty.read(buf[read_start..]);
var seq_start: usize = 0;
while (seq_start < n) {
const result = try parser.parse(buf[seq_start..n], paste_allocator);
if (result.n == 0) {
// copy the read to the beginning. We don't use memcpy because
// this could be overlapping, and it's also rare
const initial_start = seq_start;
while (seq_start < n) : (seq_start += 1) {
buf[seq_start - initial_start] = buf[seq_start];
}
read_start = seq_start - initial_start + 1;
continue :read_loop;
}
read_start = 0;
seq_start += result.n;
const event = result.event orelse continue;
try handleEventGeneric(self, self.vaxis, &cache, Event, event, paste_allocator);
}
}
},
}
}
};
}
// Use return on the self.postEvent's so it can either return error union or void
pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Event: type, event: anytype, paste_allocator: ?std.mem.Allocator) !void {
switch (builtin.os.tag) {
.windows => {
switch (event) {
.winsize => |ws| {
if (@hasField(Event, "winsize")) {
return self.postEvent(.{ .winsize = ws });
}
},
.key_press => |key| {
if (@hasField(Event, "key_press")) {
// HACK: yuck. there has to be a better way
var mut_key = key;
if (key.text) |text| {
mut_key.text = cache.put(text);
}
return self.postEvent(.{ .key_press = mut_key });
}
},
.key_release => |key| {
if (@hasField(Event, "key_release")) {
// HACK: yuck. there has to be a better way
var mut_key = key;
if (key.text) |text| {
mut_key.text = cache.put(text);
}
return self.postEvent(.{ .key_release = mut_key });
}
},
.cap_da1 => {
std.Thread.Futex.wake(&vx.query_futex, 10);
},
.mouse => {}, // Unsupported currently
else => {},
}
},
else => {
switch (event) {
.key_press => |key| {
if (@hasField(Event, "key_press")) {
// HACK: yuck. there has to be a better way
var mut_key = key;
if (key.text) |text| {
mut_key.text = cache.put(text);
}
return self.postEvent(.{ .key_press = mut_key });
}
},
.key_release => |key| {
if (@hasField(Event, "key_release")) {
// HACK: yuck. there has to be a better way
var mut_key = key;
if (key.text) |text| {
mut_key.text = cache.put(text);
}
return self.postEvent(.{ .key_release = mut_key });
}
},
.mouse => |mouse| {
if (@hasField(Event, "mouse")) {
return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
}
},
.focus_in => {
if (@hasField(Event, "focus_in")) {
return self.postEvent(.focus_in);
}
},
.focus_out => {
if (@hasField(Event, "focus_out")) {
return self.postEvent(.focus_out);
}
},
.paste_start => {
if (@hasField(Event, "paste_start")) {
return self.postEvent(.paste_start);
}
},
.paste_end => {
if (@hasField(Event, "paste_end")) {
return self.postEvent(.paste_end);
}
},
.paste => |text| {
if (@hasField(Event, "paste")) {
return self.postEvent(.{ .paste = text });
} else {
if (paste_allocator) |_|
paste_allocator.?.free(text);
}
},
.color_report => |report| {
if (@hasField(Event, "color_report")) {
return self.postEvent(.{ .color_report = report });
}
},
.color_scheme => |scheme| {
if (@hasField(Event, "color_scheme")) {
return self.postEvent(.{ .color_scheme = scheme });
}
},
.cap_kitty_keyboard => {
log.info("kitty keyboard capability detected", .{});
vx.caps.kitty_keyboard = true;
},
.cap_kitty_graphics => {
if (!vx.caps.kitty_graphics) {
log.info("kitty graphics capability detected", .{});
vx.caps.kitty_graphics = true;
}
},
.cap_rgb => {
log.info("rgb capability detected", .{});
vx.caps.rgb = true;
},
.cap_unicode => {
log.info("unicode capability detected", .{});
vx.caps.unicode = .unicode;
vx.screen.width_method = .unicode;
},
.cap_sgr_pixels => {
log.info("pixel mouse capability detected", .{});
vx.caps.sgr_pixels = true;
},
.cap_color_scheme_updates => {
log.info("color_scheme_updates capability detected", .{});
vx.caps.color_scheme_updates = true;
},
.cap_da1 => {
std.Thread.Futex.wake(&vx.query_futex, 10);
},
.winsize => |winsize| {
vx.state.in_band_resize = true;
if (@hasField(Event, "winsize")) {
return self.postEvent(.{ .winsize = winsize });
}
},
}
},
}
}

50
deps/libvaxis/src/Mouse.zig vendored Normal file
View File

@ -0,0 +1,50 @@
/// A mouse event
pub const Mouse = @This();
pub const Shape = enum {
default,
text,
pointer,
help,
progress,
wait,
@"ew-resize",
@"ns-resize",
cell,
};
pub const Button = enum(u8) {
left,
middle,
right,
none,
wheel_up = 64,
wheel_down = 65,
wheel_right = 66,
wheel_left = 67,
button_8 = 128,
button_9 = 129,
button_10 = 130,
button_11 = 131,
};
pub const Modifiers = packed struct(u3) {
shift: bool = false,
alt: bool = false,
ctrl: bool = false,
};
pub const Type = enum {
press,
release,
motion,
drag,
};
col: usize,
row: usize,
xoffset: usize = 0,
yoffset: usize = 0,
button: Button,
mods: Modifiers,
type: Type,

1130
deps/libvaxis/src/Parser.zig vendored Normal file

File diff suppressed because it is too large Load Diff

78
deps/libvaxis/src/Screen.zig vendored Normal file
View File

@ -0,0 +1,78 @@
const std = @import("std");
const assert = std.debug.assert;
const Cell = @import("Cell.zig");
const Shape = @import("Mouse.zig").Shape;
const Image = @import("Image.zig");
const Winsize = @import("main.zig").Winsize;
const Unicode = @import("Unicode.zig");
const Method = @import("gwidth.zig").Method;
const Screen = @This();
width: usize = 0,
height: usize = 0,
width_pix: usize = 0,
height_pix: usize = 0,
buf: []Cell = undefined,
cursor_row: usize = 0,
cursor_col: usize = 0,
cursor_vis: bool = false,
unicode: *const Unicode = undefined,
width_method: Method = .wcwidth,
mouse_shape: Shape = .default,
cursor_shape: Cell.CursorShape = .default,
pub fn init(alloc: std.mem.Allocator, winsize: Winsize, unicode: *const Unicode) !Screen {
const w = winsize.cols;
const h = winsize.rows;
const self = Screen{
.buf = try alloc.alloc(Cell, w * h),
.width = w,
.height = h,
.width_pix = winsize.x_pixel,
.height_pix = winsize.y_pixel,
.unicode = unicode,
};
const base_cell: Cell = .{};
@memset(self.buf, base_cell);
return self;
}
pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void {
alloc.free(self.buf);
}
/// writes a cell to a location. 0 indexed
pub fn writeCell(self: *Screen, col: usize, row: usize, cell: Cell) void {
if (self.width <= col) {
// column out of bounds
return;
}
if (self.height <= row) {
// height out of bounds
return;
}
const i = (row * self.width) + col;
assert(i < self.buf.len);
self.buf[i] = cell;
}
pub fn readCell(self: *Screen, col: usize, row: usize) ?Cell {
if (self.width <= col) {
// column out of bounds
return null;
}
if (self.height <= row) {
// height out of bounds
return null;
}
const i = (row * self.width) + col;
assert(i < self.buf.len);
return self.buf[i];
}

28
deps/libvaxis/src/Unicode.zig vendored Normal file
View File

@ -0,0 +1,28 @@
const std = @import("std");
const grapheme = @import("grapheme");
const DisplayWidth = @import("DisplayWidth");
/// A thin wrapper around zg data
const Unicode = @This();
grapheme_data: grapheme.GraphemeData,
width_data: DisplayWidth.DisplayWidthData,
/// initialize all unicode data vaxis may possibly need
pub fn init(alloc: std.mem.Allocator) !Unicode {
return .{
.grapheme_data = try grapheme.GraphemeData.init(alloc),
.width_data = try DisplayWidth.DisplayWidthData.init(alloc),
};
}
/// free all data
pub fn deinit(self: *const Unicode) void {
self.grapheme_data.deinit();
self.width_data.deinit();
}
/// creates a grapheme iterator based on str
pub fn graphemeIterator(self: *const Unicode, str: []const u8) grapheme.Iterator {
return grapheme.Iterator.init(str, &self.grapheme_data);
}

1203
deps/libvaxis/src/Vaxis.zig vendored Normal file

File diff suppressed because it is too large Load Diff

854
deps/libvaxis/src/Window.zig vendored Normal file
View File

@ -0,0 +1,854 @@
const std = @import("std");
const Screen = @import("Screen.zig");
const Cell = @import("Cell.zig");
const Mouse = @import("Mouse.zig");
const Segment = @import("Cell.zig").Segment;
const Unicode = @import("Unicode.zig");
const gw = @import("gwidth.zig");
const Window = @This();
pub const Size = union(enum) {
expand,
limit: usize,
};
/// horizontal offset from the screen
x_off: usize,
/// vertical offset from the screen
y_off: usize,
/// width of the window. This can't be larger than the terminal screen
width: usize,
/// height of the window. This can't be larger than the terminal screen
height: usize,
screen: *Screen,
/// Deprecated. Use `child` instead
///
/// Creates a new window with offset relative to parent and size clamped to the
/// parent's size. Windows do not retain a reference to their parent and are
/// unaware of resizes.
pub fn initChild(
self: Window,
x_off: usize,
y_off: usize,
width: Size,
height: Size,
) Window {
const resolved_width = switch (width) {
.expand => self.width -| x_off,
.limit => |w| blk: {
if (w + x_off > self.width) {
break :blk self.width -| x_off;
}
break :blk w;
},
};
const resolved_height = switch (height) {
.expand => self.height -| y_off,
.limit => |h| blk: {
if (h + y_off > self.height) {
break :blk self.height -| y_off;
}
break :blk h;
},
};
return Window{
.x_off = x_off + self.x_off,
.y_off = y_off + self.y_off,
.width = resolved_width,
.height = resolved_height,
.screen = self.screen,
};
}
pub const ChildOptions = struct {
x_off: usize = 0,
y_off: usize = 0,
/// the width of the resulting child, including any borders
width: Size = .expand,
/// the height of the resulting child, including any borders
height: Size = .expand,
border: BorderOptions = .{},
};
pub const BorderOptions = struct {
style: Cell.Style = .{},
where: union(enum) {
none,
all,
top,
right,
bottom,
left,
other: Locations,
} = .none,
glyphs: Glyphs = .single_rounded,
pub const Locations = packed struct {
top: bool = false,
right: bool = false,
bottom: bool = false,
left: bool = false,
};
pub const Glyphs = union(enum) {
single_rounded,
single_square,
/// custom border glyphs. each glyph should be one cell wide and the
/// following indices apply:
/// [0] = top left
/// [1] = horizontal
/// [2] = top right
/// [3] = vertical
/// [4] = bottom right
/// [5] = bottom left
custom: [6][]const u8,
};
const single_rounded: [6][]const u8 = .{ "", "", "", "", "", "" };
const single_square: [6][]const u8 = .{ "", "", "", "", "", "" };
};
/// create a child window
pub fn child(self: Window, opts: ChildOptions) Window {
var result = self.initChild(opts.x_off, opts.y_off, opts.width, opts.height);
const glyphs = switch (opts.border.glyphs) {
.single_rounded => BorderOptions.single_rounded,
.single_square => BorderOptions.single_square,
.custom => |custom| custom,
};
const top_left: Cell.Character = .{ .grapheme = glyphs[0], .width = 1 };
const horizontal: Cell.Character = .{ .grapheme = glyphs[1], .width = 1 };
const top_right: Cell.Character = .{ .grapheme = glyphs[2], .width = 1 };
const vertical: Cell.Character = .{ .grapheme = glyphs[3], .width = 1 };
const bottom_right: Cell.Character = .{ .grapheme = glyphs[4], .width = 1 };
const bottom_left: Cell.Character = .{ .grapheme = glyphs[5], .width = 1 };
const style = opts.border.style;
const h = result.height;
const w = result.width;
const loc: BorderOptions.Locations = switch (opts.border.where) {
.none => return result,
.all => .{ .top = true, .bottom = true, .right = true, .left = true },
.bottom => .{ .bottom = true },
.right => .{ .right = true },
.left => .{ .left = true },
.top => .{ .top = true },
.other => |loc| loc,
};
if (loc.top) {
var i: usize = 0;
while (i < w) : (i += 1) {
result.writeCell(i, 0, .{ .char = horizontal, .style = style });
}
}
if (loc.bottom) {
var i: usize = 0;
while (i < w) : (i += 1) {
result.writeCell(i, h -| 1, .{ .char = horizontal, .style = style });
}
}
if (loc.left) {
var i: usize = 0;
while (i < h) : (i += 1) {
result.writeCell(0, i, .{ .char = vertical, .style = style });
}
}
if (loc.right) {
var i: usize = 0;
while (i < h) : (i += 1) {
result.writeCell(w -| 1, i, .{ .char = vertical, .style = style });
}
}
// draw corners
if (loc.top and loc.left)
result.writeCell(0, 0, .{ .char = top_left, .style = style });
if (loc.top and loc.right)
result.writeCell(w - 1, 0, .{ .char = top_right, .style = style });
if (loc.bottom and loc.left)
result.writeCell(0, h -| 1, .{ .char = bottom_left, .style = style });
if (loc.bottom and loc.right)
result.writeCell(w - 1, h -| 1, .{ .char = bottom_right, .style = style });
const x_off: usize = if (loc.left) 1 else 0;
const y_off: usize = if (loc.top) 1 else 0;
const h_delt: usize = if (loc.bottom) 1 else 0;
const w_delt: usize = if (loc.right) 1 else 0;
const h_ch: usize = h -| y_off -| h_delt;
const w_ch: usize = w -| x_off -| w_delt;
return result.initChild(x_off, y_off, .{ .limit = w_ch }, .{ .limit = h_ch });
}
/// writes a cell to the location in the window
pub fn writeCell(self: Window, col: usize, row: usize, cell: Cell) void {
if (self.height == 0 or self.width == 0) return;
if (self.height <= row or self.width <= col) return;
self.screen.writeCell(col + self.x_off, row + self.y_off, cell);
}
/// reads a cell at the location in the window
pub fn readCell(self: Window, col: usize, row: usize) ?Cell {
if (self.height == 0 or self.width == 0) return null;
if (self.height <= row or self.width <= col) return null;
return self.screen.readCell(col + self.x_off, row + self.y_off);
}
/// fills the window with the default cell
pub fn clear(self: Window) void {
self.fill(.{ .default = true });
}
/// returns the width of the grapheme. This depends on the terminal capabilities
pub fn gwidth(self: Window, str: []const u8) usize {
return gw.gwidth(str, self.screen.width_method, &self.screen.unicode.width_data) catch 1;
}
/// fills the window with the provided cell
pub fn fill(self: Window, cell: Cell) void {
if (self.screen.width < self.x_off)
return;
if (self.screen.height < self.y_off)
return;
if (self.x_off == 0 and self.width == self.screen.width) {
// we have a full width window, therefore contiguous memory.
const start = self.y_off * self.width;
const end = @min(start + (self.height * self.width), self.screen.buf.len);
@memset(self.screen.buf[start..end], cell);
} else {
// Non-contiguous. Iterate over rows an memset
var row: usize = self.y_off;
const last_row = @min(self.height + self.y_off, self.screen.height);
while (row < last_row) : (row += 1) {
const start = self.x_off + (row * self.screen.width);
const end = @min(start + self.width, start + (self.screen.width - self.x_off));
@memset(self.screen.buf[start..end], cell);
}
}
}
/// hide the cursor
pub fn hideCursor(self: Window) void {
self.screen.cursor_vis = false;
}
/// show the cursor at the given coordinates, 0 indexed
pub fn showCursor(self: Window, col: usize, row: usize) void {
if (self.height == 0 or self.width == 0) return;
if (self.height <= row or self.width <= col) return;
self.screen.cursor_vis = true;
self.screen.cursor_row = row + self.y_off;
self.screen.cursor_col = col + self.x_off;
}
pub fn setCursorShape(self: Window, shape: Cell.CursorShape) void {
self.screen.cursor_shape = shape;
}
/// Options to use when printing Segments to a window
pub const PrintOptions = struct {
/// vertical offset to start printing at
row_offset: usize = 0,
/// horizontal offset to start printing at
col_offset: usize = 0,
/// wrap behavior for printing
wrap: enum {
/// wrap at grapheme boundaries
grapheme,
/// wrap at word boundaries
word,
/// stop printing after one line
none,
} = .grapheme,
/// when true, print will write to the screen for rendering. When false,
/// nothing is written. The return value describes the size of the wrapped
/// text
commit: bool = true,
};
pub const PrintResult = struct {
col: usize,
row: usize,
overflow: bool,
};
/// prints segments to the window. returns true if the text overflowed with the
/// given wrap strategy and size.
pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) !PrintResult {
var row = opts.row_offset;
switch (opts.wrap) {
.grapheme => {
var col: usize = opts.col_offset;
const overflow: bool = blk: for (segments) |segment| {
var iter = self.screen.unicode.graphemeIterator(segment.text);
while (iter.next()) |grapheme| {
if (col >= self.width) {
row += 1;
col = 0;
}
if (row >= self.height) break :blk true;
const s = grapheme.bytes(segment.text);
if (std.mem.eql(u8, s, "\n")) {
row +|= 1;
col = 0;
continue;
}
const w = self.gwidth(s);
if (w == 0) continue;
if (opts.commit) self.writeCell(col, row, .{
.char = .{
.grapheme = s,
.width = w,
},
.style = segment.style,
.link = segment.link,
.wrapped = col + w >= self.width,
});
col += w;
}
} else false;
if (col >= self.width) {
row += 1;
col = 0;
}
return .{
.row = row,
.col = col,
.overflow = overflow,
};
},
.word => {
var col: usize = opts.col_offset;
var overflow: bool = false;
var soft_wrapped: bool = false;
outer: for (segments) |segment| {
var line_iter: LineIterator = .{ .buf = segment.text };
while (line_iter.next()) |line| {
defer {
// We only set soft_wrapped to false if a segment actually contains a linebreak
if (line_iter.has_break) {
soft_wrapped = false;
row += 1;
col = 0;
}
}
var iter: WhitespaceTokenizer = .{ .buf = line };
while (iter.next()) |token| {
switch (token) {
.whitespace => |len| {
if (soft_wrapped) continue;
for (0..len) |_| {
if (col >= self.width) {
col = 0;
row += 1;
break;
}
if (opts.commit) {
self.writeCell(col, row, .{
.char = .{
.grapheme = " ",
.width = 1,
},
.style = segment.style,
.link = segment.link,
});
}
col += 1;
}
},
.word => |word| {
const width = self.gwidth(word);
if (width + col > self.width and width < self.width) {
row += 1;
col = 0;
}
var grapheme_iterator = self.screen.unicode.graphemeIterator(word);
while (grapheme_iterator.next()) |grapheme| {
soft_wrapped = false;
if (row >= self.height) {
overflow = true;
break :outer;
}
const s = grapheme.bytes(word);
const w = self.gwidth(s);
if (opts.commit) self.writeCell(col, row, .{
.char = .{
.grapheme = s,
.width = w,
},
.style = segment.style,
.link = segment.link,
});
col += w;
if (col >= self.width) {
row += 1;
col = 0;
soft_wrapped = true;
}
}
},
}
}
}
}
return .{
// remove last row counter
.row = row,
.col = col,
.overflow = overflow,
};
},
.none => {
var col: usize = opts.col_offset;
const overflow: bool = blk: for (segments) |segment| {
var iter = self.screen.unicode.graphemeIterator(segment.text);
while (iter.next()) |grapheme| {
if (col >= self.width) break :blk true;
const s = grapheme.bytes(segment.text);
if (std.mem.eql(u8, s, "\n")) break :blk true;
const w = self.gwidth(s);
if (w == 0) continue;
if (opts.commit) self.writeCell(col, row, .{
.char = .{
.grapheme = s,
.width = w,
},
.style = segment.style,
.link = segment.link,
});
col +|= w;
}
} else false;
return .{
.row = row,
.col = col,
.overflow = overflow,
};
},
}
return false;
}
/// print a single segment. This is just a shortcut for print(&.{segment}, opts)
pub fn printSegment(self: Window, segment: Segment, opts: PrintOptions) !PrintResult {
return self.print(&.{segment}, opts);
}
/// scrolls the window down one row (IE inserts a blank row at the bottom of the
/// screen and shifts all rows up one)
pub fn scroll(self: Window, n: usize) void {
if (n > self.height) return;
var row = self.y_off;
while (row < self.height - n) : (row += 1) {
const dst_start = (row * self.width) + self.x_off;
const dst_end = dst_start + self.width;
const src_start = ((row + n) * self.width) + self.x_off;
const src_end = src_start + self.width;
@memcpy(self.screen.buf[dst_start..dst_end], self.screen.buf[src_start..src_end]);
}
const last_row = self.child(.{
.y_off = self.height - n,
});
last_row.clear();
}
/// returns the mouse event if the mouse event occurred within the window. If
/// the mouse event occurred outside the window, null is returned
pub fn hasMouse(win: Window, mouse: ?Mouse) ?Mouse {
const event = mouse orelse return null;
if (event.col >= win.x_off and
event.col < (win.x_off + win.width) and
event.row >= win.y_off and
event.row < (win.y_off + win.height)) return event else return null;
}
test "Window size set" {
var parent = Window{
.x_off = 0,
.y_off = 0,
.width = 20,
.height = 20,
.screen = undefined,
};
const ch = parent.initChild(1, 1, .expand, .expand);
try std.testing.expectEqual(19, ch.width);
try std.testing.expectEqual(19, ch.height);
}
test "Window size set too big" {
var parent = Window{
.x_off = 0,
.y_off = 0,
.width = 20,
.height = 20,
.screen = undefined,
};
const ch = parent.initChild(0, 0, .{ .limit = 21 }, .{ .limit = 21 });
try std.testing.expectEqual(20, ch.width);
try std.testing.expectEqual(20, ch.height);
}
test "Window size set too big with offset" {
var parent = Window{
.x_off = 0,
.y_off = 0,
.width = 20,
.height = 20,
.screen = undefined,
};
const ch = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
try std.testing.expectEqual(10, ch.width);
try std.testing.expectEqual(10, ch.height);
}
test "Window size nested offsets" {
var parent = Window{
.x_off = 1,
.y_off = 1,
.width = 20,
.height = 20,
.screen = undefined,
};
const ch = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
try std.testing.expectEqual(11, ch.x_off);
try std.testing.expectEqual(11, ch.y_off);
}
test "print: grapheme" {
const alloc = std.testing.allocator_instance.allocator();
const unicode = try Unicode.init(alloc);
defer unicode.deinit();
var screen: Screen = .{ .width_method = .unicode, .unicode = &unicode };
const win: Window = .{
.x_off = 0,
.y_off = 0,
.width = 4,
.height = 2,
.screen = &screen,
};
const opts: PrintOptions = .{
.commit = false,
.wrap = .grapheme,
};
{
var segments = [_]Segment{
.{ .text = "a" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(1, result.col);
try std.testing.expectEqual(0, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "abcd" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(0, result.col);
try std.testing.expectEqual(1, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "abcde" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(1, result.col);
try std.testing.expectEqual(1, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "abcdefgh" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(0, result.col);
try std.testing.expectEqual(2, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "abcdefghi" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(0, result.col);
try std.testing.expectEqual(2, result.row);
try std.testing.expectEqual(true, result.overflow);
}
}
test "print: word" {
const alloc = std.testing.allocator_instance.allocator();
const unicode = try Unicode.init(alloc);
defer unicode.deinit();
var screen: Screen = .{
.width_method = .unicode,
.unicode = &unicode,
};
const win: Window = .{
.x_off = 0,
.y_off = 0,
.width = 4,
.height = 2,
.screen = &screen,
};
const opts: PrintOptions = .{
.commit = false,
.wrap = .word,
};
{
var segments = [_]Segment{
.{ .text = "a" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(1, result.col);
try std.testing.expectEqual(0, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = " " },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(1, result.col);
try std.testing.expectEqual(0, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = " a" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(2, result.col);
try std.testing.expectEqual(0, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "a b" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(3, result.col);
try std.testing.expectEqual(0, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "a b c" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(1, result.col);
try std.testing.expectEqual(1, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "hello" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(1, result.col);
try std.testing.expectEqual(1, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "hi tim" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(3, result.col);
try std.testing.expectEqual(1, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "hello tim" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(0, result.col);
try std.testing.expectEqual(2, result.row);
try std.testing.expectEqual(true, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "hello ti" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(0, result.col);
try std.testing.expectEqual(2, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "h" },
.{ .text = "e" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(2, result.col);
try std.testing.expectEqual(0, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "h" },
.{ .text = "e" },
.{ .text = "l" },
.{ .text = "l" },
.{ .text = "o" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(1, result.col);
try std.testing.expectEqual(1, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "he\n" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(0, result.col);
try std.testing.expectEqual(1, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "he\n\n" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(0, result.col);
try std.testing.expectEqual(2, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "not now" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(3, result.col);
try std.testing.expectEqual(1, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "note now" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(3, result.col);
try std.testing.expectEqual(1, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "note" },
.{ .text = " now" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(3, result.col);
try std.testing.expectEqual(1, result.row);
try std.testing.expectEqual(false, result.overflow);
}
{
var segments = [_]Segment{
.{ .text = "note " },
.{ .text = "now" },
};
const result = try win.print(&segments, opts);
try std.testing.expectEqual(3, result.col);
try std.testing.expectEqual(1, result.row);
try std.testing.expectEqual(false, result.overflow);
}
}
/// Iterates a slice of bytes by linebreaks. Lines are split by '\r', '\n', or '\r\n'
const LineIterator = struct {
buf: []const u8,
index: usize = 0,
has_break: bool = true,
fn next(self: *LineIterator) ?[]const u8 {
if (self.index >= self.buf.len) return null;
const start = self.index;
const end = std.mem.indexOfAnyPos(u8, self.buf, self.index, "\r\n") orelse {
if (start == 0) self.has_break = false;
self.index = self.buf.len;
return self.buf[start..];
};
self.index = end;
self.consumeCR();
self.consumeLF();
return self.buf[start..end];
}
// consumes a \n byte
fn consumeLF(self: *LineIterator) void {
if (self.index >= self.buf.len) return;
if (self.buf[self.index] == '\n') self.index += 1;
}
// consumes a \r byte
fn consumeCR(self: *LineIterator) void {
if (self.index >= self.buf.len) return;
if (self.buf[self.index] == '\r') self.index += 1;
}
};
/// Returns tokens of text and whitespace
const WhitespaceTokenizer = struct {
buf: []const u8,
index: usize = 0,
const Token = union(enum) {
// the length of whitespace. Tab = 8
whitespace: usize,
word: []const u8,
};
fn next(self: *WhitespaceTokenizer) ?Token {
if (self.index >= self.buf.len) return null;
const Mode = enum {
whitespace,
word,
};
const first = self.buf[self.index];
const mode: Mode = if (first == ' ' or first == '\t') .whitespace else .word;
switch (mode) {
.whitespace => {
var len: usize = 0;
while (self.index < self.buf.len) : (self.index += 1) {
switch (self.buf[self.index]) {
' ' => len += 1,
'\t' => len += 8,
else => break,
}
}
return .{ .whitespace = len };
},
.word => {
const start = self.index;
while (self.index < self.buf.len) : (self.index += 1) {
switch (self.buf[self.index]) {
' ', '\t' => break,
else => {},
}
}
return .{ .word = self.buf[start..self.index] };
},
}
}
};

207
deps/libvaxis/src/aio.zig vendored Normal file
View File

@ -0,0 +1,207 @@
const build_options = @import("build_options");
const builtin = @import("builtin");
const std = @import("std");
const vaxis = @import("main.zig");
const handleEventGeneric = @import("Loop.zig").handleEventGeneric;
const log = std.log.scoped(.vaxis_aio);
const Yield = enum { no_state, took_event };
pub fn Loop(T: type) type {
if (!build_options.aio) {
@compileError(
\\build_options.aio is not enabled.
\\Use `LoopWithModules` instead to provide `aio` and `coro` modules from outside vaxis.
);
}
return LoopWithModules(T, @import("aio"), @import("coro"));
}
/// zig-aio based event loop
/// <https://github.com/Cloudef/zig-aio>
pub fn LoopWithModules(T: type, aio: type, coro: type) type {
return struct {
const Event = T;
winsize_task: ?coro.Task.Generic2(winsizeTask) = null,
reader_task: ?coro.Task.Generic2(ttyReaderTask) = null,
queue: std.BoundedArray(T, 512) = .{},
source: aio.EventSource,
fatal: bool = false,
pub fn init() !@This() {
return .{ .source = try aio.EventSource.init() };
}
pub fn deinit(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) void {
vx.deviceStatusReport(tty.anyWriter()) catch {};
if (self.winsize_task) |task| task.cancel();
if (self.reader_task) |task| task.cancel();
self.source.deinit();
self.* = undefined;
}
fn winsizeInner(self: *@This(), tty: *vaxis.Tty) !void {
const Context = struct {
loop: *@TypeOf(self.*),
tty: *vaxis.Tty,
winsize: ?vaxis.Winsize = null,
fn cb(ptr: *anyopaque) void {
std.debug.assert(coro.current() == null);
const ctx: *@This() = @ptrCast(@alignCast(ptr));
ctx.winsize = vaxis.Tty.getWinsize(ctx.tty.fd) catch return;
ctx.loop.source.notify();
}
};
// keep on stack
var ctx: Context = .{ .loop = self, .tty = tty };
if (builtin.target.os.tag != .windows) {
if (@hasField(Event, "winsize")) {
const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb };
try vaxis.Tty.notifyWinsize(handler);
}
}
while (true) {
try coro.io.single(aio.WaitEventSource{ .source = &self.source });
if (ctx.winsize) |winsize| {
if (!@hasField(Event, "winsize")) unreachable;
ctx.loop.postEvent(.{ .winsize = winsize }) catch {};
ctx.winsize = null;
}
}
}
fn winsizeTask(self: *@This(), tty: *vaxis.Tty) void {
self.winsizeInner(tty) catch |err| {
if (err != error.Canceled) log.err("winsize: {}", .{err});
self.fatal = true;
};
}
fn windowsReadEvent(tty: *vaxis.Tty) !vaxis.Event {
var state: vaxis.Tty.EventState = .{};
while (true) {
var bytes_read: usize = 0;
var input_record: vaxis.Tty.INPUT_RECORD = undefined;
try coro.io.single(aio.ReadTty{
.tty = .{ .handle = tty.stdin },
.buffer = std.mem.asBytes(&input_record),
.out_read = &bytes_read,
});
if (try tty.eventFromRecord(&input_record, &state)) |ev| {
return ev;
}
}
}
fn ttyReaderWindows(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) !void {
var cache: vaxis.GraphemeCache = .{};
while (true) {
const event = try windowsReadEvent(tty);
try handleEventGeneric(self, vx, &cache, Event, event, null);
}
}
fn ttyReaderPosix(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void {
// initialize a grapheme cache
var cache: vaxis.GraphemeCache = .{};
// get our initial winsize
const winsize = try vaxis.Tty.getWinsize(tty.fd);
if (@hasField(Event, "winsize")) {
try self.postEvent(.{ .winsize = winsize });
}
var parser: vaxis.Parser = .{
.grapheme_data = &vx.unicode.grapheme_data,
};
const file: std.fs.File = .{ .handle = tty.fd };
while (true) {
var buf: [4096]u8 = undefined;
var n: usize = undefined;
var read_start: usize = 0;
try coro.io.single(aio.ReadTty{ .tty = file, .buffer = buf[read_start..], .out_read = &n });
var seq_start: usize = 0;
while (seq_start < n) {
const result = try parser.parse(buf[seq_start..n], paste_allocator);
if (result.n == 0) {
// copy the read to the beginning. We don't use memcpy because
// this could be overlapping, and it's also rare
const initial_start = seq_start;
while (seq_start < n) : (seq_start += 1) {
buf[seq_start - initial_start] = buf[seq_start];
}
read_start = seq_start - initial_start + 1;
continue;
}
read_start = 0;
seq_start += result.n;
const event = result.event orelse continue;
try handleEventGeneric(self, vx, &cache, Event, event, paste_allocator);
}
}
}
fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void {
return switch (builtin.target.os.tag) {
.windows => self.ttyReaderWindows(vx, tty),
else => self.ttyReaderPosix(vx, tty, paste_allocator),
} catch |err| {
if (err != error.Canceled) log.err("ttyReader: {}", .{err});
self.fatal = true;
};
}
/// Spawns tasks to handle winsize signal and tty
pub fn spawn(
self: *@This(),
scheduler: *coro.Scheduler,
vx: *vaxis.Vaxis,
tty: *vaxis.Tty,
paste_allocator: ?std.mem.Allocator,
spawn_options: coro.Scheduler.SpawnOptions,
) coro.Scheduler.SpawnError!void {
if (self.reader_task) |_| unreachable; // programming error
// This is required even if app doesn't care about winsize
// It is because it consumes the EventSource, so it can wakeup the scheduler
// Without that custom `postEvent`'s wouldn't wake up the scheduler and UI wouldn't update
self.winsize_task = try scheduler.spawn(winsizeTask, .{ self, tty }, spawn_options);
self.reader_task = try scheduler.spawn(ttyReaderTask, .{ self, vx, tty, paste_allocator }, spawn_options);
}
pub const PopEventError = error{TtyCommunicationSevered};
/// Call this in a while loop in the main event handler until it returns null
pub fn popEvent(self: *@This()) PopEventError!?T {
if (self.fatal) return error.TtyCommunicationSevered;
defer self.winsize_task.?.wakeupIf(Yield.took_event);
defer self.reader_task.?.wakeupIf(Yield.took_event);
return self.queue.popOrNull();
}
pub const PostEventError = error{Overflow};
pub fn postEvent(self: *@This(), event: T) !void {
if (coro.current()) |_| {
while (true) {
self.queue.insert(0, event) catch {
// wait for the app to take event
try coro.yield(Yield.took_event);
continue;
};
break;
}
} else {
// queue can be full, app could handle this error by spinning the scheduler
try self.queue.insert(0, event);
}
// wakes up the scheduler, so custom events update UI
self.source.notify();
}
};
}

137
deps/libvaxis/src/ctlseqs.zig vendored Normal file
View File

@ -0,0 +1,137 @@
// Queries
pub const primary_device_attrs = "\x1b[c";
pub const tertiary_device_attrs = "\x1b[=c";
pub const device_status_report = "\x1b[5n";
pub const xtversion = "\x1b[>0q";
pub const decrqm_focus = "\x1b[?1004$p";
pub const decrqm_sgr_pixels = "\x1b[?1016$p";
pub const decrqm_sync = "\x1b[?2026$p";
pub const decrqm_unicode = "\x1b[?2027$p";
pub const decrqm_color_scheme = "\x1b[?2031$p";
pub const csi_u_query = "\x1b[?u";
pub const kitty_graphics_query = "\x1b_Gi=1,a=q\x1b\\";
pub const sixel_geometry_query = "\x1b[?2;1;0S";
// mouse. We try for button motion and any motion. terminals will enable the
// last one we tried (any motion). This was added because zellij doesn't
// support any motion currently
// See: https://github.com/zellij-org/zellij/issues/1679
pub const mouse_set = "\x1b[?1002;1003;1004;1006h";
pub const mouse_set_pixels = "\x1b[?1002;1003;1004;1016h";
pub const mouse_reset = "\x1b[?1002;1003;1004;1006;1016l";
// in-band window size reports
pub const in_band_resize_set = "\x1b[?2048h";
pub const in_band_resize_reset = "\x1b[?2048l";
// sync
pub const sync_set = "\x1b[?2026h";
pub const sync_reset = "\x1b[?2026l";
// unicode
pub const unicode_set = "\x1b[?2027h";
pub const unicode_reset = "\x1b[?2027l";
// bracketed paste
pub const bp_set = "\x1b[?2004h";
pub const bp_reset = "\x1b[?2004l";
// color scheme updates
pub const color_scheme_request = "\x1b[?996n";
pub const color_scheme_set = "\x1b[?2031h";
pub const color_scheme_reset = "\x1b[?2031l";
// Key encoding
pub const csi_u_push = "\x1b[>{d}u";
pub const csi_u_pop = "\x1b[<u";
// Cursor
pub const home = "\x1b[H";
pub const cup = "\x1b[{d};{d}H";
pub const hide_cursor = "\x1b[?25l";
pub const show_cursor = "\x1b[?25h";
pub const cursor_shape = "\x1b[{d} q";
pub const ri = "\x1bM";
pub const ind = "\n";
pub const cuf = "\x1b[{d}C";
pub const cub = "\x1b[{d}D";
// Erase
pub const erase_below_cursor = "\x1b[J";
// alt screen
pub const smcup = "\x1b[?1049h";
pub const rmcup = "\x1b[?1049l";
// sgr reset all
pub const sgr_reset = "\x1b[m";
// colors
pub const fg_base = "\x1b[3{d}m";
pub const fg_bright = "\x1b[9{d}m";
pub const bg_base = "\x1b[4{d}m";
pub const bg_bright = "\x1b[10{d}m";
pub const fg_reset = "\x1b[39m";
pub const bg_reset = "\x1b[49m";
pub const ul_reset = "\x1b[59m";
pub const fg_indexed = "\x1b[38:5:{d}m";
pub const bg_indexed = "\x1b[48:5:{d}m";
pub const ul_indexed = "\x1b[58:5:{d}m";
pub const fg_rgb = "\x1b[38:2:{d}:{d}:{d}m";
pub const bg_rgb = "\x1b[48:2:{d}:{d}:{d}m";
pub const ul_rgb = "\x1b[58:2:{d}:{d}:{d}m";
pub const fg_indexed_legacy = "\x1b[38;5;{d}m";
pub const bg_indexed_legacy = "\x1b[48;5;{d}m";
pub const ul_indexed_legacy = "\x1b[58;5;{d}m";
pub const fg_rgb_legacy = "\x1b[38;2;{d};{d};{d}m";
pub const bg_rgb_legacy = "\x1b[48;2;{d};{d};{d}m";
pub const ul_rgb_legacy = "\x1b[58;2;{d};{d};{d}m";
// Underlines
pub const ul_off = "\x1b[24m"; // NOTE: this could be \x1b[4:0m but is not as widely supported
pub const ul_single = "\x1b[4m";
pub const ul_double = "\x1b[4:2m";
pub const ul_curly = "\x1b[4:3m";
pub const ul_dotted = "\x1b[4:4m";
pub const ul_dashed = "\x1b[4:5m";
// Attributes
pub const bold_set = "\x1b[1m";
pub const dim_set = "\x1b[2m";
pub const italic_set = "\x1b[3m";
pub const blink_set = "\x1b[5m";
pub const reverse_set = "\x1b[7m";
pub const invisible_set = "\x1b[8m";
pub const strikethrough_set = "\x1b[9m";
pub const bold_dim_reset = "\x1b[22m";
pub const italic_reset = "\x1b[23m";
pub const blink_reset = "\x1b[25m";
pub const reverse_reset = "\x1b[27m";
pub const invisible_reset = "\x1b[28m";
pub const strikethrough_reset = "\x1b[29m";
// OSC sequences
pub const osc2_set_title = "\x1b]2;{s}\x1b\\";
pub const osc8 = "\x1b]8;{s};{s}\x1b\\";
pub const osc8_clear = "\x1b]8;;\x1b\\";
pub const osc9_notify = "\x1b]9;{s}\x1b\\";
pub const osc777_notify = "\x1b]777;notify;{s};{s}\x1b\\";
pub const osc22_mouse_shape = "\x1b]22;{s}\x1b\\";
pub const osc52_clipboard_copy = "\x1b]52;c;{s}\x1b\\";
pub const osc52_clipboard_request = "\x1b]52;c;?\x1b\\";
// Kitty graphics
pub const kitty_graphics_clear = "\x1b_Ga=d\x1b\\";
pub const kitty_graphics_preamble = "\x1b_Ga=p,i={d}";
pub const kitty_graphics_closing = ",C=1\x1b\\";
// Color control sequences
pub const osc4_query = "\x1b]4;{d};?\x1b\\"; // color index {d}
pub const osc4_reset = "\x1b]104\x1b\\"; // this resets _all_ color indexes
pub const osc10_query = "\x1b]10;?\x1b\\"; // fg
pub const osc10_reset = "\x1b]110\x1b\\"; // reset fg to terminal default
pub const osc11_query = "\x1b]11;?\x1b\\"; // bg
pub const osc11_reset = "\x1b]111\x1b\\"; // reset bg to terminal default
pub const osc12_query = "\x1b]12;?\x1b\\"; // cursor color
pub const osc12_reset = "\x1b]112\x1b\\"; // reset cursor to terminal default

28
deps/libvaxis/src/event.zig vendored Normal file
View File

@ -0,0 +1,28 @@
pub const Key = @import("Key.zig");
pub const Mouse = @import("Mouse.zig");
pub const Color = @import("Cell.zig").Color;
pub const Winsize = @import("main.zig").Winsize;
/// The events that Vaxis emits internally
pub const Event = union(enum) {
key_press: Key,
key_release: Key,
mouse: Mouse,
focus_in,
focus_out,
paste_start, // bracketed paste start
paste_end, // bracketed paste end
paste: []const u8, // osc 52 paste, caller must free
color_report: Color.Report, // osc 4, 10, 11, 12 response
color_scheme: Color.Scheme,
winsize: Winsize,
// these are delivered as discovered terminal capabilities
cap_kitty_keyboard,
cap_kitty_graphics,
cap_rgb,
cap_sgr_pixels,
cap_unicode,
cap_da1,
cap_color_scheme_updates,
};

80
deps/libvaxis/src/gwidth.zig vendored Normal file
View File

@ -0,0 +1,80 @@
const std = @import("std");
const unicode = std.unicode;
const testing = std.testing;
const DisplayWidth = @import("DisplayWidth");
const code_point = @import("code_point");
/// the method to use when calculating the width of a grapheme
pub const Method = enum {
unicode,
wcwidth,
no_zwj,
};
/// returns the width of the provided string, as measured by the method chosen
pub fn gwidth(str: []const u8, method: Method, data: *const DisplayWidth.DisplayWidthData) !usize {
switch (method) {
.unicode => {
const dw: DisplayWidth = .{ .data = data };
return dw.strWidth(str);
},
.wcwidth => {
var total: usize = 0;
var iter: code_point.Iterator = .{ .bytes = str };
while (iter.next()) |cp| {
const w = switch (cp.code) {
// undo an override in zg for emoji skintone selectors
0x1f3fb...0x1f3ff,
=> 2,
else => data.codePointWidth(cp.code),
};
if (w < 0) continue;
total += @intCast(w);
}
return total;
},
.no_zwj => {
var out: [256]u8 = undefined;
if (str.len > out.len) return error.OutOfMemory;
const n = std.mem.replacementSize(u8, str, "\u{200D}", "");
_ = std.mem.replace(u8, str, "\u{200D}", "", &out);
return gwidth(out[0..n], .unicode, data);
},
}
}
test "gwidth: a" {
const alloc = testing.allocator_instance.allocator();
const data = try DisplayWidth.DisplayWidthData.init(alloc);
defer data.deinit();
try testing.expectEqual(1, try gwidth("a", .unicode, &data));
try testing.expectEqual(1, try gwidth("a", .wcwidth, &data));
try testing.expectEqual(1, try gwidth("a", .no_zwj, &data));
}
test "gwidth: emoji with ZWJ" {
const alloc = testing.allocator_instance.allocator();
const data = try DisplayWidth.DisplayWidthData.init(alloc);
defer data.deinit();
try testing.expectEqual(2, try gwidth("👩‍🚀", .unicode, &data));
try testing.expectEqual(4, try gwidth("👩‍🚀", .wcwidth, &data));
try testing.expectEqual(4, try gwidth("👩‍🚀", .no_zwj, &data));
}
test "gwidth: emoji with VS16 selector" {
const alloc = testing.allocator_instance.allocator();
const data = try DisplayWidth.DisplayWidthData.init(alloc);
defer data.deinit();
try testing.expectEqual(2, try gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .unicode, &data));
try testing.expectEqual(1, try gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .wcwidth, &data));
try testing.expectEqual(2, try gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .no_zwj, &data));
}
test "gwidth: emoji with skin tone selector" {
const alloc = testing.allocator_instance.allocator();
const data = try DisplayWidth.DisplayWidthData.init(alloc);
defer data.deinit();
try testing.expectEqual(2, try gwidth("👋🏿", .unicode, &data));
try testing.expectEqual(4, try gwidth("👋🏿", .wcwidth, &data));
try testing.expectEqual(2, try gwidth("👋🏿", .no_zwj, &data));
}

92
deps/libvaxis/src/main.zig vendored Normal file
View File

@ -0,0 +1,92 @@
const std = @import("std");
const builtin = @import("builtin");
const build_options = @import("build_options");
pub const Vaxis = @import("Vaxis.zig");
pub const Loop = @import("Loop.zig").Loop;
pub const xev = @import("xev.zig");
pub const aio = @import("aio.zig");
pub const zigimg = @import("zigimg");
pub const Queue = @import("queue.zig").Queue;
pub const Key = @import("Key.zig");
pub const Cell = @import("Cell.zig");
pub const Segment = Cell.Segment;
pub const PrintOptions = Window.PrintOptions;
pub const Style = Cell.Style;
pub const Color = Cell.Color;
pub const Image = @import("Image.zig");
pub const Mouse = @import("Mouse.zig");
pub const Screen = @import("Screen.zig");
pub const AllocatingScreen = @import("InternalScreen.zig");
pub const Parser = @import("Parser.zig");
pub const Window = @import("Window.zig");
pub const widgets = @import("widgets.zig");
pub const gwidth = @import("gwidth.zig");
pub const ctlseqs = @import("ctlseqs.zig");
pub const GraphemeCache = @import("GraphemeCache.zig");
pub const grapheme = @import("grapheme");
pub const Event = @import("event.zig").Event;
pub const Unicode = @import("Unicode.zig");
/// The target TTY implementation
pub const Tty = switch (builtin.os.tag) {
.windows => @import("windows/Tty.zig"),
else => @import("posix/Tty.zig"),
};
/// The size of the terminal screen
pub const Winsize = struct {
rows: usize,
cols: usize,
x_pixel: usize,
y_pixel: usize,
};
/// Initialize a Vaxis application.
pub fn init(alloc: std.mem.Allocator, opts: Vaxis.Options) !Vaxis {
return Vaxis.init(alloc, opts);
}
/// Resets terminal state on a panic, then calls the default zig panic handler
pub fn panic_handler(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
if (Tty.global_tty) |gty| {
const reset: []const u8 = ctlseqs.csi_u_pop ++
ctlseqs.mouse_reset ++
ctlseqs.bp_reset ++
ctlseqs.rmcup;
gty.anyWriter().writeAll(reset) catch {};
gty.deinit();
}
std.builtin.default_panic(msg, error_return_trace, ret_addr);
}
pub const log_scopes = enum {
vaxis,
};
/// the vaxis logo. In PixelCode
pub const logo =
\\▄ ▄ ▄▄▄ ▄ ▄ ▄▄▄ ▄▄▄
\\█ █ █▄▄▄█ ▀▄ ▄▀ █ █ ▀
\\▀▄ ▄▀ █ █ ▄▀▄ █ ▀▀▀▄
\\ ▀▄▀ █ █ █ █ ▄█▄ ▀▄▄▄▀
;
test {
_ = @import("gwidth.zig");
_ = @import("Cell.zig");
_ = @import("Key.zig");
_ = @import("Parser.zig");
_ = @import("Window.zig");
_ = @import("gwidth.zig");
_ = @import("queue.zig");
if (build_options.text_input)
_ = @import("widgets/TextInput.zig");
}

178
deps/libvaxis/src/posix/Tty.zig vendored Normal file
View File

@ -0,0 +1,178 @@
//! TTY implementation conforming to posix standards
const Posix = @This();
const std = @import("std");
const builtin = @import("builtin");
const posix = std.posix;
const Winsize = @import("../main.zig").Winsize;
/// the original state of the terminal, prior to calling makeRaw
termios: posix.termios,
/// The file descriptor of the tty
fd: posix.fd_t,
pub const SignalHandler = struct {
context: *anyopaque,
callback: *const fn (context: *anyopaque) void,
};
/// global signal handlers
var handlers: [8]SignalHandler = undefined;
var handler_mutex: std.Thread.Mutex = .{};
var handler_idx: usize = 0;
/// global tty instance, used in case of a panic. Not guaranteed to work if
/// for some reason there are multiple TTYs open under a single vaxis
/// compilation unit - but this is better than nothing
pub var global_tty: ?Posix = null;
/// initializes a Tty instance by opening /dev/tty and "making it raw". A
/// signal handler is installed for SIGWINCH. No callbacks are installed, be
/// sure to register a callback when initializing the event loop
pub fn init() !Posix {
// Open our tty
const fd = try posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0);
// Set the termios of the tty
const termios = try makeRaw(fd);
var act = posix.Sigaction{
.handler = .{ .handler = Posix.handleWinch },
.mask = switch (builtin.os.tag) {
.macos => 0,
.linux => posix.empty_sigset,
.freebsd => posix.empty_sigset,
else => @compileError("os not supported"),
},
.flags = 0,
};
try posix.sigaction(posix.SIG.WINCH, &act, null);
const self: Posix = .{
.fd = fd,
.termios = termios,
};
global_tty = self;
return self;
}
/// release resources associated with the Tty return it to its original state
pub fn deinit(self: Posix) void {
posix.tcsetattr(self.fd, .FLUSH, self.termios) catch |err| {
std.log.err("couldn't restore terminal: {}", .{err});
};
if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos
posix.close(self.fd);
}
/// Write bytes to the tty
pub fn write(self: *const Posix, bytes: []const u8) !usize {
return posix.write(self.fd, bytes);
}
pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize {
const self: *const Posix = @ptrCast(@alignCast(ptr));
return posix.write(self.fd, bytes);
}
pub fn anyWriter(self: *const Posix) std.io.AnyWriter {
return .{
.context = self,
.writeFn = Posix.opaqueWrite,
};
}
pub fn read(self: *const Posix, buf: []u8) !usize {
return posix.read(self.fd, buf);
}
pub fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize {
const self: *const Posix = @ptrCast(@alignCast(ptr));
return posix.read(self.fd, buf);
}
pub fn anyReader(self: *const Posix) std.io.AnyReader {
return .{
.context = self,
.readFn = Posix.opaqueRead,
};
}
/// Install a signal handler for winsize. A maximum of 8 handlers may be
/// installed
pub fn notifyWinsize(handler: SignalHandler) !void {
handler_mutex.lock();
defer handler_mutex.unlock();
if (handler_idx == handlers.len) return error.OutOfMemory;
handlers[handler_idx] = handler;
handler_idx += 1;
}
fn handleWinch(_: c_int) callconv(.C) void {
handler_mutex.lock();
defer handler_mutex.unlock();
var i: usize = 0;
while (i < handler_idx) : (i += 1) {
const handler = handlers[i];
handler.callback(handler.context);
}
}
/// makeRaw enters the raw state for the terminal.
pub fn makeRaw(fd: posix.fd_t) !posix.termios {
const state = try posix.tcgetattr(fd);
var raw = state;
// see termios(3)
raw.iflag.IGNBRK = false;
raw.iflag.BRKINT = false;
raw.iflag.PARMRK = false;
raw.iflag.ISTRIP = false;
raw.iflag.INLCR = false;
raw.iflag.IGNCR = false;
raw.iflag.ICRNL = false;
raw.iflag.IXON = false;
raw.oflag.OPOST = false;
raw.lflag.ECHO = false;
raw.lflag.ECHONL = false;
raw.lflag.ICANON = false;
raw.lflag.ISIG = false;
raw.lflag.IEXTEN = false;
raw.cflag.CSIZE = .CS8;
raw.cflag.PARENB = false;
raw.cc[@intFromEnum(posix.V.MIN)] = 1;
raw.cc[@intFromEnum(posix.V.TIME)] = 0;
try posix.tcsetattr(fd, .FLUSH, raw);
return state;
}
/// Get the window size from the kernel
pub fn getWinsize(fd: posix.fd_t) !Winsize {
var winsize = posix.winsize{
.ws_row = 0,
.ws_col = 0,
.ws_xpixel = 0,
.ws_ypixel = 0,
};
const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize));
if (posix.errno(err) == .SUCCESS)
return Winsize{
.rows = winsize.ws_row,
.cols = winsize.ws_col,
.x_pixel = winsize.ws_xpixel,
.y_pixel = winsize.ws_ypixel,
};
return error.IoctlError;
}
pub fn bufferedWriter(self: *const Posix) std.io.BufferedWriter(4096, std.io.AnyWriter) {
return std.io.bufferedWriter(self.anyWriter());
}

325
deps/libvaxis/src/queue.zig vendored Normal file
View File

@ -0,0 +1,325 @@
const std = @import("std");
const assert = std.debug.assert;
const atomic = std.atomic;
const Condition = std.Thread.Condition;
/// Thread safe. Fixed size. Blocking push and pop.
pub fn Queue(
comptime T: type,
comptime size: usize,
) type {
return struct {
buf: [size]T = undefined,
read_index: usize = 0,
write_index: usize = 0,
mutex: std.Thread.Mutex = .{},
// blocks when the buffer is full
not_full: Condition = .{},
// ...or empty
not_empty: Condition = .{},
const Self = @This();
/// Pop an item from the queue. Blocks until an item is available.
pub fn pop(self: *Self) T {
self.mutex.lock();
defer self.mutex.unlock();
while (self.isEmptyLH()) {
self.not_empty.wait(&self.mutex);
}
std.debug.assert(!self.isEmptyLH());
if (self.isFullLH()) {
// If we are full, wake up a push that might be
// waiting here.
self.not_full.signal();
}
const result = self.buf[self.mask(self.read_index)];
self.read_index = self.mask2(self.read_index + 1);
return result;
}
/// Push an item into the queue. Blocks until an item has been
/// put in the queue.
pub fn push(self: *Self, item: T) void {
self.mutex.lock();
defer self.mutex.unlock();
while (self.isFullLH()) {
self.not_full.wait(&self.mutex);
}
if (self.isEmptyLH()) {
// If we were empty, wake up a pop if it was waiting.
self.not_empty.signal();
}
std.debug.assert(!self.isFullLH());
self.buf[self.mask(self.write_index)] = item;
self.write_index = self.mask2(self.write_index + 1);
}
/// Push an item into the queue. Returns true when the item
/// was successfully placed in the queue, false if the queue
/// was full.
pub fn tryPush(self: *Self, item: T) bool {
self.mutex.lock();
if (self.isFullLH()) {
self.mutex.unlock();
return false;
}
self.mutex.unlock();
self.push(item);
return true;
}
/// Pop an item from the queue. Returns null when no item is
/// available.
pub fn tryPop(self: *Self) ?T {
self.mutex.lock();
if (self.isEmptyLH()) {
self.mutex.unlock();
return null;
}
self.mutex.unlock();
return self.pop();
}
/// Poll the queue. This call blocks until events are in the queue
pub fn poll(self: *Self) void {
self.mutex.lock();
defer self.mutex.unlock();
while (self.isEmptyLH()) {
self.not_empty.wait(&self.mutex);
}
std.debug.assert(!self.isEmptyLH());
}
fn isEmptyLH(self: Self) bool {
return self.write_index == self.read_index;
}
fn isFullLH(self: Self) bool {
return self.mask2(self.write_index + self.buf.len) ==
self.read_index;
}
/// Returns `true` if the queue is empty and `false` otherwise.
pub fn isEmpty(self: *Self) bool {
self.mutex.lock();
defer self.mutex.unlock();
return self.isEmptyLH();
}
/// Returns `true` if the queue is full and `false` otherwise.
pub fn isFull(self: *Self) bool {
self.mutex.lock();
defer self.mutex.unlock();
return self.isFullLH();
}
/// Returns the length
fn len(self: Self) usize {
const wrap_offset = 2 * self.buf.len *
@intFromBool(self.write_index < self.read_index);
const adjusted_write_index = self.write_index + wrap_offset;
return adjusted_write_index - self.read_index;
}
/// Returns `index` modulo the length of the backing slice.
fn mask(self: Self, index: usize) usize {
return index % self.buf.len;
}
/// Returns `index` modulo twice the length of the backing slice.
fn mask2(self: Self, index: usize) usize {
return index % (2 * self.buf.len);
}
};
}
const testing = std.testing;
const cfg = Thread.SpawnConfig{ .allocator = testing.allocator };
test "Queue: simple push / pop" {
var queue: Queue(u8, 16) = .{};
queue.push(1);
queue.push(2);
const pop = queue.pop();
try testing.expectEqual(1, pop);
try testing.expectEqual(2, queue.pop());
}
const Thread = std.Thread;
fn testPushPop(q: *Queue(u8, 2)) !void {
q.push(3);
try testing.expectEqual(2, q.pop());
}
test "Fill, wait to push, pop once in another thread" {
var queue: Queue(u8, 2) = .{};
queue.push(1);
queue.push(2);
const t = try Thread.spawn(cfg, testPushPop, .{&queue});
try testing.expectEqual(false, queue.tryPush(3));
try testing.expectEqual(1, queue.pop());
t.join();
try testing.expectEqual(3, queue.pop());
try testing.expectEqual(null, queue.tryPop());
}
fn testPush(q: *Queue(u8, 2)) void {
q.push(0);
q.push(1);
q.push(2);
q.push(3);
q.push(4);
}
test "Try to pop, fill from another thread" {
var queue: Queue(u8, 2) = .{};
const thread = try Thread.spawn(cfg, testPush, .{&queue});
for (0..5) |idx| {
try testing.expectEqual(@as(u8, @intCast(idx)), queue.pop());
}
thread.join();
}
fn sleepyPop(q: *Queue(u8, 2)) !void {
// First we wait for the queue to be full.
while (!q.isFull())
try Thread.yield();
// Then we spuriously wake it up, because that's a thing that can
// happen.
q.not_full.signal();
q.not_empty.signal();
// Then give the other thread a good chance of waking up. It's not
// clear that yield guarantees the other thread will be scheduled,
// so we'll throw a sleep in here just to be sure. The queue is
// still full and the push in the other thread is still blocked
// waiting for space.
try Thread.yield();
std.time.sleep(std.time.ns_per_s);
// Finally, let that other thread go.
try std.testing.expectEqual(1, q.pop());
// This won't continue until the other thread has had a chance to
// put at least one item in the queue.
while (!q.isFull())
try Thread.yield();
// But we want to ensure that there's a second push waiting, so
// here's another sleep.
std.time.sleep(std.time.ns_per_s / 2);
// Another spurious wake...
q.not_full.signal();
q.not_empty.signal();
// And another chance for the other thread to see that it's
// spurious and go back to sleep.
try Thread.yield();
std.time.sleep(std.time.ns_per_s / 2);
// Pop that thing and we're done.
try std.testing.expectEqual(2, q.pop());
}
test "Fill, block, fill, block" {
// Fill the queue, block while trying to write another item, have
// a background thread unblock us, then block while trying to
// write yet another thing. Have the background thread unblock
// that too (after some time) then drain the queue. This test
// fails if the while loop in `push` is turned into an `if`.
var queue: Queue(u8, 2) = .{};
const thread = try Thread.spawn(cfg, sleepyPop, .{&queue});
queue.push(1);
queue.push(2);
const now = std.time.milliTimestamp();
queue.push(3); // This one should block.
const then = std.time.milliTimestamp();
// Just to make sure the sleeps are yielding to this thread, make
// sure it took at least 900ms to do the push.
try std.testing.expect(then - now > 900);
// This should block again, waiting for the other thread.
queue.push(4);
// And once that push has gone through, the other thread's done.
thread.join();
try std.testing.expectEqual(3, queue.pop());
try std.testing.expectEqual(4, queue.pop());
}
fn sleepyPush(q: *Queue(u8, 1)) !void {
// Try to ensure the other thread has already started trying to pop.
try Thread.yield();
std.time.sleep(std.time.ns_per_s / 2);
// Spurious wake
q.not_full.signal();
q.not_empty.signal();
try Thread.yield();
std.time.sleep(std.time.ns_per_s / 2);
// Stick something in the queue so it can be popped.
q.push(1);
// Ensure it's been popped.
while (!q.isEmpty())
try Thread.yield();
// Give the other thread time to block again.
try Thread.yield();
std.time.sleep(std.time.ns_per_s / 2);
// Spurious wake
q.not_full.signal();
q.not_empty.signal();
q.push(2);
}
test "Drain, block, drain, block" {
// This is like fill/block/fill/block, but on the pop end. This
// test should fail if the `while` loop in `pop` is turned into an
// `if`.
var queue: Queue(u8, 1) = .{};
const thread = try Thread.spawn(cfg, sleepyPush, .{&queue});
try std.testing.expectEqual(1, queue.pop());
try std.testing.expectEqual(2, queue.pop());
thread.join();
}
fn readerThread(q: *Queue(u8, 1)) !void {
try testing.expectEqual(1, q.pop());
}
test "2 readers" {
// 2 threads read, one thread writes
var queue: Queue(u8, 1) = .{};
const t1 = try Thread.spawn(cfg, readerThread, .{&queue});
const t2 = try Thread.spawn(cfg, readerThread, .{&queue});
try Thread.yield();
std.time.sleep(std.time.ns_per_s / 2);
queue.push(1);
queue.push(1);
t1.join();
t2.join();
}
fn writerThread(q: *Queue(u8, 1)) !void {
q.push(1);
}
test "2 writers" {
var queue: Queue(u8, 1) = .{};
const t1 = try Thread.spawn(cfg, writerThread, .{&queue});
const t2 = try Thread.spawn(cfg, writerThread, .{&queue});
try testing.expectEqual(1, queue.pop());
try testing.expectEqual(1, queue.pop());
t1.join();
t2.join();
}

17
deps/libvaxis/src/widgets.zig vendored Normal file
View File

@ -0,0 +1,17 @@
//! Specialized TUI Widgets
const opts = @import("build_options");
pub const border = @import("widgets/border.zig");
pub const alignment = @import("widgets/alignment.zig");
pub const Scrollbar = @import("widgets/Scrollbar.zig");
pub const Table = @import("widgets/Table.zig");
pub const ScrollView = @import("widgets/ScrollView.zig");
pub const LineNumbers = @import("widgets/LineNumbers.zig");
pub const TextView = @import("widgets/TextView.zig");
pub const CodeView = @import("widgets/CodeView.zig");
pub const Terminal = @import("widgets/terminal/Terminal.zig");
// Widgets with dependencies
pub const TextInput = if (opts.text_input) @import("widgets/TextInput.zig") else undefined;

112
deps/libvaxis/src/widgets/CodeView.zig vendored Normal file
View File

@ -0,0 +1,112 @@
const std = @import("std");
const vaxis = @import("../main.zig");
const ScrollView = vaxis.widgets.ScrollView;
const LineNumbers = vaxis.widgets.LineNumbers;
pub const DrawOptions = struct {
highlighted_line: usize = 0,
draw_line_numbers: bool = true,
indentation: usize = 0,
};
pub const Buffer = vaxis.widgets.TextView.Buffer;
scroll_view: ScrollView = .{ .vertical_scrollbar = null },
highlighted_style: vaxis.Style = .{ .bg = .{ .index = 0 } },
indentation_cell: vaxis.Cell = .{
.char = .{
.grapheme = "",
.width = 1,
},
.style = .{ .dim = true },
},
pub fn input(self: *@This(), key: vaxis.Key) void {
self.scroll_view.input(key);
}
pub fn draw(self: *@This(), win: vaxis.Window, buffer: Buffer, opts: DrawOptions) void {
const pad_left: usize = if (opts.draw_line_numbers) LineNumbers.numDigits(buffer.rows) +| 1 else 0;
self.scroll_view.draw(win, .{
.cols = buffer.cols + pad_left,
.rows = buffer.rows,
});
if (opts.draw_line_numbers) {
var nl: LineNumbers = .{
.highlighted_line = opts.highlighted_line,
.num_lines = buffer.rows +| 1,
};
nl.draw(win.child(.{
.x_off = 0,
.y_off = 0,
.width = .{ .limit = pad_left },
.height = .{ .limit = win.height },
}), self.scroll_view.scroll.y);
}
self.drawCode(win.child(.{ .x_off = pad_left }), buffer, opts);
}
fn drawCode(self: *@This(), win: vaxis.Window, buffer: Buffer, opts: DrawOptions) void {
const Pos = struct { x: usize = 0, y: usize = 0 };
var pos: Pos = .{};
var byte_index: usize = 0;
var is_indentation = true;
const bounds = self.scroll_view.bounds(win);
for (buffer.grapheme.items(.len), buffer.grapheme.items(.offset), 0..) |g_len, g_offset, index| {
if (bounds.above(pos.y)) {
break;
}
const cluster = buffer.content.items[g_offset..][0..g_len];
defer byte_index += cluster.len;
if (std.mem.eql(u8, cluster, "\n")) {
if (index == buffer.grapheme.len - 1) {
break;
}
pos.y += 1;
pos.x = 0;
is_indentation = true;
continue;
} else if (bounds.below(pos.y)) {
continue;
}
const highlighted_line = pos.y +| 1 == opts.highlighted_line;
var style: vaxis.Style = if (highlighted_line) self.highlighted_style else .{};
if (buffer.style_map.get(byte_index)) |meta| {
const tmp = style.bg;
style = buffer.style_list.items[meta];
style.bg = tmp;
}
const width = win.gwidth(cluster);
defer pos.x +|= width;
if (!bounds.colInside(pos.x)) {
continue;
}
if (opts.indentation > 0 and !std.mem.eql(u8, cluster, " ")) {
is_indentation = false;
}
if (is_indentation and opts.indentation > 0 and pos.x % opts.indentation == 0) {
var cell = self.indentation_cell;
cell.style.bg = style.bg;
self.scroll_view.writeCell(win, pos.x, pos.y, cell);
} else {
self.scroll_view.writeCell(win, pos.x, pos.y, .{
.char = .{ .grapheme = cluster, .width = width },
.style = style,
});
}
if (highlighted_line) {
for (pos.x +| width..bounds.x2) |x| {
self.scroll_view.writeCell(win, x, pos.y, .{ .style = style });
}
}
}
}

View File

@ -0,0 +1,54 @@
const std = @import("std");
const vaxis = @import("../main.zig");
const digits = "0123456789";
num_lines: usize = std.math.maxInt(usize),
highlighted_line: usize = 0,
style: vaxis.Style = .{ .dim = true },
highlighted_style: vaxis.Style = .{ .dim = true, .bg = .{ .index = 0 } },
pub fn extractDigit(v: usize, n: usize) usize {
return (v / (std.math.powi(usize, 10, n) catch unreachable)) % 10;
}
pub fn numDigits(v: usize) usize {
return switch (v) {
0...9 => 1,
10...99 => 2,
100...999 => 3,
1000...9999 => 4,
10000...99999 => 5,
100000...999999 => 6,
1000000...9999999 => 7,
10000000...99999999 => 8,
else => 0,
};
}
pub fn draw(self: @This(), win: vaxis.Window, y_scroll: usize) void {
for (1 + y_scroll..self.num_lines) |line| {
if (line - 1 >= y_scroll +| win.height) {
break;
}
const highlighted = line == self.highlighted_line;
const num_digits = numDigits(line);
for (0..num_digits) |i| {
const digit = extractDigit(line, i);
win.writeCell(win.width -| (i + 2), line -| (y_scroll +| 1), .{
.char = .{
.width = 1,
.grapheme = digits[digit .. digit + 1],
},
.style = if (highlighted) self.highlighted_style else self.style,
});
}
if (highlighted) {
for (num_digits + 1..win.width) |i| {
win.writeCell(i, line -| (y_scroll +| 1), .{
.style = if (highlighted) self.highlighted_style else self.style,
});
}
}
}
}

128
deps/libvaxis/src/widgets/ScrollView.zig vendored Normal file
View File

@ -0,0 +1,128 @@
const std = @import("std");
const vaxis = @import("../main.zig");
pub const Scroll = struct {
x: usize = 0,
y: usize = 0,
pub fn restrictTo(self: *@This(), w: usize, h: usize) void {
self.x = @min(self.x, w);
self.y = @min(self.y, h);
}
};
pub const VerticalScrollbar = struct {
character: vaxis.Cell.Character = .{ .grapheme = "", .width = 1 },
fg: vaxis.Style = .{},
bg: vaxis.Style = .{ .fg = .{ .index = 8 } },
};
scroll: Scroll = .{},
vertical_scrollbar: ?VerticalScrollbar = .{},
/// Standard input mappings.
/// It is not neccessary to use this, you can set `scroll` manually.
pub fn input(self: *@This(), key: vaxis.Key) void {
if (key.matches(vaxis.Key.right, .{})) {
self.scroll.x +|= 1;
} else if (key.matches(vaxis.Key.right, .{ .shift = true })) {
self.scroll.x +|= 32;
} else if (key.matches(vaxis.Key.left, .{})) {
self.scroll.x -|= 1;
} else if (key.matches(vaxis.Key.left, .{ .shift = true })) {
self.scroll.x -|= 32;
} else if (key.matches(vaxis.Key.up, .{})) {
self.scroll.y -|= 1;
} else if (key.matches(vaxis.Key.page_up, .{})) {
self.scroll.y -|= 32;
} else if (key.matches(vaxis.Key.down, .{})) {
self.scroll.y +|= 1;
} else if (key.matches(vaxis.Key.page_down, .{})) {
self.scroll.y +|= 32;
} else if (key.matches(vaxis.Key.end, .{})) {
self.scroll.y = std.math.maxInt(usize);
} else if (key.matches(vaxis.Key.home, .{})) {
self.scroll.y = 0;
}
}
/// Must be called before doing any `writeCell` calls.
pub fn draw(self: *@This(), parent: vaxis.Window, content_size: struct {
cols: usize,
rows: usize,
}) void {
const content_cols = if (self.vertical_scrollbar) |_| content_size.cols +| 1 else content_size.cols;
const max_scroll_x = content_cols -| parent.width;
const max_scroll_y = content_size.rows -| parent.height;
self.scroll.restrictTo(max_scroll_x, max_scroll_y);
if (self.vertical_scrollbar) |opts| {
const vbar: vaxis.widgets.Scrollbar = .{
.character = opts.character,
.style = opts.fg,
.total = content_size.rows,
.view_size = parent.height,
.top = self.scroll.y,
};
const bg = parent.child(.{
.x_off = parent.width -| opts.character.width,
.width = .{ .limit = opts.character.width },
.height = .{ .limit = parent.height },
});
bg.fill(.{ .char = opts.character, .style = opts.bg });
vbar.draw(bg);
}
}
pub const BoundingBox = struct {
x1: usize,
y1: usize,
x2: usize,
y2: usize,
pub inline fn below(self: @This(), row: usize) bool {
return row < self.y1;
}
pub inline fn above(self: @This(), row: usize) bool {
return row >= self.y2;
}
pub inline fn rowInside(self: @This(), row: usize) bool {
return row >= self.y1 and row < self.y2;
}
pub inline fn colInside(self: @This(), col: usize) bool {
return col >= self.x1 and col < self.x2;
}
pub inline fn inside(self: @This(), col: usize, row: usize) bool {
return self.rowInside(row) and self.colInside(col);
}
};
/// Boundary of the content, useful for culling to improve draw performance.
pub fn bounds(self: *@This(), parent: vaxis.Window) BoundingBox {
const right_pad: usize = if (self.vertical_scrollbar != null) 1 else 0;
return .{
.x1 = self.scroll.x,
.y1 = self.scroll.y,
.x2 = self.scroll.x +| parent.width -| right_pad,
.y2 = self.scroll.y +| parent.height,
};
}
/// Use this function instead of `Window.writeCell` to draw your cells and they will magically scroll.
pub fn writeCell(self: *@This(), parent: vaxis.Window, col: usize, row: usize, cell: vaxis.Cell) void {
const b = self.bounds(parent);
if (!b.inside(col, row)) return;
const win = parent.child(.{ .width = .{ .limit = b.x2 - b.x1 }, .height = .{ .limit = b.y2 - b.y1 } });
win.writeCell(col -| self.scroll.x, row -| self.scroll.y, cell);
}
/// Use this function instead of `Window.readCell` to read the correct cell in scrolling context.
pub fn readCell(self: *@This(), parent: vaxis.Window, col: usize, row: usize) ?vaxis.Cell {
const b = self.bounds(parent);
if (!b.inside(col, row)) return;
const win = parent.child(.{ .width = .{ .limit = b.width }, .height = .{ .limit = b.height } });
return win.readCell(col -| self.scroll.x, row -| self.scroll.y);
}

33
deps/libvaxis/src/widgets/Scrollbar.zig vendored Normal file
View File

@ -0,0 +1,33 @@
const std = @import("std");
const vaxis = @import("../main.zig");
const Scrollbar = @This();
/// character to use for the scrollbar
character: vaxis.Cell.Character = .{ .grapheme = "", .width = 1 },
/// style to draw the bar character with
style: vaxis.Style = .{},
/// index of the top of the visible area
top: usize = 0,
/// total items in the list
total: usize,
/// total items that fit within the view area
view_size: usize,
pub fn draw(self: Scrollbar, win: vaxis.Window) void {
// don't draw when 0 items
if (self.total < 1) return;
// don't draw when all items can be shown
if (self.view_size >= self.total) return;
const bar_height = @max(std.math.divCeil(usize, self.view_size * win.height, self.total) catch unreachable, 1);
const bar_top = self.top * win.height / self.total;
var i: usize = 0;
while (i < bar_height) : (i += 1)
win.writeCell(0, i + bar_top, .{ .char = self.character, .style = self.style });
}

163
deps/libvaxis/src/widgets/Table.zig vendored Normal file
View File

@ -0,0 +1,163 @@
const std = @import("std");
const fmt = std.fmt;
const heap = std.heap;
const mem = std.mem;
const meta = std.meta;
const vaxis = @import("../main.zig");
/// Table Context for maintaining state and drawing Tables with `drawTable()`.
pub const TableContext = struct {
/// Current selected Row of the Table.
row: usize = 0,
/// Current selected Column of the Table.
col: usize = 0,
/// Starting point within the Data List.
start: usize = 0,
/// Active status of the Table.
active: bool = false,
/// The Background Color for Selected Rows and Column Headers.
selected_bg: vaxis.Cell.Color,
/// First Column Header Background Color
hdr_bg_1: vaxis.Cell.Color = .{ .rgb = [_]u8{ 64, 64, 64 } },
/// Second Column Header Background Color
hdr_bg_2: vaxis.Cell.Color = .{ .rgb = [_]u8{ 8, 8, 24 } },
/// First Row Background Color
row_bg_1: vaxis.Cell.Color = .{ .rgb = [_]u8{ 32, 32, 32 } },
/// Second Row Background Color
row_bg_2: vaxis.Cell.Color = .{ .rgb = [_]u8{ 8, 8, 8 } },
/// Y Offset for drawing to the parent Window.
y_off: usize = 0,
/// Column Width
/// Note, this should be treated as Read Only. The Column Width will be calculated during `drawTable()`.
col_width: usize = 0,
};
/// Draw a Table for the TUI.
pub fn drawTable(
/// This should be an ArenaAllocator that can be deinitialized after each event call.
/// The Allocator is only used in two cases:
/// 1. If a cell is a non-String. If the Allocator is not provided, those cells will show "[unsupported (TypeName)]".
/// 2. To show that a value is too large to fit into a cell. If the Allocator is not provided, they'll just be cutoff.
alloc: ?mem.Allocator,
/// The parent Window to draw to.
win: vaxis.Window,
/// Headers for the Table
headers: []const []const u8,
/// This must be an ArrayList.
data_list: anytype,
// The Table Context for this Table.
table_ctx: *TableContext,
) !void {
const table_win = win.initChild(
0,
table_ctx.y_off,
.{ .limit = win.width },
.{ .limit = win.height },
);
table_ctx.col_width = table_win.width / headers.len;
if (table_ctx.col_width % 2 != 0) table_ctx.col_width +|= 1;
while (table_ctx.col_width * headers.len < table_win.width - 1) table_ctx.col_width +|= 1;
if (table_ctx.col > headers.len - 1) table_ctx.*.col = headers.len - 1;
for (headers[0..], 0..) |hdr_txt, idx| {
const hdr_bg =
if (table_ctx.active and idx == table_ctx.col) table_ctx.selected_bg else if (idx % 2 == 0) table_ctx.hdr_bg_1 else table_ctx.hdr_bg_2;
const hdr_win = table_win.initChild(
idx * table_ctx.col_width,
0,
.{ .limit = table_ctx.col_width },
.{ .limit = 1 },
);
var hdr = vaxis.widgets.alignment.center(hdr_win, @min(table_ctx.col_width -| 1, hdr_txt.len +| 1), 1);
hdr_win.fill(.{ .style = .{ .bg = hdr_bg } });
var seg = [_]vaxis.Cell.Segment{.{
.text = if (hdr_txt.len > table_ctx.col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{hdr_txt[0..(table_ctx.col_width -| 4)]}) else hdr_txt,
.style = .{
.bg = hdr_bg,
.bold = true,
.ul_style = if (idx == table_ctx.col) .single else .dotted,
},
}};
_ = try hdr.print(seg[0..], .{ .wrap = .word });
}
const max_items = if (data_list.items.len > table_win.height -| 1) table_win.height -| 1 else data_list.items.len;
var end = table_ctx.*.start + max_items;
if (end > data_list.items.len) end = data_list.items.len;
table_ctx.*.start = tableStart: {
if (table_ctx.row == 0)
break :tableStart 0;
if (table_ctx.row < table_ctx.start)
break :tableStart table_ctx.start - (table_ctx.start - table_ctx.row);
if (table_ctx.row >= data_list.items.len - 1)
table_ctx.*.row = data_list.items.len - 1;
if (table_ctx.row >= end)
break :tableStart table_ctx.start + (table_ctx.row - end + 1);
break :tableStart table_ctx.start;
};
end = table_ctx.*.start + max_items;
if (end > data_list.items.len) end = data_list.items.len;
for (data_list.items[table_ctx.start..end], 0..) |data, idx| {
const row_bg =
if (table_ctx.active and table_ctx.start + idx == table_ctx.row) table_ctx.selected_bg else if (idx % 2 == 0) table_ctx.row_bg_1 else table_ctx.row_bg_2;
const row_win = table_win.initChild(
0,
1 + idx,
.{ .limit = table_win.width },
.{ .limit = 1 },
);
const DataT = @TypeOf(data);
if (DataT == []const u8) {
row_win.fill(.{ .style = .{ .bg = row_bg } });
var seg = [_]vaxis.Cell.Segment{.{
.text = if (data.len > table_ctx.col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{data[0..(table_ctx.col_width -| 4)]}) else data,
.style = .{ .bg = row_bg },
}};
_ = try row_win.print(seg[0..], .{ .wrap = .word });
return;
}
const item_fields = meta.fields(DataT);
inline for (item_fields[0..], 0..) |item_field, item_idx| {
const item = @field(data, item_field.name);
const ItemT = @TypeOf(item);
const item_win = row_win.initChild(
item_idx * table_ctx.col_width,
0,
.{ .limit = table_ctx.col_width },
.{ .limit = 1 },
);
const item_txt = switch (ItemT) {
[]const u8 => item,
else => nonStr: {
switch (@typeInfo(ItemT)) {
.Optional => {
const opt_item = item orelse break :nonStr "-";
switch (@typeInfo(ItemT).Optional.child) {
[]const u8 => break :nonStr opt_item,
else => {
break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{any}", .{opt_item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)});
},
}
},
else => {
break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{any}", .{item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)});
},
}
},
};
item_win.fill(.{ .style = .{ .bg = row_bg } });
var seg = [_]vaxis.Cell.Segment{.{
.text = if (item_txt.len > table_ctx.col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{item_txt[0..(table_ctx.col_width -| 4)]}) else item_txt,
.style = .{ .bg = row_bg },
}};
_ = try item_win.print(seg[0..], .{ .wrap = .word });
}
}
}

346
deps/libvaxis/src/widgets/TextInput.zig vendored Normal file
View File

@ -0,0 +1,346 @@
const std = @import("std");
const assert = std.debug.assert;
const Key = @import("../Key.zig");
const Cell = @import("../Cell.zig");
const Window = @import("../Window.zig");
const GapBuffer = @import("gap_buffer").GapBuffer;
const Unicode = @import("../Unicode.zig");
const TextInput = @This();
/// The events that this widget handles
const Event = union(enum) {
key_press: Key,
};
const ellipsis: Cell.Character = .{ .grapheme = "", .width = 1 };
// Index of our cursor
cursor_idx: usize = 0,
grapheme_count: usize = 0,
buf: GapBuffer(u8),
/// the number of graphemes to skip when drawing. Used for horizontal scrolling
draw_offset: usize = 0,
/// the column we placed the cursor the last time we drew
prev_cursor_col: usize = 0,
/// the grapheme index of the cursor the last time we drew
prev_cursor_idx: usize = 0,
/// approximate distance from an edge before we scroll
scroll_offset: usize = 4,
unicode: *const Unicode,
pub fn init(alloc: std.mem.Allocator, unicode: *const Unicode) TextInput {
return TextInput{
.buf = GapBuffer(u8).init(alloc),
.unicode = unicode,
};
}
pub fn deinit(self: *TextInput) void {
self.buf.deinit();
}
pub fn update(self: *TextInput, event: Event) !void {
switch (event) {
.key_press => |key| {
if (key.matches(Key.backspace, .{})) {
if (self.cursor_idx == 0) return;
try self.deleteBeforeCursor();
} else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) {
if (self.cursor_idx == self.grapheme_count) return;
try self.deleteAtCursor();
} else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) {
if (self.cursor_idx > 0) self.cursor_idx -= 1;
} else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) {
if (self.cursor_idx < self.grapheme_count) self.cursor_idx += 1;
} else if (key.matches('a', .{ .ctrl = true }) or key.matches(Key.home, .{})) {
self.cursor_idx = 0;
} else if (key.matches('e', .{ .ctrl = true }) or key.matches(Key.end, .{})) {
self.cursor_idx = self.grapheme_count;
} else if (key.matches('k', .{ .ctrl = true })) {
try self.deleteToEnd();
} else if (key.matches('u', .{ .ctrl = true })) {
try self.deleteToStart();
} else if (key.text) |text| {
try self.buf.insertSliceBefore(self.byteOffsetToCursor(), text);
self.cursor_idx += 1;
self.grapheme_count += 1;
}
},
}
}
/// insert text at the cursor position
pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) !void {
var iter = self.unicode.graphemeIterator(data);
var byte_offset_to_cursor = self.byteOffsetToCursor();
while (iter.next()) |text| {
try self.buf.insertSliceBefore(byte_offset_to_cursor, text.bytes(data));
byte_offset_to_cursor += text.len;
self.cursor_idx += 1;
self.grapheme_count += 1;
}
}
pub fn sliceToCursor(self: *TextInput, buf: []u8) []const u8 {
const offset = self.byteOffsetToCursor();
assert(offset <= buf.len); // provided buf was too small
if (offset <= self.buf.items.len) {
@memcpy(buf[0..offset], self.buf.items[0..offset]);
} else {
@memcpy(buf[0..self.buf.items.len], self.buf.items);
const second_half = self.buf.secondHalf();
const copy_len = offset - self.buf.items.len;
@memcpy(buf[self.buf.items.len .. self.buf.items.len + copy_len], second_half[0..copy_len]);
}
return buf[0..offset];
}
/// calculates the display width from the draw_offset to the cursor
fn widthToCursor(self: *TextInput, win: Window) usize {
var width: usize = 0;
var first_iter = self.unicode.graphemeIterator(self.buf.items);
var i: usize = 0;
while (first_iter.next()) |grapheme| {
defer i += 1;
if (i < self.draw_offset) {
continue;
}
if (i == self.cursor_idx) return width;
const g = grapheme.bytes(self.buf.items);
width += win.gwidth(g);
}
const second_half = self.buf.secondHalf();
var second_iter = self.unicode.graphemeIterator(second_half);
while (second_iter.next()) |grapheme| {
defer i += 1;
if (i < self.draw_offset) {
continue;
}
if (i == self.cursor_idx) return width;
const g = grapheme.bytes(second_half);
width += win.gwidth(g);
}
return width;
}
pub fn draw(self: *TextInput, win: Window) void {
if (self.cursor_idx < self.draw_offset) self.draw_offset = self.cursor_idx;
if (win.width == 0) return;
while (true) {
const width = self.widthToCursor(win);
if (width >= win.width) {
self.draw_offset +|= width - win.width + 1;
continue;
} else break;
}
self.prev_cursor_idx = self.cursor_idx;
self.prev_cursor_col = 0;
// assumption!! the gap is never within a grapheme
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
var first_iter = self.unicode.graphemeIterator(self.buf.items);
var col: usize = 0;
var i: usize = 0;
while (first_iter.next()) |grapheme| {
if (i < self.draw_offset) {
i += 1;
continue;
}
const g = grapheme.bytes(self.buf.items);
const w = win.gwidth(g);
if (col + w >= win.width) {
win.writeCell(win.width - 1, 0, .{ .char = ellipsis });
break;
}
win.writeCell(col, 0, .{
.char = .{
.grapheme = g,
.width = w,
},
});
col += w;
i += 1;
if (i == self.cursor_idx) self.prev_cursor_col = col;
}
const second_half = self.buf.secondHalf();
var second_iter = self.unicode.graphemeIterator(second_half);
while (second_iter.next()) |grapheme| {
if (i < self.draw_offset) {
i += 1;
continue;
}
const g = grapheme.bytes(second_half);
const w = win.gwidth(g);
if (col + w > win.width) {
win.writeCell(win.width - 1, 0, .{ .char = ellipsis });
break;
}
win.writeCell(col, 0, .{
.char = .{
.grapheme = g,
.width = w,
},
});
col += w;
i += 1;
if (i == self.cursor_idx) self.prev_cursor_col = col;
}
if (self.draw_offset > 0) {
win.writeCell(0, 0, .{ .char = ellipsis });
}
win.showCursor(self.prev_cursor_col, 0);
}
pub fn clearAndFree(self: *TextInput) void {
self.buf.clearAndFree();
self.reset();
}
pub fn clearRetainingCapacity(self: *TextInput) void {
self.buf.clearRetainingCapacity();
self.reset();
}
pub fn toOwnedSlice(self: *TextInput) ![]const u8 {
defer self.reset();
return self.buf.toOwnedSlice();
}
fn reset(self: *TextInput) void {
self.cursor_idx = 0;
self.grapheme_count = 0;
self.draw_offset = 0;
self.prev_cursor_col = 0;
self.prev_cursor_idx = 0;
}
// returns the number of bytes before the cursor
// (since GapBuffers are strictly speaking not contiguous, this is a number in 0..realLength()
// which would need to be fed to realIndex() to get an actual offset into self.buf.items.ptr)
pub fn byteOffsetToCursor(self: TextInput) usize {
// assumption! the gap is never in the middle of a grapheme
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
var iter = self.unicode.graphemeIterator(self.buf.items);
var offset: usize = 0;
var i: usize = 0;
while (iter.next()) |grapheme| {
if (i == self.cursor_idx) break;
offset += grapheme.len;
i += 1;
} else {
var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf());
while (second_iter.next()) |grapheme| {
if (i == self.cursor_idx) break;
offset += grapheme.len;
i += 1;
}
}
return offset;
}
fn deleteToEnd(self: *TextInput) !void {
const offset = self.byteOffsetToCursor();
try self.buf.replaceRangeAfter(offset, self.buf.realLength() - offset, &.{});
self.grapheme_count = self.cursor_idx;
}
fn deleteToStart(self: *TextInput) !void {
const offset = self.byteOffsetToCursor();
try self.buf.replaceRangeBefore(0, offset, &.{});
self.grapheme_count -= self.cursor_idx;
self.cursor_idx = 0;
}
fn deleteBeforeCursor(self: *TextInput) !void {
// assumption! the gap is never in the middle of a grapheme
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
var iter = self.unicode.graphemeIterator(self.buf.items);
var offset: usize = 0;
var i: usize = 1;
while (iter.next()) |grapheme| {
if (i == self.cursor_idx) {
try self.buf.replaceRangeBefore(offset, grapheme.len, &.{});
self.cursor_idx -= 1;
self.grapheme_count -= 1;
return;
}
offset += grapheme.len;
i += 1;
} else {
var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf());
while (second_iter.next()) |grapheme| {
if (i == self.cursor_idx) {
try self.buf.replaceRangeBefore(offset, grapheme.len, &.{});
self.cursor_idx -= 1;
self.grapheme_count -= 1;
return;
}
offset += grapheme.len;
i += 1;
}
}
}
fn deleteAtCursor(self: *TextInput) !void {
// assumption! the gap is never in the middle of a grapheme
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
var iter = self.unicode.graphemeIterator(self.buf.items);
var offset: usize = 0;
var i: usize = 1;
while (iter.next()) |grapheme| {
if (i == self.cursor_idx + 1) {
try self.buf.replaceRangeAfter(offset, grapheme.len, &.{});
self.grapheme_count -= 1;
return;
}
offset += grapheme.len;
i += 1;
} else {
var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf());
while (second_iter.next()) |grapheme| {
if (i == self.cursor_idx + 1) {
try self.buf.replaceRangeAfter(offset, grapheme.len, &.{});
self.grapheme_count -= 1;
return;
}
offset += grapheme.len;
i += 1;
}
}
}
test "assertion" {
const alloc = std.testing.allocator_instance.allocator();
const unicode = try Unicode.init(alloc);
defer unicode.deinit();
const astronaut = "👩‍🚀";
const astronaut_emoji: Key = .{
.text = astronaut,
.codepoint = try std.unicode.utf8Decode(astronaut[0..4]),
};
var input = TextInput.init(std.testing.allocator, &unicode);
defer input.deinit();
for (0..6) |_| {
try input.update(.{ .key_press = astronaut_emoji });
}
}
test "sliceToCursor" {
const alloc = std.testing.allocator_instance.allocator();
const unicode = try Unicode.init(alloc);
defer unicode.deinit();
var input = init(alloc, &unicode);
defer input.deinit();
try input.insertSliceAtCursor("hello, world");
input.cursor_idx = 2;
var buf: [32]u8 = undefined;
try std.testing.expectEqualStrings("he", input.sliceToCursor(&buf));
input.buf.moveGap(3);
input.cursor_idx = 5;
try std.testing.expectEqualStrings("hello", input.sliceToCursor(&buf));
}

191
deps/libvaxis/src/widgets/TextView.zig vendored Normal file
View File

@ -0,0 +1,191 @@
const std = @import("std");
const vaxis = @import("../main.zig");
const grapheme = @import("grapheme");
const DisplayWidth = @import("DisplayWidth");
const ScrollView = vaxis.widgets.ScrollView;
pub const BufferWriter = struct {
pub const Error = error{OutOfMemory};
pub const Writer = std.io.GenericWriter(@This(), Error, write);
allocator: std.mem.Allocator,
buffer: *Buffer,
gd: *const grapheme.GraphemeData,
wd: *const DisplayWidth.DisplayWidthData,
pub fn write(self: @This(), bytes: []const u8) Error!usize {
try self.buffer.append(self.allocator, .{
.bytes = bytes,
.gd = self.gd,
.wd = self.wd,
});
return bytes.len;
}
pub fn writer(self: @This()) Writer {
return .{ .context = self };
}
};
pub const Buffer = struct {
const StyleList = std.ArrayListUnmanaged(vaxis.Style);
const StyleMap = std.HashMapUnmanaged(usize, usize, std.hash_map.AutoContext(usize), std.hash_map.default_max_load_percentage);
pub const Content = struct {
bytes: []const u8,
gd: *const grapheme.GraphemeData,
wd: *const DisplayWidth.DisplayWidthData,
};
pub const Style = struct {
begin: usize,
end: usize,
style: vaxis.Style,
};
pub const Error = error{OutOfMemory};
grapheme: std.MultiArrayList(grapheme.Grapheme) = .{},
content: std.ArrayListUnmanaged(u8) = .{},
style_list: StyleList = .{},
style_map: StyleMap = .{},
rows: usize = 0,
cols: usize = 0,
// used when appending to a buffer
last_cols: usize = 0,
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
self.style_map.deinit(allocator);
self.style_list.deinit(allocator);
self.grapheme.deinit(allocator);
self.content.deinit(allocator);
self.* = undefined;
}
/// Clears all buffer data.
pub fn clear(self: *@This(), allocator: std.mem.Allocator) void {
self.deinit(allocator);
self.* = .{};
}
/// Replaces contents of the buffer, all previous buffer data is lost.
pub fn update(self: *@This(), allocator: std.mem.Allocator, content: Content) Error!void {
self.clear(allocator);
errdefer self.clear(allocator);
try self.append(allocator, content);
}
/// Appends content to the buffer.
pub fn append(self: *@This(), allocator: std.mem.Allocator, content: Content) Error!void {
var cols: usize = self.last_cols;
var iter = grapheme.Iterator.init(content.bytes, content.gd);
const dw: DisplayWidth = .{ .data = content.wd };
while (iter.next()) |g| {
try self.grapheme.append(allocator, .{
.len = g.len,
.offset = @as(u32, @intCast(self.content.items.len)) + g.offset,
});
const cluster = g.bytes(content.bytes);
if (std.mem.eql(u8, cluster, "\n")) {
self.cols = @max(self.cols, cols);
cols = 0;
continue;
}
cols +|= dw.strWidth(cluster);
}
try self.content.appendSlice(allocator, content.bytes);
self.last_cols = cols;
self.cols = @max(self.cols, cols);
self.rows +|= std.mem.count(u8, content.bytes, "\n");
}
/// Clears all styling data.
pub fn clearStyle(self: *@This(), allocator: std.mem.Allocator) void {
self.style_list.deinit(allocator);
self.style_map.deinit(allocator);
}
/// Update style for range of the buffer contents.
pub fn updateStyle(self: *@This(), allocator: std.mem.Allocator, style: Style) Error!void {
const style_index = blk: {
for (self.style_list.items, 0..) |s, i| {
if (std.meta.eql(s, style.style)) {
break :blk i;
}
}
try self.style_list.append(allocator, style.style);
break :blk self.style_list.items.len - 1;
};
for (style.begin..style.end) |i| {
try self.style_map.put(allocator, i, style_index);
}
}
pub fn writer(
self: *@This(),
allocator: std.mem.Allocator,
gd: *const grapheme.GraphemeData,
wd: *const DisplayWidth.DisplayWidthData,
) BufferWriter.Writer {
return .{
.context = .{
.allocator = allocator,
.buffer = self,
.gd = gd,
.wd = wd,
},
};
}
};
scroll_view: ScrollView = .{},
pub fn input(self: *@This(), key: vaxis.Key) void {
self.scroll_view.input(key);
}
pub fn draw(self: *@This(), win: vaxis.Window, buffer: Buffer) void {
self.scroll_view.draw(win, .{ .cols = buffer.cols, .rows = buffer.rows });
const Pos = struct { x: usize = 0, y: usize = 0 };
var pos: Pos = .{};
var byte_index: usize = 0;
const bounds = self.scroll_view.bounds(win);
for (buffer.grapheme.items(.len), buffer.grapheme.items(.offset), 0..) |g_len, g_offset, index| {
if (bounds.above(pos.y)) {
break;
}
const cluster = buffer.content.items[g_offset..][0..g_len];
defer byte_index += cluster.len;
if (std.mem.eql(u8, cluster, "\n")) {
if (index == buffer.grapheme.len - 1) {
break;
}
pos.y +|= 1;
pos.x = 0;
continue;
} else if (bounds.below(pos.y)) {
continue;
}
const width = win.gwidth(cluster);
defer pos.x +|= width;
if (!bounds.colInside(pos.x)) {
continue;
}
const style: vaxis.Style = blk: {
if (buffer.style_map.get(byte_index)) |style_index| {
break :blk buffer.style_list.items[style_index];
}
break :blk .{};
};
self.scroll_view.writeCell(win, pos.x, pos.y, .{
.char = .{ .grapheme = cluster, .width = width },
.style = style,
});
}
}

View File

@ -0,0 +1,7 @@
const Window = @import("../Window.zig");
pub fn center(parent: Window, cols: usize, rows: usize) Window {
const y_off = (parent.height / 2) -| (rows / 2);
const x_off = (parent.width / 2) -| (cols / 2);
return parent.initChild(x_off, y_off, .{ .limit = cols }, .{ .limit = rows });
}

52
deps/libvaxis/src/widgets/border.zig vendored Normal file
View File

@ -0,0 +1,52 @@
const Cell = @import("../Cell.zig");
const Window = @import("../Window.zig");
const Style = Cell.Style;
const Character = Cell.Character;
const horizontal = Character{ .grapheme = "", .width = 1 };
const vertical = Character{ .grapheme = "", .width = 1 };
const top_left = Character{ .grapheme = "", .width = 1 };
const top_right = Character{ .grapheme = "", .width = 1 };
const bottom_right = Character{ .grapheme = "", .width = 1 };
const bottom_left = Character{ .grapheme = "", .width = 1 };
pub fn all(win: Window, style: Style) Window {
const h = win.height;
const w = win.width;
win.writeCell(0, 0, .{ .char = top_left, .style = style });
win.writeCell(0, h -| 1, .{ .char = bottom_left, .style = style });
win.writeCell(w -| 1, 0, .{ .char = top_right, .style = style });
win.writeCell(w -| 1, h -| 1, .{ .char = bottom_right, .style = style });
var i: usize = 1;
while (i < (h -| 1)) : (i += 1) {
win.writeCell(0, i, .{ .char = vertical, .style = style });
win.writeCell(w -| 1, i, .{ .char = vertical, .style = style });
}
i = 1;
while (i < w -| 1) : (i += 1) {
win.writeCell(i, 0, .{ .char = horizontal, .style = style });
win.writeCell(i, h -| 1, .{ .char = horizontal, .style = style });
}
return win.initChild(1, 1, .{ .limit = w -| 2 }, .{ .limit = h -| 2 });
}
pub fn right(win: Window, style: Style) Window {
const h = win.height;
const w = win.width;
var i: usize = 0;
while (i < h) : (i += 1) {
win.writeCell(w -| 1, i, .{ .char = vertical, .style = style });
}
return win.initChild(0, 0, .{ .limit = w -| 1 }, .expand);
}
pub fn bottom(win: Window, style: Style) Window {
const h = win.height;
const w = win.width;
var i: usize = 0;
while (i < w) : (i += 1) {
win.writeCell(i, h -| 1, .{ .char = horizontal, .style = style });
}
return win.initChild(0, 0, .expand, .{ .limit = h -| 1 });
}

View File

@ -0,0 +1,117 @@
const Command = @This();
const std = @import("std");
const builtin = @import("builtin");
const Pty = @import("Pty.zig");
const Terminal = @import("Terminal.zig");
const posix = std.posix;
argv: []const []const u8,
working_directory: ?[]const u8,
// Set after spawn()
pid: ?std.posix.pid_t = null,
env_map: *const std.process.EnvMap,
pty: Pty,
pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void {
var arena_allocator = std.heap.ArenaAllocator.init(allocator);
defer arena_allocator.deinit();
const arena = arena_allocator.allocator();
const argv_buf = try arena.allocSentinel(?[*:0]const u8, self.argv.len, null);
for (self.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr;
const envp = try createEnvironFromMap(arena, self.env_map);
const pid = try std.posix.fork();
if (pid == 0) {
// we are the child
_ = std.os.linux.setsid();
// set the controlling terminal
var u: c_uint = std.posix.STDIN_FILENO;
if (posix.system.ioctl(self.pty.tty, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError;
// set up io
try posix.dup2(self.pty.tty, std.posix.STDIN_FILENO);
try posix.dup2(self.pty.tty, std.posix.STDOUT_FILENO);
try posix.dup2(self.pty.tty, std.posix.STDERR_FILENO);
posix.close(self.pty.tty);
if (self.pty.pty > 2) posix.close(self.pty.pty);
if (self.working_directory) |wd| {
try std.posix.chdir(wd);
}
// exec
const err = std.posix.execvpeZ(argv_buf.ptr[0].?, argv_buf.ptr, envp);
_ = err catch {};
}
// we are the parent
self.pid = @intCast(pid);
if (!Terminal.global_sigchild_installed) {
Terminal.global_sigchild_installed = true;
var act = posix.Sigaction{
.handler = .{ .handler = handleSigChild },
.mask = switch (builtin.os.tag) {
.macos => 0,
.linux => posix.empty_sigset,
else => @compileError("os not supported"),
},
.flags = 0,
};
try posix.sigaction(posix.SIG.CHLD, &act, null);
}
return;
}
fn handleSigChild(_: c_int) callconv(.C) void {
const result = std.posix.waitpid(-1, 0);
Terminal.global_vt_mutex.lock();
defer Terminal.global_vt_mutex.unlock();
if (Terminal.global_vts) |vts| {
var vt = vts.get(result.pid) orelse return;
vt.event_queue.push(.exited);
}
}
pub fn kill(self: *Command) void {
if (self.pid) |pid| {
std.posix.kill(pid, std.posix.SIG.TERM) catch {};
self.pid = null;
}
}
/// Creates a null-deliminated environment variable block in the format expected by POSIX, from a
/// hash map plus options.
fn createEnvironFromMap(
arena: std.mem.Allocator,
map: *const std.process.EnvMap,
) ![:null]?[*:0]u8 {
const envp_count: usize = map.count();
const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null);
var i: usize = 0;
{
var it = map.iterator();
while (it.next()) |pair| {
envp_buf[i] = try std.fmt.allocPrintZ(arena, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* });
i += 1;
}
}
std.debug.assert(i == envp_count);
return envp_buf;
}

View File

@ -0,0 +1,184 @@
//! An ANSI VT Parser
const Parser = @This();
const std = @import("std");
const Reader = std.io.AnyReader;
const ansi = @import("ansi.zig");
const BufferedReader = std.io.BufferedReader(4096, std.io.AnyReader);
/// A terminal event
const Event = union(enum) {
print: []const u8,
c0: ansi.C0,
escape: []const u8,
ss2: u8,
ss3: u8,
csi: ansi.CSI,
osc: []const u8,
apc: []const u8,
};
buf: std.ArrayList(u8),
/// a leftover byte from a ground event
pending_byte: ?u8 = null,
pub fn parseReader(self: *Parser, buffered: *BufferedReader) !Event {
const reader = buffered.reader().any();
self.buf.clearRetainingCapacity();
while (true) {
const b = if (self.pending_byte) |p| p else try reader.readByte();
self.pending_byte = null;
switch (b) {
// Escape sequence
0x1b => {
const next = try reader.readByte();
switch (next) {
0x4E => return .{ .ss2 = try reader.readByte() },
0x4F => return .{ .ss3 = try reader.readByte() },
0x50 => try skipUntilST(reader), // DCS
0x58 => try skipUntilST(reader), // SOS
0x5B => return self.parseCsi(reader), // CSI
0x5D => return self.parseOsc(reader), // OSC
0x5E => try skipUntilST(reader), // PM
0x5F => return self.parseApc(reader), // APC
0x20...0x2F => {
try self.buf.append(next);
return self.parseEscape(reader); // ESC
},
else => {
try self.buf.append(next);
return .{ .escape = self.buf.items };
},
}
},
// C0 control
0x00...0x1a,
0x1c...0x1f,
=> return .{ .c0 = @enumFromInt(b) },
else => {
try self.buf.append(b);
return self.parseGround(buffered);
},
}
}
}
inline fn parseGround(self: *Parser, reader: *BufferedReader) !Event {
var buf: [1]u8 = undefined;
{
std.debug.assert(self.buf.items.len > 0);
// Handle first byte
const len = try std.unicode.utf8ByteSequenceLength(self.buf.items[0]);
var i: usize = 1;
while (i < len) : (i += 1) {
const read = try reader.read(&buf);
if (read == 0) return error.EOF;
try self.buf.append(buf[0]);
}
}
while (true) {
if (reader.start == reader.end) return .{ .print = self.buf.items };
const n = try reader.read(&buf);
if (n == 0) return error.EOF;
const b = buf[0];
switch (b) {
0x00...0x1f => {
self.pending_byte = b;
return .{ .print = self.buf.items };
},
else => {
try self.buf.append(b);
const len = try std.unicode.utf8ByteSequenceLength(b);
var i: usize = 1;
while (i < len) : (i += 1) {
const read = try reader.read(&buf);
if (read == 0) return error.EOF;
try self.buf.append(buf[0]);
}
},
}
}
}
/// parse until b >= 0x30
inline fn parseEscape(self: *Parser, reader: Reader) !Event {
while (true) {
const b = try reader.readByte();
switch (b) {
0x20...0x2F => continue,
else => {
try self.buf.append(b);
return .{ .escape = self.buf.items };
},
}
}
}
inline fn parseApc(self: *Parser, reader: Reader) !Event {
while (true) {
const b = try reader.readByte();
switch (b) {
0x00...0x17,
0x19,
0x1c...0x1f,
=> continue,
0x1b => {
try reader.skipBytes(1, .{ .buf_size = 1 });
return .{ .apc = self.buf.items };
},
else => try self.buf.append(b),
}
}
}
/// Skips sequences until we see an ST (String Terminator, ESC \)
inline fn skipUntilST(reader: Reader) !void {
try reader.skipUntilDelimiterOrEof('\x1b');
try reader.skipBytes(1, .{ .buf_size = 1 });
}
/// Parses an OSC sequence
inline fn parseOsc(self: *Parser, reader: Reader) !Event {
while (true) {
const b = try reader.readByte();
switch (b) {
0x00...0x06,
0x08...0x17,
0x19,
0x1c...0x1f,
=> continue,
0x1b => {
try reader.skipBytes(1, .{ .buf_size = 1 });
return .{ .osc = self.buf.items };
},
0x07 => return .{ .osc = self.buf.items },
else => try self.buf.append(b),
}
}
}
inline fn parseCsi(self: *Parser, reader: Reader) !Event {
var intermediate: ?u8 = null;
var pm: ?u8 = null;
while (true) {
const b = try reader.readByte();
switch (b) {
0x20...0x2F => intermediate = b,
0x30...0x3B => try self.buf.append(b),
0x3C...0x3F => pm = b, // we only allow one
// Really we should execute C0 controls, but we just ignore them
0x40...0xFF => return .{
.csi = .{
.intermediate = intermediate,
.private_marker = pm,
.params = self.buf.items,
.final = b,
},
},
else => continue,
}
}
}

View File

@ -0,0 +1,59 @@
//! A PTY pair
const Pty = @This();
const std = @import("std");
const builtin = @import("builtin");
const Winsize = @import("../../main.zig").Winsize;
const posix = std.posix;
pty: posix.fd_t,
tty: posix.fd_t,
/// opens a new tty/pty pair
pub fn init() !Pty {
switch (builtin.os.tag) {
.linux => return openPtyLinux(),
else => @compileError("unsupported os"),
}
}
/// closes the tty and pty
pub fn deinit(self: Pty) void {
posix.close(self.pty);
posix.close(self.tty);
}
/// sets the size of the pty
pub fn setSize(self: Pty, ws: Winsize) !void {
const _ws: posix.winsize = .{
.ws_row = @truncate(ws.rows),
.ws_col = @truncate(ws.cols),
.ws_xpixel = @truncate(ws.x_pixel),
.ws_ypixel = @truncate(ws.y_pixel),
};
if (posix.system.ioctl(self.pty, posix.T.IOCSWINSZ, @intFromPtr(&_ws)) != 0)
return error.SetWinsizeError;
}
fn openPtyLinux() !Pty {
const p = try posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
errdefer posix.close(p);
// unlockpt
var n: c_uint = 0;
if (posix.system.ioctl(p, posix.T.IOCSPTLCK, @intFromPtr(&n)) != 0) return error.IoctlError;
// ptsname
if (posix.system.ioctl(p, posix.T.IOCGPTN, @intFromPtr(&n)) != 0) return error.IoctlError;
var buf: [16]u8 = undefined;
const sname = try std.fmt.bufPrint(&buf, "/dev/pts/{d}", .{n});
std.log.err("pts: {s}", .{sname});
const t = try posix.open(sname, .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
return .{
.pty = p,
.tty = t,
};
}

View File

@ -0,0 +1,513 @@
const std = @import("std");
const assert = std.debug.assert;
const vaxis = @import("../../main.zig");
const ansi = @import("ansi.zig");
const log = std.log.scoped(.vaxis_terminal);
const Screen = @This();
pub const Cell = struct {
char: std.ArrayList(u8) = undefined,
style: vaxis.Style = .{},
uri: std.ArrayList(u8) = undefined,
uri_id: std.ArrayList(u8) = undefined,
width: u8 = 1,
wrapped: bool = false,
dirty: bool = true,
pub fn erase(self: *Cell, bg: vaxis.Color) void {
self.char.clearRetainingCapacity();
self.char.append(' ') catch unreachable; // we never completely free this list
self.style = .{};
self.style.bg = bg;
self.uri.clearRetainingCapacity();
self.uri_id.clearRetainingCapacity();
self.width = 1;
self.wrapped = false;
self.dirty = true;
}
pub fn copyFrom(self: *Cell, src: Cell) !void {
self.char.clearRetainingCapacity();
try self.char.appendSlice(src.char.items);
self.style = src.style;
self.uri.clearRetainingCapacity();
try self.uri.appendSlice(src.uri.items);
self.uri_id.clearRetainingCapacity();
try self.uri_id.appendSlice(src.uri_id.items);
self.width = src.width;
self.wrapped = src.wrapped;
self.dirty = true;
}
};
pub const Cursor = struct {
style: vaxis.Style = .{},
uri: std.ArrayList(u8) = undefined,
uri_id: std.ArrayList(u8) = undefined,
col: usize = 0,
row: usize = 0,
pending_wrap: bool = false,
shape: vaxis.Cell.CursorShape = .default,
visible: bool = true,
pub fn isOutsideScrollingRegion(self: Cursor, sr: ScrollingRegion) bool {
return self.row < sr.top or
self.row > sr.bottom or
self.col < sr.left or
self.col > sr.right;
}
pub fn isInsideScrollingRegion(self: Cursor, sr: ScrollingRegion) bool {
return !self.isOutsideScrollingRegion(sr);
}
};
pub const ScrollingRegion = struct {
top: usize,
bottom: usize,
left: usize,
right: usize,
pub fn contains(self: ScrollingRegion, col: usize, row: usize) bool {
return col >= self.left and
col <= self.right and
row >= self.top and
row <= self.bottom;
}
};
width: usize = 0,
height: usize = 0,
scrolling_region: ScrollingRegion,
buf: []Cell = undefined,
cursor: Cursor = .{},
csi_u_flags: vaxis.Key.KittyFlags = @bitCast(@as(u5, 0)),
/// sets each cell to the default cell
pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !Screen {
var screen = Screen{
.buf = try alloc.alloc(Cell, w * h),
.scrolling_region = .{
.top = 0,
.bottom = h - 1,
.left = 0,
.right = w - 1,
},
.width = w,
.height = h,
};
for (screen.buf, 0..) |_, i| {
screen.buf[i] = .{
.char = try std.ArrayList(u8).initCapacity(alloc, 1),
.uri = std.ArrayList(u8).init(alloc),
.uri_id = std.ArrayList(u8).init(alloc),
};
try screen.buf[i].char.append(' ');
}
return screen;
}
pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void {
for (self.buf, 0..) |_, i| {
self.buf[i].char.deinit();
self.buf[i].uri.deinit();
self.buf[i].uri_id.deinit();
}
alloc.free(self.buf);
}
/// copies the visible area to the destination screen
pub fn copyTo(self: *Screen, dst: *Screen) !void {
dst.cursor = self.cursor;
for (self.buf, 0..) |cell, i| {
if (!cell.dirty) continue;
self.buf[i].dirty = false;
const grapheme = cell.char.items;
dst.buf[i].char.clearRetainingCapacity();
try dst.buf[i].char.appendSlice(grapheme);
dst.buf[i].width = cell.width;
dst.buf[i].style = cell.style;
}
}
pub fn readCell(self: *Screen, col: usize, row: usize) ?vaxis.Cell {
if (self.width < col) {
// column out of bounds
return null;
}
if (self.height < row) {
// height out of bounds
return null;
}
const i = (row * self.width) + col;
assert(i < self.buf.len);
const cell = self.buf[i];
return .{
.char = .{ .grapheme = cell.char.items, .width = cell.width },
.style = cell.style,
};
}
/// returns true if the current cursor position is within the scrolling region
pub fn withinScrollingRegion(self: Screen) bool {
return self.scrolling_region.contains(self.cursor.col, self.cursor.row);
}
/// writes a cell to a location. 0 indexed
pub fn print(
self: *Screen,
grapheme: []const u8,
width: u8,
wrap: bool,
) !void {
if (self.cursor.pending_wrap) {
try self.index();
self.cursor.col = self.scrolling_region.left;
}
if (self.cursor.col >= self.width) return;
if (self.cursor.row >= self.height) return;
const col = self.cursor.col;
const row = self.cursor.row;
const i = (row * self.width) + col;
assert(i < self.buf.len);
self.buf[i].char.clearRetainingCapacity();
self.buf[i].char.appendSlice(grapheme) catch {
log.warn("couldn't write grapheme", .{});
};
self.buf[i].uri.clearRetainingCapacity();
self.buf[i].uri.appendSlice(self.cursor.uri.items) catch {
log.warn("couldn't write uri", .{});
};
self.buf[i].uri_id.clearRetainingCapacity();
self.buf[i].uri_id.appendSlice(self.cursor.uri_id.items) catch {
log.warn("couldn't write uri_id", .{});
};
self.buf[i].style = self.cursor.style;
self.buf[i].width = width;
self.buf[i].dirty = true;
if (wrap and self.cursor.col >= self.width - 1) self.cursor.pending_wrap = true;
self.cursor.col += width;
}
/// IND
pub fn index(self: *Screen) !void {
self.cursor.pending_wrap = false;
if (self.cursor.isOutsideScrollingRegion(self.scrolling_region)) {
// Outside, we just move cursor down one
self.cursor.row = @min(self.height - 1, self.cursor.row + 1);
return;
}
// We are inside the scrolling region
if (self.cursor.row == self.scrolling_region.bottom) {
// Inside scrolling region *and* at bottom of screen, we scroll contents up and insert a
// blank line
// TODO: scrollback if scrolling region is entire visible screen
try self.deleteLine(1);
return;
}
self.cursor.row += 1;
}
pub fn sgr(self: *Screen, seq: ansi.CSI) void {
if (seq.params.len == 0) {
self.cursor.style = .{};
return;
}
var iter = seq.iterator(u8);
while (iter.next()) |ps| {
switch (ps) {
0 => self.cursor.style = .{},
1 => self.cursor.style.bold = true,
2 => self.cursor.style.dim = true,
3 => self.cursor.style.italic = true,
4 => {
const kind: vaxis.Style.Underline = if (iter.next_is_sub)
@enumFromInt(iter.next() orelse 1)
else
.single;
self.cursor.style.ul_style = kind;
},
5 => self.cursor.style.blink = true,
7 => self.cursor.style.reverse = true,
8 => self.cursor.style.invisible = true,
9 => self.cursor.style.strikethrough = true,
21 => self.cursor.style.ul_style = .double,
22 => {
self.cursor.style.bold = false;
self.cursor.style.dim = false;
},
23 => self.cursor.style.italic = false,
24 => self.cursor.style.ul_style = .off,
25 => self.cursor.style.blink = false,
27 => self.cursor.style.reverse = false,
28 => self.cursor.style.invisible = false,
29 => self.cursor.style.strikethrough = false,
30...37 => self.cursor.style.fg = .{ .index = ps - 30 },
38 => {
// must have another parameter
const kind = iter.next() orelse return;
switch (kind) {
2 => { // rgb
const r = r: {
// First param can be empty
var ps_r = iter.next() orelse return;
if (iter.is_empty)
ps_r = iter.next() orelse return;
break :r ps_r;
};
const g = iter.next() orelse return;
const b = iter.next() orelse return;
self.cursor.style.fg = .{ .rgb = .{ r, g, b } };
},
5 => {
const idx = iter.next() orelse return;
self.cursor.style.fg = .{ .index = idx };
}, // index
else => return,
}
},
39 => self.cursor.style.fg = .default,
40...47 => self.cursor.style.bg = .{ .index = ps - 40 },
48 => {
// must have another parameter
const kind = iter.next() orelse return;
switch (kind) {
2 => { // rgb
const r = r: {
// First param can be empty
var ps_r = iter.next() orelse return;
if (iter.is_empty)
ps_r = iter.next() orelse return;
break :r ps_r;
};
const g = iter.next() orelse return;
const b = iter.next() orelse return;
self.cursor.style.bg = .{ .rgb = .{ r, g, b } };
},
5 => {
const idx = iter.next() orelse return;
self.cursor.style.bg = .{ .index = idx };
}, // index
else => return,
}
},
49 => self.cursor.style.bg = .default,
90...97 => self.cursor.style.fg = .{ .index = ps - 90 + 8 },
100...107 => self.cursor.style.bg = .{ .index = ps - 100 + 8 },
else => continue,
}
}
}
pub fn cursorUp(self: *Screen, n: usize) void {
self.cursor.pending_wrap = false;
if (self.withinScrollingRegion())
self.cursor.row = @max(
self.cursor.row -| n,
self.scrolling_region.top,
)
else
self.cursor.row -|= n;
}
pub fn cursorLeft(self: *Screen, n: usize) void {
self.cursor.pending_wrap = false;
if (self.withinScrollingRegion())
self.cursor.col = @max(
self.cursor.col -| n,
self.scrolling_region.left,
)
else
self.cursor.col = self.cursor.col -| n;
}
pub fn cursorRight(self: *Screen, n: usize) void {
self.cursor.pending_wrap = false;
if (self.withinScrollingRegion())
self.cursor.col = @min(
self.cursor.col + n,
self.scrolling_region.right,
)
else
self.cursor.col = @min(
self.cursor.col + n,
self.width - 1,
);
}
pub fn cursorDown(self: *Screen, n: usize) void {
self.cursor.pending_wrap = false;
if (self.withinScrollingRegion())
self.cursor.row = @min(
self.scrolling_region.bottom,
self.cursor.row + n,
)
else
self.cursor.row = @min(
self.height -| 1,
self.cursor.row + n,
);
}
pub fn eraseRight(self: *Screen) void {
self.cursor.pending_wrap = false;
const end = (self.cursor.row * self.width) + (self.width);
var i = (self.cursor.row * self.width) + self.cursor.col;
while (i < end) : (i += 1) {
self.buf[i].erase(self.cursor.style.bg);
}
}
pub fn eraseLeft(self: *Screen) void {
self.cursor.pending_wrap = false;
const start = self.cursor.row * self.width;
const end = start + self.cursor.col + 1;
var i = start;
while (i < end) : (i += 1) {
self.buf[i].erase(self.cursor.style.bg);
}
}
pub fn eraseLine(self: *Screen) void {
self.cursor.pending_wrap = false;
const start = self.cursor.row * self.width;
const end = start + self.width;
var i = start;
while (i < end) : (i += 1) {
self.buf[i].erase(self.cursor.style.bg);
}
}
/// delete n lines from the bottom of the scrolling region
pub fn deleteLine(self: *Screen, n: usize) !void {
if (n == 0) return;
// Don't delete if outside scroll region
if (!self.withinScrollingRegion()) return;
self.cursor.pending_wrap = false;
// Number of rows from here to bottom of scroll region or n
const cnt = @min(self.scrolling_region.bottom - self.cursor.row + 1, n);
const stride = (self.width) * cnt;
var row: usize = self.scrolling_region.top;
while (row <= self.scrolling_region.bottom) : (row += 1) {
var col: usize = self.scrolling_region.left;
while (col <= self.scrolling_region.right) : (col += 1) {
const i = (row * self.width) + col;
if (row + cnt > self.scrolling_region.bottom)
self.buf[i].erase(self.cursor.style.bg)
else
try self.buf[i].copyFrom(self.buf[i + stride]);
}
}
}
/// insert n lines at the top of the scrolling region
pub fn insertLine(self: *Screen, n: usize) !void {
if (n == 0) return;
self.cursor.pending_wrap = false;
// Don't insert if outside scroll region
if (!self.withinScrollingRegion()) return;
const adjusted_n = @min(self.scrolling_region.bottom - self.cursor.row, n);
const stride = (self.width) * adjusted_n;
var row: usize = self.scrolling_region.bottom;
while (row >= self.scrolling_region.top + adjusted_n) : (row -|= 1) {
var col: usize = self.scrolling_region.left;
while (col <= self.scrolling_region.right) : (col += 1) {
const i = (row * self.width) + col;
try self.buf[i].copyFrom(self.buf[i - stride]);
}
}
row = self.scrolling_region.top;
while (row < self.scrolling_region.top + adjusted_n) : (row += 1) {
var col: usize = self.scrolling_region.left;
while (col <= self.scrolling_region.right) : (col += 1) {
const i = (row * self.width) + col;
self.buf[i].erase(self.cursor.style.bg);
}
}
}
pub fn eraseBelow(self: *Screen) void {
self.eraseRight();
// start is the first column of the row below us
const start = (self.cursor.row * self.width) + (self.width);
var i = start;
while (i < self.buf.len) : (i += 1) {
self.buf[i].erase(self.cursor.style.bg);
}
}
pub fn eraseAbove(self: *Screen) void {
self.eraseLeft();
// start is the first column of the row below us
const start: usize = 0;
const end = self.cursor.row * self.width;
var i = start;
while (i < end) : (i += 1) {
self.buf[i].erase(self.cursor.style.bg);
}
}
pub fn eraseAll(self: *Screen) void {
var i: usize = 0;
while (i < self.buf.len) : (i += 1) {
self.buf[i].erase(self.cursor.style.bg);
}
}
pub fn deleteCharacters(self: *Screen, n: usize) !void {
if (!self.withinScrollingRegion()) return;
self.cursor.pending_wrap = false;
var col = self.cursor.col;
while (col <= self.scrolling_region.right) : (col += 1) {
if (col + n <= self.scrolling_region.right)
try self.buf[col].copyFrom(self.buf[col + n])
else
self.buf[col].erase(self.cursor.style.bg);
}
}
pub fn reverseIndex(self: *Screen) !void {
if (self.cursor.row != self.scrolling_region.top or
self.cursor.col < self.scrolling_region.left or
self.cursor.col > self.scrolling_region.right)
self.cursorUp(1)
else
try self.scrollDown(1);
}
pub fn scrollDown(self: *Screen, n: usize) !void {
const cur_row = self.cursor.row;
const cur_col = self.cursor.col;
const wrap = self.cursor.pending_wrap;
defer {
self.cursor.row = cur_row;
self.cursor.col = cur_col;
self.cursor.pending_wrap = wrap;
}
self.cursor.col = self.scrolling_region.left;
self.cursor.row = self.scrolling_region.top;
try self.insertLine(n);
}

View File

@ -0,0 +1,794 @@
//! A virtual terminal widget
const Terminal = @This();
const std = @import("std");
const builtin = @import("builtin");
const ansi = @import("ansi.zig");
pub const Command = @import("Command.zig");
const Parser = @import("Parser.zig");
const Pty = @import("Pty.zig");
const vaxis = @import("../../main.zig");
const Winsize = vaxis.Winsize;
const Screen = @import("Screen.zig");
const DisplayWidth = @import("DisplayWidth");
const Key = vaxis.Key;
const Queue = vaxis.Queue(Event, 16);
const code_point = @import("code_point");
const key = @import("key.zig");
pub const Event = union(enum) {
exited,
redraw,
bell,
title_change: []const u8,
pwd_change: []const u8,
};
const grapheme = @import("grapheme");
const posix = std.posix;
const log = std.log.scoped(.terminal);
pub const Options = struct {
scrollback_size: usize = 500,
winsize: Winsize = .{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 },
initial_working_directory: ?[]const u8 = null,
};
pub const Mode = struct {
origin: bool = false,
autowrap: bool = true,
cursor: bool = true,
sync: bool = false,
};
pub const InputEvent = union(enum) {
key_press: vaxis.Key,
};
pub var global_vt_mutex: std.Thread.Mutex = .{};
pub var global_vts: ?std.AutoHashMap(i32, *Terminal) = null;
pub var global_sigchild_installed: bool = false;
allocator: std.mem.Allocator,
scrollback_size: usize,
pty: Pty,
cmd: Command,
thread: ?std.Thread = null,
/// the screen we draw from
front_screen: Screen,
front_mutex: std.Thread.Mutex = .{},
/// the back screens
back_screen: *Screen = undefined,
back_screen_pri: Screen,
back_screen_alt: Screen,
// only applies to primary screen
scroll_offset: usize = 0,
back_mutex: std.Thread.Mutex = .{},
// dirty is protected by back_mutex. Only access this field when you hold that mutex
dirty: bool = false,
unicode: *const vaxis.Unicode,
should_quit: bool = false,
mode: Mode = .{},
tab_stops: std.ArrayList(u16),
title: std.ArrayList(u8),
working_directory: std.ArrayList(u8),
last_printed: []const u8 = "",
event_queue: Queue = .{},
/// initialize a Terminal. This sets the size of the underlying pty and allocates the sizes of the
/// screen
pub fn init(
allocator: std.mem.Allocator,
argv: []const []const u8,
env: *const std.process.EnvMap,
unicode: *const vaxis.Unicode,
opts: Options,
) !Terminal {
// Verify we have an absolute path
if (opts.initial_working_directory) |pwd| {
if (!std.fs.path.isAbsolute(pwd)) return error.InvalidWorkingDirectory;
}
const pty = try Pty.init();
try pty.setSize(opts.winsize);
const cmd: Command = .{
.argv = argv,
.env_map = env,
.pty = pty,
.working_directory = opts.initial_working_directory,
};
var tabs = try std.ArrayList(u16).initCapacity(allocator, opts.winsize.cols / 8);
var col: u16 = 0;
while (col < opts.winsize.cols) : (col += 8) {
try tabs.append(col);
}
return .{
.allocator = allocator,
.pty = pty,
.cmd = cmd,
.scrollback_size = opts.scrollback_size,
.front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
.back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size),
.back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
.unicode = unicode,
.tab_stops = tabs,
.title = std.ArrayList(u8).init(allocator),
.working_directory = std.ArrayList(u8).init(allocator),
};
}
/// release all resources of the Terminal
pub fn deinit(self: *Terminal) void {
self.should_quit = true;
pid: {
global_vt_mutex.lock();
defer global_vt_mutex.unlock();
var vts = global_vts orelse break :pid;
if (self.cmd.pid) |pid|
_ = vts.remove(pid);
if (vts.count() == 0) {
vts.deinit();
global_vts = null;
}
}
self.cmd.kill();
if (self.thread) |thread| {
// write an EOT into the tty to trigger a read on our thread
const EOT = "\x04";
_ = std.posix.write(self.pty.tty, EOT) catch {};
thread.join();
self.thread = null;
}
self.pty.deinit();
self.front_screen.deinit(self.allocator);
self.back_screen_pri.deinit(self.allocator);
self.back_screen_alt.deinit(self.allocator);
self.tab_stops.deinit();
self.title.deinit();
self.working_directory.deinit();
}
pub fn spawn(self: *Terminal) !void {
if (self.thread != null) return;
self.back_screen = &self.back_screen_pri;
try self.cmd.spawn(self.allocator);
self.working_directory.clearRetainingCapacity();
if (self.cmd.working_directory) |pwd| {
try self.working_directory.appendSlice(pwd);
} else {
const pwd = std.fs.cwd();
var buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const out_path = try std.os.getFdPath(pwd.fd, &buffer);
try self.working_directory.appendSlice(out_path);
}
{
// add to our global list
global_vt_mutex.lock();
defer global_vt_mutex.unlock();
if (global_vts == null)
global_vts = std.AutoHashMap(i32, *Terminal).init(self.allocator);
if (self.cmd.pid) |pid|
try global_vts.?.put(pid, self);
}
self.thread = try std.Thread.spawn(.{}, Terminal.run, .{self});
}
/// resize the screen. Locks access to the back screen. Should only be called from the main thread.
/// This is safe to call every render cycle: there is a guard to only perform a resize if the size
/// of the window has changed.
pub fn resize(self: *Terminal, ws: Winsize) !void {
// don't deinit with no size change
if (ws.cols == self.front_screen.width and
ws.rows == self.front_screen.height)
return;
self.back_mutex.lock();
defer self.back_mutex.unlock();
self.front_screen.deinit(self.allocator);
self.front_screen = try Screen.init(self.allocator, ws.cols, ws.rows);
self.back_screen_pri.deinit(self.allocator);
self.back_screen_alt.deinit(self.allocator);
self.back_screen_pri = try Screen.init(self.allocator, ws.cols, ws.rows + self.scrollback_size);
self.back_screen_alt = try Screen.init(self.allocator, ws.cols, ws.rows);
try self.pty.setSize(ws);
}
pub fn draw(self: *Terminal, win: vaxis.Window) !void {
if (self.back_mutex.tryLock()) {
defer self.back_mutex.unlock();
// We keep this as a separate condition so we don't deadlock by obtaining the lock but not
// having sync
if (!self.mode.sync) {
try self.back_screen.copyTo(&self.front_screen);
self.dirty = false;
}
}
var row: usize = 0;
while (row < self.front_screen.height) : (row += 1) {
var col: usize = 0;
while (col < self.front_screen.width) {
const cell = self.front_screen.readCell(col, row) orelse continue;
win.writeCell(col, row, cell);
col += @max(cell.char.width, 1);
}
}
if (self.mode.cursor) {
win.setCursorShape(self.front_screen.cursor.shape);
win.showCursor(self.front_screen.cursor.col, self.front_screen.cursor.row);
}
}
pub fn tryEvent(self: *Terminal) ?Event {
return self.event_queue.tryPop();
}
pub fn update(self: *Terminal, event: InputEvent) !void {
switch (event) {
.key_press => |k| try key.encode(self.anyWriter(), k, true, self.back_screen.csi_u_flags),
}
}
fn opaqueWrite(ptr: *const anyopaque, buf: []const u8) !usize {
const self: *const Terminal = @ptrCast(@alignCast(ptr));
return posix.write(self.pty.pty, buf);
}
pub fn anyWriter(self: *const Terminal) std.io.AnyWriter {
return .{
.context = self,
.writeFn = Terminal.opaqueWrite,
};
}
fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize {
const self: *const Terminal = @ptrCast(@alignCast(ptr));
return posix.read(self.pty.pty, buf);
}
fn anyReader(self: *const Terminal) std.io.AnyReader {
return .{
.context = self,
.readFn = Terminal.opaqueRead,
};
}
/// process the output from the command on the pty
fn run(self: *Terminal) !void {
var parser: Parser = .{
.buf = try std.ArrayList(u8).initCapacity(self.allocator, 128),
};
defer parser.buf.deinit();
// Use our anyReader to make a buffered reader, then get *that* any reader
var reader = std.io.bufferedReader(self.anyReader());
while (!self.should_quit) {
const event = try parser.parseReader(&reader);
self.back_mutex.lock();
defer self.back_mutex.unlock();
if (!self.dirty and self.event_queue.tryPush(.redraw))
self.dirty = true;
switch (event) {
.print => |str| {
var iter = grapheme.Iterator.init(str, &self.unicode.grapheme_data);
while (iter.next()) |g| {
const gr = g.bytes(str);
// TODO: use actual instead of .unicode
const w = try vaxis.gwidth.gwidth(gr, .unicode, &self.unicode.width_data);
try self.back_screen.print(gr, @truncate(w), self.mode.autowrap);
}
},
.c0 => |b| try self.handleC0(b),
.escape => |esc| {
const final = esc[esc.len - 1];
switch (final) {
'B' => {}, // TODO: handle charsets
// Index
'D' => try self.back_screen.index(),
// Next Line
'E' => {
try self.back_screen.index();
self.carriageReturn();
},
// Horizontal Tab Set
'H' => {
const already_set: bool = for (self.tab_stops.items) |ts| {
if (ts == self.back_screen.cursor.col) break true;
} else false;
if (already_set) continue;
try self.tab_stops.append(@truncate(self.back_screen.cursor.col));
std.mem.sort(u16, self.tab_stops.items, {}, std.sort.asc(u16));
},
// Reverse Index
'M' => try self.back_screen.reverseIndex(),
else => log.info("unhandled escape: {s}", .{esc}),
}
},
.ss2 => |ss2| log.info("unhandled ss2: {c}", .{ss2}),
.ss3 => |ss3| log.info("unhandled ss3: {c}", .{ss3}),
.csi => |seq| {
switch (seq.final) {
// Cursor up
'A', 'k' => {
var iter = seq.iterator(u16);
const delta = iter.next() orelse 1;
self.back_screen.cursorUp(delta);
},
// Cursor Down
'B' => {
var iter = seq.iterator(u16);
const delta = iter.next() orelse 1;
self.back_screen.cursorDown(delta);
},
// Cursor Right
'C' => {
var iter = seq.iterator(u16);
const delta = iter.next() orelse 1;
self.back_screen.cursorRight(delta);
},
// Cursor Left
'D', 'j' => {
var iter = seq.iterator(u16);
const delta = iter.next() orelse 1;
self.back_screen.cursorLeft(delta);
},
// Cursor Next Line
'E' => {
var iter = seq.iterator(u16);
const delta = iter.next() orelse 1;
self.back_screen.cursorDown(delta);
self.carriageReturn();
},
// Cursor Previous Line
'F' => {
var iter = seq.iterator(u16);
const delta = iter.next() orelse 1;
self.back_screen.cursorUp(delta);
self.carriageReturn();
},
// Horizontal Position Absolute
'G', '`' => {
var iter = seq.iterator(u16);
const col = iter.next() orelse 1;
self.back_screen.cursor.col = col -| 1;
if (self.back_screen.cursor.col < self.back_screen.scrolling_region.left)
self.back_screen.cursor.col = self.back_screen.scrolling_region.left;
if (self.back_screen.cursor.col > self.back_screen.scrolling_region.right)
self.back_screen.cursor.col = self.back_screen.scrolling_region.right;
self.back_screen.cursor.pending_wrap = false;
},
// Cursor Absolute Position
'H', 'f' => {
var iter = seq.iterator(u16);
const row = iter.next() orelse 1;
const col = iter.next() orelse 1;
self.back_screen.cursor.col = col -| 1;
self.back_screen.cursor.row = row -| 1;
self.back_screen.cursor.pending_wrap = false;
},
// Cursor Horizontal Tab
'I' => {
var iter = seq.iterator(u16);
const n = iter.next() orelse 1;
self.horizontalTab(n);
},
// Erase In Display
'J' => {
// TODO: selective erase (private_marker == '?')
var iter = seq.iterator(u16);
const kind = iter.next() orelse 0;
switch (kind) {
0 => self.back_screen.eraseBelow(),
1 => self.back_screen.eraseAbove(),
2 => self.back_screen.eraseAll(),
3 => {},
else => {},
}
},
// Erase in Line
'K' => {
// TODO: selective erase (private_marker == '?')
var iter = seq.iterator(u8);
const ps = iter.next() orelse 0;
switch (ps) {
0 => self.back_screen.eraseRight(),
1 => self.back_screen.eraseLeft(),
2 => self.back_screen.eraseLine(),
else => continue,
}
},
// Insert Lines
'L' => {
var iter = seq.iterator(u16);
const n = iter.next() orelse 1;
try self.back_screen.insertLine(n);
},
// Delete Lines
'M' => {
var iter = seq.iterator(u16);
const n = iter.next() orelse 1;
try self.back_screen.deleteLine(n);
},
// Delete Character
'P' => {
var iter = seq.iterator(u16);
const n = iter.next() orelse 1;
try self.back_screen.deleteCharacters(n);
},
// Scroll Up
'S' => {
var iter = seq.iterator(u16);
const n = iter.next() orelse 1;
const cur_row = self.back_screen.cursor.row;
const cur_col = self.back_screen.cursor.col;
const wrap = self.back_screen.cursor.pending_wrap;
defer {
self.back_screen.cursor.row = cur_row;
self.back_screen.cursor.col = cur_col;
self.back_screen.cursor.pending_wrap = wrap;
}
self.back_screen.cursor.col = self.back_screen.scrolling_region.left;
self.back_screen.cursor.row = self.back_screen.scrolling_region.top;
try self.back_screen.deleteLine(n);
},
// Scroll Down
'T' => {
var iter = seq.iterator(u16);
const n = iter.next() orelse 1;
try self.back_screen.scrollDown(n);
},
// Tab Control
'W' => {
if (seq.private_marker) |pm| {
if (pm != '?') continue;
var iter = seq.iterator(u16);
const n = iter.next() orelse continue;
if (n != 5) continue;
self.tab_stops.clearRetainingCapacity();
var col: u16 = 0;
while (col < self.back_screen.width) : (col += 8) {
try self.tab_stops.append(col);
}
}
},
'X' => {
self.back_screen.cursor.pending_wrap = false;
var iter = seq.iterator(u16);
const n = iter.next() orelse 1;
const start = self.back_screen.cursor.row * self.back_screen.width + self.back_screen.cursor.col;
const end = @max(
self.back_screen.cursor.row * self.back_screen.width + self.back_screen.width,
n,
1, // In case n == 0
);
var i: usize = start;
while (i < end) : (i += 1) {
self.back_screen.buf[i].erase(self.back_screen.cursor.style.bg);
}
},
'Z' => {
var iter = seq.iterator(u16);
const n = iter.next() orelse 1;
self.horizontalBackTab(n);
},
// Cursor Horizontal Position Relative
'a' => {
var iter = seq.iterator(u16);
const n = iter.next() orelse 1;
self.back_screen.cursor.pending_wrap = false;
const max_end = if (self.mode.origin)
self.back_screen.scrolling_region.right
else
self.back_screen.width - 1;
self.back_screen.cursor.col = @min(
self.back_screen.cursor.col + max_end,
self.back_screen.cursor.col + n,
);
},
// Repeat Previous Character
'b' => {
var iter = seq.iterator(u16);
const n = iter.next() orelse 1;
// TODO: maybe not .unicode
const w = try vaxis.gwidth.gwidth(self.last_printed, .unicode, &self.unicode.width_data);
var i: usize = 0;
while (i < n) : (i += 1) {
try self.back_screen.print(self.last_printed, @truncate(w), self.mode.autowrap);
}
},
// Device Attributes
'c' => {
if (seq.private_marker) |pm| {
switch (pm) {
// Secondary
'>' => try self.anyWriter().writeAll("\x1B[>1;69;0c"),
'=' => try self.anyWriter().writeAll("\x1B[=0000c"),
else => log.info("unhandled CSI: {}", .{seq}),
}
} else {
// Primary
try self.anyWriter().writeAll("\x1B[?62;22c");
}
},
// Cursor Vertical Position Absolute
'd' => {
self.back_screen.cursor.pending_wrap = false;
var iter = seq.iterator(u16);
const n = iter.next() orelse 1;
const max = if (self.mode.origin)
self.back_screen.scrolling_region.bottom
else
self.back_screen.height -| 1;
self.back_screen.cursor.pending_wrap = false;
self.back_screen.cursor.row = @min(
max,
n -| 1,
);
},
// Cursor Vertical Position Absolute
'e' => {
var iter = seq.iterator(u16);
const n = iter.next() orelse 1;
self.back_screen.cursor.pending_wrap = false;
self.back_screen.cursor.row = @min(
self.back_screen.width -| 1,
n -| 1,
);
},
// Tab Clear
'g' => {
var iter = seq.iterator(u16);
const n = iter.next() orelse 0;
switch (n) {
0 => {
const current = try self.tab_stops.toOwnedSlice();
defer self.tab_stops.allocator.free(current);
self.tab_stops.clearRetainingCapacity();
for (current) |stop| {
if (stop == self.back_screen.cursor.col) continue;
try self.tab_stops.append(stop);
}
},
3 => self.tab_stops.clearAndFree(),
else => log.info("unhandled CSI: {}", .{seq}),
}
},
'h', 'l' => {
var iter = seq.iterator(u16);
const mode = iter.next() orelse continue;
// There is only one collision (mode = 4), and we don't support the private
// version of it
if (seq.private_marker != null and mode == 4) continue;
self.setMode(mode, seq.final == 'h');
},
'm' => {
if (seq.intermediate == null and seq.private_marker == null) {
self.back_screen.sgr(seq);
}
// TODO: private marker and intermediates
},
'n' => {
var iter = seq.iterator(u16);
const ps = iter.next() orelse 0;
if (seq.intermediate == null and seq.private_marker == null) {
switch (ps) {
5 => try self.anyWriter().writeAll("\x1b[0n"),
6 => try self.anyWriter().print("\x1b[{d};{d}R", .{
self.back_screen.cursor.row + 1,
self.back_screen.cursor.col + 1,
}),
else => log.info("unhandled CSI: {}", .{seq}),
}
}
},
'p' => {
var iter = seq.iterator(u16);
const ps = iter.next() orelse 0;
if (seq.intermediate) |int| {
switch (int) {
// report mode
'$' => {
switch (ps) {
2026 => try self.anyWriter().writeAll("\x1b[?2026;2$p"),
else => {
std.log.warn("unhandled mode: {}", .{ps});
try self.anyWriter().print("\x1b[?{d};0$p", .{ps});
},
}
},
else => log.info("unhandled CSI: {}", .{seq}),
}
}
},
'q' => {
if (seq.intermediate) |int| {
switch (int) {
' ' => {
var iter = seq.iterator(u8);
const shape = iter.next() orelse 0;
self.back_screen.cursor.shape = @enumFromInt(shape);
},
else => {},
}
}
if (seq.private_marker) |pm| {
switch (pm) {
// XTVERSION
'>' => try self.anyWriter().print(
"\x1bP>|libvaxis {s}\x1B\\",
.{"dev"},
),
else => log.info("unhandled CSI: {}", .{seq}),
}
}
},
'r' => {
if (seq.intermediate) |_| {
// TODO: XTRESTORE
continue;
}
if (seq.private_marker) |_| {
// TODO: DECCARA
continue;
}
// DECSTBM
var iter = seq.iterator(u16);
const top = iter.next() orelse 1;
const bottom = iter.next() orelse self.back_screen.height;
self.back_screen.scrolling_region.top = top -| 1;
self.back_screen.scrolling_region.bottom = bottom -| 1;
self.back_screen.cursor.pending_wrap = false;
if (self.mode.origin) {
self.back_screen.cursor.col = self.back_screen.scrolling_region.left;
self.back_screen.cursor.row = self.back_screen.scrolling_region.top;
} else {
self.back_screen.cursor.col = 0;
self.back_screen.cursor.row = 0;
}
},
else => log.info("unhandled CSI: {}", .{seq}),
}
},
.osc => |osc| {
const semicolon = std.mem.indexOfScalar(u8, osc, ';') orelse {
log.info("unhandled osc: {s}", .{osc});
continue;
};
const ps = std.fmt.parseUnsigned(u8, osc[0..semicolon], 10) catch {
log.info("unhandled osc: {s}", .{osc});
continue;
};
switch (ps) {
0 => {
self.title.clearRetainingCapacity();
try self.title.appendSlice(osc[semicolon + 1 ..]);
self.event_queue.push(.{ .title_change = self.title.items });
},
7 => {
// OSC 7 ; file:// <hostname> <pwd>
log.err("osc: {s}", .{osc});
self.working_directory.clearRetainingCapacity();
const scheme = "file://";
const start = std.mem.indexOfScalarPos(u8, osc, semicolon + 2 + scheme.len + 1, '/') orelse {
log.info("unknown OSC 7 format: {s}", .{osc});
continue;
};
const enc = osc[start..];
var i: usize = 0;
while (i < enc.len) : (i += 1) {
const b = if (enc[i] == '%') blk: {
defer i += 2;
break :blk try std.fmt.parseUnsigned(u8, enc[i + 1 .. i + 3], 16);
} else enc[i];
try self.working_directory.append(b);
}
self.event_queue.push(.{ .pwd_change = self.working_directory.items });
},
else => log.info("unhandled osc: {s}", .{osc}),
}
},
.apc => |apc| log.info("unhandled apc: {s}", .{apc}),
}
}
}
inline fn handleC0(self: *Terminal, b: ansi.C0) !void {
switch (b) {
.NUL, .SOH, .STX => {},
.EOT => {}, // we send EOT to quit the read thread
.ENQ => {},
.BEL => self.event_queue.push(.bell),
.BS => self.back_screen.cursorLeft(1),
.HT => self.horizontalTab(1),
.LF, .VT, .FF => try self.back_screen.index(),
.CR => self.carriageReturn(),
.SO => {}, // TODO: Charset shift out
.SI => {}, // TODO: Charset shift in
else => log.warn("unhandled C0: 0x{x}", .{@intFromEnum(b)}),
}
}
pub fn setMode(self: *Terminal, mode: u16, val: bool) void {
switch (mode) {
7 => self.mode.autowrap = val,
25 => self.mode.cursor = val,
1049 => {
if (val)
self.back_screen = &self.back_screen_alt
else
self.back_screen = &self.back_screen_pri;
var i: usize = 0;
while (i < self.back_screen.buf.len) : (i += 1) {
self.back_screen.buf[i].dirty = true;
}
},
2026 => self.mode.sync = val,
else => return,
}
}
pub fn carriageReturn(self: *Terminal) void {
self.back_screen.cursor.pending_wrap = false;
self.back_screen.cursor.col = if (self.mode.origin)
self.back_screen.scrolling_region.left
else if (self.back_screen.cursor.col >= self.back_screen.scrolling_region.left)
self.back_screen.scrolling_region.left
else
0;
}
pub fn horizontalTab(self: *Terminal, n: usize) void {
// Get the current cursor position
const col = self.back_screen.cursor.col;
// Find desired final position
var i: usize = 0;
const final = for (self.tab_stops.items) |ts| {
if (ts <= col) continue;
i += 1;
if (i == n) break ts;
} else self.back_screen.width - 1;
// Move right the delta
self.back_screen.cursorRight(final -| col);
}
pub fn horizontalBackTab(self: *Terminal, n: usize) void {
// Get the current cursor position
const col = self.back_screen.cursor.col;
// Find the index of the next backtab
const idx = for (self.tab_stops.items, 0..) |ts, i| {
if (ts <= col) continue;
break i;
} else self.tab_stops.items.len - 1;
const final = if (self.mode.origin)
@max(self.tab_stops.items[idx -| (n -| 1)], self.back_screen.scrolling_region.left)
else
self.tab_stops.items[idx -| (n -| 1)];
// Move left the delta
self.back_screen.cursorLeft(final - col);
}

View File

@ -0,0 +1,143 @@
const std = @import("std");
/// Control bytes. See man 7 ascii
pub const C0 = enum(u8) {
NUL = 0x00,
SOH = 0x01,
STX = 0x02,
ETX = 0x03,
EOT = 0x04,
ENQ = 0x05,
ACK = 0x06,
BEL = 0x07,
BS = 0x08,
HT = 0x09,
LF = 0x0a,
VT = 0x0b,
FF = 0x0c,
CR = 0x0d,
SO = 0x0e,
SI = 0x0f,
DLE = 0x10,
DC1 = 0x11,
DC2 = 0x12,
DC3 = 0x13,
DC4 = 0x14,
NAK = 0x15,
SYN = 0x16,
ETB = 0x17,
CAN = 0x18,
EM = 0x19,
SUB = 0x1a,
ESC = 0x1b,
FS = 0x1c,
GS = 0x1d,
RS = 0x1e,
US = 0x1f,
};
pub const CSI = struct {
intermediate: ?u8 = null,
private_marker: ?u8 = null,
final: u8,
params: []const u8,
pub fn hasIntermediate(self: CSI, b: u8) bool {
return b == self.intermediate orelse return false;
}
pub fn hasPrivateMarker(self: CSI, b: u8) bool {
return b == self.private_marker orelse return false;
}
pub fn iterator(self: CSI, comptime T: type) ParamIterator(T) {
return .{ .bytes = self.params };
}
pub fn format(
self: CSI,
comptime layout: []const u8,
opts: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = layout;
_ = opts;
if (self.private_marker == null and self.intermediate == null)
try std.fmt.format(writer, "CSI {s} {c}", .{
self.params,
self.final,
})
else if (self.private_marker != null and self.intermediate == null)
try std.fmt.format(writer, "CSI {c} {s} {c}", .{
self.private_marker.?,
self.params,
self.final,
})
else if (self.private_marker == null and self.intermediate != null)
try std.fmt.format(writer, "CSI {s} {c} {c}", .{
self.params,
self.intermediate.?,
self.final,
})
else
try std.fmt.format(writer, "CSI {c} {s} {c} {c}", .{
self.private_marker.?,
self.params,
self.intermediate.?,
self.final,
});
}
};
pub fn ParamIterator(T: type) type {
return struct {
const Self = @This();
bytes: []const u8,
idx: usize = 0,
/// indicates the next parameter will be a sub parameter of the current
next_is_sub: bool = false,
/// indicates the current parameter was an empty string
is_empty: bool = false,
pub fn next(self: *Self) ?T {
// reset state
self.next_is_sub = false;
self.is_empty = false;
const start = self.idx;
var val: T = 0;
while (self.idx < self.bytes.len) {
defer self.idx += 1; // defer so we trigger on return as well
const b = self.bytes[self.idx];
switch (b) {
0x30...0x39 => {
val = (val * 10) + (b - 0x30);
if (self.idx == self.bytes.len - 1) return val;
},
':', ';' => {
self.next_is_sub = b == ':';
self.is_empty = self.idx == start;
return val;
},
else => return null,
}
}
return null;
}
/// verifies there are at least n more parameters
pub fn hasAtLeast(self: *Self, n: usize) bool {
const start = self.idx;
defer self.idx = start;
var i: usize = 0;
while (self.next()) |_| {
i += 1;
if (i >= n) return true;
}
return i >= n;
}
};
}

View File

@ -0,0 +1,172 @@
const std = @import("std");
const vaxis = @import("../../main.zig");
pub fn encode(
writer: std.io.AnyWriter,
key: vaxis.Key,
press: bool,
kitty_flags: vaxis.Key.KittyFlags,
) !void {
const flags: u5 = @bitCast(kitty_flags);
switch (press) {
true => {
switch (flags) {
0 => try legacy(writer, key),
else => unreachable, // TODO: kitty encodings
}
},
false => {},
}
}
fn legacy(writer: std.io.AnyWriter, key: vaxis.Key) !void {
// If we have text, we always write it directly
if (key.text) |text| {
try writer.writeAll(text);
return;
}
const shift = 0b00000001;
const alt = 0b00000010;
const ctrl = 0b00000100;
const effective_mods: u8 = blk: {
const mods: u8 = @bitCast(key.mods);
break :blk mods & (shift | alt | ctrl);
};
// If we have no mods and an ascii byte, write it directly
if (effective_mods == 0 and key.codepoint <= 0x7F) {
const b: u8 = @truncate(key.codepoint);
try writer.writeByte(b);
return;
}
// If we are lowercase ascii and ctrl, we map to a control byte
if (effective_mods == ctrl and key.codepoint >= 'a' and key.codepoint <= 'z') {
const b: u8 = @truncate(key.codepoint);
try writer.writeByte(b -| 0x60);
return;
}
// If we are printable ascii + alt
if (effective_mods == alt and key.codepoint >= ' ' and key.codepoint < 0x7F) {
const b: u8 = @truncate(key.codepoint);
try writer.print("\x1b{c}", .{b});
return;
}
// If we are ctrl + alt + lowercase ascii
if (effective_mods == (ctrl | alt) and key.codepoint >= 'a' and key.codepoint <= 'z') {
// convert to control sequence
try writer.print("\x1b{d}", .{key.codepoint - 0x60});
}
const def = switch (key.codepoint) {
vaxis.Key.escape => escape,
vaxis.Key.enter,
vaxis.Key.kp_enter,
=> enter,
vaxis.Key.tab => tab,
vaxis.Key.backspace => backspace,
vaxis.Key.insert,
vaxis.Key.kp_insert,
=> insert,
vaxis.Key.delete,
vaxis.Key.kp_delete,
=> delete,
vaxis.Key.left,
vaxis.Key.kp_left,
=> left,
vaxis.Key.right,
vaxis.Key.kp_right,
=> right,
vaxis.Key.up,
vaxis.Key.kp_up,
=> up,
vaxis.Key.down,
vaxis.Key.kp_down,
=> down,
vaxis.Key.page_up,
vaxis.Key.kp_page_up,
=> page_up,
vaxis.Key.page_down,
vaxis.Key.kp_page_down,
=> page_down,
vaxis.Key.home,
vaxis.Key.kp_home,
=> home,
vaxis.Key.end,
vaxis.Key.kp_end,
=> end,
vaxis.Key.f1 => f1,
vaxis.Key.f2 => f2,
vaxis.Key.f3 => f3_legacy,
vaxis.Key.f4 => f4,
vaxis.Key.f5 => f5,
vaxis.Key.f6 => f6,
vaxis.Key.f7 => f7,
vaxis.Key.f8 => f8,
vaxis.Key.f9 => f9,
vaxis.Key.f10 => f10,
vaxis.Key.f11 => f11,
vaxis.Key.f12 => f12,
else => return, // TODO: more keys
};
switch (effective_mods) {
0 => {
if (def.number == 1)
switch (key.codepoint) {
vaxis.Key.f1,
vaxis.Key.f2,
vaxis.Key.f3,
vaxis.Key.f4,
=> try writer.print("\x1bO{c}", .{def.suffix}),
else => try writer.print("\x1b[{c}", .{def.suffix}),
}
else
try writer.print("\x1b[{d}{c}", .{ def.number, def.suffix });
},
else => try writer.print("\x1b[{d};{d}{c}", .{ def.number, effective_mods + 1, def.suffix }),
}
}
const Definition = struct {
number: u21,
suffix: u8,
};
const escape: Definition = .{ .number = 27, .suffix = 'u' };
const enter: Definition = .{ .number = 13, .suffix = 'u' };
const tab: Definition = .{ .number = 9, .suffix = 'u' };
const backspace: Definition = .{ .number = 127, .suffix = 'u' };
const insert: Definition = .{ .number = 2, .suffix = '~' };
const delete: Definition = .{ .number = 3, .suffix = '~' };
const left: Definition = .{ .number = 1, .suffix = 'D' };
const right: Definition = .{ .number = 1, .suffix = 'C' };
const up: Definition = .{ .number = 1, .suffix = 'A' };
const down: Definition = .{ .number = 1, .suffix = 'B' };
const page_up: Definition = .{ .number = 5, .suffix = '~' };
const page_down: Definition = .{ .number = 6, .suffix = '~' };
const home: Definition = .{ .number = 1, .suffix = 'H' };
const end: Definition = .{ .number = 1, .suffix = 'F' };
const caps_lock: Definition = .{ .number = 57358, .suffix = 'u' };
const scroll_lock: Definition = .{ .number = 57359, .suffix = 'u' };
const num_lock: Definition = .{ .number = 57360, .suffix = 'u' };
const print_screen: Definition = .{ .number = 57361, .suffix = 'u' };
const pause: Definition = .{ .number = 57362, .suffix = 'u' };
const menu: Definition = .{ .number = 57363, .suffix = 'u' };
const f1: Definition = .{ .number = 1, .suffix = 'P' };
const f2: Definition = .{ .number = 1, .suffix = 'Q' };
const f3: Definition = .{ .number = 13, .suffix = '~' };
const f3_legacy: Definition = .{ .number = 1, .suffix = 'R' };
const f4: Definition = .{ .number = 1, .suffix = 'S' };
const f5: Definition = .{ .number = 15, .suffix = '~' };
const f6: Definition = .{ .number = 17, .suffix = '~' };
const f7: Definition = .{ .number = 18, .suffix = '~' };
const f8: Definition = .{ .number = 19, .suffix = '~' };
const f9: Definition = .{ .number = 20, .suffix = '~' };
const f10: Definition = .{ .number = 21, .suffix = '~' };
const f11: Definition = .{ .number = 23, .suffix = '~' };
const f12: Definition = .{ .number = 24, .suffix = '~' };

497
deps/libvaxis/src/windows/Tty.zig vendored Normal file
View File

@ -0,0 +1,497 @@
//! A Windows TTY implementation, using virtual terminal process output and
//! native windows input
const Tty = @This();
const std = @import("std");
const Event = @import("../event.zig").Event;
const Key = @import("../Key.zig");
const Mouse = @import("../Mouse.zig");
const Parser = @import("../Parser.zig");
const windows = std.os.windows;
stdin: windows.HANDLE,
stdout: windows.HANDLE,
initial_codepage: c_uint,
initial_input_mode: u32,
initial_output_mode: u32,
// a buffer to write key text into
buf: [4]u8 = undefined,
/// The last mouse button that was pressed. We store the previous state of button presses on each
/// mouse event so we can detect which button was released
last_mouse_button_press: u16 = 0,
pub var global_tty: ?Tty = null;
const utf8_codepage: c_uint = 65001;
const InputMode = struct {
const enable_window_input: u32 = 0x0008; // resize events
const enable_mouse_input: u32 = 0x0010;
const enable_extended_flags: u32 = 0x0080; // allows mouse events
pub fn rawMode() u32 {
return enable_window_input | enable_mouse_input | enable_extended_flags;
}
};
const OutputMode = struct {
const enable_processed_output: u32 = 0x0001; // handle control sequences
const enable_virtual_terminal_processing: u32 = 0x0004; // handle ANSI sequences
const disable_newline_auto_return: u32 = 0x0008; // disable inserting a new line when we write at the last column
const enable_lvb_grid_worldwide: u32 = 0x0010; // enables reverse video and underline
fn rawMode() u32 {
return enable_processed_output |
enable_virtual_terminal_processing |
disable_newline_auto_return |
enable_lvb_grid_worldwide;
}
};
pub fn init() !Tty {
const stdin = try windows.GetStdHandle(windows.STD_INPUT_HANDLE);
const stdout = try windows.GetStdHandle(windows.STD_OUTPUT_HANDLE);
// get initial modes
var initial_input_mode: windows.DWORD = undefined;
var initial_output_mode: windows.DWORD = undefined;
const initial_output_codepage = windows.kernel32.GetConsoleOutputCP();
{
if (windows.kernel32.GetConsoleMode(stdin, &initial_input_mode) == 0) {
return windows.unexpectedError(windows.kernel32.GetLastError());
}
if (windows.kernel32.GetConsoleMode(stdout, &initial_output_mode) == 0) {
return windows.unexpectedError(windows.kernel32.GetLastError());
}
}
// set new modes
{
if (SetConsoleMode(stdin, InputMode.rawMode()) == 0)
return windows.unexpectedError(windows.kernel32.GetLastError());
if (SetConsoleMode(stdout, OutputMode.rawMode()) == 0)
return windows.unexpectedError(windows.kernel32.GetLastError());
if (windows.kernel32.SetConsoleOutputCP(utf8_codepage) == 0)
return windows.unexpectedError(windows.kernel32.GetLastError());
}
const self: Tty = .{
.stdin = stdin,
.stdout = stdout,
.initial_codepage = initial_output_codepage,
.initial_input_mode = initial_input_mode,
.initial_output_mode = initial_output_mode,
};
// save a copy of this tty as the global_tty for panic handling
global_tty = self;
return self;
}
pub fn deinit(self: Tty) void {
_ = windows.kernel32.SetConsoleOutputCP(self.initial_codepage);
_ = SetConsoleMode(self.stdin, self.initial_input_mode);
_ = SetConsoleMode(self.stdout, self.initial_output_mode);
windows.CloseHandle(self.stdin);
windows.CloseHandle(self.stdout);
}
pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize {
const self: *const Tty = @ptrCast(@alignCast(ptr));
return windows.WriteFile(self.stdout, bytes, null);
}
pub fn anyWriter(self: *const Tty) std.io.AnyWriter {
return .{
.context = self,
.writeFn = Tty.opaqueWrite,
};
}
pub fn bufferedWriter(self: *const Tty) std.io.BufferedWriter(4096, std.io.AnyWriter) {
return std.io.bufferedWriter(self.anyWriter());
}
pub fn nextEvent(self: *Tty, parser: *Parser, paste_allocator: ?std.mem.Allocator) !Event {
// We use a loop so we can ignore certain events
var state: EventState = .{};
while (true) {
var event_count: u32 = 0;
var input_record: INPUT_RECORD = undefined;
if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == 0)
return windows.unexpectedError(windows.kernel32.GetLastError());
if (try self.eventFromRecord(&input_record, &state, parser, paste_allocator)) |ev| {
return ev;
}
}
}
pub const EventState = struct {
ansi_buf: [128]u8 = undefined,
ansi_idx: usize = 0,
utf16_buf: [2]u16 = undefined,
utf16_half: bool = false,
};
pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventState, parser: *Parser, paste_allocator: ?std.mem.Allocator) !?Event {
switch (record.EventType) {
0x0001 => { // Key event
const event = record.Event.KeyEvent;
if (state.utf16_half) half: {
state.utf16_half = false;
state.utf16_buf[1] = event.uChar.UnicodeChar;
const codepoint: u21 = std.unicode.utf16DecodeSurrogatePair(&state.utf16_buf) catch break :half;
const n = std.unicode.utf8Encode(codepoint, &self.buf) catch return null;
const key: Key = .{
.codepoint = codepoint,
.base_layout_codepoint = codepoint,
.mods = translateMods(event.dwControlKeyState),
.text = self.buf[0..n],
};
switch (event.bKeyDown) {
0 => return .{ .key_release = key },
else => return .{ .key_press = key },
}
}
const base_layout: u16 = switch (event.wVirtualKeyCode) {
0x00 => blk: { // delivered when we get an escape sequence or a unicode codepoint
if (state.ansi_idx == 0 and event.uChar.AsciiChar != 27)
break :blk event.uChar.UnicodeChar;
state.ansi_buf[state.ansi_idx] = event.uChar.AsciiChar;
state.ansi_idx += 1;
if (state.ansi_idx <= 2) return null;
const result = try parser.parse(state.ansi_buf[0..state.ansi_idx], paste_allocator);
return if (result.n == 0) null else evt: {
state.ansi_idx = 0;
break :evt result.event;
};
},
0x08 => Key.backspace,
0x09 => Key.tab,
0x0D => Key.enter,
0x13 => Key.pause,
0x14 => Key.caps_lock,
0x1B => Key.escape,
0x20 => Key.space,
0x21 => Key.page_up,
0x22 => Key.page_down,
0x23 => Key.end,
0x24 => Key.home,
0x25 => Key.left,
0x26 => Key.up,
0x27 => Key.right,
0x28 => Key.down,
0x2c => Key.print_screen,
0x2d => Key.insert,
0x2e => Key.delete,
0x30...0x39 => |k| k,
0x41...0x5a => |k| k + 0x20, // translate to lowercase
0x5b => Key.left_meta,
0x5c => Key.right_meta,
0x60 => Key.kp_0,
0x61 => Key.kp_1,
0x62 => Key.kp_2,
0x63 => Key.kp_3,
0x64 => Key.kp_4,
0x65 => Key.kp_5,
0x66 => Key.kp_6,
0x67 => Key.kp_7,
0x68 => Key.kp_8,
0x69 => Key.kp_9,
0x6a => Key.kp_multiply,
0x6b => Key.kp_add,
0x6c => Key.kp_separator,
0x6d => Key.kp_subtract,
0x6e => Key.kp_decimal,
0x6f => Key.kp_divide,
0x70 => Key.f1,
0x71 => Key.f2,
0x72 => Key.f3,
0x73 => Key.f4,
0x74 => Key.f5,
0x75 => Key.f6,
0x76 => Key.f8,
0x77 => Key.f8,
0x78 => Key.f9,
0x79 => Key.f10,
0x7a => Key.f11,
0x7b => Key.f12,
0x7c => Key.f13,
0x7d => Key.f14,
0x7e => Key.f15,
0x7f => Key.f16,
0x80 => Key.f17,
0x81 => Key.f18,
0x82 => Key.f19,
0x83 => Key.f20,
0x84 => Key.f21,
0x85 => Key.f22,
0x86 => Key.f23,
0x87 => Key.f24,
0x90 => Key.num_lock,
0x91 => Key.scroll_lock,
0xa0 => Key.left_shift,
0xa1 => Key.right_shift,
0xa2 => Key.left_control,
0xa3 => Key.right_control,
0xa4 => Key.left_alt,
0xa5 => Key.right_alt,
0xad => Key.mute_volume,
0xae => Key.lower_volume,
0xaf => Key.raise_volume,
0xb0 => Key.media_track_next,
0xb1 => Key.media_track_previous,
0xb2 => Key.media_stop,
0xb3 => Key.media_play_pause,
0xba => ';',
0xbb => '+',
0xbc => ',',
0xbd => '-',
0xbe => '.',
0xbf => '/',
0xc0 => '`',
0xdb => '[',
0xdc => '\\',
0xdd => ']',
0xde => '\'',
else => return null,
};
if (std.unicode.utf16IsHighSurrogate(base_layout)) {
state.utf16_buf[0] = base_layout;
state.utf16_half = true;
return null;
}
if (std.unicode.utf16IsLowSurrogate(base_layout)) {
return null;
}
var codepoint: u21 = base_layout;
var text: ?[]const u8 = null;
switch (event.uChar.UnicodeChar) {
0x00...0x1F => {},
else => |cp| {
codepoint = cp;
const n = try std.unicode.utf8Encode(codepoint, &self.buf);
text = self.buf[0..n];
},
}
const key: Key = .{
.codepoint = codepoint,
.base_layout_codepoint = base_layout,
.mods = translateMods(event.dwControlKeyState),
.text = text,
};
switch (event.bKeyDown) {
0 => return .{ .key_release = key },
else => return .{ .key_press = key },
}
},
0x0002 => { // Mouse event
// see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str
const event = record.Event.MouseEvent;
// High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative
// is wheel_down
// Low word represents button state
const mouse_wheel_direction: i16 = blk: {
const wheelu32: u32 = event.dwButtonState >> 16;
const wheelu16: u16 = @truncate(wheelu32);
break :blk @bitCast(wheelu16);
};
const buttons: u16 = @truncate(event.dwButtonState);
// save the current state when we are done
defer self.last_mouse_button_press = buttons;
const button_xor = self.last_mouse_button_press ^ buttons;
var event_type: Mouse.Type = .press;
const btn: Mouse.Button = switch (button_xor) {
0x0000 => blk: {
// Check wheel event
if (event.dwEventFlags & 0x0004 > 0) {
if (mouse_wheel_direction > 0)
break :blk .wheel_up
else
break :blk .wheel_down;
}
// If we have no change but one of the buttons is still pressed we have a
// drag event. Find out which button is held down
if (buttons > 0 and event.dwEventFlags & 0x0001 > 0) {
event_type = .drag;
if (buttons & 0x0001 > 0) break :blk .left;
if (buttons & 0x0002 > 0) break :blk .right;
if (buttons & 0x0004 > 0) break :blk .middle;
if (buttons & 0x0008 > 0) break :blk .button_8;
if (buttons & 0x0010 > 0) break :blk .button_9;
}
if (event.dwEventFlags & 0x0001 > 0) event_type = .motion;
break :blk .none;
},
0x0001 => blk: {
if (buttons & 0x0001 == 0) event_type = .release;
break :blk .left;
},
0x0002 => blk: {
if (buttons & 0x0002 == 0) event_type = .release;
break :blk .right;
},
0x0004 => blk: {
if (buttons & 0x0004 == 0) event_type = .release;
break :blk .middle;
},
0x0008 => blk: {
if (buttons & 0x0008 == 0) event_type = .release;
break :blk .button_8;
},
0x0010 => blk: {
if (buttons & 0x0010 == 0) event_type = .release;
break :blk .button_9;
},
else => {
std.log.warn("unknown mouse event: {}", .{event});
return null;
},
};
const shift: u32 = 0x0010;
const alt: u32 = 0x0001 | 0x0002;
const ctrl: u32 = 0x0004 | 0x0008;
const mods: Mouse.Modifiers = .{
.shift = event.dwControlKeyState & shift > 0,
.alt = event.dwControlKeyState & alt > 0,
.ctrl = event.dwControlKeyState & ctrl > 0,
};
const mouse: Mouse = .{
.col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index
.row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index
.mods = mods,
.type = event_type,
.button = btn,
};
return .{ .mouse = mouse };
},
0x0004 => { // Screen resize events
// NOTE: Even though the event comes with a size, it may not be accurate. We ask for
// the size directly when we get this event
var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
if (windows.kernel32.GetConsoleScreenBufferInfo(self.stdout, &console_info) == 0) {
return windows.unexpectedError(windows.kernel32.GetLastError());
}
const window_rect = console_info.srWindow;
const width = window_rect.Right - window_rect.Left + 1;
const height = window_rect.Bottom - window_rect.Top + 1;
return .{
.winsize = .{
.cols = @intCast(width),
.rows = @intCast(height),
.x_pixel = 0,
.y_pixel = 0,
},
};
},
0x0010 => { // Focus events
switch (record.Event.FocusEvent.bSetFocus) {
0 => return .focus_out,
else => return .focus_in,
}
},
else => {},
}
return null;
}
fn translateMods(mods: u32) Key.Modifiers {
const left_alt: u32 = 0x0002;
const right_alt: u32 = 0x0001;
const left_ctrl: u32 = 0x0008;
const right_ctrl: u32 = 0x0004;
const caps: u32 = 0x0080;
const num_lock: u32 = 0x0020;
const shift: u32 = 0x0010;
const alt: u32 = left_alt | right_alt;
const ctrl: u32 = left_ctrl | right_ctrl;
return .{
.shift = mods & shift > 0,
.alt = mods & alt > 0,
.ctrl = mods & ctrl > 0,
.caps_lock = mods & caps > 0,
.num_lock = mods & num_lock > 0,
};
}
// From gitub.com/ziglibs/zig-windows-console. Thanks :)
//
// Events
const union_unnamed_248 = extern union {
UnicodeChar: windows.WCHAR,
AsciiChar: windows.CHAR,
};
pub const KEY_EVENT_RECORD = extern struct {
bKeyDown: windows.BOOL,
wRepeatCount: windows.WORD,
wVirtualKeyCode: windows.WORD,
wVirtualScanCode: windows.WORD,
uChar: union_unnamed_248,
dwControlKeyState: windows.DWORD,
};
pub const PKEY_EVENT_RECORD = *KEY_EVENT_RECORD;
pub const MOUSE_EVENT_RECORD = extern struct {
dwMousePosition: windows.COORD,
dwButtonState: windows.DWORD,
dwControlKeyState: windows.DWORD,
dwEventFlags: windows.DWORD,
};
pub const PMOUSE_EVENT_RECORD = *MOUSE_EVENT_RECORD;
pub const WINDOW_BUFFER_SIZE_RECORD = extern struct {
dwSize: windows.COORD,
};
pub const PWINDOW_BUFFER_SIZE_RECORD = *WINDOW_BUFFER_SIZE_RECORD;
pub const MENU_EVENT_RECORD = extern struct {
dwCommandId: windows.UINT,
};
pub const PMENU_EVENT_RECORD = *MENU_EVENT_RECORD;
pub const FOCUS_EVENT_RECORD = extern struct {
bSetFocus: windows.BOOL,
};
pub const PFOCUS_EVENT_RECORD = *FOCUS_EVENT_RECORD;
const union_unnamed_249 = extern union {
KeyEvent: KEY_EVENT_RECORD,
MouseEvent: MOUSE_EVENT_RECORD,
WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD,
MenuEvent: MENU_EVENT_RECORD,
FocusEvent: FOCUS_EVENT_RECORD,
};
pub const INPUT_RECORD = extern struct {
EventType: windows.WORD,
Event: union_unnamed_249,
};
pub const PINPUT_RECORD = *INPUT_RECORD;
pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(windows.WINAPI) windows.BOOL;
// TODO: remove this in zig 0.13.0
pub extern "kernel32" fn SetConsoleMode(in_hConsoleHandle: windows.HANDLE, in_dwMode: windows.DWORD) callconv(windows.WINAPI) windows.BOOL;

271
deps/libvaxis/src/xev.zig vendored Normal file
View File

@ -0,0 +1,271 @@
const std = @import("std");
const xev = @import("xev");
const Tty = @import("main.zig").Tty;
const Winsize = @import("main.zig").Winsize;
const Vaxis = @import("Vaxis.zig");
const Parser = @import("Parser.zig");
const Key = @import("Key.zig");
const Mouse = @import("Mouse.zig");
const Color = @import("Cell.zig").Color;
const log = std.log.scoped(.vaxis_xev);
pub const Event = union(enum) {
key_press: Key,
key_release: Key,
mouse: Mouse,
focus_in,
focus_out,
paste_start, // bracketed paste start
paste_end, // bracketed paste end
paste: []const u8, // osc 52 paste, caller must free
color_report: Color.Report, // osc 4, 10, 11, 12 response
color_scheme: Color.Scheme,
winsize: Winsize,
};
pub fn TtyWatcher(comptime Userdata: type) type {
return struct {
const Self = @This();
file: xev.File,
tty: *Tty,
read_buf: [4096]u8,
read_buf_start: usize,
read_cmp: xev.Completion,
winsize_wakeup: xev.Async,
winsize_cmp: xev.Completion,
callback: *const fn (
ud: ?*Userdata,
loop: *xev.Loop,
watcher: *Self,
event: Event,
) xev.CallbackAction,
ud: ?*Userdata,
vx: *Vaxis,
parser: Parser,
pub fn init(
self: *Self,
tty: *Tty,
vaxis: *Vaxis,
loop: *xev.Loop,
userdata: ?*Userdata,
callback: *const fn (
ud: ?*Userdata,
loop: *xev.Loop,
watcher: *Self,
event: Event,
) xev.CallbackAction,
) !void {
self.* = .{
.tty = tty,
.file = xev.File.initFd(tty.fd),
.read_buf = undefined,
.read_buf_start = 0,
.read_cmp = .{},
.winsize_wakeup = try xev.Async.init(),
.winsize_cmp = .{},
.callback = callback,
.ud = userdata,
.vx = vaxis,
.parser = .{ .grapheme_data = &vaxis.unicode.grapheme_data },
};
self.file.read(
loop,
&self.read_cmp,
.{ .slice = &self.read_buf },
Self,
self,
Self.ttyReadCallback,
);
self.winsize_wakeup.wait(
loop,
&self.winsize_cmp,
Self,
self,
winsizeCallback,
);
const handler: Tty.SignalHandler = .{
.context = self,
.callback = Self.signalCallback,
};
try Tty.notifyWinsize(handler);
}
fn signalCallback(ptr: *anyopaque) void {
const self: *Self = @ptrCast(@alignCast(ptr));
self.winsize_wakeup.notify() catch |err| {
log.warn("couldn't wake up winsize callback: {}", .{err});
};
}
fn ttyReadCallback(
ud: ?*Self,
loop: *xev.Loop,
c: *xev.Completion,
_: xev.File,
buf: xev.ReadBuffer,
r: xev.ReadError!usize,
) xev.CallbackAction {
const n = r catch |err| {
log.err("read error: {}", .{err});
return .disarm;
};
const self = ud orelse unreachable;
// reset read start state
self.read_buf_start = 0;
var seq_start: usize = 0;
parse_loop: while (seq_start < n) {
const result = self.parser.parse(buf.slice[seq_start..n], null) catch |err| {
log.err("couldn't parse input: {}", .{err});
return .disarm;
};
if (result.n == 0) {
// copy the read to the beginning. We don't use memcpy because
// this could be overlapping, and it's also rare
const initial_start = seq_start;
while (seq_start < n) : (seq_start += 1) {
self.read_buf[seq_start - initial_start] = self.read_buf[seq_start];
}
self.read_buf_start = seq_start - initial_start + 1;
return .rearm;
}
seq_start += n;
const event_inner = result.event orelse {
log.debug("unknown event: {s}", .{self.read_buf[seq_start - n + 1 .. seq_start]});
continue :parse_loop;
};
// Capture events we want to bubble up
const event: ?Event = switch (event_inner) {
.key_press => |key| .{ .key_press = key },
.key_release => |key| .{ .key_release = key },
.mouse => |mouse| .{ .mouse = mouse },
.focus_in => .focus_in,
.focus_out => .focus_out,
.paste_start => .paste_start,
.paste_end => .paste_end,
.paste => |paste| .{ .paste = paste },
.color_report => |report| .{ .color_report = report },
.color_scheme => |scheme| .{ .color_scheme = scheme },
.winsize => |ws| .{ .winsize = ws },
// capability events which we handle below
.cap_kitty_keyboard,
.cap_kitty_graphics,
.cap_rgb,
.cap_unicode,
.cap_sgr_pixels,
.cap_color_scheme_updates,
.cap_da1,
=> null, // handled below
};
if (event) |ev| {
const action = self.callback(self.ud, loop, self, ev);
switch (action) {
.disarm => return .disarm,
else => continue :parse_loop,
}
}
switch (event_inner) {
.key_press,
.key_release,
.mouse,
.focus_in,
.focus_out,
.paste_start,
.paste_end,
.paste,
.color_report,
.color_scheme,
.winsize,
=> unreachable, // handled above
.cap_kitty_keyboard => {
log.info("kitty keyboard capability detected", .{});
self.vx.caps.kitty_keyboard = true;
},
.cap_kitty_graphics => {
if (!self.vx.caps.kitty_graphics) {
log.info("kitty graphics capability detected", .{});
self.vx.caps.kitty_graphics = true;
}
},
.cap_rgb => {
log.info("rgb capability detected", .{});
self.vx.caps.rgb = true;
},
.cap_unicode => {
log.info("unicode capability detected", .{});
self.vx.caps.unicode = .unicode;
self.vx.screen.width_method = .unicode;
},
.cap_sgr_pixels => {
log.info("pixel mouse capability detected", .{});
self.vx.caps.sgr_pixels = true;
},
.cap_color_scheme_updates => {
log.info("color_scheme_updates capability detected", .{});
self.vx.caps.color_scheme_updates = true;
},
.cap_da1 => {
self.vx.enableDetectedFeatures(self.tty.anyWriter()) catch |err| {
log.err("couldn't enable features: {}", .{err});
};
},
}
}
self.file.read(
loop,
c,
.{ .slice = &self.read_buf },
Self,
self,
Self.ttyReadCallback,
);
return .disarm;
}
fn winsizeCallback(
ud: ?*Self,
l: *xev.Loop,
c: *xev.Completion,
r: xev.Async.WaitError!void,
) xev.CallbackAction {
_ = r catch |err| {
log.err("async error: {}", .{err});
return .disarm;
};
const self = ud orelse unreachable; // no userdata
const winsize = Tty.getWinsize(self.tty.fd) catch |err| {
log.err("couldn't get winsize: {}", .{err});
return .disarm;
};
const ret = self.callback(self.ud, l, self, .{ .winsize = winsize });
if (ret == .disarm) return .disarm;
self.winsize_wakeup.wait(
l,
c,
Self,
self,
winsizeCallback,
);
return .disarm;
}
};
}

21
deps/libxev/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Mitchell Hashimoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

315
deps/libxev/README.md vendored Normal file
View File

@ -0,0 +1,315 @@
# libxev
libxev is a cross-platform event loop. libxev provides a unified event loop
abstraction for non-blocking IO, timers, signals, events, and more that
works on macOS, Windows, Linux, and WebAssembly (browser and WASI). It is
written in [Zig](https://ziglang.org/) but exports a C-compatible API (which
further makes it compatible with any language out there that can communicate
with C APIs).
**Project Status: 🐲 Unstable, alpha-ish quality.** The feature list is quite
good across multiple platforms, but there are plenty of missing features.
The project hasn't been well tested in real-world environments and there
are lots of low-hanging fruit for performance optimization. I'm not promising
any API compatibility at this point, either. If you want a production ready,
high quality, generalized event loop implementation check out
[libuv](https://libuv.org/), libev, etc.
**Why a new event loop library?** A few reasons. One, I think Zig lacks
a generalized event loop comparable to libuv in features ("generalized"
being a key word here). Two, I wanted to build a library like this around
the design patterns of [io_uring](https://unixism.net/loti/what_is_io_uring.html),
even mimicking its style on top of other OS primitives (
[credit to this awesome blog post](https://tigerbeetle.com/blog/a-friendly-abstraction-over-iouring-and-kqueue/)).
Three, I wanted an event loop library that could build to WebAssembly
(both WASI and freestanding) and that didn't really fit well
into the goals of API style of existing libraries without bringing in
something super heavy like Emscripten. The motivation for this library
primarily though is scratching my own itch!
## Features
**Cross-platform.** Linux (`io_uring` and `epoll`), macOS (`kqueue`),
WebAssembly + WASI (`poll_oneoff`, threaded and non-threaded runtimes).
(Windows support is planned and coming soon)
**[Proactor API](https://en.wikipedia.org/wiki/Proactor_pattern).** Work
is submitted to the libxev event loop and the caller is notified of
work _completion_, as opposed to work _readiness_.
**Zero runtime allocations.** This helps make runtime performance more
predictable and makes libxev well suited for embedded environments.
**Timers, TCP, UDP, Files, Processes.** High-level platform-agnostic APIs for
interacting with timers, TCP/UDP sockets, files, processes, and more. For
platforms that don't support async IO, the file operations are automatically
scheduled to a thread pool.
**Generic Thread Pool (Optional).** You can create a generic thread pool,
configure its resource utilization, and use this to perform custom background
tasks. The thread pool is used by some backends to do non-blocking tasks that
don't have reliable non-blocking APIs (such as local file operations with
`kqueue`). The thread pool can be shared across multiple threads and event
loops to optimize resource utilization.
**Low-level and High-Level API.** The high-level API is platform-agnostic
but has some opinionated behavior and limited flexibility. The high-level
API is recommended but the low-level API is always an available escape hatch.
The low-level API is platform-specific and provides a mechanism for libxev
users to squeeze out maximum performance. The low-level API is _just enough
abstraction_ above the OS interface to make it easier to use without
sacrificing noticable performance.
**Tree Shaking (Zig).** This is a feature of Zig, but substantially benefits
libraries such as libxev. Zig will only include function calls and features
that you actually use. If you don't use a particular kind of high-level
watcher (such as UDP sockets), then the functionality related to that
abstraction is not compiled into your final binary at all. This lets libxev
support optional "nice-to-have" functionality that may be considered
"bloat" in some cases, but the end user doesn't have to pay for it.
**Dependency-free.** libxev has no dependencies other than the built-in
OS APIs at runtime. The C library depends on libc. This makes it very
easy to cross-compile.
### Roadmap
There are plenty of missing features that I still want to add:
* Pipe high-level API
* Signal handlers
* Filesystem events
* Windows backend
* Freestanding WebAssembly support via an external event loop (i.e. the browser)
And more...
### Performance
There is plenty of room for performance improvements, and I want to be
fully clear that I haven't done a lot of optimization work. Still,
performance is looking good. I've tried to port many of
[libuv benchmarks](https://github.com/libuv/libuv) to use the libxev
API.
I won't post specific benchmark results until I have a better
environment to run them in. As a _very broad generalization_,
you shouldn't notice a slowdown using libxev compared to other
major event loops. This may differ on a feature-by-feature basis, and
if you can show really poor performance in an issue I'm interested
in resolving it!
## Example
The example below shows an identical program written in Zig and in C
that uses libxev to run a single 5s timer. This is almost silly how
simple it is but is meant to just convey the overall feel of the library
rather than a practical use case.
<table>
<tr>
<td> Zig </td> <td> C </td>
</tr>
<tr>
<td>
```zig
const xev = @import("xev");
pub fn main() !void {
var loop = try xev.Loop.init(.{});
defer loop.deinit();
const w = try xev.Timer.init();
defer w.deinit();
// 5s timer
var c: xev.Completion = undefined;
w.run(&loop, &c, 5000, void, null, &timerCallback);
try loop.run(.until_done);
}
fn timerCallback(
userdata: ?*void,
loop: *xev.Loop,
c: *xev.Completion,
result: xev.Timer.RunError!void,
) xev.CallbackAction {
_ = userdata;
_ = loop;
_ = c;
_ = result catch unreachable;
return .disarm;
}
```
</td>
<td>
```zig
#include <stddef.h>
#include <stdio.h>
#include <xev.h>
xev_cb_action timerCallback(xev_loop* loop, xev_completion* c, int result, void *userdata) {
return XEV_DISARM;
}
int main(void) {
xev_loop loop;
if (xev_loop_init(&loop) != 0) {
printf("xev_loop_init failure\n");
return 1;
}
xev_watcher w;
if (xev_timer_init(&w) != 0) {
printf("xev_timer_init failure\n");
return 1;
}
xev_completion c;
xev_timer_run(&w, &loop, &c, 5000, NULL, &timerCallback);
xev_loop_run(&loop, XEV_RUN_UNTIL_DONE);
xev_timer_deinit(&w);
xev_loop_deinit(&loop);
return 0;
}
```
</td>
</tr>
</table>
## Installation (Zig)
**These instructions are for Zig downstream users only.** If you are
using the C API to libxev, see the "Build" section.
This package works with the Zig package manager introduced in Zig 0.11.
Create a `build.zig.zon` file like this:
```zig
.{
.name = "my-project",
.version = "0.0.0",
.dependencies = .{
.libxev = .{
.url = "https://github.com/mitchellh/libxev/archive/<git-ref-here>.tar.gz",
.hash = "12208070233b17de6be05e32af096a6760682b48598323234824def41789e993432c",
},
},
}
```
And in your `build.zig`:
```zig
const xev = b.dependency("libxev", .{ .target = target, .optimize = optimize });
exe.addModule("xev", xev.module("xev"));
```
## Documentation
🚧 Documentation is a work-in-progress. 🚧
Currently, documentation is available in three forms: **man pages**,
**examples**, and **code comments.** In the future, I plan on writing detailed
guides and API documentation in website form, but that isn't currently
available.
### Man Pages
The man pages are relatively detailed! `xev(7)` will
give you a good overview of the entire library. `xev-zig(7)` and
`xev-c(7)` will provide overviews of the Zig and C API, respectively.
From there, API-specifc man pages such as `xev_loop_init(3)` are
available. This is the best documentation currently.
There are multiple ways to browse the man pages. The most immediately friendly
is to just browse the raw man page sources in the `docs/` directory in
your web browser. The man page source is a _markdown-like_ syntax so it
renders _okay_ in your browser via GitHub.
Another approach is to run `zig build -Dman-pages` and the man pages
will be available in `zig-out`. This requires
[scdoc](https://git.sr.ht/~sircmpwn/scdoc)
to be installed (this is available in most package managers).
Once you've built the man pages, you can render them by path:
```
$ man zig-out/share/man/man7/xev.7
```
And the final approach is to install libxev via your favorite package
manager (if and when available), which should hopefully put your man pages
into your man path, so you can just do `man 7 xev`.
### Examples
There are examples available in the `examples/` folder. The examples are
available in both C and Zig, and you can tell which one is which using
the file extension.
To build an example, use the following:
```
$ zig build -Dexample-name=_basic.zig
...
$ zig-out/bin/example-basic
...
```
The `-Dexample-name` value should be the filename including the extension.
### Code Comments
The Zig code is well commented. If you're comfortable reading code comments
you can find a lot of insight within them. The source is in the `src/`
directory.
# Build
Build requires the installation of the latest [Zig nightly](https://ziglang.org/download/).
**libxev has no other build dependencies.**
Once installed, `zig build install` on its own will build the full library and output
a [FHS-compatible](https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard)
directory in `zig-out`. You can customize the output directory with the
`--prefix` flag.
## Tests
libxev has a large and growing test suite. To run the tests for the current
platform:
```sh
$ zig build test
...
```
This will run all the tests for all the supported features for the current
host platform. For example, on Linux this will run both the full io_uring
and epoll test suite.
**You can build and run tests for other platforms** by cross-compiling the
test executable, copying it to a target machine and executing it. For example,
the below shows how to cross-compile and build the tests for macOS from Linux:
```sh
$ zig build -Dtarget=aarch64-macos -Dinstall-tests
...
$ file zig-out/bin/xev-test
zig-out/bin/xev-test: Mach-O 64-bit arm64 executable
```
**WASI is a special-case.** You can run tests for WASI if you have
[wasmtime](https://wasmtime.dev/) installed:
```
$ zig build test -Dtarget=wasm32-wasi -Dwasmtime
...
```

348
deps/libxev/build.zig vendored Normal file
View File

@ -0,0 +1,348 @@
const std = @import("std");
const CompileStep = std.build.Step.Compile;
const ScdocStep = @import("src/build/ScdocStep.zig");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
_ = b.addModule("xev", .{ .root_source_file = b.path("src/main.zig") });
const man_pages = b.option(
bool,
"man-pages",
"Set to true to build man pages. Requires scdoc. Defaults to true if scdoc is found.",
) orelse if (b.findProgram(&[_][]const u8{"scdoc"}, &[_][]const u8{})) |_|
true
else |err| switch (err) {
error.FileNotFound => false,
else => return err,
};
const bench_name = b.option(
[]const u8,
"bench-name",
"Build and install a single benchmark",
);
const bench_install = b.option(
bool,
"bench",
"Install the benchmark binaries to zig-out/bench",
) orelse (bench_name != null);
const example_name = b.option(
[]const u8,
"example-name",
"Build and install a single example",
);
const example_install = b.option(
bool,
"example",
"Install the example binaries to zig-out/example",
) orelse (example_name != null);
const test_install = b.option(
bool,
"install-tests",
"Install the test binaries into zig-out",
) orelse false;
// Our tests require libc on Linux and Mac. Note that libxev itself
// does NOT require libc.
const test_libc = switch (target.result.os.tag) {
.linux, .macos => true,
else => false,
};
// We always build our test exe as part of `zig build` so that
// we can easily run it manually without digging through the cache.
const test_exe = b.addTest(.{
.name = "xev-test",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
if (test_libc) test_exe.linkLibC(); // Tests depend on libc, libxev does not
if (test_install) b.installArtifact(test_exe);
// zig build test test binary and runner.
const tests_run = b.addRunArtifact(test_exe);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&tests_run.step);
// Static C lib
const static_c_lib: ?*std.Build.Step.Compile = if (target.result.os.tag != .wasi) lib: {
const static_lib = b.addStaticLibrary(.{
.name = "xev",
.root_source_file = b.path("src/c_api.zig"),
.target = target,
.optimize = optimize,
});
static_lib.linkLibC();
// Link required libraries if targeting Windows
if (target.result.os.tag == .windows) {
static_lib.linkSystemLibrary("ws2_32");
static_lib.linkSystemLibrary("mswsock");
}
b.installArtifact(static_lib);
b.default_step.dependOn(&static_lib.step);
const static_binding_test = b.addExecutable(.{
.name = "static-binding-test",
.target = target,
.optimize = optimize,
});
static_binding_test.linkLibC();
static_binding_test.addIncludePath(b.path("include"));
static_binding_test.addCSourceFile(.{
.file = b.path("examples/_basic.c"),
.flags = &[_][]const u8{ "-Wall", "-Wextra", "-pedantic", "-std=c99", "-D_POSIX_C_SOURCE=199309L" },
});
static_binding_test.linkLibrary(static_lib);
if (test_install) b.installArtifact(static_binding_test);
const static_binding_test_run = b.addRunArtifact(static_binding_test);
test_step.dependOn(&static_binding_test_run.step);
break :lib static_lib;
} else null;
// Dynamic C lib. We only build this if this is the native target so we
// can link to libxml2 on our native system.
if (target.query.isNative()) {
const dynamic_lib_name = "xev";
const dynamic_lib = b.addSharedLibrary(.{
.name = dynamic_lib_name,
.root_source_file = b.path("src/c_api.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(dynamic_lib);
b.default_step.dependOn(&dynamic_lib.step);
const dynamic_binding_test = b.addExecutable(.{
.name = "dynamic-binding-test",
.target = target,
.optimize = optimize,
});
dynamic_binding_test.linkLibC();
dynamic_binding_test.addIncludePath(b.path("include"));
dynamic_binding_test.addCSourceFile(.{
.file = b.path("examples/_basic.c"),
.flags = &[_][]const u8{ "-Wall", "-Wextra", "-pedantic", "-std=c99" },
});
dynamic_binding_test.linkLibrary(dynamic_lib);
if (test_install) b.installArtifact(dynamic_binding_test);
const dynamic_binding_test_run = b.addRunArtifact(dynamic_binding_test);
test_step.dependOn(&dynamic_binding_test_run.step);
}
// C Headers
const c_header = b.addInstallFileWithDir(
b.path("include/xev.h"),
.header,
"xev.h",
);
b.getInstallStep().dependOn(&c_header.step);
// pkg-config
{
const file = try b.cache_root.join(b.allocator, &[_][]const u8{"libxev.pc"});
const pkgconfig_file = try std.fs.cwd().createFile(file, .{});
const writer = pkgconfig_file.writer();
try writer.print(
\\prefix={s}
\\includedir=${{prefix}}/include
\\libdir=${{prefix}}/lib
\\
\\Name: libxev
\\URL: https://github.com/mitchellh/libxev
\\Description: High-performance, cross-platform event loop
\\Version: 0.1.0
\\Cflags: -I${{includedir}}
\\Libs: -L${{libdir}} -lxev
, .{b.install_prefix});
defer pkgconfig_file.close();
b.getInstallStep().dependOn(&b.addInstallFileWithDir(
.{ .cwd_relative = file },
.prefix,
"share/pkgconfig/libxev.pc",
).step);
}
// Benchmarks
_ = try benchTargets(b, target, optimize, bench_install, bench_name);
// Examples
_ = try exampleTargets(b, target, optimize, static_c_lib, example_install, example_name);
// Man pages
if (man_pages) {
const scdoc_step = ScdocStep.create(b);
try scdoc_step.install();
}
}
fn benchTargets(
b: *std.Build,
target: std.Build.ResolvedTarget,
mode: std.builtin.OptimizeMode,
install: bool,
install_name: ?[]const u8,
) !std.StringHashMap(*std.Build.Step.Compile) {
_ = mode;
var map = std.StringHashMap(*std.Build.Step.Compile).init(b.allocator);
// Open the directory
const c_dir_path = "src/bench";
var c_dir = try std.fs.cwd().openDir(comptime thisDir() ++ "/" ++ c_dir_path, .{ .iterate = true });
defer c_dir.close();
// Go through and add each as a step
var c_dir_it = c_dir.iterate();
while (try c_dir_it.next()) |entry| {
// Get the index of the last '.' so we can strip the extension.
const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue;
if (index == 0) continue;
// Name of the app and full path to the entrypoint.
const name = entry.name[0..index];
const path = try std.fs.path.join(b.allocator, &[_][]const u8{
c_dir_path,
entry.name,
});
// If we have specified a specific name, only install that one.
if (install_name) |n| {
if (!std.mem.eql(u8, n, name)) continue;
}
// Executable builder.
const c_exe = b.addExecutable(.{
.name = name,
.root_source_file = b.path(path),
.target = target,
.optimize = .ReleaseFast, // benchmarks are always release fast
});
c_exe.root_module.addImport("xev", b.modules.get("xev").?);
if (install) {
const install_step = b.addInstallArtifact(c_exe, .{
.dest_dir = .{ .override = .{ .custom = "bench" } },
});
b.getInstallStep().dependOn(&install_step.step);
}
// Store the mapping
try map.put(try b.allocator.dupe(u8, name), c_exe);
}
return map;
}
fn exampleTargets(
b: *std.Build,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
c_lib_: ?*std.Build.Step.Compile,
install: bool,
install_name: ?[]const u8,
) !void {
// Ignore if we're not installing
if (!install) return;
// Open the directory
const c_dir_path = (comptime thisDir()) ++ "/examples";
var c_dir = try std.fs.cwd().openDir(c_dir_path, .{ .iterate = true });
defer c_dir.close();
// Go through and add each as a step
var c_dir_it = c_dir.iterate();
while (try c_dir_it.next()) |entry| {
// Get the index of the last '.' so we can strip the extension.
const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue;
if (index == 0) continue;
// If we have specified a specific name, only install that one.
if (install_name) |n| {
if (!std.mem.eql(u8, n, entry.name)) continue;
}
// Name of the app and full path to the entrypoint.
const name = entry.name[0..index];
const path = try std.fs.path.join(b.allocator, &[_][]const u8{
c_dir_path,
entry.name,
});
const is_zig = std.mem.eql(u8, entry.name[index + 1 ..], "zig");
if (is_zig) {
const c_exe = b.addExecutable(.{
.name = name,
.root_source_file = .{ .cwd_relative = path },
.target = target,
.optimize = optimize,
});
c_exe.root_module.addImport("xev", b.modules.get("xev").?);
if (install) {
const install_step = b.addInstallArtifact(c_exe, .{
.dest_dir = .{ .override = .{ .custom = "example" } },
});
b.getInstallStep().dependOn(&install_step.step);
}
} else {
const c_lib = c_lib_ orelse return error.UnsupportedPlatform;
const c_exe = b.addExecutable(.{
.name = name,
.target = target,
.optimize = optimize,
});
c_exe.linkLibC();
c_exe.addIncludePath(b.path("include"));
c_exe.addCSourceFile(.{
.file = .{ .cwd_relative = path },
.flags = &[_][]const u8{
"-Wall",
"-Wextra",
"-pedantic",
"-std=c99",
"-D_POSIX_C_SOURCE=199309L",
},
});
c_exe.linkLibrary(c_lib);
if (install) {
const install_step = b.addInstallArtifact(c_exe, .{
.dest_dir = .{ .override = .{ .custom = "example" } },
});
b.getInstallStep().dependOn(&install_step.step);
}
}
// If we have specified a specific name, only install that one.
if (install_name) |_| break;
} else {
if (install_name) |n| {
std.debug.print("No example file named: {s}\n", .{n});
std.debug.print("Choices:\n", .{});
var c_dir_it2 = c_dir.iterate();
while (try c_dir_it2.next()) |entry| {
std.debug.print("\t{s}\n", .{entry.name});
}
return error.InvalidExampleName;
}
}
}
/// Path to the directory with the build.zig.
fn thisDir() []const u8 {
return std.fs.path.dirname(@src().file) orelse unreachable;
}

6
deps/libxev/build.zig.zon vendored Normal file
View File

@ -0,0 +1,6 @@
.{
.name = "libxev",
.minimum_zig_version = "0.12.0-dev.3191+9cf28d1e9",
.paths = .{""},
.version = "0.0.0",
}

103
deps/libxev/include/xev.h vendored Normal file
View File

@ -0,0 +1,103 @@
#ifndef XEV_H
#define XEV_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
/* TODO(mitchellh): we should use platform detection to set the correct
* byte sizes here. We choose some overly large values for now so that
* we can retain ABI compatibility. */
const size_t XEV_SIZEOF_LOOP = 512;
const size_t XEV_SIZEOF_COMPLETION = 320;
const size_t XEV_SIZEOF_WATCHER = 256;
const size_t XEV_SIZEOF_THREADPOOL = 64;
const size_t XEV_SIZEOF_THREADPOOL_BATCH = 24;
const size_t XEV_SIZEOF_THREADPOOL_TASK = 24;
const size_t XEV_SIZEOF_THREADPOOL_CONFIG = 64;
#if __STDC_VERSION__ >= 201112L || __cplusplus >= 201103L
typedef max_align_t XEV_ALIGN_T;
#else
// max_align_t is usually synonymous with the largest scalar type, which is long double on most platforms, and its alignment requirement is either 8 or 16.
typedef long double XEV_ALIGN_T;
#endif
/* There's a ton of preprocessor directives missing here for real cross-platform
* compatibility. I'm going to defer to the community or future issues to help
* plug those holes. For now, we get some stuff working we can test! */
/* Opaque types. These types have a size defined so that they can be
* statically allocated but they are not to be accessed. */
// todo: give struct individual alignment, instead of max alignment
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_LOOP - sizeof(XEV_ALIGN_T)]; } xev_loop;
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_COMPLETION - sizeof(XEV_ALIGN_T)]; } xev_completion;
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_WATCHER - sizeof(XEV_ALIGN_T)]; } xev_watcher;
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL - sizeof(XEV_ALIGN_T)]; } xev_threadpool;
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL_BATCH - sizeof(XEV_ALIGN_T)]; } xev_threadpool_batch;
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL_TASK - sizeof(XEV_ALIGN_T)]; } xev_threadpool_task;
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL_CONFIG - sizeof(XEV_ALIGN_T)]; } xev_threadpool_config;
/* Callback types. */
typedef enum { XEV_DISARM = 0, XEV_REARM = 1 } xev_cb_action;
typedef void (*xev_task_cb)(xev_threadpool_task* t);
typedef xev_cb_action (*xev_timer_cb)(xev_loop* l, xev_completion* c, int result, void* userdata);
typedef xev_cb_action (*xev_async_cb)(xev_loop* l, xev_completion* c, int result, void* userdata);
typedef enum {
XEV_RUN_NO_WAIT = 0,
XEV_RUN_ONCE = 1,
XEV_RUN_UNTIL_DONE = 2,
} xev_run_mode_t;
typedef enum {
XEV_COMPLETION_DEAD = 0,
XEV_COMPLETION_ACTIVE = 1,
} xev_completion_state_t;
/* Documentation for functions can be found in man pages or online. I
* purposely do not add docs to the header so that you can quickly scan
* all exported functions. */
int xev_loop_init(xev_loop* loop);
void xev_loop_deinit(xev_loop* loop);
int xev_loop_run(xev_loop* loop, xev_run_mode_t mode);
int64_t xev_loop_now(xev_loop* loop);
void xev_loop_update_now(xev_loop* loop);
void xev_completion_zero(xev_completion* c);
xev_completion_state_t xev_completion_state(xev_completion* c);
void xev_threadpool_config_init(xev_threadpool_config* config);
void xev_threadpool_config_set_stack_size(xev_threadpool_config* config, uint32_t v);
void xev_threadpool_config_set_max_threads(xev_threadpool_config* config, uint32_t v);
int xev_threadpool_init(xev_threadpool* pool, xev_threadpool_config* config);
void xev_threadpool_deinit(xev_threadpool* pool);
void xev_threadpool_shutdown(xev_threadpool* pool);
void xev_threadpool_schedule(xev_threadpool* pool, xev_threadpool_batch *batch);
void xev_threadpool_task_init(xev_threadpool_task* t, xev_task_cb cb);
void xev_threadpool_batch_init(xev_threadpool_batch* b);
void xev_threadpool_batch_push_task(xev_threadpool_batch* b, xev_threadpool_task *t);
void xev_threadpool_batch_push_batch(xev_threadpool_batch* b, xev_threadpool_batch *other);
int xev_timer_init(xev_watcher *w);
void xev_timer_deinit(xev_watcher *w);
void xev_timer_run(xev_watcher *w, xev_loop* loop, xev_completion* c, uint64_t next_ms, void* userdata, xev_timer_cb cb);
void xev_timer_reset(xev_watcher *w, xev_loop* loop, xev_completion* c, xev_completion *c_cancel, uint64_t next_ms, void* userdata, xev_timer_cb cb);
void xev_timer_cancel(xev_watcher *w, xev_loop* loop, xev_completion* c, xev_completion* c_cancel, void* userdata, xev_timer_cb cb);
int xev_async_init(xev_watcher *w);
void xev_async_deinit(xev_watcher *w);
int xev_async_notify(xev_watcher *w);
void xev_async_wait(xev_watcher *w, xev_loop* loop, xev_completion* c, void* userdata, xev_async_cb cb);
#ifdef __cplusplus
}
#endif
#endif /* XEV_H */

829
deps/libxev/src/ThreadPool.zig vendored Normal file
View File

@ -0,0 +1,829 @@
//! Thread pool copied almost directly from Zap[1]. In @kprotty's own words:
//! lock-free, allocation-free* (excluding spawning threads), supports batch
//! scheduling, and dynamically spawns threads while handling thread spawn
//! failure. I highly recommend reading @kprotty's incredible blog post[2] on
//! this topic.
//!
//! The original file in Zap is licensed under the MIT license, and the
//! license and copyright is reproduced below. The libxev project is also
//! MIT licensed so the entire project (including this file) are equally
//! licensed. This is just a convenience note for any OSS users, contributors,
//! etc.
//!
//! MIT License
//!
//! Copyright (c) 2021 kprotty
//!
//! Permission is hereby granted, free of charge, to any person obtaining a copy
//! of this software and associated documentation files (the "Software"), to deal
//! in the Software without restriction, including without limitation the rights
//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//! copies of the Software, and to permit persons to whom the Software is
//! furnished to do so, subject to the following conditions:
//!
//! The above copyright notice and this permission notice shall be included in all
//! copies or substantial portions of the Software.
//!
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//! SOFTWARE.
//!
//! [1]: https://github.com/kprotty/zap
//! [2]: https://zig.news/kprotty/resource-efficient-thread-pools-with-zig-3291
const ThreadPool = @This();
const std = @import("std");
const assert = std.debug.assert;
const Atomic = std.atomic.Value;
stack_size: u32,
max_threads: u32,
sync: Atomic(u32) = Atomic(u32).init(@bitCast(Sync{})),
idle_event: Event = .{},
join_event: Event = .{},
run_queue: Node.Queue = .{},
threads: Atomic(?*Thread) = Atomic(?*Thread).init(null),
const Sync = packed struct {
/// Tracks the number of threads not searching for Tasks
idle: u14 = 0,
/// Tracks the number of threads spawned
spawned: u14 = 0,
/// What you see is what you get
unused: bool = false,
/// Used to not miss notifications while state = waking
notified: bool = false,
/// The current state of the thread pool
state: enum(u2) {
/// A notification can be issued to wake up a sleeping as the "waking thread".
pending = 0,
/// The state was notifiied with a signal. A thread is woken up.
/// The first thread to transition to `waking` becomes the "waking thread".
signaled,
/// There is a "waking thread" among us.
/// No other thread should be woken up until the waking thread transitions the state.
waking,
/// The thread pool was terminated. Start decremented `spawned` so that it can be joined.
shutdown,
} = .pending,
};
/// Configuration options for the thread pool.
/// TODO: add CPU core affinity?
pub const Config = struct {
stack_size: u32 = (std.Thread.SpawnConfig{}).stack_size,
max_threads: u32 = 0,
};
/// Statically initialize the thread pool using the configuration.
pub fn init(config: Config) ThreadPool {
return .{
.stack_size = @max(1, config.stack_size),
.max_threads = if (config.max_threads > 0)
config.max_threads
else
@intCast(std.Thread.getCpuCount() catch 1),
};
}
/// Wait for a thread to call shutdown() on the thread pool and kill the worker threads.
pub fn deinit(self: *ThreadPool) void {
self.join();
self.* = undefined;
}
/// A Task represents the unit of Work / Job / Execution that the ThreadPool schedules.
/// The user provides a `callback` which is invoked when the *Task can run on a thread.
pub const Task = struct {
node: Node = .{},
callback: *const fn (*Task) void,
};
/// An unordered collection of Tasks which can be submitted for scheduling as a group.
pub const Batch = struct {
len: usize = 0,
head: ?*Task = null,
tail: ?*Task = null,
/// Create a batch from a single task.
pub fn from(task: *Task) Batch {
return Batch{
.len = 1,
.head = task,
.tail = task,
};
}
/// Another batch into this one, taking ownership of its tasks.
pub fn push(self: *Batch, batch: Batch) void {
if (batch.len == 0) return;
if (self.len == 0) {
self.* = batch;
} else {
self.tail.?.node.next = if (batch.head) |h| &h.node else null;
self.tail = batch.tail;
self.len += batch.len;
}
}
};
/// Schedule a batch of tasks to be executed by some thread on the thread pool.
pub fn schedule(self: *ThreadPool, batch: Batch) void {
// Sanity check
if (batch.len == 0) {
return;
}
// Extract out the Node's from the Tasks
var list = Node.List{
.head = &batch.head.?.node,
.tail = &batch.tail.?.node,
};
// Push the task Nodes to the most approriate queue
if (Thread.current) |thread| {
thread.run_buffer.push(&list) catch thread.run_queue.push(list);
} else {
self.run_queue.push(list);
}
// Try to notify a thread
const is_waking = false;
return self.notify(is_waking);
}
inline fn notify(self: *ThreadPool, is_waking: bool) void {
// Fast path to check the Sync state to avoid calling into notifySlow().
// If we're waking, then we need to update the state regardless
if (!is_waking) {
const sync: Sync = @bitCast(self.sync.load(.monotonic));
if (sync.notified) {
return;
}
}
return self.notifySlow(is_waking);
}
noinline fn notifySlow(self: *ThreadPool, is_waking: bool) void {
var sync: Sync = @bitCast(self.sync.load(.monotonic));
while (sync.state != .shutdown) {
const can_wake = is_waking or (sync.state == .pending);
if (is_waking) {
assert(sync.state == .waking);
}
var new_sync = sync;
new_sync.notified = true;
if (can_wake and sync.idle > 0) { // wake up an idle thread
new_sync.state = .signaled;
} else if (can_wake and sync.spawned < self.max_threads) { // spawn a new thread
new_sync.state = .signaled;
new_sync.spawned += 1;
} else if (is_waking) { // no other thread to pass on "waking" status
new_sync.state = .pending;
} else if (sync.notified) { // nothing to update
return;
}
// Release barrier synchronizes with Acquire in wait()
// to ensure pushes to run queues happen before observing a posted notification.
sync = @bitCast(self.sync.cmpxchgWeak(
@bitCast(sync),
@bitCast(new_sync),
.release,
.monotonic,
) orelse {
// We signaled to notify an idle thread
if (can_wake and sync.idle > 0) {
return self.idle_event.notify();
}
// We signaled to spawn a new thread
if (can_wake and sync.spawned < self.max_threads) {
const spawn_config = std.Thread.SpawnConfig{ .stack_size = self.stack_size };
const thread = std.Thread.spawn(spawn_config, Thread.run, .{self}) catch return self.unregister(null);
return thread.detach();
}
return;
});
}
}
noinline fn wait(self: *ThreadPool, _is_waking: bool) error{Shutdown}!bool {
var is_idle = false;
var is_waking = _is_waking;
var sync: Sync = @bitCast(self.sync.load(.monotonic));
while (true) {
if (sync.state == .shutdown) return error.Shutdown;
if (is_waking) assert(sync.state == .waking);
// Consume a notification made by notify().
if (sync.notified) {
var new_sync = sync;
new_sync.notified = false;
if (is_idle)
new_sync.idle -= 1;
if (sync.state == .signaled)
new_sync.state = .waking;
// Acquire barrier synchronizes with notify()
// to ensure that pushes to run queue are observed after wait() returns.
sync = @bitCast(self.sync.cmpxchgWeak(
@bitCast(sync),
@bitCast(new_sync),
.acquire,
.monotonic,
) orelse {
return is_waking or (sync.state == .signaled);
});
// No notification to consume.
// Mark this thread as idle before sleeping on the idle_event.
} else if (!is_idle) {
var new_sync = sync;
new_sync.idle += 1;
if (is_waking)
new_sync.state = .pending;
sync = @bitCast(self.sync.cmpxchgWeak(
@bitCast(sync),
@bitCast(new_sync),
.monotonic,
.monotonic,
) orelse {
is_waking = false;
is_idle = true;
continue;
});
// Wait for a signal by either notify() or shutdown() without wasting cpu cycles.
// TODO: Add I/O polling here.
} else {
self.idle_event.wait();
sync = @bitCast(self.sync.load(.monotonic));
}
}
}
/// Marks the thread pool as shutdown
pub noinline fn shutdown(self: *ThreadPool) void {
var sync: Sync = @bitCast(self.sync.load(.monotonic));
while (sync.state != .shutdown) {
var new_sync = sync;
new_sync.notified = true;
new_sync.state = .shutdown;
new_sync.idle = 0;
// Full barrier to synchronize with both wait() and notify()
sync = @bitCast(self.sync.cmpxchgWeak(
@bitCast(sync),
@bitCast(new_sync),
.acq_rel,
.monotonic,
) orelse {
// Wake up any threads sleeping on the idle_event.
// TODO: I/O polling notification here.
if (sync.idle > 0) self.idle_event.shutdown();
return;
});
}
}
fn register(noalias self: *ThreadPool, noalias thread: *Thread) void {
// Push the thread onto the threads stack in a lock-free manner.
var threads = self.threads.load(.monotonic);
while (true) {
thread.next = threads;
threads = self.threads.cmpxchgWeak(
threads,
thread,
.release,
.monotonic,
) orelse break;
}
}
fn unregister(noalias self: *ThreadPool, noalias maybe_thread: ?*Thread) void {
// Un-spawn one thread, either due to a failed OS thread spawning or the thread is exitting.
const one_spawned: u32 = @bitCast(Sync{ .spawned = 1 });
const sync: Sync = @bitCast(self.sync.fetchSub(one_spawned, .release));
assert(sync.spawned > 0);
// The last thread to exit must wake up the thread pool join()er
// who will start the chain to shutdown all the threads.
if (sync.state == .shutdown and sync.spawned == 1) {
self.join_event.notify();
}
// If this is a thread pool thread, wait for a shutdown signal by the thread pool join()er.
const thread = maybe_thread orelse return;
thread.join_event.wait();
// After receiving the shutdown signal, shutdown the next thread in the pool.
// We have to do that without touching the thread pool itself since it's memory is invalidated by now.
// So just follow our .next link.
const next_thread = thread.next orelse return;
next_thread.join_event.notify();
}
fn join(self: *ThreadPool) void {
// Wait for the thread pool to be shutdown() then for all threads to enter a joinable state
var sync: Sync = @bitCast(self.sync.load(.monotonic));
if (!(sync.state == .shutdown and sync.spawned == 0)) {
self.join_event.wait();
sync = @bitCast(self.sync.load(.monotonic));
}
assert(sync.state == .shutdown);
assert(sync.spawned == 0);
// If there are threads, start off the chain sending it the shutdown signal.
// The thread receives the shutdown signal and sends it to the next thread, and the next..
const thread = self.threads.load(.acquire) orelse return;
thread.join_event.notify();
}
const Thread = struct {
next: ?*Thread = null,
target: ?*Thread = null,
join_event: Event = .{},
run_queue: Node.Queue = .{},
run_buffer: Node.Buffer = .{},
threadlocal var current: ?*Thread = null;
/// Thread entry point which runs a worker for the ThreadPool
fn run(thread_pool: *ThreadPool) void {
var self = Thread{};
current = &self;
thread_pool.register(&self);
defer thread_pool.unregister(&self);
var is_waking = false;
while (true) {
is_waking = thread_pool.wait(is_waking) catch return;
while (self.pop(thread_pool)) |result| {
if (result.pushed or is_waking)
thread_pool.notify(is_waking);
is_waking = false;
const task: *Task = @fieldParentPtr("node", result.node);
(task.callback)(task);
}
}
}
/// Try to dequeue a Node/Task from the ThreadPool.
/// Spurious reports of dequeue() returning empty are allowed.
fn pop(noalias self: *Thread, noalias thread_pool: *ThreadPool) ?Node.Buffer.Stole {
// Check our local buffer first
if (self.run_buffer.pop()) |node| {
return Node.Buffer.Stole{
.node = node,
.pushed = false,
};
}
// Then check our local queue
if (self.run_buffer.consume(&self.run_queue)) |stole| {
return stole;
}
// Then the global queue
if (self.run_buffer.consume(&thread_pool.run_queue)) |stole| {
return stole;
}
// TODO: add optimistic I/O polling here
// Then try work stealing from other threads
var num_threads: u32 = @as(Sync, @bitCast(thread_pool.sync.load(.monotonic))).spawned;
while (num_threads > 0) : (num_threads -= 1) {
// Traverse the stack of registered threads on the thread pool
const target = self.target orelse thread_pool.threads.load(.acquire) orelse unreachable;
self.target = target.next;
// Try to steal from their queue first to avoid contention (the target steal's from queue last).
if (self.run_buffer.consume(&target.run_queue)) |stole| {
return stole;
}
// Skip stealing from the buffer if we're the target.
// We still steal from our own queue above given it may have just been locked the first time we tried.
if (target == self) {
continue;
}
// Steal from the buffer of a remote thread as a last resort
if (self.run_buffer.steal(&target.run_buffer)) |stole| {
return stole;
}
}
return null;
}
};
/// An event which stores 1 semaphore token and is multi-threaded safe.
/// The event can be shutdown(), waking up all wait()ing threads and
/// making subsequent wait()'s return immediately.
const Event = struct {
state: Atomic(u32) = Atomic(u32).init(EMPTY),
const EMPTY = 0;
const WAITING = 1;
const NOTIFIED = 2;
const SHUTDOWN = 3;
/// Wait for and consume a notification
/// or wait for the event to be shutdown entirely
noinline fn wait(self: *Event) void {
var acquire_with: u32 = EMPTY;
var state = self.state.load(.monotonic);
while (true) {
// If we're shutdown then exit early.
// Acquire barrier to ensure operations before the shutdown() are seen after the wait().
// Shutdown is rare so it's better to have an Acquire barrier here instead of on CAS failure + load which are common.
if (state == SHUTDOWN) {
@fence(.acquire);
return;
}
// Consume a notification when it pops up.
// Acquire barrier to ensure operations before the notify() appear after the wait().
if (state == NOTIFIED) {
state = self.state.cmpxchgWeak(
state,
acquire_with,
.acquire,
.monotonic,
) orelse return;
continue;
}
// There is no notification to consume, we should wait on the event by ensuring its WAITING.
if (state != WAITING) blk: {
state = self.state.cmpxchgWeak(
state,
WAITING,
.monotonic,
.monotonic,
) orelse break :blk;
continue;
}
// Wait on the event until a notify() or shutdown().
// If we wake up to a notification, we must acquire it with WAITING instead of EMPTY
// since there may be other threads sleeping on the Futex who haven't been woken up yet.
//
// Acquiring to WAITING will make the next notify() or shutdown() wake a sleeping futex thread
// who will either exit on SHUTDOWN or acquire with WAITING again, ensuring all threads are awoken.
// This unfortunately results in the last notify() or shutdown() doing an extra futex wake but that's fine.
std.Thread.Futex.wait(&self.state, WAITING);
state = self.state.load(.monotonic);
acquire_with = WAITING;
}
}
/// Post a notification to the event if it doesn't have one already
/// then wake up a waiting thread if there is one as well.
fn notify(self: *Event) void {
return self.wake(NOTIFIED, 1);
}
/// Marks the event as shutdown, making all future wait()'s return immediately.
/// Then wakes up any threads currently waiting on the Event.
fn shutdown(self: *Event) void {
return self.wake(SHUTDOWN, std.math.maxInt(u32));
}
fn wake(self: *Event, release_with: u32, wake_threads: u32) void {
// Update the Event to notifty it with the new `release_with` state (either NOTIFIED or SHUTDOWN).
// Release barrier to ensure any operations before this are this to happen before the wait() in the other threads.
const state = self.state.swap(release_with, .release);
// Only wake threads sleeping in futex if the state is WAITING.
// Avoids unnecessary wake ups.
if (state == WAITING) {
std.Thread.Futex.wake(&self.state, wake_threads);
}
}
};
/// Linked list intrusive memory node and lock-free data structures to operate with it
const Node = struct {
next: ?*Node = null,
/// A linked list of Nodes
const List = struct {
head: *Node,
tail: *Node,
};
/// An unbounded multi-producer-(non blocking)-multi-consumer queue of Node pointers.
const Queue = struct {
stack: Atomic(usize) = Atomic(usize).init(0),
cache: ?*Node = null,
const HAS_CACHE: usize = 0b01;
const IS_CONSUMING: usize = 0b10;
const PTR_MASK: usize = ~(HAS_CACHE | IS_CONSUMING);
comptime {
assert(@alignOf(Node) >= ((IS_CONSUMING | HAS_CACHE) + 1));
}
fn push(noalias self: *Queue, list: List) void {
var stack = self.stack.load(.monotonic);
while (true) {
// Attach the list to the stack (pt. 1)
list.tail.next = @ptrFromInt(stack & PTR_MASK);
// Update the stack with the list (pt. 2).
// Don't change the HAS_CACHE and IS_CONSUMING bits of the consumer.
var new_stack = @intFromPtr(list.head);
assert(new_stack & ~PTR_MASK == 0);
new_stack |= (stack & ~PTR_MASK);
// Push to the stack with a release barrier for the consumer to see the proper list links.
stack = self.stack.cmpxchgWeak(
stack,
new_stack,
.release,
.monotonic,
) orelse break;
}
}
fn tryAcquireConsumer(self: *Queue) error{ Empty, Contended }!?*Node {
var stack = self.stack.load(.monotonic);
while (true) {
if (stack & IS_CONSUMING != 0)
return error.Contended; // The queue already has a consumer.
if (stack & (HAS_CACHE | PTR_MASK) == 0)
return error.Empty; // The queue is empty when there's nothing cached and nothing in the stack.
// When we acquire the consumer, also consume the pushed stack if the cache is empty.
var new_stack = stack | HAS_CACHE | IS_CONSUMING;
if (stack & HAS_CACHE == 0) {
assert(stack & PTR_MASK != 0);
new_stack &= ~PTR_MASK;
}
// Acquire barrier on getting the consumer to see cache/Node updates done by previous consumers
// and to ensure our cache/Node updates in pop() happen after that of previous consumers.
stack = self.stack.cmpxchgWeak(
stack,
new_stack,
.acquire,
.monotonic,
) orelse return self.cache orelse @ptrFromInt(stack & PTR_MASK);
}
}
fn releaseConsumer(noalias self: *Queue, noalias consumer: ?*Node) void {
// Stop consuming and remove the HAS_CACHE bit as well if the consumer's cache is empty.
// When HAS_CACHE bit is zeroed, the next consumer will acquire the pushed stack nodes.
var remove = IS_CONSUMING;
if (consumer == null)
remove |= HAS_CACHE;
// Release the consumer with a release barrier to ensure cache/node accesses
// happen before the consumer was released and before the next consumer starts using the cache.
self.cache = consumer;
const stack = self.stack.fetchSub(remove, .release);
assert(stack & remove != 0);
}
fn pop(noalias self: *Queue, noalias consumer_ref: *?*Node) ?*Node {
// Check the consumer cache (fast path)
if (consumer_ref.*) |node| {
consumer_ref.* = node.next;
return node;
}
// Load the stack to see if there was anything pushed that we could grab.
var stack = self.stack.load(.monotonic);
assert(stack & IS_CONSUMING != 0);
if (stack & PTR_MASK == 0) {
return null;
}
// Nodes have been pushed to the stack, grab then with an Acquire barrier to see the Node links.
stack = self.stack.swap(HAS_CACHE | IS_CONSUMING, .acquire);
assert(stack & IS_CONSUMING != 0);
assert(stack & PTR_MASK != 0);
const node: *Node = @ptrFromInt(stack & PTR_MASK);
consumer_ref.* = node.next;
return node;
}
};
/// A bounded single-producer, multi-consumer ring buffer for node pointers.
const Buffer = struct {
head: Atomic(Index) = Atomic(Index).init(0),
tail: Atomic(Index) = Atomic(Index).init(0),
array: [capacity]Atomic(*Node) = undefined,
const Index = u32;
const capacity = 256; // Appears to be a pretty good trade-off in space vs contended throughput
comptime {
assert(std.math.maxInt(Index) >= capacity);
assert(std.math.isPowerOfTwo(capacity));
}
fn push(noalias self: *Buffer, noalias list: *List) error{Overflow}!void {
var head = self.head.load(.monotonic);
var tail = self.tail.raw; // we're the only thread that can change this
while (true) {
var size = tail -% head;
assert(size <= capacity);
// Push nodes from the list to the buffer if it's not empty..
if (size < capacity) {
var nodes: ?*Node = list.head;
while (size < capacity) : (size += 1) {
const node = nodes orelse break;
nodes = node.next;
// Array written atomically with weakest ordering since it could be getting atomically read by steal().
self.array[tail % capacity].store(node, .unordered);
tail +%= 1;
}
// Release barrier synchronizes with Acquire loads for steal()ers to see the array writes.
self.tail.store(tail, .release);
// Update the list with the nodes we pushed to the buffer and try again if there's more.
list.head = nodes orelse return;
std.atomic.spinLoopHint();
head = self.head.load(.monotonic);
continue;
}
// Try to steal/overflow half of the tasks in the buffer to make room for future push()es.
// Migrating half amortizes the cost of stealing while requiring future pops to still use the buffer.
// Acquire barrier to ensure the linked list creation after the steal only happens after we succesfully steal.
var migrate = size / 2;
head = self.head.cmpxchgWeak(
head,
head +% migrate,
.acquire,
.monotonic,
) orelse {
// Link the migrated Nodes together
const first = self.array[head % capacity].raw;
while (migrate > 0) : (migrate -= 1) {
const prev = self.array[head % capacity].raw;
head +%= 1;
prev.next = self.array[head % capacity].raw;
}
// Append the list that was supposed to be pushed to the end of the migrated Nodes
const last = self.array[(head -% 1) % capacity].raw;
last.next = list.head;
list.tail.next = null;
// Return the migrated nodes + the original list as overflowed
list.head = first;
return error.Overflow;
};
}
}
fn pop(self: *Buffer) ?*Node {
var head = self.head.load(.monotonic);
const tail = self.tail.raw; // we're the only thread that can change this
while (true) {
// Quick sanity check and return null when not empty
const size = tail -% head;
assert(size <= capacity);
if (size == 0) {
return null;
}
// Dequeue with an acquire barrier to ensure any writes done to the Node
// only happen after we succesfully claim it from the array.
head = self.head.cmpxchgWeak(
head,
head +% 1,
.acquire,
.monotonic,
) orelse return self.array[head % capacity].raw;
}
}
const Stole = struct {
node: *Node,
pushed: bool,
};
fn consume(noalias self: *Buffer, noalias queue: *Queue) ?Stole {
var consumer = queue.tryAcquireConsumer() catch return null;
defer queue.releaseConsumer(consumer);
const head = self.head.load(.monotonic);
const tail = self.tail.raw; // we're the only thread that can change this
const size = tail -% head;
assert(size <= capacity);
assert(size == 0); // we should only be consuming if our array is empty
// Pop nodes from the queue and push them to our array.
// Atomic stores to the array as steal() threads may be atomically reading from it.
var pushed: Index = 0;
while (pushed < capacity) : (pushed += 1) {
const node = queue.pop(&consumer) orelse break;
self.array[(tail +% pushed) % capacity].store(node, .unordered);
}
// We will be returning one node that we stole from the queue.
// Get an extra, and if that's not possible, take one from our array.
const node = queue.pop(&consumer) orelse blk: {
if (pushed == 0) return null;
pushed -= 1;
break :blk self.array[(tail +% pushed) % capacity].raw;
};
// Update the array tail with the nodes we pushed to it.
// Release barrier to synchronize with Acquire barrier in steal()'s to see the written array Nodes.
if (pushed > 0) self.tail.store(tail +% pushed, .release);
return Stole{
.node = node,
.pushed = pushed > 0,
};
}
fn steal(noalias self: *Buffer, noalias buffer: *Buffer) ?Stole {
const head = self.head.load(.monotonic);
const tail = self.tail.raw; // we're the only thread that can change this
const size = tail -% head;
assert(size <= capacity);
assert(size == 0); // we should only be stealing if our array is empty
while (true) : (std.atomic.spinLoopHint()) {
const buffer_head = buffer.head.load(.acquire);
const buffer_tail = buffer.tail.load(.acquire);
// Overly large size indicates the the tail was updated a lot after the head was loaded.
// Reload both and try again.
const buffer_size = buffer_tail -% buffer_head;
if (buffer_size > capacity) {
continue;
}
// Try to steal half (divCeil) to amortize the cost of stealing from other threads.
const steal_size = buffer_size - (buffer_size / 2);
if (steal_size == 0) {
return null;
}
// Copy the nodes we will steal from the target's array to our own.
// Atomically load from the target buffer array as it may be pushing and atomically storing to it.
// Atomic store to our array as other steal() threads may be atomically loading from it as above.
var i: Index = 0;
while (i < steal_size) : (i += 1) {
const node = buffer.array[(buffer_head +% i) % capacity].load(.unordered);
self.array[(tail +% i) % capacity].store(node, .unordered);
}
// Try to commit the steal from the target buffer using:
// - an Acquire barrier to ensure that we only interact with the stolen Nodes after the steal was committed.
// - a Release barrier to ensure that the Nodes are copied above prior to the committing of the steal
// because if they're copied after the steal, the could be getting rewritten by the target's push().
_ = buffer.head.cmpxchgStrong(
buffer_head,
buffer_head +% steal_size,
.acq_rel,
.monotonic,
) orelse {
// Pop one from the nodes we stole as we'll be returning it
const pushed = steal_size - 1;
const node = self.array[(tail +% pushed) % capacity].raw;
// Update the array tail with the nodes we pushed to it.
// Release barrier to synchronize with Acquire barrier in steal()'s to see the written array Nodes.
if (pushed > 0) self.tail.store(tail +% pushed, .release);
return Stole{
.node = node,
.pushed = pushed > 0,
};
};
}
}
};
};

1971
deps/libxev/src/backend/epoll.zig vendored Normal file

File diff suppressed because it is too large Load Diff

1745
deps/libxev/src/backend/io_uring.zig vendored Normal file

File diff suppressed because it is too large Load Diff

2356
deps/libxev/src/backend/iocp.zig vendored Normal file

File diff suppressed because it is too large Load Diff

2645
deps/libxev/src/backend/kqueue.zig vendored Normal file

File diff suppressed because it is too large Load Diff

1637
deps/libxev/src/backend/wasi_poll.zig vendored Normal file

File diff suppressed because it is too large Load Diff

105
deps/libxev/src/bench/async1.zig vendored Normal file
View File

@ -0,0 +1,105 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Instant = std.time.Instant;
const xev = @import("xev");
pub const std_options: std.Options = .{
.log_level = .info,
};
// Tune-ables
pub const NUM_PINGS = 1000 * 1000;
pub fn main() !void {
try run(1);
}
pub fn run(comptime thread_count: comptime_int) !void {
var loop = try xev.Loop.init(.{});
defer loop.deinit();
// Initialize all our threads
var contexts: [thread_count]Thread = undefined;
var threads: [contexts.len]std.Thread = undefined;
var comps: [contexts.len]xev.Completion = undefined;
for (&contexts, 0..) |*ctx, i| {
ctx.* = try Thread.init();
ctx.main_async.wait(&loop, &comps[i], Thread, ctx, mainAsyncCallback);
threads[i] = try std.Thread.spawn(.{}, Thread.threadMain, .{ctx});
}
const start_time = try Instant.now();
try loop.run(.until_done);
for (&threads) |thr| thr.join();
const end_time = try Instant.now();
const elapsed = @as(f64, @floatFromInt(end_time.since(start_time)));
std.log.info("async{d}: {d:.2} seconds ({d:.2}/sec)", .{
thread_count,
elapsed / 1e9,
NUM_PINGS / (elapsed / 1e9),
});
}
fn mainAsyncCallback(
ud: ?*Thread,
_: *xev.Loop,
_: *xev.Completion,
r: xev.Async.WaitError!void,
) xev.CallbackAction {
_ = r catch unreachable;
const self = ud.?;
self.worker_async.notify() catch unreachable;
self.main_sent += 1;
self.main_seen += 1;
return if (self.main_sent >= NUM_PINGS) .disarm else .rearm;
}
/// The thread state
const Thread = struct {
loop: xev.Loop,
worker_async: xev.Async,
main_async: xev.Async,
worker_sent: usize = 0,
worker_seen: usize = 0,
main_sent: usize = 0,
main_seen: usize = 0,
pub fn init() !Thread {
return .{
.loop = try xev.Loop.init(.{}),
.worker_async = try xev.Async.init(),
.main_async = try xev.Async.init(),
};
}
pub fn threadMain(self: *Thread) !void {
// Kick us off
try self.main_async.notify();
// Start our waiter
var c: xev.Completion = undefined;
self.worker_async.wait(&self.loop, &c, Thread, self, asyncCallback);
// Run
try self.loop.run(.until_done);
if (self.worker_sent < NUM_PINGS) @panic("FAIL");
}
fn asyncCallback(
ud: ?*Thread,
_: *xev.Loop,
_: *xev.Completion,
r: xev.Async.WaitError!void,
) xev.CallbackAction {
_ = r catch unreachable;
const self = ud.?;
self.main_async.notify() catch unreachable;
self.worker_sent += 1;
self.worker_seen += 1;
return if (self.worker_sent >= NUM_PINGS) .disarm else .rearm;
}
};

10
deps/libxev/src/bench/async2.zig vendored Normal file
View File

@ -0,0 +1,10 @@
const std = @import("std");
const run = @import("async1.zig").run;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(2);
}

10
deps/libxev/src/bench/async4.zig vendored Normal file
View File

@ -0,0 +1,10 @@
const std = @import("std");
const run = @import("async1.zig").run;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(4);
}

10
deps/libxev/src/bench/async8.zig vendored Normal file
View File

@ -0,0 +1,10 @@
const std = @import("std");
const run = @import("async1.zig").run;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(8);
}

View File

@ -0,0 +1,81 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Instant = std.time.Instant;
const xev = @import("xev");
pub const std_options: std.Options = .{
.log_level = .info,
};
// Tune-ables
pub const NUM_PINGS = 1000 * 1000;
pub fn main() !void {
try run(1);
}
pub fn run(comptime thread_count: comptime_int) !void {
var thread_pool = xev.ThreadPool.init(.{});
defer thread_pool.deinit();
defer thread_pool.shutdown();
var loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer loop.deinit();
// Create our async
notifier = try xev.Async.init();
defer notifier.deinit();
const userdata: ?*void = null;
var c: xev.Completion = undefined;
notifier.wait(&loop, &c, void, userdata, &asyncCallback);
// Initialize all our threads
var threads: [thread_count]std.Thread = undefined;
for (&threads) |*thr| {
thr.* = try std.Thread.spawn(.{}, threadMain, .{});
}
const start_time = try Instant.now();
try loop.run(.until_done);
for (&threads) |thr| thr.join();
const end_time = try Instant.now();
const elapsed = @as(f64, @floatFromInt(end_time.since(start_time)));
std.log.info("async_pummel_{d}: {d} callbacks in {d:.2} seconds ({d:.2}/sec)", .{
thread_count,
callbacks,
elapsed / 1e9,
@as(f64, @floatFromInt(callbacks)) / (elapsed / 1e9),
});
}
var callbacks: usize = 0;
var notifier: xev.Async = undefined;
var state: enum { running, stop, stopped } = .running;
fn asyncCallback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
r: xev.Async.WaitError!void,
) xev.CallbackAction {
_ = r catch unreachable;
callbacks += 1;
if (callbacks < NUM_PINGS) return .rearm;
// We're done
state = .stop;
while (state != .stopped) std.time.sleep(0);
return .disarm;
}
fn threadMain() !void {
while (state == .running) try notifier.notify();
state = .stopped;
}

View File

@ -0,0 +1,10 @@
const std = @import("std");
const run = @import("async_pummel_1.zig").run;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(2);
}

View File

@ -0,0 +1,10 @@
const std = @import("std");
const run = @import("async_pummel_1.zig").run;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(4);
}

View File

@ -0,0 +1,10 @@
const std = @import("std");
const run = @import("async_pummel_1.zig").run;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(8);
}

View File

@ -0,0 +1,61 @@
const std = @import("std");
const Instant = std.time.Instant;
const xev = @import("xev");
pub const NUM_TIMERS: usize = 10 * 1000 * 1000;
pub fn main() !void {
var thread_pool = xev.ThreadPool.init(.{});
defer thread_pool.deinit();
defer thread_pool.shutdown();
var loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer loop.deinit();
const GPA = std.heap.GeneralPurposeAllocator(.{});
var gpa: GPA = .{};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var cs = try alloc.alloc(xev.Completion, NUM_TIMERS);
defer alloc.free(cs);
const before_all = try Instant.now();
var i: usize = 0;
var timeout: u64 = 1;
while (i < NUM_TIMERS) : (i += 1) {
if (i % 1000 == 0) timeout += 1;
const timer = try xev.Timer.init();
timer.run(&loop, &cs[i], timeout, void, null, timerCallback);
}
const before_run = try Instant.now();
try loop.run(.until_done);
const after_run = try Instant.now();
const after_all = try Instant.now();
std.log.info("{d:.2} seconds total", .{@as(f64, @floatFromInt(after_all.since(before_all))) / 1e9});
std.log.info("{d:.2} seconds init", .{@as(f64, @floatFromInt(before_run.since(before_all))) / 1e9});
std.log.info("{d:.2} seconds dispatch", .{@as(f64, @floatFromInt(after_run.since(before_run))) / 1e9});
std.log.info("{d:.2} seconds cleanup", .{@as(f64, @floatFromInt(after_all.since(after_run))) / 1e9});
}
pub const std_options: std.Options = .{
.log_level = .info,
};
var timer_callback_count: usize = 0;
fn timerCallback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
result: xev.Timer.RunError!void,
) xev.CallbackAction {
_ = result catch unreachable;
timer_callback_count += 1;
return .disarm;
}

359
deps/libxev/src/bench/ping-pongs.zig vendored Normal file
View File

@ -0,0 +1,359 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Instant = std.time.Instant;
const xev = @import("xev");
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
var thread_pool = xev.ThreadPool.init(.{});
defer thread_pool.deinit();
defer thread_pool.shutdown();
var loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer loop.deinit();
const GPA = std.heap.GeneralPurposeAllocator(.{});
var gpa: GPA = .{};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var server_loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer server_loop.deinit();
var server = try Server.init(alloc, &server_loop);
defer server.deinit();
try server.start();
// Start our echo server
const server_thr = try std.Thread.spawn(.{}, Server.threadMain, .{&server});
// Start our client
var client_loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer client_loop.deinit();
var client = try Client.init(alloc, &client_loop);
defer client.deinit();
try client.start();
const start_time = try Instant.now();
try client_loop.run(.until_done);
server_thr.join();
const end_time = try Instant.now();
const elapsed = @as(f64, @floatFromInt(end_time.since(start_time)));
std.log.info("{d:.2} roundtrips/s", .{@as(f64, @floatFromInt(client.pongs)) / (elapsed / 1e9)});
std.log.info("{d:.2} seconds total", .{elapsed / 1e9});
}
/// Memory pools for things that need stable pointers
const BufferPool = std.heap.MemoryPool([4096]u8);
const CompletionPool = std.heap.MemoryPool(xev.Completion);
const TCPPool = std.heap.MemoryPool(xev.TCP);
/// The client state
const Client = struct {
loop: *xev.Loop,
completion_pool: CompletionPool,
read_buf: [1024]u8,
pongs: u64,
state: usize = 0,
stop: bool = false,
pub const PING = "PING\n";
pub fn init(alloc: Allocator, loop: *xev.Loop) !Client {
return .{
.loop = loop,
.completion_pool = CompletionPool.init(alloc),
.read_buf = undefined,
.pongs = 0,
.state = 0,
.stop = false,
};
}
pub fn deinit(self: *Client) void {
self.completion_pool.deinit();
}
/// Must be called with stable self pointer.
pub fn start(self: *Client) !void {
const addr = try std.net.Address.parseIp4("127.0.0.1", 3131);
const socket = try xev.TCP.init(addr);
const c = try self.completion_pool.create();
socket.connect(self.loop, c, addr, Client, self, connectCallback);
}
fn connectCallback(
self_: ?*Client,
l: *xev.Loop,
c: *xev.Completion,
socket: xev.TCP,
r: xev.TCP.ConnectError!void,
) xev.CallbackAction {
_ = r catch unreachable;
const self = self_.?;
// Send message
socket.write(l, c, .{ .slice = PING[0..PING.len] }, Client, self, writeCallback);
// Read
const c_read = self.completion_pool.create() catch unreachable;
socket.read(l, c_read, .{ .slice = &self.read_buf }, Client, self, readCallback);
return .disarm;
}
fn writeCallback(
self_: ?*Client,
l: *xev.Loop,
c: *xev.Completion,
s: xev.TCP,
b: xev.WriteBuffer,
r: xev.TCP.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
_ = l;
_ = s;
_ = b;
// Put back the completion.
self_.?.completion_pool.destroy(c);
return .disarm;
}
fn readCallback(
self_: ?*Client,
l: *xev.Loop,
c: *xev.Completion,
socket: xev.TCP,
buf: xev.ReadBuffer,
r: xev.TCP.ReadError!usize,
) xev.CallbackAction {
const self = self_.?;
const n = r catch unreachable;
const data = buf.slice[0..n];
// Count the number of pings in our message
var i: usize = 0;
while (i < n) : (i += 1) {
assert(data[i] == PING[self.state]);
self.state = (self.state + 1) % (PING.len);
if (self.state == 0) {
self.pongs += 1;
// If we're done then exit
if (self.pongs > 500_000) {
socket.shutdown(l, c, Client, self, shutdownCallback);
return .disarm;
}
// Send another ping
const c_ping = self.completion_pool.create() catch unreachable;
socket.write(l, c_ping, .{ .slice = PING[0..PING.len] }, Client, self, writeCallback);
}
}
// Read again
return .rearm;
}
fn shutdownCallback(
self_: ?*Client,
l: *xev.Loop,
c: *xev.Completion,
socket: xev.TCP,
r: xev.TCP.ShutdownError!void,
) xev.CallbackAction {
_ = r catch {};
const self = self_.?;
socket.close(l, c, Client, self, closeCallback);
return .disarm;
}
fn closeCallback(
self_: ?*Client,
l: *xev.Loop,
c: *xev.Completion,
socket: xev.TCP,
r: xev.TCP.CloseError!void,
) xev.CallbackAction {
_ = l;
_ = socket;
_ = r catch unreachable;
const self = self_.?;
self.stop = true;
self.completion_pool.destroy(c);
return .disarm;
}
};
/// The server state
const Server = struct {
loop: *xev.Loop,
buffer_pool: BufferPool,
completion_pool: CompletionPool,
socket_pool: TCPPool,
stop: bool,
pub fn init(alloc: Allocator, loop: *xev.Loop) !Server {
return .{
.loop = loop,
.buffer_pool = BufferPool.init(alloc),
.completion_pool = CompletionPool.init(alloc),
.socket_pool = TCPPool.init(alloc),
.stop = false,
};
}
pub fn deinit(self: *Server) void {
self.buffer_pool.deinit();
self.completion_pool.deinit();
self.socket_pool.deinit();
}
/// Must be called with stable self pointer.
pub fn start(self: *Server) !void {
const addr = try std.net.Address.parseIp4("127.0.0.1", 3131);
var socket = try xev.TCP.init(addr);
const c = try self.completion_pool.create();
try socket.bind(addr);
try socket.listen(std.os.linux.SOMAXCONN);
socket.accept(self.loop, c, Server, self, acceptCallback);
}
pub fn threadMain(self: *Server) !void {
try self.loop.run(.until_done);
}
fn destroyBuf(self: *Server, buf: []const u8) void {
self.buffer_pool.destroy(
@alignCast(
@as(*[4096]u8, @ptrFromInt(@intFromPtr(buf.ptr))),
),
);
}
fn acceptCallback(
self_: ?*Server,
l: *xev.Loop,
c: *xev.Completion,
r: xev.TCP.AcceptError!xev.TCP,
) xev.CallbackAction {
const self = self_.?;
// Create our socket
const socket = self.socket_pool.create() catch unreachable;
socket.* = r catch unreachable;
// Start reading -- we can reuse c here because its done.
const buf = self.buffer_pool.create() catch unreachable;
socket.read(l, c, .{ .slice = buf }, Server, self, readCallback);
return .disarm;
}
fn readCallback(
self_: ?*Server,
loop: *xev.Loop,
c: *xev.Completion,
socket: xev.TCP,
buf: xev.ReadBuffer,
r: xev.TCP.ReadError!usize,
) xev.CallbackAction {
const self = self_.?;
const n = r catch |err| switch (err) {
error.EOF => {
self.destroyBuf(buf.slice);
socket.shutdown(loop, c, Server, self, shutdownCallback);
return .disarm;
},
else => {
self.destroyBuf(buf.slice);
self.completion_pool.destroy(c);
std.log.warn("server read unexpected err={}", .{err});
return .disarm;
},
};
// Echo it back
const c_echo = self.completion_pool.create() catch unreachable;
const buf_write = self.buffer_pool.create() catch unreachable;
@memcpy(buf_write, buf.slice[0..n]);
socket.write(loop, c_echo, .{ .slice = buf_write[0..n] }, Server, self, writeCallback);
// Read again
return .rearm;
}
fn writeCallback(
self_: ?*Server,
l: *xev.Loop,
c: *xev.Completion,
s: xev.TCP,
buf: xev.WriteBuffer,
r: xev.TCP.WriteError!usize,
) xev.CallbackAction {
_ = l;
_ = s;
_ = r catch unreachable;
// We do nothing for write, just put back objects into the pool.
const self = self_.?;
self.completion_pool.destroy(c);
self.buffer_pool.destroy(
@alignCast(
@as(*[4096]u8, @ptrFromInt(@intFromPtr(buf.slice.ptr))),
),
);
return .disarm;
}
fn shutdownCallback(
self_: ?*Server,
l: *xev.Loop,
c: *xev.Completion,
s: xev.TCP,
r: xev.TCP.ShutdownError!void,
) xev.CallbackAction {
_ = r catch {};
const self = self_.?;
s.close(l, c, Server, self, closeCallback);
return .disarm;
}
fn closeCallback(
self_: ?*Server,
l: *xev.Loop,
c: *xev.Completion,
socket: xev.TCP,
r: xev.TCP.CloseError!void,
) xev.CallbackAction {
_ = l;
_ = r catch unreachable;
_ = socket;
const self = self_.?;
self.stop = true;
self.completion_pool.destroy(c);
return .disarm;
}
};

177
deps/libxev/src/bench/ping-udp1.zig vendored Normal file
View File

@ -0,0 +1,177 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Instant = std.time.Instant;
const xev = @import("xev");
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(1);
}
pub fn run(comptime count: comptime_int) !void {
var thread_pool = xev.ThreadPool.init(.{});
defer thread_pool.deinit();
defer thread_pool.shutdown();
var loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer loop.deinit();
const addr = try std.net.Address.parseIp4("127.0.0.1", 3131);
var pingers: [count]Pinger = undefined;
for (&pingers) |*p| {
p.* = try Pinger.init(addr);
try p.start(&loop);
}
const start_time = try Instant.now();
try loop.run(.until_done);
const end_time = try Instant.now();
const total: usize = total: {
var total: usize = 0;
for (&pingers) |p| total += p.pongs;
break :total total;
};
const elapsed = @as(f64, @floatFromInt(end_time.since(start_time)));
std.log.info("ping_pongs: {d} pingers, ~{d:.0} roundtrips/s", .{
count,
@as(f64, @floatFromInt(total)) / (elapsed / 1e9),
});
}
const Pinger = struct {
udp: xev.UDP,
addr: std.net.Address,
state: usize = 0,
pongs: u64 = 0,
read_buf: [1024]u8 = undefined,
c_read: xev.Completion = undefined,
c_write: xev.Completion = undefined,
state_read: xev.UDP.State = undefined,
state_write: xev.UDP.State = undefined,
op_count: u8 = 0,
pub const PING = "PING\n";
pub fn init(addr: std.net.Address) !Pinger {
return .{
.udp = try xev.UDP.init(addr),
.state = 0,
.pongs = 0,
.addr = addr,
};
}
pub fn start(self: *Pinger, loop: *xev.Loop) !void {
try self.udp.bind(self.addr);
self.udp.read(
loop,
&self.c_read,
&self.state_read,
.{ .slice = &self.read_buf },
Pinger,
self,
Pinger.readCallback,
);
self.write(loop);
}
pub fn write(self: *Pinger, loop: *xev.Loop) void {
self.udp.write(
loop,
&self.c_write,
&self.state_write,
self.addr,
.{ .slice = PING[0..PING.len] },
Pinger,
self,
writeCallback,
);
}
pub fn readCallback(
self_: ?*Pinger,
loop: *xev.Loop,
c: *xev.Completion,
_: *xev.UDP.State,
_: std.net.Address,
socket: xev.UDP,
buf: xev.ReadBuffer,
r: xev.UDP.ReadError!usize,
) xev.CallbackAction {
_ = c;
_ = socket;
const self = self_.?;
const n = r catch unreachable;
const data = buf.slice[0..n];
var i: usize = 0;
while (i < n) : (i += 1) {
assert(data[i] == PING[self.state]);
self.state = (self.state + 1) % (PING.len);
if (self.state == 0) {
self.pongs += 1;
// If we're done then exit
if (self.pongs > 500_000) {
self.udp.close(loop, &self.c_read, Pinger, self, closeCallback);
return .disarm;
}
self.op_count += 1;
if (self.op_count == 2) {
self.op_count = 0;
// Send another ping
self.write(loop);
}
}
}
return .rearm;
}
pub fn writeCallback(
self_: ?*Pinger,
loop: *xev.Loop,
_: *xev.Completion,
_: *xev.UDP.State,
_: xev.UDP,
_: xev.WriteBuffer,
r: xev.UDP.WriteError!usize,
) xev.CallbackAction {
const self = self_.?;
self.op_count += 1;
if (self.op_count == 2) {
self.op_count = 0;
// Send another ping
self.write(loop);
}
_ = r catch unreachable;
return .disarm;
}
pub fn closeCallback(
_: ?*Pinger,
_: *xev.Loop,
_: *xev.Completion,
_: xev.UDP,
r: xev.UDP.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
};

147
deps/libxev/src/bench/udp_pummel_1v1.zig vendored Normal file
View File

@ -0,0 +1,147 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Instant = std.time.Instant;
const xev = @import("xev");
const EXPECTED = "RANG TANG DING DONG I AM THE JAPANESE SANDMAN";
/// This is a global var decremented for the test without any locks. That's
/// how the original is written and that's how we're going to do it.
var packet_counter: usize = 1e6;
var send_cb_called: usize = 0;
var recv_cb_called: usize = 0;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(1, 1);
}
pub fn run(comptime n_senders: comptime_int, comptime n_receivers: comptime_int) !void {
const base_port = 12345;
var thread_pool = xev.ThreadPool.init(.{});
defer thread_pool.deinit();
defer thread_pool.shutdown();
var loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer loop.deinit();
var receivers: [n_receivers]Receiver = undefined;
for (&receivers, 0..) |*r, i| {
const addr = try std.net.Address.parseIp4("127.0.0.1", @as(u16, @intCast(base_port + i)));
r.* = .{ .udp = try xev.UDP.init(addr) };
try r.udp.bind(addr);
r.udp.read(
&loop,
&r.c_recv,
&r.udp_state,
.{ .slice = &r.recv_buf },
Receiver,
r,
Receiver.readCallback,
);
}
var senders: [n_senders]Sender = undefined;
for (&senders, 0..) |*s, i| {
const addr = try std.net.Address.parseIp4(
"127.0.0.1",
@as(u16, @intCast(base_port + (i % n_receivers))),
);
s.* = .{ .udp = try xev.UDP.init(addr) };
s.udp.write(
&loop,
&s.c_send,
&s.udp_state,
addr,
.{ .slice = EXPECTED },
Sender,
s,
Sender.writeCallback,
);
}
const start_time = try Instant.now();
try loop.run(.until_done);
const end_time = try Instant.now();
const elapsed = @as(f64, @floatFromInt(end_time.since(start_time)));
std.log.info("udp_pummel_{d}v{d}: {d:.0}f/s received, {d:.0}f/s sent, {d} received, {d} sent in {d:.1} seconds", .{
n_senders,
n_receivers,
@as(f64, @floatFromInt(recv_cb_called)) / (elapsed / std.time.ns_per_s),
@as(f64, @floatFromInt(send_cb_called)) / (elapsed / std.time.ns_per_s),
recv_cb_called,
send_cb_called,
elapsed / std.time.ns_per_s,
});
}
const Sender = struct {
udp: xev.UDP,
udp_state: xev.UDP.State = undefined,
c_send: xev.Completion = undefined,
fn writeCallback(
_: ?*Sender,
l: *xev.Loop,
_: *xev.Completion,
_: *xev.UDP.State,
_: xev.UDP,
_: xev.WriteBuffer,
r: xev.UDP.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
if (packet_counter == 0) {
l.stop();
return .disarm;
}
packet_counter -|= 1;
send_cb_called += 1;
return .rearm;
}
};
const Receiver = struct {
udp: xev.UDP,
udp_state: xev.UDP.State = undefined,
c_recv: xev.Completion = undefined,
recv_buf: [65536]u8 = undefined,
fn readCallback(
_: ?*Receiver,
_: *xev.Loop,
_: *xev.Completion,
_: *xev.UDP.State,
_: std.net.Address,
_: xev.UDP,
b: xev.ReadBuffer,
r: xev.UDP.ReadError!usize,
) xev.CallbackAction {
const n = r catch |err| {
switch (err) {
error.EOF => {},
else => std.log.warn("err={}", .{err}),
}
return .disarm;
};
if (!std.mem.eql(u8, b.slice[0..n], EXPECTED)) {
@panic("Unexpected data.");
}
recv_cb_called += 1;
return .rearm;
}
};

163
deps/libxev/src/build/ScdocStep.zig vendored Normal file
View File

@ -0,0 +1,163 @@
const std = @import("std");
const mem = std.mem;
const fs = std.fs;
const Step = std.Build.Step;
const Build = std.Build;
/// ScdocStep generates man pages using scdoc(1).
///
/// It reads all the raw pages from src_path and writes them to out_path.
/// src_path is typically "docs/" relative to the build root and out_path is
/// the build cache.
///
/// The man pages can be installed by calling install() on the step.
const ScdocStep = @This();
step: Step,
builder: *Build,
/// path to read man page sources from, defaults to the "doc/" subdirectory
/// from the build.zig file. This must be an absolute path.
src_path: []const u8,
/// path where the generated man pages will be written (NOT installed). This
/// defaults to build cache root.
out_path: []const u8,
pub fn create(builder: *Build) *ScdocStep {
const self = builder.allocator.create(ScdocStep) catch unreachable;
self.* = init(builder);
return self;
}
pub fn init(builder: *Build) ScdocStep {
return ScdocStep{
.builder = builder,
.step = Step.init(.{
.id = .custom,
.name = "generate man pages",
.owner = builder,
.makeFn = make,
}),
.src_path = builder.pathFromRoot("docs/"),
.out_path = builder.cache_root.join(builder.allocator, &[_][]const u8{
"man",
}) catch unreachable,
};
}
fn make(step: *std.Build.Step, _: std.Progress.Node) !void {
const self: *ScdocStep = @fieldParentPtr("step", step);
// Create our cache path
// TODO(mitchellh): ideally this would be pure zig
{
const command = try std.fmt.allocPrint(
self.builder.allocator,
"rm -f {[path]s}/* && mkdir -p {[path]s}",
.{ .path = self.out_path },
);
_ = self.builder.run(&[_][]const u8{ "sh", "-c", command });
}
// Find all our man pages which are in our src path ending with ".scd".
var dir = try fs.openDirAbsolute(self.src_path, .{ .iterate = true });
defer dir.close();
var iter = dir.iterate();
while (try iter.next()) |*entry| {
// We only want "scd" files to generate.
if (!mem.eql(u8, fs.path.extension(entry.name), ".scd")) {
continue;
}
const src = try fs.path.join(
self.builder.allocator,
&[_][]const u8{ self.src_path, entry.name },
);
const dst = try fs.path.join(
self.builder.allocator,
&[_][]const u8{ self.out_path, entry.name[0..(entry.name.len - 4)] },
);
const command = try std.fmt.allocPrint(
self.builder.allocator,
"scdoc < {s} > {s}",
.{ src, dst },
);
_ = self.builder.run(&[_][]const u8{ "sh", "-c", command });
}
}
pub fn install(self: *ScdocStep) !void {
// Ensure that `zig build install` depends on our generation step first.
self.builder.getInstallStep().dependOn(&self.step);
// Then run our install step which looks at what we made out of our
// generation and moves it to the install prefix.
const install_step = InstallStep.create(self.builder, self);
self.builder.getInstallStep().dependOn(&install_step.step);
}
/// Install man pages, create using install() on ScdocStep.
const InstallStep = struct {
step: Step,
builder: *Build,
scdoc: *ScdocStep,
pub fn create(builder: *Build, scdoc: *ScdocStep) *InstallStep {
const self = builder.allocator.create(InstallStep) catch unreachable;
self.* = InstallStep.init(builder, scdoc);
self.step.dependOn(&scdoc.step);
return self;
}
fn init(builder: *Build, scdoc: *ScdocStep) InstallStep {
return InstallStep{
.builder = builder,
.step = Step.init(.{
.id = .custom,
.name = "install man pages",
.owner = builder,
.makeFn = InstallStep.make,
}),
.scdoc = scdoc,
};
}
fn make(step: *Step, progress: std.Progress.Node) !void {
const self: *InstallStep = @fieldParentPtr("step", step);
// Get our absolute output path
var path = self.scdoc.out_path;
if (!fs.path.isAbsolute(path)) {
path = self.builder.pathFromRoot(path);
}
// Find all our man pages which are in our src path ending with ".scd".
var dir = try fs.openDirAbsolute(path, .{ .iterate = true });
defer dir.close();
var iter = dir.iterate();
while (try iter.next()) |*entry| {
// We expect filenames to be "foo.3" and this gets us "3"
const section = entry.name[(entry.name.len - 1)..];
const src = try fs.path.join(
self.builder.allocator,
&[_][]const u8{ path, entry.name },
);
const output = try std.fmt.allocPrint(
self.builder.allocator,
"share/man/man{s}/{s}",
.{ section, entry.name },
);
const fileStep = self.builder.addInstallFile(
.{ .cwd_relative = src },
output,
);
try fileStep.step.make(progress);
}
}
};

355
deps/libxev/src/c_api.zig vendored Normal file
View File

@ -0,0 +1,355 @@
// This file contains the C bindings that are exported when building
// the system libraries.
//
// WHERE IS THE DOCUMENTATION? Note that all the documentation for the C
// interface is in the man pages. The header file xev.h purposely has no
// documentation so that its concise and easy to see the list of exported
// functions.
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const xev = @import("main.zig");
export fn xev_loop_init(loop: *xev.Loop) c_int {
// TODO: overflow
loop.* = xev.Loop.init(.{}) catch |err| return errorCode(err);
return 0;
}
export fn xev_loop_deinit(loop: *xev.Loop) void {
loop.deinit();
}
export fn xev_loop_run(loop: *xev.Loop, mode: xev.RunMode) c_int {
loop.run(mode) catch |err| return errorCode(err);
return 0;
}
export fn xev_loop_now(loop: *xev.Loop) i64 {
return loop.now();
}
export fn xev_loop_update_now(loop: *xev.Loop) void {
loop.update_now();
}
export fn xev_completion_zero(c: *xev.Completion) void {
c.* = .{};
}
export fn xev_completion_state(c: *xev.Completion) xev.CompletionState {
return c.state();
}
//-------------------------------------------------------------------
// ThreadPool
export fn xev_threadpool_config_init(cfg: *xev.ThreadPool.Config) void {
cfg.* = .{};
}
export fn xev_threadpool_config_set_stack_size(
cfg: *xev.ThreadPool.Config,
v: u32,
) void {
cfg.stack_size = v;
}
export fn xev_threadpool_config_set_max_threads(
cfg: *xev.ThreadPool.Config,
v: u32,
) void {
cfg.max_threads = v;
}
export fn xev_threadpool_init(
threadpool: *xev.ThreadPool,
cfg_: ?*xev.ThreadPool.Config,
) c_int {
const cfg: xev.ThreadPool.Config = if (cfg_) |v| v.* else .{};
threadpool.* = xev.ThreadPool.init(cfg);
return 0;
}
export fn xev_threadpool_deinit(threadpool: *xev.ThreadPool) void {
threadpool.deinit();
}
export fn xev_threadpool_shutdown(threadpool: *xev.ThreadPool) void {
threadpool.shutdown();
}
export fn xev_threadpool_schedule(
pool: *xev.ThreadPool,
batch: *xev.ThreadPool.Batch,
) void {
pool.schedule(batch.*);
}
export fn xev_threadpool_task_init(
t: *xev.ThreadPool.Task,
cb: *const fn (*xev.ThreadPool.Task) callconv(.C) void,
) void {
const extern_t = @as(*Task, @ptrCast(@alignCast(t)));
extern_t.c_callback = cb;
t.* = .{
.callback = (struct {
fn callback(inner_t: *xev.ThreadPool.Task) void {
const outer_t: *Task = @alignCast(@fieldParentPtr(
"data",
@as(*Task.Data, @ptrCast(inner_t)),
));
outer_t.c_callback(inner_t);
}
}).callback,
};
}
export fn xev_threadpool_batch_init(b: *xev.ThreadPool.Batch) void {
b.* = .{};
}
export fn xev_threadpool_batch_push_task(
b: *xev.ThreadPool.Batch,
t: *xev.ThreadPool.Task,
) void {
b.push(xev.ThreadPool.Batch.from(t));
}
export fn xev_threadpool_batch_push_batch(
b: *xev.ThreadPool.Batch,
other: *xev.ThreadPool.Batch,
) void {
b.push(other.*);
}
//-------------------------------------------------------------------
// Timers
export fn xev_timer_init(v: *xev.Timer) c_int {
v.* = xev.Timer.init() catch |err| return errorCode(err);
return 0;
}
export fn xev_timer_deinit(v: *xev.Timer) void {
v.deinit();
}
export fn xev_timer_run(
v: *xev.Timer,
loop: *xev.Loop,
c: *xev.Completion,
next_ms: u64,
userdata: ?*anyopaque,
cb: *const fn (
*xev.Loop,
*xev.Completion,
c_int,
?*anyopaque,
) callconv(.C) xev.CallbackAction,
) void {
const Callback = @typeInfo(@TypeOf(cb)).Pointer.child;
const extern_c = @as(*Completion, @ptrCast(@alignCast(c)));
extern_c.c_callback = @as(*const anyopaque, @ptrCast(cb));
v.run(loop, c, next_ms, anyopaque, userdata, (struct {
fn callback(
ud: ?*anyopaque,
cb_loop: *xev.Loop,
cb_c: *xev.Completion,
r: xev.Timer.RunError!void,
) xev.CallbackAction {
const cb_extern_c = @as(*Completion, @ptrCast(cb_c));
const cb_c_callback = @as(
*const Callback,
@ptrCast(@alignCast(cb_extern_c.c_callback)),
);
return @call(.auto, cb_c_callback, .{
cb_loop,
cb_c,
if (r) |_| 0 else |err| errorCode(err),
ud,
});
}
}).callback);
}
export fn xev_timer_reset(
v: *xev.Timer,
loop: *xev.Loop,
c: *xev.Completion,
c_cancel: *xev.Completion,
next_ms: u64,
userdata: ?*anyopaque,
cb: *const fn (
*xev.Loop,
*xev.Completion,
c_int,
?*anyopaque,
) callconv(.C) xev.CallbackAction,
) void {
const Callback = @typeInfo(@TypeOf(cb)).Pointer.child;
const extern_c = @as(*Completion, @ptrCast(@alignCast(c)));
extern_c.c_callback = @as(*const anyopaque, @ptrCast(cb));
v.reset(loop, c, c_cancel, next_ms, anyopaque, userdata, (struct {
fn callback(
ud: ?*anyopaque,
cb_loop: *xev.Loop,
cb_c: *xev.Completion,
r: xev.Timer.RunError!void,
) xev.CallbackAction {
const cb_extern_c = @as(*Completion, @ptrCast(cb_c));
const cb_c_callback = @as(
*const Callback,
@ptrCast(@alignCast(cb_extern_c.c_callback)),
);
return @call(.auto, cb_c_callback, .{
cb_loop,
cb_c,
if (r) |_| 0 else |err| errorCode(err),
ud,
});
}
}).callback);
}
export fn xev_timer_cancel(
v: *xev.Timer,
loop: *xev.Loop,
c_timer: *xev.Completion,
c_cancel: *xev.Completion,
userdata: ?*anyopaque,
cb: *const fn (
*xev.Loop,
*xev.Completion,
c_int,
?*anyopaque,
) callconv(.C) xev.CallbackAction,
) void {
const Callback = @typeInfo(@TypeOf(cb)).Pointer.child;
const extern_c = @as(*Completion, @ptrCast(@alignCast(c_cancel)));
extern_c.c_callback = @as(*const anyopaque, @ptrCast(cb));
v.cancel(loop, c_timer, c_cancel, anyopaque, userdata, (struct {
fn callback(
ud: ?*anyopaque,
cb_loop: *xev.Loop,
cb_c: *xev.Completion,
r: xev.Timer.CancelError!void,
) xev.CallbackAction {
const cb_extern_c = @as(*Completion, @ptrCast(cb_c));
const cb_c_callback = @as(
*const Callback,
@ptrCast(@alignCast(cb_extern_c.c_callback)),
);
return @call(.auto, cb_c_callback, .{
cb_loop,
cb_c,
if (r) |_| 0 else |err| errorCode(err),
ud,
});
}
}).callback);
}
//-------------------------------------------------------------------
// Async
export fn xev_async_init(v: *xev.Async) c_int {
v.* = xev.Async.init() catch |err| return errorCode(err);
return 0;
}
export fn xev_async_deinit(v: *xev.Async) void {
v.deinit();
}
export fn xev_async_notify(v: *xev.Async) c_int {
v.notify() catch |err| return errorCode(err);
return 0;
}
export fn xev_async_wait(
v: *xev.Async,
loop: *xev.Loop,
c: *xev.Completion,
userdata: ?*anyopaque,
cb: *const fn (
*xev.Loop,
*xev.Completion,
c_int,
?*anyopaque,
) callconv(.C) xev.CallbackAction,
) void {
const Callback = @typeInfo(@TypeOf(cb)).Pointer.child;
const extern_c = @as(*Completion, @ptrCast(@alignCast(c)));
extern_c.c_callback = @as(*const anyopaque, @ptrCast(cb));
v.wait(loop, c, anyopaque, userdata, (struct {
fn callback(
ud: ?*anyopaque,
cb_loop: *xev.Loop,
cb_c: *xev.Completion,
r: xev.Async.WaitError!void,
) xev.CallbackAction {
const cb_extern_c = @as(*Completion, @ptrCast(cb_c));
const cb_c_callback = @as(
*const Callback,
@ptrCast(@alignCast(cb_extern_c.c_callback)),
);
return @call(.auto, cb_c_callback, .{
cb_loop,
cb_c,
if (r) |_| 0 else |err| errorCode(err),
ud,
});
}
}).callback);
}
//-------------------------------------------------------------------
// Sync with xev.h
/// Since we can't pass the callback at comptime with C, we have to
/// have an additional field on completions to store our callback pointer.
/// We just tack it onto the end of the memory chunk that C programs allocate
/// for completions.
const Completion = extern struct {
const Data = [@sizeOf(xev.Completion)]u8;
data: Data,
c_callback: *const anyopaque,
};
const Task = extern struct {
const Data = [@sizeOf(xev.ThreadPool.Task)]u8;
data: Data,
c_callback: *const fn (*xev.ThreadPool.Task) callconv(.C) void,
};
/// Returns the unique error code for an error.
fn errorCode(err: anyerror) c_int {
// TODO(mitchellh): This is a bad idea because its not stable across
// code changes. For now we just document that error codes are not
// stable but that is not useful at all!
return @intFromError(err);
}
test "c-api sizes" {
// This tests the sizes that are defined in the C API. We must ensure
// that our main structure sizes never exceed these so that the C ABI
// is maintained.
//
// THE MAGIC NUMBERS ARE KEPT IN SYNC WITH "include/xev.h"
const testing = std.testing;
try testing.expect(@sizeOf(xev.Loop) <= 512);
try testing.expect(@sizeOf(Completion) <= 320);
try testing.expect(@sizeOf(xev.Async) <= 256);
try testing.expect(@sizeOf(xev.Timer) <= 256);
try testing.expectEqual(@as(usize, 48), @sizeOf(xev.ThreadPool));
try testing.expectEqual(@as(usize, 24), @sizeOf(xev.ThreadPool.Batch));
try testing.expectEqual(@as(usize, 24), @sizeOf(Task));
try testing.expectEqual(@as(usize, 8), @sizeOf(xev.ThreadPool.Config));
}

58
deps/libxev/src/debug.zig vendored Normal file
View File

@ -0,0 +1,58 @@
const std = @import("std");
inline fn indent(depth: usize, writer: anytype) !void {
for (0..depth) |_| try writer.writeByte(' ');
}
pub fn describe(comptime T: type, writer: anytype, depth: usize) !void {
const type_info = @typeInfo(T);
switch (type_info) {
.Type,
.Void,
.Bool,
.NoReturn,
.Int,
.Float,
.Pointer,
.Array,
.ComptimeFloat,
.ComptimeInt,
.Undefined,
.Null,
.Optional,
.ErrorUnion,
.ErrorSet,
.Enum,
.Fn,
.Opaque,
.Frame,
.AnyFrame,
.Vector,
.EnumLiteral,
=> {
try writer.print("{s} ({d} bytes)", .{ @typeName(T), @sizeOf(T) });
},
.Union => |s| {
try writer.print("{s} ({d} bytes) {{\n", .{ @typeName(T), @sizeOf(T) });
inline for (s.fields) |f| {
try indent(depth + 4, writer);
try writer.print("{s}: ", .{f.name});
try describe(f.type, writer, depth + 4);
try writer.writeByte('\n');
}
try indent(depth, writer);
try writer.writeByte('}');
},
.Struct => |s| {
try writer.print("{s} ({d} bytes) {{\n", .{ @typeName(T), @sizeOf(T) });
inline for (s.fields) |f| {
try indent(depth + 4, writer);
try writer.print("{s}: ", .{f.name});
try describe(f.type, writer, depth + 4);
try writer.writeByte('\n');
}
try indent(depth, writer);
try writer.writeByte('}');
},
}
}

379
deps/libxev/src/heap.zig vendored Normal file
View File

@ -0,0 +1,379 @@
const std = @import("std");
const assert = std.debug.assert;
/// An intrusive heap implementation backed by a pairing heap[1] implementation.
///
/// Why? Intrusive data structures require the element type to hold the metadata
/// required for the structure, rather than an additional container structure.
/// There are numerous pros/cons that are documented well by Boost[2]. For Zig,
/// I think the primary benefits are making data structures allocation free
/// (rather, shifting allocation up to the consumer which can choose how they
/// want the memory to be available). There are various costs to this such as
/// the costs of pointer chasing, larger memory overhead, requiring the element
/// type to be aware of its container, etc. But for certain use cases an intrusive
/// data structure can yield much better performance.
///
/// Usage notes:
/// - The element T is expected to have a field "heap" of type InstrusiveHeapField.
/// See the tests for a full example of how to set this.
/// - You can easily make this a min or max heap by inverting the result of
/// "less" below.
///
/// [1]: https://en.wikipedia.org/wiki/Pairing_heap
/// [2]: https://www.boost.org/doc/libs/1_64_0/doc/html/intrusive/intrusive_vs_nontrusive.html
pub fn Intrusive(
comptime T: type,
comptime Context: type,
comptime less: *const fn (ctx: Context, a: *T, b: *T) bool,
) type {
return struct {
const Self = @This();
root: ?*T = null,
context: Context,
/// Insert a new element v into the heap. An element v can only
/// be a member of a single heap at any given time. When compiled
/// with runtime-safety, assertions will help verify this property.
pub fn insert(self: *Self, v: *T) void {
self.root = if (self.root) |root| self.meld(v, root) else v;
}
/// Look at the next minimum value but do not remove it.
pub fn peek(self: *Self) ?*T {
return self.root;
}
/// Delete the minimum value from the heap and return it.
pub fn deleteMin(self: *Self) ?*T {
const root = self.root orelse return null;
self.root = if (root.heap.child) |child|
self.combine_siblings(child)
else
null;
// Clear pointers with runtime safety so we can verify on
// insert that values aren't incorrectly being set multiple times.
root.heap = .{};
return root;
}
/// Remove the value v from the heap.
pub fn remove(self: *Self, v: *T) void {
// If v doesn't have a previous value, this must be the root
// element. If it is NOT the root element, v can't be in this
// heap and we trigger an assertion failure.
const prev = v.heap.prev orelse {
assert(self.root.? == v);
_ = self.deleteMin();
return;
};
// Detach "v" from the tree and clean up any links so it
// is as if this node never nexisted. The previous value
// must point to the proper next value and the pointers
// must all be cleaned up.
if (v.heap.next) |next| next.heap.prev = prev;
if (prev.heap.child == v)
prev.heap.child = v.heap.next
else
prev.heap.next = v.heap.next;
v.heap.prev = null;
v.heap.next = null;
// If we have children, then we need to merge them back in.
const child = v.heap.child orelse return;
v.heap.child = null;
const x = self.combine_siblings(child);
self.root = self.meld(x, self.root.?);
}
/// Meld (union) two heaps together. This isn't a generalized
/// union. It assumes that a.heap.next is null so this is only
/// meant in specific scenarios in the pairing heap where meld
/// is expected.
///
/// For example, when melding a new value "v" with an existing
/// root "root", "v" must always be the first param.
fn meld(self: *Self, a: *T, b: *T) *T {
assert(a.heap.next == null);
if (less(self.context, a, b)) {
// B points back to A
b.heap.prev = a;
// If B has siblings, then A inherits B's siblings
// and B's immediate sibling must point back to A to
// maintain the doubly linked list.
if (b.heap.next) |b_next| {
a.heap.next = b_next;
b_next.heap.prev = a;
b.heap.next = null;
}
// If A has a child, then B becomes the leftmost sibling
// of that child.
if (a.heap.child) |a_child| {
b.heap.next = a_child;
a_child.heap.prev = b;
}
// B becomes the leftmost child of A
a.heap.child = b;
return a;
}
// Replace A with B in the tree. Any of B's children
// become siblings of A. A becomes the leftmost child of B.
// A points back to B
b.heap.prev = a.heap.prev;
a.heap.prev = b;
if (b.heap.child) |b_child| {
a.heap.next = b_child;
b_child.heap.prev = a;
}
b.heap.child = a;
return b;
}
/// Combine the siblings of the leftmost value "left" into a single
/// new rooted with the minimum value.
fn combine_siblings(self: *Self, left: *T) *T {
left.heap.prev = null;
// Merge pairs right
var root: *T = root: {
var a: *T = left;
while (true) {
var b = a.heap.next orelse break :root a;
a.heap.next = null;
b = self.meld(a, b);
a = b.heap.next orelse break :root b;
}
};
// Merge pairs left
while (true) {
var b = root.heap.prev orelse return root;
b.heap.next = null;
root = self.meld(b, root);
}
}
};
}
/// The state that is required for IntrusiveHeap element types. This
/// should be set as the "heap" field in the type T.
pub fn IntrusiveField(comptime T: type) type {
return struct {
child: ?*T = null,
prev: ?*T = null,
next: ?*T = null,
};
}
test "heap" {
const Elem = struct {
const Self = @This();
value: usize = 0,
heap: IntrusiveField(Self) = .{},
};
const Heap = Intrusive(Elem, void, (struct {
fn less(ctx: void, a: *Elem, b: *Elem) bool {
_ = ctx;
return a.value < b.value;
}
}).less);
var a: Elem = .{ .value = 12 };
var b: Elem = .{ .value = 24 };
var c: Elem = .{ .value = 7 };
var d: Elem = .{ .value = 9 };
var h: Heap = .{ .context = {} };
h.insert(&a);
h.insert(&b);
h.insert(&c);
h.insert(&d);
h.remove(&d);
const testing = std.testing;
try testing.expect(h.deleteMin().?.value == 7);
try testing.expect(h.deleteMin().?.value == 12);
try testing.expect(h.deleteMin().?.value == 24);
try testing.expect(h.deleteMin() == null);
}
test "heap remove root" {
const Elem = struct {
const Self = @This();
value: usize = 0,
heap: IntrusiveField(Self) = .{},
};
const Heap = Intrusive(Elem, void, (struct {
fn less(ctx: void, a: *Elem, b: *Elem) bool {
_ = ctx;
return a.value < b.value;
}
}).less);
var a: Elem = .{ .value = 12 };
var b: Elem = .{ .value = 24 };
var h: Heap = .{ .context = {} };
h.insert(&a);
h.insert(&b);
h.remove(&a);
const testing = std.testing;
try testing.expect(h.deleteMin().?.value == 24);
try testing.expect(h.deleteMin() == null);
}
test "heap remove with children" {
const Elem = struct {
const Self = @This();
value: usize = 0,
heap: IntrusiveField(Self) = .{},
};
const Heap = Intrusive(Elem, void, (struct {
fn less(ctx: void, a: *Elem, b: *Elem) bool {
_ = ctx;
return a.value < b.value;
}
}).less);
var a: Elem = .{ .value = 36 };
var b: Elem = .{ .value = 24 };
var c: Elem = .{ .value = 12 };
var h: Heap = .{ .context = {} };
h.insert(&a);
h.insert(&b);
h.insert(&c);
h.remove(&b);
const testing = std.testing;
try testing.expect(h.deleteMin().?.value == 12);
try testing.expect(h.deleteMin().?.value == 36);
try testing.expect(h.deleteMin() == null);
}
test "heap equal values" {
const testing = std.testing;
const Elem = struct {
const Self = @This();
value: usize = 0,
heap: IntrusiveField(Self) = .{},
};
const Heap = Intrusive(Elem, void, (struct {
fn less(ctx: void, a: *Elem, b: *Elem) bool {
_ = ctx;
return a.value < b.value;
}
}).less);
var a: Elem = .{ .value = 1 };
var b: Elem = .{ .value = 2 };
var c: Elem = .{ .value = 3 };
var d: Elem = .{ .value = 4 };
var h: Heap = .{ .context = {} };
h.insert(&a);
h.insert(&b);
h.insert(&c);
h.insert(&d);
try testing.expect(h.deleteMin().?.value == 1);
try testing.expect(h.deleteMin().?.value == 2);
try testing.expect(h.deleteMin().?.value == 3);
try testing.expect(h.deleteMin().?.value == 4);
try testing.expect(h.deleteMin() == null);
}
test "heap: million values" {
const testing = std.testing;
const alloc = testing.allocator;
const Elem = struct {
const Self = @This();
value: usize = 0,
heap: IntrusiveField(Self) = .{},
};
const Heap = Intrusive(Elem, void, (struct {
fn less(ctx: void, a: *Elem, b: *Elem) bool {
_ = ctx;
return a.value < b.value;
}
}).less);
const NUM_TIMERS: usize = 1000 * 1000;
var elems = try alloc.alloc(Elem, NUM_TIMERS);
defer alloc.free(elems);
var i: usize = 0;
var value: usize = 0;
while (i < NUM_TIMERS) : (i += 1) {
if (i % 100 == 0) value += 1;
elems[i] = .{ .value = value };
}
var h: Heap = .{ .context = {} };
for (elems) |*elem| {
h.insert(elem);
}
var count: usize = 0;
var last: usize = 0;
while (h.deleteMin()) |elem| {
count += 1;
try testing.expect(elem.value >= last);
last = elem.value;
}
try testing.expect(h.deleteMin() == null);
try testing.expect(count == NUM_TIMERS);
}
test "heap: dangling next pointer" {
const testing = std.testing;
const Elem = struct {
const Self = @This();
value: usize = 0,
heap: IntrusiveField(Self) = .{},
};
const Heap = Intrusive(Elem, void, (struct {
fn less(ctx: void, a: *Elem, b: *Elem) bool {
_ = ctx;
return a.value < b.value;
}
}).less);
var a: Elem = .{ .value = 2 };
var b: Elem = .{ .value = 4 };
var c: Elem = .{ .value = 5 };
var d: Elem = .{ .value = 1 };
var e: Elem = .{ .value = 3 };
var h: Heap = .{ .context = {} };
h.insert(&a);
h.insert(&b);
h.insert(&c);
h.insert(&d);
h.insert(&e);
try testing.expect(h.deleteMin().?.value == 1);
try testing.expect(h.deleteMin().?.value == 2);
try testing.expect(h.deleteMin().?.value == 3);
try testing.expect(h.deleteMin().?.value == 4);
try testing.expect(h.deleteMin().?.value == 5);
try testing.expect(h.deleteMin() == null);
}

98
deps/libxev/src/linux/timerfd.zig vendored Normal file
View File

@ -0,0 +1,98 @@
const std = @import("std");
const linux = std.os.linux;
const posix = std.posix;
/// Timerfd is a wrapper around the timerfd system calls. See the
/// timerfd_create man page for information on timerfd and associated
/// system calls.
///
/// This is a small wrapper around timerfd to make it slightly more
/// pleasant to use, but may not expose all available functionality.
/// For maximum control you should use the syscalls directly.
pub const Timerfd = struct {
/// The timerfd file descriptor for use with poll, etc.
fd: i32,
/// timerfd_create
pub fn init(clock: Clock, flags: linux.TFD) !Timerfd {
const res = linux.timerfd_create(@intFromEnum(clock), flags);
return switch (posix.errno(res)) {
.SUCCESS => .{ .fd = @as(i32, @intCast(res)) },
else => error.UnknownError,
};
}
pub fn deinit(self: *const Timerfd) void {
posix.close(self.fd);
}
/// timerfd_settime
pub fn set(
self: *const Timerfd,
flags: linux.TFD.TIMER,
new_value: *const Spec,
old_value: ?*Spec,
) !void {
const res = linux.timerfd_settime(
self.fd,
flags,
@as(*const linux.itimerspec, @ptrCast(new_value)),
@as(?*linux.itimerspec, @ptrCast(old_value)),
);
return switch (posix.errno(res)) {
.SUCCESS => {},
else => error.UnknownError,
};
}
/// timerfd_gettime
pub fn get(self: *const Timerfd) !Spec {
var out: Spec = undefined;
const res = linux.timerfd_gettime(self.fd, @as(*linux.itimerspec, @ptrCast(&out)));
return switch (posix.errno(res)) {
.SUCCESS => out,
else => error.UnknownError,
};
}
/// The clocks available for a Timerfd. This is a non-exhaustive enum
/// so that unsupported values can be attempted to be passed into the
/// system calls.
pub const Clock = enum(i32) {
realtime = 0,
monotonic = 1,
boottime = 7,
realtime_alarm = 8,
boottime_alarm = 9,
_,
};
/// itimerspec
pub const Spec = extern struct {
interval: TimeSpec = .{},
value: TimeSpec = .{},
};
/// timespec
pub const TimeSpec = extern struct {
seconds: isize = 0,
nanoseconds: isize = 0,
};
};
test Timerfd {
const testing = std.testing;
var t = try Timerfd.init(.monotonic, .{});
defer t.deinit();
// Set
try t.set(.{}, &.{ .value = .{ .seconds = 60 } }, null);
try testing.expect((try t.get()).value.seconds > 0);
// Disarm
var old: Timerfd.Spec = undefined;
try t.set(.{}, &.{ .value = .{ .seconds = 0 } }, &old);
try testing.expect(old.value.seconds > 0);
}

77
deps/libxev/src/loop.zig vendored Normal file
View File

@ -0,0 +1,77 @@
//! Common loop structures. The actual loop implementation is in backend-specific
//! files such as linux/io_uring.zig.
const std = @import("std");
const assert = std.debug.assert;
const xev = @import("main.zig");
/// Common options across backends. Not all options apply to all backends.
/// Read the doc comment for individual fields to learn what backends they
/// apply to.
pub const Options = struct {
/// The number of queued completions that can be in flight before
/// requiring interaction with the kernel.
///
/// Backends: io_uring
entries: u32 = 256,
/// A thread pool to use for blocking operations. If the backend doesn't
/// need to perform any blocking operations then no threads will ever
/// be spawned. If the backend does need to perform blocking operations
/// on a thread and no thread pool is provided, the operations will simply
/// fail. Unless you're trying to really optimize for space, it is
/// recommended you provide a thread pool.
///
/// Backends: epoll, kqueue
thread_pool: ?*xev.ThreadPool = null,
};
/// The loop run mode -- all backends are required to support this in some way.
/// Backends may provide backend-specific APIs that behave slightly differently
/// or in a more configurable way.
pub const RunMode = enum(c_int) {
/// Run the event loop once. If there are no blocking operations ready,
/// return immediately.
no_wait = 0,
/// Run the event loop once, waiting for at least one blocking operation
/// to complete.
once = 1,
/// Run the event loop until it is "done". "Doneness" is defined as
/// there being no more completions that are active.
until_done = 2,
};
/// The result type for callbacks. This should be used by all loop
/// implementations and higher level abstractions in order to control
/// what to do after the loop completes.
pub const CallbackAction = enum(c_int) {
/// The request is complete and is not repeated. For example, a read
/// callback only fires once and is no longer watched for reads. You
/// can always free memory associated with the completion prior to
/// returning this.
disarm = 0,
/// Requeue the same operation request with the same parameters
/// with the event loop. This makes it easy to repeat a read, timer,
/// etc. This rearms the request EXACTLY as-is. For example, the
/// low-level timer interface for io_uring uses an absolute timeout.
/// If you rearm the timer, it will fire immediately because the absolute
/// timeout will be in the past.
///
/// The completion is reused so it is not safe to use the same completion
/// for anything else.
rearm = 1,
};
/// The state that a completion can be in.
pub const CompletionState = enum(c_int) {
/// The completion is not being used and is ready to be configured
/// for new work.
dead = 0,
/// The completion is part of an event loop. This may be already waited
/// on or in the process of being registered.
active = 1,
};

174
deps/libxev/src/main.zig vendored Normal file
View File

@ -0,0 +1,174 @@
const std = @import("std");
const builtin = @import("builtin");
/// The low-level IO interfaces using the recommended compile-time
/// interface for the target system.
const xev = Backend.default().Api();
pub usingnamespace xev;
//pub usingnamespace Epoll;
/// System-specific interfaces. Note that they are always pub for
/// all systems but if you reference them and force them to be analyzed
/// the proper system APIs must exist. Due to Zig's lazy analysis, if you
/// don't use any interface it will NOT be compiled (yay!).
pub const IO_Uring = Xev(.io_uring, @import("backend/io_uring.zig"));
pub const Epoll = Xev(.epoll, @import("backend/epoll.zig"));
pub const Kqueue = Xev(.kqueue, @import("backend/kqueue.zig"));
pub const WasiPoll = Xev(.wasi_poll, @import("backend/wasi_poll.zig"));
pub const IOCP = Xev(.iocp, @import("backend/iocp.zig"));
/// Generic thread pool implementation.
pub const ThreadPool = @import("ThreadPool.zig");
/// This stream (lowercase s) can be used as a namespace to access
/// Closeable, Writeable, Readable, etc. so that custom streams
/// can be constructed.
pub const stream = @import("watcher/stream.zig");
/// The backend types.
pub const Backend = enum {
io_uring,
epoll,
kqueue,
wasi_poll,
iocp,
/// Returns a recommend default backend from inspecting the system.
pub fn default() Backend {
return @as(?Backend, switch (builtin.os.tag) {
.linux => .io_uring,
.ios, .macos => .kqueue,
.wasi => .wasi_poll,
.windows => .iocp,
else => null,
}) orelse {
@compileLog(builtin.os);
@compileError("no default backend for this target");
};
}
/// Returns the Api (return value of Xev) for the given backend type.
pub fn Api(comptime self: Backend) type {
return switch (self) {
.io_uring => IO_Uring,
.epoll => Epoll,
.kqueue => Kqueue,
.wasi_poll => WasiPoll,
.iocp => IOCP,
};
}
};
/// Creates the Xev API based on a backend type.
///
/// For the default backend type for your system (i.e. io_uring on Linux),
/// this is the main API you interact with. It is `usingnamespaced` into
/// the "xev" package so you'd use types such as `xev.Loop`, `xev.Completion`,
/// etc.
///
/// Unless you're using a custom or specific backend type, you do NOT ever
/// need to call the Xev function itself.
pub fn Xev(comptime be: Backend, comptime T: type) type {
return struct {
const Self = @This();
const loop = @import("loop.zig");
/// The backend that this is. This is supplied at comptime so
/// it is up to the caller to say the right thing. This lets custom
/// implementations also "quack" like an implementation.
pub const backend = be;
/// The core loop APIs.
pub const Loop = T.Loop;
pub const Completion = T.Completion;
pub const Result = T.Result;
pub const ReadBuffer = T.ReadBuffer;
pub const WriteBuffer = T.WriteBuffer;
pub const Options = loop.Options;
pub const RunMode = loop.RunMode;
pub const CallbackAction = loop.CallbackAction;
pub const CompletionState = loop.CompletionState;
/// Error types
pub const AcceptError = T.AcceptError;
pub const CancelError = T.CancelError;
pub const CloseError = T.CloseError;
pub const ConnectError = T.ConnectError;
pub const ShutdownError = T.ShutdownError;
pub const WriteError = T.WriteError;
pub const ReadError = T.ReadError;
/// The high-level helper interfaces that make it easier to perform
/// common tasks. These may not work with all possible Loop implementations.
pub const Async = @import("watcher/async.zig").Async(Self);
pub const File = @import("watcher/file.zig").File(Self);
pub const Process = @import("watcher/process.zig").Process(Self);
pub const Stream = stream.GenericStream(Self);
pub const Timer = @import("watcher/timer.zig").Timer(Self);
pub const TCP = @import("watcher/tcp.zig").TCP(Self);
pub const UDP = @import("watcher/udp.zig").UDP(Self);
/// The callback of the main Loop operations. Higher level interfaces may
/// use a different callback mechanism.
pub const Callback = *const fn (
userdata: ?*anyopaque,
loop: *Loop,
completion: *Completion,
result: Result,
) CallbackAction;
/// A way to access the raw type.
pub const Sys = T;
/// A callback that does nothing and immediately disarms. This
/// implements xev.Callback and is the default value for completions.
pub fn noopCallback(
_: ?*anyopaque,
_: *Loop,
_: *Completion,
_: Result,
) CallbackAction {
return .disarm;
}
test {
@import("std").testing.refAllDecls(@This());
}
test "completion is zero-able" {
const c: Completion = .{};
_ = c;
}
};
}
test {
// Tested on all platforms
_ = @import("heap.zig");
_ = @import("queue.zig");
_ = @import("queue_mpsc.zig");
_ = ThreadPool;
// Test the C API
if (builtin.os.tag != .wasi) _ = @import("c_api.zig");
// OS-specific tests
switch (builtin.os.tag) {
.linux => {
_ = Epoll;
_ = IO_Uring;
_ = @import("linux/timerfd.zig");
},
.wasi => {
//_ = WasiPoll;
_ = @import("backend/wasi_poll.zig");
},
.windows => {
_ = @import("backend/iocp.zig");
},
else => {},
}
}

101
deps/libxev/src/queue.zig vendored Normal file
View File

@ -0,0 +1,101 @@
const std = @import("std");
const assert = std.debug.assert;
/// An intrusive queue implementation. The type T must have a field
/// "next" of type `?*T`.
///
/// For those unaware, an intrusive variant of a data structure is one in which
/// the data type in the list has the pointer to the next element, rather
/// than a higher level "node" or "container" type. The primary benefit
/// of this (and the reason we implement this) is that it defers all memory
/// management to the caller: the data structure implementation doesn't need
/// to allocate "nodes" to contain each element. Instead, the caller provides
/// the element and how its allocated is up to them.
pub fn Intrusive(comptime T: type) type {
return struct {
const Self = @This();
/// Head is the front of the queue and tail is the back of the queue.
head: ?*T = null,
tail: ?*T = null,
/// Enqueue a new element to the back of the queue.
pub fn push(self: *Self, v: *T) void {
assert(v.next == null);
if (self.tail) |tail| {
// If we have elements in the queue, then we add a new tail.
tail.next = v;
self.tail = v;
} else {
// No elements in the queue we setup the initial state.
self.head = v;
self.tail = v;
}
}
/// Dequeue the next element from the queue.
pub fn pop(self: *Self) ?*T {
// The next element is in "head".
const next = self.head orelse return null;
// If the head and tail are equal this is the last element
// so we also set tail to null so we can now be empty.
if (self.head == self.tail) self.tail = null;
// Head is whatever is next (if we're the last element,
// this will be null);
self.head = next.next;
// We set the "next" field to null so that this element
// can be inserted again.
next.next = null;
return next;
}
/// Returns true if the queue is empty.
pub fn empty(self: *const Self) bool {
return self.head == null;
}
};
}
test Intrusive {
const testing = std.testing;
// Types
const Elem = struct {
const Self = @This();
next: ?*Self = null,
};
const Queue = Intrusive(Elem);
var q: Queue = .{};
try testing.expect(q.empty());
// Elems
var elems: [10]Elem = .{.{}} ** 10;
// One
try testing.expect(q.pop() == null);
q.push(&elems[0]);
try testing.expect(!q.empty());
try testing.expect(q.pop().? == &elems[0]);
try testing.expect(q.pop() == null);
try testing.expect(q.empty());
// Two
try testing.expect(q.pop() == null);
q.push(&elems[0]);
q.push(&elems[1]);
try testing.expect(q.pop().? == &elems[0]);
try testing.expect(q.pop().? == &elems[1]);
try testing.expect(q.pop() == null);
// Interleaved
try testing.expect(q.pop() == null);
q.push(&elems[0]);
try testing.expect(q.pop().? == &elems[0]);
q.push(&elems[1]);
try testing.expect(q.pop().? == &elems[1]);
try testing.expect(q.pop() == null);
}

116
deps/libxev/src/queue_mpsc.zig vendored Normal file
View File

@ -0,0 +1,116 @@
const std = @import("std");
const assert = std.debug.assert;
/// An intrusive MPSC (multi-provider, single consumer) queue implementation.
/// The type T must have a field "next" of type `?*T`.
///
/// This is an implementatin of a Vyukov Queue[1].
/// TODO(mitchellh): I haven't audited yet if I got all the atomic operations
/// correct. I was short term more focused on getting something that seemed
/// to work; I need to make sure it actually works.
///
/// For those unaware, an intrusive variant of a data structure is one in which
/// the data type in the list has the pointer to the next element, rather
/// than a higher level "node" or "container" type. The primary benefit
/// of this (and the reason we implement this) is that it defers all memory
/// management to the caller: the data structure implementation doesn't need
/// to allocate "nodes" to contain each element. Instead, the caller provides
/// the element and how its allocated is up to them.
///
/// [1]: https://www.1024cores.net/home/lock-free-algorithms/queues/intrusive-mpsc-node-based-queue
pub fn Intrusive(comptime T: type) type {
return struct {
const Self = @This();
/// Head is the front of the queue and tail is the back of the queue.
head: *T,
tail: *T,
stub: T,
/// Initialize the queue. This requires a stable pointer to itself.
/// This must be called before the queue is used concurrently.
pub fn init(self: *Self) void {
self.head = &self.stub;
self.tail = &self.stub;
self.stub.next = null;
}
/// Push an item onto the queue. This can be called by any number
/// of producers.
pub fn push(self: *Self, v: *T) void {
@atomicStore(?*T, &v.next, null, .unordered);
const prev = @atomicRmw(*T, &self.head, .Xchg, v, .acq_rel);
@atomicStore(?*T, &prev.next, v, .release);
}
/// Pop the first in element from the queue. This must be called
/// by only a single consumer at any given time.
pub fn pop(self: *Self) ?*T {
var tail = @atomicLoad(*T, &self.tail, .unordered);
var next_ = @atomicLoad(?*T, &tail.next, .acquire);
if (tail == &self.stub) {
const next = next_ orelse return null;
@atomicStore(*T, &self.tail, next, .unordered);
tail = next;
next_ = @atomicLoad(?*T, &tail.next, .acquire);
}
if (next_) |next| {
@atomicStore(*T, &self.tail, next, .release);
tail.next = null;
return tail;
}
const head = @atomicLoad(*T, &self.head, .unordered);
if (tail != head) return null;
self.push(&self.stub);
next_ = @atomicLoad(?*T, &tail.next, .acquire);
if (next_) |next| {
@atomicStore(*T, &self.tail, next, .unordered);
tail.next = null;
return tail;
}
return null;
}
};
}
test Intrusive {
const testing = std.testing;
// Types
const Elem = struct {
const Self = @This();
next: ?*Self = null,
};
const Queue = Intrusive(Elem);
var q: Queue = undefined;
q.init();
// Elems
var elems: [10]Elem = .{.{}} ** 10;
// One
try testing.expect(q.pop() == null);
q.push(&elems[0]);
try testing.expect(q.pop().? == &elems[0]);
try testing.expect(q.pop() == null);
// Two
try testing.expect(q.pop() == null);
q.push(&elems[0]);
q.push(&elems[1]);
try testing.expect(q.pop().? == &elems[0]);
try testing.expect(q.pop().? == &elems[1]);
try testing.expect(q.pop() == null);
// // Interleaved
try testing.expect(q.pop() == null);
q.push(&elems[0]);
try testing.expect(q.pop().? == &elems[0]);
q.push(&elems[1]);
try testing.expect(q.pop().? == &elems[1]);
try testing.expect(q.pop() == null);
}

630
deps/libxev/src/watcher/async.zig vendored Normal file
View File

@ -0,0 +1,630 @@
/// "Wake up" an event loop from any thread using an async completion.
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const posix = std.posix;
const common = @import("common.zig");
pub fn Async(comptime xev: type) type {
return switch (xev.backend) {
// Supported, uses eventfd
.io_uring,
.epoll,
=> AsyncEventFd(xev),
// Supported, uses the backend API
.wasi_poll => AsyncLoopState(xev, xev.Loop.threaded),
// Supported, uses mach ports
.kqueue => AsyncMachPort(xev),
.iocp => AsyncIOCP(xev),
};
}
/// Async implementation using eventfd (Linux).
fn AsyncEventFd(comptime xev: type) type {
return struct {
const Self = @This();
/// The error that can come in the wait callback.
pub const WaitError = xev.ReadError;
/// eventfd file descriptor
fd: posix.fd_t,
/// Create a new async. An async can be assigned to exactly one loop
/// to be woken up. The completion must be allocated in advance.
pub fn init() !Self {
return .{
.fd = try std.posix.eventfd(0, 0),
};
}
/// Clean up the async. This will forcibly deinitialize any resources
/// and may result in erroneous wait callbacks to be fired.
pub fn deinit(self: *Self) void {
std.posix.close(self.fd);
}
/// Wait for a message on this async. Note that async messages may be
/// coalesced (or they may not be) so you should not expect a 1:1 mapping
/// between send and wait.
///
/// Just like the rest of libxev, the wait must be re-queued if you want
/// to continue to be notified of async events.
///
/// You should NOT register an async with multiple loops (the same loop
/// is fine -- but unnecessary). The behavior when waiting on multiple
/// loops is undefined.
pub fn wait(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!void,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.read = .{
.fd = self.fd,
.buffer = .{ .array = undefined },
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
if (r.read) |v| assert(v > 0) else |err| err,
});
}
}).callback,
};
loop.add(c);
}
/// Notify a loop to wake up synchronously. This should never block forever
/// (it will always EVENTUALLY succeed regardless of if the loop is currently
/// ticking or not).
///
/// The "c" value is the completion associated with the "wait".
///
/// Internal details subject to change but if you're relying on these
/// details then you may want to consider using a lower level interface
/// using the loop directly:
///
/// - linux+io_uring: eventfd is used. If the eventfd write would block
/// (EAGAIN) then we assume success because the eventfd is full.
///
pub fn notify(self: Self) !void {
// We want to just write "1" in the correct byte order as our host.
const val = @as([8]u8, @bitCast(@as(u64, 1)));
_ = posix.write(self.fd, &val) catch |err| switch (err) {
error.WouldBlock => return,
else => return err,
};
}
/// Common tests
pub usingnamespace AsyncTests(xev, Self);
};
}
/// Async implementation using mach ports (Darwin).
///
/// This allocates a mach port per async request and sends to that mach
/// port to wake up the loop and trigger the completion.
fn AsyncMachPort(comptime xev: type) type {
return struct {
const Self = @This();
/// The error that can come in the wait callback.
pub const WaitError = xev.Sys.MachPortError;
/// Missing Mach APIs from Zig stdlib. Data from xnu: osfmk/mach/port.h
const mach_port_flavor_t = c_int;
const mach_port_limits = extern struct { mpl_qlimit: c_uint };
const MACH_PORT_LIMITS_INFO = 1;
extern "c" fn mach_port_set_attributes(
task: posix.system.ipc_space_t,
name: posix.system.mach_port_name_t,
flavor: mach_port_flavor_t,
info: *anyopaque,
count: posix.system.mach_msg_type_number_t,
) posix.system.kern_return_t;
extern "c" fn mach_port_destroy(
task: posix.system.ipc_space_t,
name: posix.system.mach_port_name_t,
) posix.system.kern_return_t;
/// The mach port
port: posix.system.mach_port_name_t,
/// Create a new async. An async can be assigned to exactly one loop
/// to be woken up. The completion must be allocated in advance.
pub fn init() !Self {
const mach_self = posix.system.mach_task_self();
// Allocate the port
var mach_port: posix.system.mach_port_name_t = undefined;
switch (posix.system.getKernError(posix.system.mach_port_allocate(
mach_self,
@intFromEnum(posix.system.MACH_PORT_RIGHT.RECEIVE),
&mach_port,
))) {
.SUCCESS => {}, // Success
else => return error.MachPortAllocFailed,
}
errdefer _ = mach_port_destroy(mach_self, mach_port);
// Insert a send right into the port since we also use this to send
switch (posix.system.getKernError(posix.system.mach_port_insert_right(
mach_self,
mach_port,
mach_port,
@intFromEnum(posix.system.MACH_MSG_TYPE.MAKE_SEND),
))) {
.SUCCESS => {}, // Success
else => return error.MachPortAllocFailed,
}
// Modify the port queue size to be 1 because we are only
// using it for notifications and not for any other purpose.
var limits: mach_port_limits = .{ .mpl_qlimit = 1 };
switch (posix.system.getKernError(mach_port_set_attributes(
mach_self,
mach_port,
MACH_PORT_LIMITS_INFO,
&limits,
@sizeOf(@TypeOf(limits)),
))) {
.SUCCESS => {}, // Success
else => return error.MachPortAllocFailed,
}
return .{
.port = mach_port,
};
}
/// Clean up the async. This will forcibly deinitialize any resources
/// and may result in erroneous wait callbacks to be fired.
pub fn deinit(self: *Self) void {
_ = mach_port_destroy(
posix.system.mach_task_self(),
self.port,
);
}
/// Wait for a message on this async. Note that async messages may be
/// coalesced (or they may not be) so you should not expect a 1:1 mapping
/// between send and wait.
///
/// Just like the rest of libxev, the wait must be re-queued if you want
/// to continue to be notified of async events.
///
/// You should NOT register an async with multiple loops (the same loop
/// is fine -- but unnecessary). The behavior when waiting on multiple
/// loops is undefined.
pub fn wait(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!void,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.machport = .{
.port = self.port,
.buffer = .{ .array = undefined },
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
// Drain the mach port so that we only fire one
// notification even if many are queued.
drain(c_inner.op.machport.port);
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
if (r.machport) |_| {} else |err| err,
});
}
}).callback,
};
loop.add(c);
}
/// Drain the given mach port. All message bodies are discarded.
fn drain(port: posix.system.mach_port_name_t) void {
var message: struct {
header: posix.system.mach_msg_header_t,
} = undefined;
while (true) {
switch (posix.system.getMachMsgError(posix.system.mach_msg(
&message.header,
posix.system.MACH_RCV_MSG | posix.system.MACH_RCV_TIMEOUT,
0,
@sizeOf(@TypeOf(message)),
port,
posix.system.MACH_MSG_TIMEOUT_NONE,
posix.system.MACH_PORT_NULL,
))) {
// This means a read would've blocked, so we drained.
.RCV_TIMED_OUT => return,
// We dequeued, so we want to loop again.
.SUCCESS => {},
// We dequeued but the message had a body. We ignore
// message bodies for async so we are happy to discard
// it and continue.
.RCV_TOO_LARGE => {},
else => |err| {
std.log.warn("mach msg drain err, may duplicate async wakeups err={}", .{err});
return;
},
}
}
}
/// Notify a loop to wake up synchronously. This should never block forever
/// (it will always EVENTUALLY succeed regardless of if the loop is currently
/// ticking or not).
pub fn notify(self: Self) !void {
// This constructs an empty mach message. It has no data.
var msg: posix.system.mach_msg_header_t = .{
// We use COPY_SEND which will not increment any send ref
// counts because it'll reuse the existing send right.
.msgh_bits = @intFromEnum(posix.system.MACH_MSG_TYPE.COPY_SEND),
.msgh_size = @sizeOf(posix.system.mach_msg_header_t),
.msgh_remote_port = self.port,
.msgh_local_port = posix.system.MACH_PORT_NULL,
.msgh_voucher_port = undefined,
.msgh_id = undefined,
};
return switch (posix.system.getMachMsgError(
posix.system.mach_msg(
&msg,
posix.system.MACH_SEND_MSG | posix.system.MACH_SEND_TIMEOUT,
msg.msgh_size,
0,
posix.system.MACH_PORT_NULL,
0, // Fail instantly if the port is full
posix.system.MACH_PORT_NULL,
),
)) {
.SUCCESS => {},
else => |e| {
std.log.warn("mach msg err={}", .{e});
return error.MachMsgFailed;
},
// This is okay because it means that there was no more buffer
// space meaning that the port will wake up.
.SEND_NO_BUFFER => {},
// This means that the send would've blocked because the
// queue is full. We assume success because the port is full.
.SEND_TIMED_OUT => {},
};
}
/// Common tests
pub usingnamespace AsyncTests(xev, Self);
};
}
/// Async implementation that is deferred to the backend implementation
/// loop state. This is kind of a hacky implementation and not recommended
/// but its the only way currently to get asyncs to work on WASI.
fn AsyncLoopState(comptime xev: type, comptime threaded: bool) type {
// TODO: we don't support threaded loop state async. We _can_ it just
// isn't done yet. To support it we need to have some sort of mutex
// to guard waiter below.
if (threaded) return struct {};
return struct {
const Self = @This();
wakeup: bool = false,
waiter: ?struct {
loop: *xev.Loop,
c: *xev.Completion,
} = null,
/// The error that can come in the wait callback.
pub const WaitError = xev.Sys.AsyncError;
pub fn init() !Self {
return .{};
}
pub fn deinit(self: *Self) void {
_ = self;
}
pub fn wait(
self: *Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!void,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.async_wait = .{},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
if (r.async_wait) |_| {} else |err| err,
});
}
}).callback,
};
loop.add(c);
self.waiter = .{
.loop = loop,
.c = c,
};
if (self.wakeup) self.notify() catch {};
}
pub fn notify(self: *Self) !void {
if (self.waiter) |w|
w.loop.async_notify(w.c)
else
self.wakeup = true;
}
/// Common tests
pub usingnamespace AsyncTests(xev, Self);
};
}
/// Async implementation for IOCP.
fn AsyncIOCP(comptime xev: type) type {
return struct {
const Self = @This();
const windows = std.os.windows;
pub const WaitError = xev.Sys.AsyncError;
guard: std.Thread.Mutex = .{},
wakeup: bool = false,
waiter: ?struct {
loop: *xev.Loop,
c: *xev.Completion,
} = null,
pub fn init() !Self {
return Self{};
}
pub fn deinit(self: *Self) void {
_ = self;
}
pub fn wait(
self: *Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!void,
) xev.CallbackAction,
) void {
c.* = xev.Completion{
.op = .{ .async_wait = .{} },
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
if (r.async_wait) |_| {} else |err| err,
});
}
}).callback,
};
loop.add(c);
self.guard.lock();
defer self.guard.unlock();
self.waiter = .{
.loop = loop,
.c = c,
};
if (self.wakeup) loop.async_notify(c);
}
pub fn notify(self: *Self) !void {
self.guard.lock();
defer self.guard.unlock();
if (self.waiter) |w| {
w.loop.async_notify(w.c);
} else {
self.wakeup = true;
}
}
/// Common tests
pub usingnamespace AsyncTests(xev, Self);
};
}
fn AsyncTests(comptime xev: type, comptime Impl: type) type {
return struct {
test "async" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var notifier = try Impl.init();
defer notifier.deinit();
// Wait
var wake: bool = false;
var c_wait: xev.Completion = undefined;
notifier.wait(&loop, &c_wait, bool, &wake, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: Impl.WaitError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
// Send a notification
try notifier.notify();
// Wait for wake
try loop.run(.until_done);
try testing.expect(wake);
}
test "async: notify first" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var notifier = try Impl.init();
defer notifier.deinit();
// Send a notification
try notifier.notify();
// Wait
var wake: bool = false;
var c_wait: xev.Completion = undefined;
notifier.wait(&loop, &c_wait, bool, &wake, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: Impl.WaitError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
// Wait for wake
try loop.run(.until_done);
try testing.expect(wake);
}
test "async batches multiple notifications" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var notifier = try Impl.init();
defer notifier.deinit();
// Send a notification many times
try notifier.notify();
try notifier.notify();
try notifier.notify();
try notifier.notify();
try notifier.notify();
// Wait
var count: u32 = 0;
var c_wait: xev.Completion = undefined;
notifier.wait(&loop, &c_wait, u32, &count, (struct {
fn callback(
ud: ?*u32,
_: *xev.Loop,
_: *xev.Completion,
r: Impl.WaitError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* += 1;
return .rearm;
}
}).callback);
// Send a notification
try notifier.notify();
// Wait for wake
try loop.run(.once);
for (0..10) |_| try loop.run(.no_wait);
try testing.expectEqual(@as(u32, 1), count);
}
};
}

7
deps/libxev/src/watcher/common.zig vendored Normal file
View File

@ -0,0 +1,7 @@
/// Convert the callback value with an opaque pointer into the userdata type
/// that we can pass to our higher level callback types.
pub fn userdataValue(comptime Userdata: type, v: ?*anyopaque) ?*Userdata {
// Void userdata is always a null pointer.
if (Userdata == void) return null;
return @ptrCast(@alignCast(v));
}

542
deps/libxev/src/watcher/file.zig vendored Normal file
View File

@ -0,0 +1,542 @@
const std = @import("std");
const builtin = @import("builtin");
const common = @import("common.zig");
const assert = std.debug.assert;
const posix = std.posix;
const main = @import("../main.zig");
const stream = @import("stream.zig");
/// File operations.
///
/// These operations typically run on the event loop thread pool, rather
/// than the core async OS APIs, because most core async OS APIs don't support
/// async operations on regular files (with many caveats attached to that
/// statement). This high-level abstraction will attempt to make the right
/// decision about what to do but this should generally be used by
/// operations that need to run on a thread pool. For operations that you're
/// sure are better supported by core async OS APIs (such as sockets, pipes,
/// TTYs, etc.), use a specific high-level abstraction like xev.TCP or
/// the generic xev.Stream.
///
/// This is a "higher-level abstraction" in libxev. The goal of higher-level
/// abstractions in libxev are to make it easier to use specific functionality
/// with the event loop, but does not promise perfect flexibility or optimal
/// performance. In almost all cases, the abstraction is good enough. But,
/// if you have specific needs or want to push for the most optimal performance,
/// use the platform-specific Loop directly.
pub fn File(comptime xev: type) type {
return struct {
const Self = @This();
const FdType = if (xev.backend == .iocp) std.windows.HANDLE else posix.socket_t;
/// The underlying file
fd: FdType,
pub usingnamespace stream.Stream(xev, Self, .{
.close = true,
.read = .read,
.write = .write,
.threadpool = true,
});
/// Initialize a File from a std.fs.File.
pub fn init(file: std.fs.File) !Self {
return .{
.fd = file.handle,
};
}
/// Initialize a File from a file descriptor.
pub fn initFd(fd: std.fs.File.Handle) Self {
return .{
.fd = fd,
};
}
/// Clean up any watcher resources. This does NOT close the file.
/// If you want to close the file you must call close or do so
/// synchronously.
pub fn deinit(self: *const File) void {
_ = self;
}
pub fn pread(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
buf: xev.ReadBuffer,
offset: u64,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
b: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction,
) void {
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = .{
.pread = .{
.fd = self.fd,
.buffer = buf,
.offset = offset,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
Self.initFd(c_inner.op.pread.fd),
c_inner.op.pread.buffer,
if (r.pread) |v| v else |err| err,
});
}
}).callback,
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.wasi_poll,
.iocp,
=> {},
.epoll => {
c.flags.threadpool = true;
},
.kqueue => {
c.flags.threadpool = true;
},
}
loop.add(c);
},
}
}
pub fn queuePWrite(
self: Self,
loop: *xev.Loop,
q: *Self.WriteQueue,
req: *Self.WriteRequest,
buf: xev.WriteBuffer,
offset: u64,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
b: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction,
) void {
// Initialize our completion
req.* = .{};
self.pwrite_init(&req.completion, buf, offset);
req.completion.userdata = q;
req.completion.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const q_inner = @as(?*Self.WriteQueue, @ptrCast(@alignCast(ud))).?;
// The queue MUST have a request because a completion
// can only be added if the queue is not empty, and
// nothing else should be popping!.
const req_inner = q_inner.pop().?;
const cb_res = pwrite_result(c_inner, r);
const action = @call(.always_inline, cb, .{
common.userdataValue(Userdata, req_inner.userdata),
l_inner,
c_inner,
cb_res.writer,
cb_res.buf,
cb_res.result,
});
// Rearm requeues this request, it doesn't return rearm
// on the actual callback here...
if (action == .rearm) q_inner.push(req_inner);
// If we have another request, add that completion next.
if (q_inner.head) |req_next| l_inner.add(&req_next.completion);
// We always disarm because the completion in the next
// request will be used if there is more to queue.
return .disarm;
}
}).callback;
// The userdata as to go on the WriteRequest because we need
// our actual completion userdata to be the WriteQueue so that
// we can process the queue.
req.userdata = @as(?*anyopaque, @ptrCast(@alignCast(userdata)));
// If the queue is empty, then we add our completion. Otherwise,
// the previously queued writes will trigger this one.
if (q.empty()) loop.add(&req.completion);
// We always add this item to our queue no matter what
q.push(req);
}
pub fn pwrite(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
buf: xev.WriteBuffer,
offset: u64,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
b: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction,
) void {
self.pwrite_init(c, buf, offset);
c.userdata = userdata;
c.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const cb_res = pwrite_result(c_inner, r);
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
cb_res.writer,
cb_res.buf,
cb_res.result,
});
}
}).callback;
loop.add(c);
}
inline fn pwrite_result(c: *xev.Completion, r: xev.Result) struct {
writer: Self,
buf: xev.WriteBuffer,
result: Self.WriteError!usize,
} {
return .{
.writer = Self.initFd(c.op.pwrite.fd),
.buf = c.op.pwrite.buffer,
.result = if (r.pwrite) |v| v else |err| err,
};
}
fn pwrite_init(
self: Self,
c: *xev.Completion,
buf: xev.WriteBuffer,
offset: u64,
) void {
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = .{
.pwrite = .{
.fd = self.fd,
.buffer = buf,
.offset = offset,
},
},
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.wasi_poll,
.iocp,
=> {},
.epoll => {
c.flags.threadpool = true;
},
.kqueue => {
c.flags.threadpool = true;
},
}
},
}
}
test "read/write" {
// wasi: local files don't work with poll (always ready)
if (builtin.os.tag == .wasi) return error.SkipZigTest;
// windows: std.fs.File is not opened with OVERLAPPED flag.
if (builtin.os.tag == .windows) return error.SkipZigTest;
const testing = std.testing;
var tpool = main.ThreadPool.init(.{});
defer tpool.deinit();
defer tpool.shutdown();
var loop = try xev.Loop.init(.{ .thread_pool = &tpool });
defer loop.deinit();
// Create our file
const path = "test_watcher_file";
const f = try std.fs.cwd().createFile(path, .{
.read = true,
.truncate = true,
});
defer f.close();
defer std.fs.cwd().deleteFile(path) catch {};
const file = try init(f);
// Perform a write and then a read
var write_buf = [_]u8{ 1, 1, 2, 3, 5, 8, 13 };
var c_write: xev.Completion = undefined;
file.write(&loop, &c_write, .{ .slice = &write_buf }, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback);
// Wait for the write
try loop.run(.until_done);
// Make sure the data is on disk
try f.sync();
const f2 = try std.fs.cwd().openFile(path, .{});
defer f2.close();
const file2 = try init(f2);
// Read
var read_buf: [128]u8 = undefined;
var read_len: usize = 0;
file2.read(&loop, &c_write, .{ .slice = &read_buf }, usize, &read_len, (struct {
fn callback(
ud: ?*usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expectEqual(read_len, write_buf.len);
try testing.expectEqualSlices(u8, &write_buf, read_buf[0..read_len]);
}
test "pread/pwrite" {
// wasi: local files don't work with poll (always ready)
if (builtin.os.tag == .wasi) return error.SkipZigTest;
// windows: std.fs.File is not opened with OVERLAPPED flag.
if (builtin.os.tag == .windows) return error.SkipZigTest;
const testing = std.testing;
var tpool = main.ThreadPool.init(.{});
defer tpool.deinit();
defer tpool.shutdown();
var loop = try xev.Loop.init(.{ .thread_pool = &tpool });
defer loop.deinit();
// Create our file
const path = "test_watcher_file";
const f = try std.fs.cwd().createFile(path, .{
.read = true,
.truncate = true,
});
defer f.close();
defer std.fs.cwd().deleteFile(path) catch {};
const file = try init(f);
// Perform a write and then a read
var write_buf = [_]u8{ 1, 1, 2, 3, 5, 8, 13 };
var c_write: xev.Completion = undefined;
file.pwrite(&loop, &c_write, .{ .slice = &write_buf }, 0, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback);
// Wait for the write
try loop.run(.until_done);
// Make sure the data is on disk
try f.sync();
const f2 = try std.fs.cwd().openFile(path, .{});
defer f2.close();
const file2 = try init(f2);
var read_buf: [128]u8 = undefined;
var read_len: usize = 0;
file2.pread(&loop, &c_write, .{ .slice = &read_buf }, 0, usize, &read_len, (struct {
fn callback(
ud: ?*usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expectEqualSlices(u8, &write_buf, read_buf[0..read_len]);
}
test "queued writes" {
// wasi: local files don't work with poll (always ready)
if (builtin.os.tag == .wasi) return error.SkipZigTest;
// windows: std.fs.File is not opened with OVERLAPPED flag.
if (builtin.os.tag == .windows) return error.SkipZigTest;
const testing = std.testing;
var tpool = main.ThreadPool.init(.{});
defer tpool.deinit();
defer tpool.shutdown();
var loop = try xev.Loop.init(.{ .thread_pool = &tpool });
defer loop.deinit();
// Create our file
const path = "test_watcher_file";
const f = try std.fs.cwd().createFile(path, .{
.read = true,
.truncate = true,
});
defer f.close();
defer std.fs.cwd().deleteFile(path) catch {};
const file = try init(f);
var write_queue: Self.WriteQueue = .{};
var write_req: [2]Self.WriteRequest = undefined;
// Perform a write and then a read
file.queueWrite(
&loop,
&write_queue,
&write_req[0],
.{ .slice = "1234" },
void,
null,
(struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback,
);
file.queueWrite(
&loop,
&write_queue,
&write_req[1],
.{ .slice = "5678" },
void,
null,
(struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback,
);
// Wait for the write
try loop.run(.until_done);
// Make sure the data is on disk
try f.sync();
const f2 = try std.fs.cwd().openFile(path, .{});
defer f2.close();
const file2 = try init(f2);
// Read
var read_buf: [128]u8 = undefined;
var read_len: usize = 0;
var c_read: xev.Completion = undefined;
file2.read(&loop, &c_read, .{ .slice = &read_buf }, usize, &read_len, (struct {
fn callback(
ud: ?*usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expectEqualSlices(u8, "12345678", read_buf[0..read_len]);
}
};
}

447
deps/libxev/src/watcher/process.zig vendored Normal file
View File

@ -0,0 +1,447 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const linux = std.os.linux;
const posix = std.posix;
const common = @import("common.zig");
/// Process management, such as waiting for process exit.
pub fn Process(comptime xev: type) type {
return switch (xev.backend) {
// Supported, uses pidfd
.io_uring,
.epoll,
=> ProcessPidFd(xev),
.kqueue => ProcessKqueue(xev),
.iocp => ProcessIocp(xev),
// Unsupported
.wasi_poll => struct {},
};
}
/// Process implementation using pidfd (Linux).
fn ProcessPidFd(comptime xev: type) type {
return struct {
const Self = @This();
/// The error that can come in the wait callback.
pub const WaitError = xev.Sys.PollError || error{
InvalidChild,
};
/// pidfd file descriptor
fd: posix.fd_t,
/// Create a new process watcher for the given pid.
pub fn init(pid: posix.pid_t) !Self {
// Note: SOCK_NONBLOCK == PIDFD_NONBLOCK but we should PR that
// over to Zig.
const res = linux.pidfd_open(pid, posix.SOCK.NONBLOCK);
const fd = switch (posix.errno(res)) {
.SUCCESS => @as(posix.fd_t, @intCast(res)),
.INVAL => return error.InvalidArgument,
.MFILE => return error.ProcessFdQuotaExceeded,
.NFILE => return error.SystemFdQuotaExceeded,
.NODEV => return error.SystemResources,
.NOMEM => return error.SystemResources,
else => |err| return posix.unexpectedErrno(err),
};
return .{
.fd = fd,
};
}
/// Clean up the process watcher.
pub fn deinit(self: *Self) void {
std.posix.close(self.fd);
}
/// Wait for the process to exit. This will automatically call
/// `waitpid` or equivalent and report the exit status.
pub fn wait(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!u32,
) xev.CallbackAction,
) void {
const events: u32 = comptime switch (xev.backend) {
.io_uring => posix.POLL.IN,
.epoll => linux.EPOLL.IN,
else => unreachable,
};
c.* = .{
.op = .{
.poll = .{
.fd = self.fd,
.events = events,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const arg: WaitError!u32 = arg: {
// If our poll failed, report that error.
_ = r.poll catch |err| break :arg err;
// We need to wait on the pidfd because it is noted as ready
const fd = c_inner.op.poll.fd;
var info: linux.siginfo_t = undefined;
const res = linux.waitid(.PIDFD, fd, &info, linux.W.EXITED);
break :arg switch (posix.errno(res)) {
.SUCCESS => @as(u32, @intCast(info.fields.common.second.sigchld.status)),
.CHILD => error.InvalidChild,
// The fd isn't ready to read, I guess?
.AGAIN => return .rearm,
else => |err| err: {
std.log.warn("unexpected process wait errno={}", .{err});
break :err error.Unexpected;
},
};
};
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
arg,
});
}
}).callback,
};
loop.add(c);
}
/// Common tests
pub usingnamespace ProcessTests(xev, Self, &.{ "sh", "-c", "exit 0" }, &.{ "sh", "-c", "exit 42" });
};
}
fn ProcessKqueue(comptime xev: type) type {
return struct {
const Self = @This();
/// The error that can come in the wait callback.
pub const WaitError = xev.Sys.ProcError;
/// The pid to watch.
pid: posix.pid_t,
/// Create a new process watcher for the given pid.
pub fn init(pid: posix.pid_t) !Self {
return .{
.pid = pid,
};
}
/// Does nothing for Kqueue.
pub fn deinit(self: *Self) void {
_ = self;
}
/// Wait for the process to exit. This will automatically call
/// `waitpid` or equivalent and report the exit status.
pub fn wait(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!u32,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.proc = .{
.pid = self.pid,
.flags = posix.system.NOTE_EXIT | posix.system.NOTE_EXITSTATUS,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
if (r.proc) |v| v else |err| err,
});
}
}).callback,
};
loop.add(c);
}
/// Common tests
pub usingnamespace ProcessTests(xev, Self, &.{ "sh", "-c", "exit 0" }, &.{ "sh", "-c", "exit 42" });
};
}
const windows = @import("../windows.zig");
fn ProcessIocp(comptime xev: type) type {
return struct {
const Self = @This();
pub const WaitError = xev.Sys.JobObjectError;
job: windows.HANDLE,
process: windows.HANDLE,
pub fn init(process: posix.pid_t) !Self {
const current_process = windows.kernel32.GetCurrentProcess();
// Duplicate the process handle so we don't rely on the caller keeping it alive
var dup_process: windows.HANDLE = undefined;
const dup_result = windows.kernel32.DuplicateHandle(
current_process,
process,
current_process,
&dup_process,
0,
windows.FALSE,
windows.DUPLICATE_SAME_ACCESS,
);
if (dup_result == 0) return windows.unexpectedError(windows.kernel32.GetLastError());
const job = try windows.exp.CreateJobObject(null, null);
errdefer _ = windows.kernel32.CloseHandle(job);
try windows.exp.AssignProcessToJobObject(job, dup_process);
return .{
.job = job,
.process = dup_process,
};
}
pub fn deinit(self: *Self) void {
_ = windows.kernel32.CloseHandle(self.job);
_ = windows.kernel32.CloseHandle(self.process);
}
pub fn wait(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!u32,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.job_object = .{
.job = self.job,
.userdata = self.process,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
if (r.job_object) |result| {
switch (result) {
.associated => {
// There was a period of time between when the job object was created
// and when it was associated with the completion port. We may have
// missed a notification, so check if it's still alive.
var exit_code: windows.DWORD = undefined;
const process: windows.HANDLE = @ptrCast(c_inner.op.job_object.userdata);
const has_code = windows.kernel32.GetExitCodeProcess(process, &exit_code) != 0;
if (!has_code) std.log.warn("unable to get exit code for process={}", .{windows.kernel32.GetLastError()});
if (exit_code == windows.exp.STILL_ACTIVE) return .rearm;
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
exit_code,
});
},
.message => |message| {
const result_inner = switch (message.type) {
.JOB_OBJECT_MSG_EXIT_PROCESS,
.JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS,
=> b: {
const process: windows.HANDLE = @ptrCast(c_inner.op.job_object.userdata);
const pid = windows.exp.kernel32.GetProcessId(process);
if (pid == 0) break :b WaitError.Unexpected;
if (message.value != pid) return .rearm;
var exit_code: windows.DWORD = undefined;
const has_code = windows.kernel32.GetExitCodeProcess(process, &exit_code) != 0;
if (!has_code) std.log.warn("unable to get exit code for process={}", .{windows.kernel32.GetLastError()});
break :b if (has_code) exit_code else WaitError.Unexpected;
},
else => return .rearm,
};
return @call(.always_inline, cb, .{ common.userdataValue(Userdata, ud), l_inner, c_inner, result_inner });
},
}
} else |err| {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
err,
});
}
}
}).callback,
};
loop.add(c);
}
/// Common tests
pub usingnamespace ProcessTests(xev, Self, &.{ "cmd.exe", "/C", "exit 0" }, &.{ "cmd.exe", "/C", "exit 42" });
};
}
fn ProcessTests(
comptime xev: type,
comptime Impl: type,
comptime argv_0: []const []const u8,
comptime argv_42: []const []const u8,
) type {
return struct {
test "process wait" {
const testing = std.testing;
const alloc = testing.allocator;
var child = std.process.Child.init(argv_0, alloc);
try child.spawn();
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var p = try Impl.init(child.id);
defer p.deinit();
// Wait
var code: ?u32 = null;
var c_wait: xev.Completion = undefined;
p.wait(&loop, &c_wait, ?u32, &code, (struct {
fn callback(
ud: ?*?u32,
_: *xev.Loop,
_: *xev.Completion,
r: Impl.WaitError!u32,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Wait for wake
try loop.run(.until_done);
try testing.expectEqual(@as(u32, 0), code.?);
}
test "process wait with non-zero exit code" {
const testing = std.testing;
const alloc = testing.allocator;
var child = std.process.Child.init(argv_42, alloc);
try child.spawn();
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var p = try Impl.init(child.id);
defer p.deinit();
// Wait
var code: ?u32 = null;
var c_wait: xev.Completion = undefined;
p.wait(&loop, &c_wait, ?u32, &code, (struct {
fn callback(
ud: ?*?u32,
_: *xev.Loop,
_: *xev.Completion,
r: Impl.WaitError!u32,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Wait for wake
try loop.run(.until_done);
try testing.expectEqual(@as(u32, 42), code.?);
}
test "process wait on a process that already exited" {
const testing = std.testing;
const alloc = testing.allocator;
var child = std.process.Child.init(argv_0, alloc);
try child.spawn();
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var p = try Impl.init(child.id);
defer p.deinit();
_ = try child.wait();
// Wait
var code: ?u32 = null;
var c_wait: xev.Completion = undefined;
p.wait(&loop, &c_wait, ?u32, &code, (struct {
fn callback(
ud: ?*?u32,
_: *xev.Loop,
_: *xev.Completion,
r: Impl.WaitError!u32,
) xev.CallbackAction {
ud.?.* = r catch 0;
return .disarm;
}
}).callback);
// Wait for wake
try loop.run(.until_done);
try testing.expectEqual(@as(u32, 0), code.?);
}
};
}

822
deps/libxev/src/watcher/stream.zig vendored Normal file
View File

@ -0,0 +1,822 @@
const std = @import("std");
const assert = std.debug.assert;
const builtin = @import("builtin");
const common = @import("common.zig");
const queue = @import("../queue.zig");
/// Options for creating a stream type. Each of the options makes the
/// functionality available for the stream.
pub const Options = struct {
read: ReadMethod,
write: WriteMethod,
close: bool,
/// True to schedule the read/write on the threadpool.
threadpool: bool = false,
pub const ReadMethod = enum { none, read, recv };
pub const WriteMethod = enum { none, write, send };
};
/// Creates a stream type that is meant to be embedded within other
/// types using "usingnamespace". A stream is something that supports read,
/// write, close, etc. The exact operations supported are defined by the
/// "options" struct.
///
/// T requirements:
/// - field named "fd" of type fd_t or socket_t
/// - decl named "initFd" to initialize a new T from a fd
///
pub fn Stream(comptime xev: type, comptime T: type, comptime options: Options) type {
return struct {
pub usingnamespace if (options.close) Closeable(xev, T, options) else struct {};
pub usingnamespace if (options.read != .none) Readable(xev, T, options) else struct {};
pub usingnamespace if (options.write != .none) Writeable(xev, T, options) else struct {};
};
}
pub fn Closeable(comptime xev: type, comptime T: type, comptime options: Options) type {
_ = options;
return struct {
const Self = T;
pub const CloseError = xev.CloseError;
/// Close the socket.
pub fn close(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
r: CloseError!void,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{ .close = .{ .fd = self.fd } },
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const fd = T.initFd(c_inner.op.close.fd);
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
fd,
if (r.close) |_| {} else |err| err,
});
}
}).callback,
};
loop.add(c);
}
};
}
pub fn Readable(comptime xev: type, comptime T: type, comptime options: Options) type {
return struct {
const Self = T;
pub const ReadError = xev.ReadError;
/// Read from the socket. This performs a single read. The callback must
/// requeue the read if additional reads want to be performed. Additional
/// reads simultaneously can be queued by calling this multiple times. Note
/// that depending on the backend, the reads can happen out of order.
pub fn read(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
buf: xev.ReadBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
b: xev.ReadBuffer,
r: ReadError!usize,
) xev.CallbackAction,
) void {
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = switch (options.read) {
.none => unreachable,
.read => .{
.read = .{
.fd = self.fd,
.buffer = buf,
},
},
.recv => .{
.recv = .{
.fd = self.fd,
.buffer = buf,
},
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return switch (options.read) {
.none => unreachable,
.recv => @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
T.initFd(c_inner.op.recv.fd),
c_inner.op.recv.buffer,
if (r.recv) |v| v else |err| err,
}),
.read => @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
T.initFd(c_inner.op.read.fd),
c_inner.op.read.buffer,
if (r.read) |v| v else |err| err,
}),
};
}
}).callback,
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.wasi_poll,
.iocp,
=> {},
.epoll => {
if (options.threadpool)
c.flags.threadpool = true
else
c.flags.dup = true;
},
.kqueue => {
if (options.threadpool) c.flags.threadpool = true;
},
}
loop.add(c);
},
}
}
};
}
pub fn Writeable(comptime xev: type, comptime T: type, comptime options: Options) type {
return struct {
const Self = T;
pub const WriteError = xev.WriteError;
/// WriteQueue is the queue of write requests for ordered writes.
/// This can be copied around.
pub const WriteQueue = queue.Intrusive(WriteRequest);
/// WriteRequest is a single request for a write. It wraps a
/// completion so that it can be inserted into the WriteQueue.
pub const WriteRequest = struct {
completion: xev.Completion = .{},
userdata: ?*anyopaque = null,
/// This is the original buffer passed to queueWrite. We have
/// to keep track of this because we may be forced to split
/// the write or rearm the write due to partial writes, but when
/// we call the final callback we want to pass the original
/// complete buffer.
full_write_buffer: xev.WriteBuffer,
next: ?*@This() = null,
/// This can be used to convert a completion pointer back to
/// a WriteRequest. This is only safe of course if the completion
/// originally is from a write request. This is useful for getting
/// the WriteRequest back in a callback from queuedWrite.
pub fn from(c: *xev.Completion) *WriteRequest {
return @fieldParentPtr("completion", c);
}
};
/// Write to the stream. This queues the writes to ensure they
/// remain in order. Queueing has a small overhead: you must
/// maintain a WriteQueue and WriteRequests instead of just
/// Completions.
///
/// If ordering isn't important, or you can maintain ordering
/// naturally in your program, consider using write since it
/// has a slightly smaller overhead.
///
/// The "CallbackAction" return value of this callback behaves slightly
/// different. The "rearm" return value will re-queue the same write
/// at the end of the queue.
///
/// It is safe to call this at anytime from the main thread.
pub fn queueWrite(
self: Self,
loop: *xev.Loop,
q: *WriteQueue,
req: *WriteRequest,
buf: xev.WriteBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
b: xev.WriteBuffer,
r: WriteError!usize,
) xev.CallbackAction,
) void {
// Initialize our completion
req.* = .{ .full_write_buffer = buf };
// Must be kept in sync with partial write logic inside the callback
self.write_init(&req.completion, buf);
req.completion.userdata = q;
req.completion.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const q_inner = @as(?*WriteQueue, @ptrCast(@alignCast(ud))).?;
// The queue MUST have a request because a completion
// can only be added if the queue is not empty, and
// nothing else should be popping!.
//
// We only peek the request here (not pop) because we may
// need to rearm this write if the write was partial.
const req_inner: *WriteRequest = q_inner.head.?;
const cb_res = write_result(c_inner, r);
var result: WriteError!usize = cb_res.result;
// Checks whether the entire buffer was written, this is
// necessary to guarantee correct ordering of writes.
// If the write was partial, it re-submits the remainder of
// the buffer.
const queued_len = writeBufferLength(cb_res.buf);
if (cb_res.result) |written_len| {
if (written_len < queued_len) {
// Write remainder of the buffer, reusing the same completion
const rem_buf = writeBufferRemainder(cb_res.buf, written_len);
cb_res.writer.write_init(&req_inner.completion, rem_buf);
req_inner.completion.userdata = q_inner;
req_inner.completion.callback = callback;
l_inner.add(&req_inner.completion);
return .disarm;
}
// We wrote the entire buffer, modify the result to indicate
// to the caller that all bytes have been written.
result = writeBufferLength(req_inner.full_write_buffer);
} else |_| {}
// We can pop previously peeked request.
_ = q_inner.pop().?;
const action = @call(.always_inline, cb, .{
common.userdataValue(Userdata, req_inner.userdata),
l_inner,
c_inner,
cb_res.writer,
req_inner.full_write_buffer,
result,
});
// Rearm requeues this request, it doesn't return rearm
// on the actual callback here...
if (action == .rearm) q_inner.push(req_inner);
// If we have another request, add that completion next.
if (q_inner.head) |req_next| l_inner.add(&req_next.completion);
// We always disarm because the completion in the next
// request will be used if there is more to queue.
return .disarm;
}
}).callback;
// The userdata as to go on the WriteRequest because we need
// our actual completion userdata to be the WriteQueue so that
// we can process the queue.
req.userdata = @as(?*anyopaque, @ptrCast(@alignCast(userdata)));
// If the queue is empty, then we add our completion. Otherwise,
// the previously queued writes will trigger this one.
if (q.empty()) loop.add(&req.completion);
// We always add this item to our queue no matter what
q.push(req);
}
/// Write to the stream. This performs a single write. Additional
/// writes can be requested by calling this multiple times.
///
/// IMPORTANT: writes are NOT queued. There is no order guarantee
/// if this is called multiple times. If ordered writes are important
/// (they usually are!) then you should only call write again once
/// the previous write callback is called.
///
/// If ordering is important, use queueWrite instead.
pub fn write(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
buf: xev.WriteBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
b: xev.WriteBuffer,
r: WriteError!usize,
) xev.CallbackAction,
) void {
self.write_init(c, buf);
c.userdata = userdata;
c.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const cb_res = write_result(c_inner, r);
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
cb_res.writer,
cb_res.buf,
cb_res.result,
});
}
}).callback;
loop.add(c);
}
/// Extracts the result from a completion for a write callback.
inline fn write_result(c: *xev.Completion, r: xev.Result) struct {
writer: Self,
buf: xev.WriteBuffer,
result: WriteError!usize,
} {
return switch (options.write) {
.none => unreachable,
.send => .{
.writer = T.initFd(c.op.send.fd),
.buf = c.op.send.buffer,
.result = if (r.send) |v| v else |err| err,
},
.write => .{
.writer = T.initFd(c.op.write.fd),
.buf = c.op.write.buffer,
.result = if (r.write) |v| v else |err| err,
},
};
}
/// Initialize the completion c for a write. This does NOT set
/// userdata or a callback.
fn write_init(
self: Self,
c: *xev.Completion,
buf: xev.WriteBuffer,
) void {
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = switch (options.write) {
.none => unreachable,
.write => .{
.write = .{
.fd = self.fd,
.buffer = buf,
},
},
.send => .{
.send = .{
.fd = self.fd,
.buffer = buf,
},
},
},
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.wasi_poll,
.iocp,
=> {},
.epoll => {
if (options.threadpool) {
c.flags.threadpool = true;
} else {
c.flags.dup = true;
}
},
.kqueue => {
if (options.threadpool) c.flags.threadpool = true;
},
}
},
}
}
/// Returns the length of the write buffer
fn writeBufferLength(buf: xev.WriteBuffer) usize {
return switch (buf) {
.slice => |slice| slice.len,
.array => |array| array.len,
};
}
/// Given a `WriteBuffer` and number of bytes written during the previous
/// write operation, returns a new `WriteBuffer` with remaining data.
fn writeBufferRemainder(buf: xev.WriteBuffer, offset: usize) xev.WriteBuffer {
switch (buf) {
.slice => |slice| {
assert(offset <= slice.len);
return .{ .slice = slice[offset..] };
},
.array => |array| {
assert(offset <= array.len);
const rem_len = array.len - offset;
var wb = xev.WriteBuffer{ .array = .{
.array = undefined,
.len = rem_len,
} };
@memcpy(
wb.array.array[0..rem_len],
array.array[offset..][0..rem_len],
);
return wb;
},
}
}
};
}
/// Creates a generic stream type that supports read, write, close. This
/// can be used for any file descriptor that would exhibit normal blocking
/// behavior on read/write. This should NOT be used for local files because
/// local files have some special properties; you should use xev.File for that.
pub fn GenericStream(comptime xev: type) type {
return struct {
const Self = @This();
/// The underlying file
fd: std.posix.fd_t,
pub usingnamespace Stream(xev, Self, .{
.close = true,
.read = .read,
.write = .write,
});
/// Initialize a generic stream from a file descriptor.
pub fn initFd(fd: std.posix.fd_t) Self {
return .{
.fd = fd,
};
}
/// Clean up any watcher resources. This does NOT close the file.
/// If you want to close the file you must call close or do so
/// synchronously.
pub fn deinit(self: *const Self) void {
_ = self;
}
test "pty: child to parent" {
const testing = std.testing;
switch (builtin.os.tag) {
.linux, .macos => {},
else => return error.SkipZigTest,
}
// Create the pty parent/child side.
var pty = try Pty.init();
defer pty.deinit();
var loop = try xev.Loop.init(.{});
defer loop.deinit();
const parent = initFd(pty.parent);
const child = initFd(pty.child);
// Read
var read_buf: [128]u8 = undefined;
var read_len: ?usize = null;
var c_read: xev.Completion = undefined;
parent.read(&loop, &c_read, .{ .slice = &read_buf }, ?usize, &read_len, (struct {
fn callback(
ud: ?*?usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// This should not block!
try loop.run(.no_wait);
try testing.expect(read_len == null);
// Send
const send_buf = "hello, world!";
var c_write: xev.Completion = undefined;
child.write(&loop, &c_write, .{ .slice = send_buf }, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
c: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = c;
_ = r catch unreachable;
return .disarm;
}
}).callback);
// The write and read should trigger
try loop.run(.until_done);
try testing.expect(read_len != null);
try testing.expectEqualSlices(u8, send_buf, read_buf[0..read_len.?]);
}
test "pty: parent to child" {
const testing = std.testing;
switch (builtin.os.tag) {
.linux, .macos => {},
else => return error.SkipZigTest,
}
// Create the pty parent/child side.
var pty = try Pty.init();
defer pty.deinit();
var loop = try xev.Loop.init(.{});
defer loop.deinit();
const parent = initFd(pty.parent);
const child = initFd(pty.child);
// Read
var read_buf: [128]u8 = undefined;
var read_len: ?usize = null;
var c_read: xev.Completion = undefined;
child.read(&loop, &c_read, .{ .slice = &read_buf }, ?usize, &read_len, (struct {
fn callback(
ud: ?*?usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// This should not block!
try loop.run(.no_wait);
try testing.expect(read_len == null);
// Send (note the newline at the end of the buf is important
// since we're in cooked mode)
const send_buf = "hello, world!\n";
var c_write: xev.Completion = undefined;
parent.write(&loop, &c_write, .{ .slice = send_buf }, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
c: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = c;
_ = r catch unreachable;
return .disarm;
}
}).callback);
// The write and read should trigger
try loop.run(.until_done);
try testing.expect(read_len != null);
try testing.expectEqualSlices(u8, send_buf, read_buf[0..read_len.?]);
}
test "pty: queued writes" {
const testing = std.testing;
switch (builtin.os.tag) {
.linux, .macos => {},
else => return error.SkipZigTest,
}
// Create the pty parent/child side.
var pty = try Pty.init();
defer pty.deinit();
var loop = try xev.Loop.init(.{});
defer loop.deinit();
const parent = initFd(pty.parent);
const child = initFd(pty.child);
// Read
var read_buf: [128]u8 = undefined;
var read_len: ?usize = null;
var c_read: xev.Completion = undefined;
child.read(&loop, &c_read, .{ .slice = &read_buf }, ?usize, &read_len, (struct {
fn callback(
ud: ?*?usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// This should not block!
try loop.run(.no_wait);
try testing.expect(read_len == null);
var write_queue: Self.WriteQueue = .{};
var write_req: [2]Self.WriteRequest = undefined;
// Send (note the newline at the end of the buf is important
// since we're in cooked mode)
parent.queueWrite(
&loop,
&write_queue,
&write_req[0],
.{ .slice = "hello, " },
void,
null,
(struct {
fn callback(
_: ?*void,
_: *xev.Loop,
c: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = c;
_ = r catch unreachable;
return .disarm;
}
}).callback,
);
var c_result: ?*xev.Completion = null;
parent.queueWrite(
&loop,
&write_queue,
&write_req[1],
.{ .slice = "world!\n" },
?*xev.Completion,
&c_result,
(struct {
fn callback(
ud: ?*?*xev.Completion,
_: *xev.Loop,
c: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = c;
return .disarm;
}
}).callback,
);
// The write and read should trigger
try loop.run(.until_done);
try testing.expect(read_len != null);
try testing.expectEqualSlices(u8, "hello, world!\n", read_buf[0..read_len.?]);
// Verify our completion is equal to our request
try testing.expect(Self.WriteRequest.from(c_result.?) == &write_req[1]);
}
};
}
/// Helper to open a pty. This isn't exposed as a public API this is only
/// used for tests.
const Pty = struct {
/// The file descriptors for the parent/child side of the pty. This refers
/// to the master/slave side respectively, and while that terminology is
/// the officially used terminology of the syscall, I will use parent/child
/// here.
parent: std.posix.fd_t,
child: std.posix.fd_t,
/// Redeclare this winsize struct so we can just use a Zig struct. This
/// layout should be correct on all tested platforms.
const Winsize = extern struct {
ws_row: u16,
ws_col: u16,
ws_xpixel: u16,
ws_ypixel: u16,
};
// libc pty.h
extern "c" fn openpty(
parent: *std.posix.fd_t,
child: *std.posix.fd_t,
name: ?[*]u8,
termios: ?*const anyopaque, // termios but we don't use it
winsize: ?*const Winsize,
) c_int;
pub fn init() !Pty {
// Reasonable size
var size: Winsize = .{
.ws_row = 80,
.ws_col = 80,
.ws_xpixel = 800,
.ws_ypixel = 600,
};
var parent_fd: std.posix.fd_t = undefined;
var child_fd: std.posix.fd_t = undefined;
if (openpty(
&parent_fd,
&child_fd,
null,
null,
&size,
) < 0)
return error.OpenptyFailed;
errdefer {
_ = std.posix.system.close(parent_fd);
_ = std.posix.system.close(child_fd);
}
return .{
.parent = parent_fd,
.child = child_fd,
};
}
pub fn deinit(self: *Pty) void {
std.posix.close(self.parent);
std.posix.close(self.child);
}
};

624
deps/libxev/src/watcher/tcp.zig vendored Normal file
View File

@ -0,0 +1,624 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const posix = std.posix;
const stream = @import("stream.zig");
const common = @import("common.zig");
/// TCP client and server.
///
/// This is a "higher-level abstraction" in libxev. The goal of higher-level
/// abstractions in libxev are to make it easier to use specific functionality
/// with the event loop, but does not promise perfect flexibility or optimal
/// performance. In almost all cases, the abstraction is good enough. But,
/// if you have specific needs or want to push for the most optimal performance,
/// use the platform-specific Loop directly.
pub fn TCP(comptime xev: type) type {
return struct {
const Self = @This();
const FdType = if (xev.backend == .iocp) std.os.windows.HANDLE else posix.socket_t;
fd: FdType,
pub usingnamespace stream.Stream(xev, Self, .{
.close = true,
.read = .recv,
.write = .send,
});
/// Initialize a new TCP with the family from the given address. Only
/// the family is used, the actual address has no impact on the created
/// resource.
pub fn init(addr: std.net.Address) !Self {
if (xev.backend == .wasi_poll) @compileError("unsupported in WASI");
const fd = if (xev.backend == .iocp)
try std.os.windows.WSASocketW(addr.any.family, posix.SOCK.STREAM, 0, null, 0, std.os.windows.ws2_32.WSA_FLAG_OVERLAPPED)
else fd: {
// On io_uring we don't use non-blocking sockets because we may
// just get EAGAIN over and over from completions.
const flags = flags: {
var flags: u32 = posix.SOCK.STREAM | posix.SOCK.CLOEXEC;
if (xev.backend != .io_uring) flags |= posix.SOCK.NONBLOCK;
break :flags flags;
};
break :fd try posix.socket(addr.any.family, flags, 0);
};
return .{
.fd = fd,
};
}
/// Initialize a TCP socket from a file descriptor.
pub fn initFd(fd: FdType) Self {
return .{
.fd = fd,
};
}
/// Bind the address to the socket.
pub fn bind(self: Self, addr: std.net.Address) !void {
if (xev.backend == .wasi_poll) @compileError("unsupported in WASI");
const fd = if (xev.backend == .iocp) @as(std.os.windows.ws2_32.SOCKET, @ptrCast(self.fd)) else self.fd;
try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(fd, &addr.any, addr.getOsSockLen());
}
/// Listen for connections on the socket. This puts the socket into passive
/// listening mode. Connections must still be accepted one at a time.
pub fn listen(self: Self, backlog: u31) !void {
if (xev.backend == .wasi_poll) @compileError("unsupported in WASI");
const fd = if (xev.backend == .iocp) @as(std.os.windows.ws2_32.SOCKET, @ptrCast(self.fd)) else self.fd;
try posix.listen(fd, backlog);
}
/// Accept a single connection.
pub fn accept(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: AcceptError!Self,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.accept = .{
.socket = self.fd,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
if (r.accept) |fd| initFd(fd) else |err| err,
});
}
}).callback,
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.kqueue,
.wasi_poll,
.iocp,
=> {},
.epoll => c.flags.dup = true,
}
loop.add(c);
}
/// Establish a connection as a client.
pub fn connect(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
addr: std.net.Address,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
r: ConnectError!void,
) xev.CallbackAction,
) void {
if (xev.backend == .wasi_poll) @compileError("unsupported in WASI");
c.* = .{
.op = .{
.connect = .{
.socket = self.fd,
.addr = addr,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
initFd(c_inner.op.connect.socket),
if (r.connect) |_| {} else |err| err,
});
}
}).callback,
};
loop.add(c);
}
/// Shutdown the socket. This always only shuts down the writer side. You
/// can use the lower level interface directly to control this if the
/// platform supports it.
pub fn shutdown(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
r: ShutdownError!void,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.shutdown = .{
.socket = self.fd,
.how = .send,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
initFd(c_inner.op.shutdown.socket),
if (r.shutdown) |_| {} else |err| err,
});
}
}).callback,
};
loop.add(c);
}
pub const AcceptError = xev.AcceptError;
pub const ConnectError = xev.ConnectError;
pub const ShutdownError = xev.ShutdownError;
test "TCP: accept/connect/send/recv/close" {
// We have no way to get a socket in WASI from a WASI context.
if (xev.backend == .wasi_poll) return error.SkipZigTest;
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
// Choose random available port (Zig #14907)
var address = try std.net.Address.parseIp4("127.0.0.1", 0);
const server = try Self.init(address);
// Bind and listen
try server.bind(address);
try server.listen(1);
// Retrieve bound port and initialize client
var sock_len = address.getOsSockLen();
const fd = if (xev.backend == .iocp) @as(std.os.windows.ws2_32.SOCKET, @ptrCast(server.fd)) else server.fd;
try posix.getsockname(fd, &address.any, &sock_len);
const client = try Self.init(address);
//const address = try std.net.Address.parseIp4("127.0.0.1", 3132);
//var server = try Self.init(address);
//var client = try Self.init(address);
// Completions we need
var c_accept: xev.Completion = undefined;
var c_connect: xev.Completion = undefined;
// Accept
var server_conn: ?Self = null;
server.accept(&loop, &c_accept, ?Self, &server_conn, (struct {
fn callback(
ud: ?*?Self,
_: *xev.Loop,
_: *xev.Completion,
r: AcceptError!Self,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Connect
var connected: bool = false;
client.connect(&loop, &c_connect, address, bool, &connected, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: ConnectError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
// Wait for the connection to be established
try loop.run(.until_done);
try testing.expect(server_conn != null);
try testing.expect(connected);
// Close the server
var server_closed = false;
server.close(&loop, &c_accept, bool, &server_closed, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: Self.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expect(server_closed);
// Send
var send_buf = [_]u8{ 1, 1, 2, 3, 5, 8, 13 };
client.write(&loop, &c_connect, .{ .slice = &send_buf }, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
c: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = c;
_ = r catch unreachable;
return .disarm;
}
}).callback);
// Receive
var recv_buf: [128]u8 = undefined;
var recv_len: usize = 0;
server_conn.?.read(&loop, &c_accept, .{ .slice = &recv_buf }, usize, &recv_len, (struct {
fn callback(
ud: ?*usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Wait for the send/receive
try loop.run(.until_done);
try testing.expectEqualSlices(u8, &send_buf, recv_buf[0..recv_len]);
// Close
server_conn.?.close(&loop, &c_accept, ?Self, &server_conn, (struct {
fn callback(
ud: ?*?Self,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: Self.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = null;
return .disarm;
}
}).callback);
client.close(&loop, &c_connect, bool, &connected, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: Self.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = false;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expect(server_conn == null);
try testing.expect(!connected);
try testing.expect(server_closed);
}
// Potentially flaky - this test could hang if the sender is unable to
// write everything to the socket for whatever reason
// (e.g. incorrectly sized buffer on the receiver side), or if the
// receiver is trying to receive while sender has nothing left to send.
//
// Overview:
// 1. Set up server and client sockets
// 2. connect & accept, set SO_SNDBUF to 8kB on the client
// 3. Try to send 1MB buffer from client to server without queuing, this _should_ fail
// and theoretically send <= 8kB, but in practice, it seems to write ~32kB.
// Asserts that <= 100kB was written
// 4. Set up a queued write with the remaining buffer, shutdown() the socket afterwards
// 5. Set up a receiver that loops until it receives the entire buffer
// 6. Assert send_buf == recv_buf
test "TCP: Queued writes" {
// We have no way to get a socket in WASI from a WASI context.
if (xev.backend == .wasi_poll) return error.SkipZigTest;
// Windows doesn't seem to respect the SNDBUF socket option.
if (builtin.os.tag == .windows) return error.SkipZigTest;
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
// Choose random available port (Zig #14907)
var address = try std.net.Address.parseIp4("127.0.0.1", 0);
const server = try Self.init(address);
// Bind and listen
try server.bind(address);
try server.listen(1);
// Retrieve bound port and initialize client
var sock_len = address.getOsSockLen();
try posix.getsockname(server.fd, &address.any, &sock_len);
const client = try Self.init(address);
// Completions we need
var c_accept: xev.Completion = undefined;
var c_connect: xev.Completion = undefined;
// Accept
var server_conn: ?Self = null;
server.accept(&loop, &c_accept, ?Self, &server_conn, (struct {
fn callback(
ud: ?*?Self,
_: *xev.Loop,
_: *xev.Completion,
r: AcceptError!Self,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Connect
var connected: bool = false;
client.connect(&loop, &c_connect, address, bool, &connected, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: ConnectError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
// Wait for the connection to be established
try loop.run(.until_done);
try testing.expect(server_conn != null);
try testing.expect(connected);
// Close the server
var server_closed = false;
server.close(&loop, &c_accept, bool, &server_closed, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: Self.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expect(server_closed);
// Unqueued send - Limit send buffer to 8kB, this should force partial writes.
try posix.setsockopt(client.fd, posix.SOL.SOCKET, posix.SO.SNDBUF, &std.mem.toBytes(@as(c_int, 8192)));
const send_buf = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 } ** 100_000;
var sent_unqueued: usize = 0;
// First we try to send the whole 1MB buffer in one write operation, this _should_ result
// in a partial write.
client.write(&loop, &c_connect, .{ .slice = &send_buf }, usize, &sent_unqueued, (struct {
fn callback(
sent_unqueued_inner: ?*usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
sent_unqueued_inner.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Make sure that we sent a small fraction of the buffer
try loop.run(.until_done);
// SO_SNDBUF doesn't seem to be respected exactly, sent_unqueued will often be ~32kB
// even though SO_SNDBUF was set to 8kB
try testing.expect(sent_unqueued < (send_buf.len / 10));
// Set up queued write
var w_queue = Self.WriteQueue{};
var wr_send: xev.TCP.WriteRequest = undefined;
var sent_queued: usize = 0;
const queued_slice = send_buf[sent_unqueued..];
client.queueWrite(&loop, &w_queue, &wr_send, .{ .slice = queued_slice }, usize, &sent_queued, (struct {
fn callback(
sent_queued_inner: ?*usize,
l: *xev.Loop,
c: *xev.Completion,
tcp: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
sent_queued_inner.?.* = r catch unreachable;
tcp.shutdown(l, c, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: Self.ShutdownError!void,
) xev.CallbackAction {
return .disarm;
}
}).callback);
return .disarm;
}
}).callback);
// Set up receiver which is going to keep reading until it reads the full
// send buffer
const Receiver = struct {
loop: *xev.Loop,
conn: Self,
completion: xev.Completion = .{},
buf: [send_buf.len]u8 = undefined,
bytes_read: usize = 0,
pub fn read(receiver: *@This()) void {
if (receiver.bytes_read == receiver.buf.len) return;
const read_buf = xev.ReadBuffer{
.slice = receiver.buf[receiver.bytes_read..],
};
receiver.conn.read(receiver.loop, &receiver.completion, read_buf, @This(), receiver, readCb);
}
pub fn readCb(
receiver_opt: ?*@This(),
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
var receiver = receiver_opt.?;
const n_bytes = r catch unreachable;
receiver.bytes_read += n_bytes;
if (receiver.bytes_read < send_buf.len) {
receiver.read();
}
return .disarm;
}
};
var receiver = Receiver{
.loop = &loop,
.conn = server_conn.?,
};
receiver.read();
// Wait for the send/receive
try loop.run(.until_done);
try testing.expectEqualSlices(u8, &send_buf, receiver.buf[0..receiver.bytes_read]);
try testing.expect(send_buf.len == sent_unqueued + sent_queued);
// Close
server_conn.?.close(&loop, &c_accept, ?Self, &server_conn, (struct {
fn callback(
ud: ?*?Self,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: Self.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = null;
return .disarm;
}
}).callback);
client.close(&loop, &c_connect, bool, &connected, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: Self.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = false;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expect(server_conn == null);
try testing.expect(!connected);
try testing.expect(server_closed);
}
};
}

411
deps/libxev/src/watcher/timer.zig vendored Normal file
View File

@ -0,0 +1,411 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const os = std.os;
/// A timer fires a callback after a specified amount of time. A timer can
/// repeat by returning "rearm" in the callback or by rescheduling the
/// start within the callback.
pub fn Timer(comptime xev: type) type {
return struct {
const Self = @This();
/// Create a new timer.
pub fn init() !Self {
return .{};
}
pub fn deinit(self: *const Self) void {
// Nothing for now.
_ = self;
}
/// Start the timer. The timer will execute in next_ms milliseconds from
/// now.
///
/// This will use the monotonic clock on your system if available so
/// this is immune to system clock changes or drift. The callback is
/// guaranteed to fire NO EARLIER THAN "next_ms" milliseconds. We can't
/// make any guarantees about exactness or time bounds because its possible
/// for your OS to just... pause.. the process for an indefinite period of
/// time.
///
/// Like everything else in libxev, if you want something to repeat, you
/// must then requeue the completion manually. This punts off one of the
/// "hard" aspects of timers: it is up to you to determine what the semantic
/// meaning of intervals are. For example, if you want a timer to repeat every
/// 10 seconds, is it every 10th second of a wall clock? every 10th second
/// after an invocation? every 10th second after the work time from the
/// invocation? You have the power to answer these questions, manually.
pub fn run(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
next_ms: u64,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: RunError!void,
) xev.CallbackAction,
) void {
_ = self;
loop.timer(c, next_ms, userdata, (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
@as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud))),
l_inner,
c_inner,
if (r.timer) |trigger| @as(RunError!void, switch (trigger) {
.request, .expiration => {},
.cancel => error.Canceled,
}) else |err| err,
});
}
}).callback);
}
/// Reset a timer to execute in next_ms milliseconds. If the timer
/// is already started, this will stop it and restart it. If the
/// timer has never been started, this is equivalent to running "run".
/// In every case, the timer callback is updated to the given userdata
/// and callback.
///
/// This requires an additional completion c_cancel to represent
/// the need to possibly cancel the previous timer. You can check
/// if c_cancel was used by checking the state() after the call.
///
/// VERY IMPORTANT: both c and c_cancel MUST NOT be undefined. They
/// must be initialized to ".{}" if being used for the first time.
pub fn reset(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
c_cancel: *xev.Completion,
next_ms: u64,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: RunError!void,
) xev.CallbackAction,
) void {
_ = self;
loop.timer_reset(c, c_cancel, next_ms, userdata, (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
@as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud))),
l_inner,
c_inner,
if (r.timer) |trigger| @as(RunError!void, switch (trigger) {
.request, .expiration => {},
.cancel => error.Canceled,
}) else |err| err,
});
}
}).callback);
}
/// Cancel a previously started timer. The timer to cancel used the completion
/// "c_cancel". A new completion "c" must be specified which will be called
/// with the callback once cancellation is complete.
///
/// The original timer will still have its callback fired but with the
/// error "error.Canceled".
pub fn cancel(
self: Self,
loop: *xev.Loop,
c_timer: *xev.Completion,
c_cancel: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: CancelError!void,
) xev.CallbackAction,
) void {
_ = self;
c_cancel.* = switch (xev.backend) {
.io_uring => .{
.op = .{
.timer_remove = .{
.timer = c_timer,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
@as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud))),
l_inner,
c_inner,
if (r.timer_remove) |_| {} else |err| err,
});
}
}).callback,
},
.epoll,
.kqueue,
.wasi_poll,
.iocp,
=> .{
.op = .{
.cancel = .{
.c = c_timer,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
@as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud))),
l_inner,
c_inner,
if (r.cancel) |_| {} else |err| err,
});
}
}).callback,
},
};
loop.add(c_cancel);
}
/// Error that could happen while running a timer.
pub const RunError = error{
/// The timer was canceled before it could expire
Canceled,
/// Some unexpected error.
Unexpected,
};
pub const CancelError = xev.CancelError;
test "timer" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var timer = try init();
defer timer.deinit();
// Add the timer
var called = false;
var c1: xev.Completion = undefined;
timer.run(&loop, &c1, 1, bool, &called, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: RunError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
// Wait
try loop.run(.until_done);
try testing.expect(called);
}
test "timer reset" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var timer = try init();
defer timer.deinit();
var c_timer: xev.Completion = .{};
var c_cancel: xev.Completion = .{};
const cb = (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: RunError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback;
// Add the timer
var canceled = false;
timer.run(&loop, &c_timer, 100_000, bool, &canceled, cb);
// Wait
try loop.run(.no_wait);
try testing.expect(!canceled);
// Reset it
timer.reset(&loop, &c_timer, &c_cancel, 1, bool, &canceled, cb);
try loop.run(.until_done);
try testing.expect(canceled);
try testing.expect(c_timer.state() == .dead);
try testing.expect(c_cancel.state() == .dead);
}
test "timer reset before tick" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var timer = try init();
defer timer.deinit();
var c_timer: xev.Completion = .{};
var c_cancel: xev.Completion = .{};
const cb = (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: RunError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback;
// Add the timer
var canceled = false;
timer.run(&loop, &c_timer, 100_000, bool, &canceled, cb);
// Reset it
timer.reset(&loop, &c_timer, &c_cancel, 1, bool, &canceled, cb);
try loop.run(.until_done);
try testing.expect(canceled);
try testing.expect(c_timer.state() == .dead);
try testing.expect(c_cancel.state() == .dead);
}
test "timer reset after trigger" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var timer = try init();
defer timer.deinit();
var c_timer: xev.Completion = .{};
var c_cancel: xev.Completion = .{};
const cb = (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: RunError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback;
// Add the timer
var canceled = false;
timer.run(&loop, &c_timer, 1, bool, &canceled, cb);
try loop.run(.until_done);
try testing.expect(canceled);
canceled = false;
// Reset it
timer.reset(&loop, &c_timer, &c_cancel, 1, bool, &canceled, cb);
try loop.run(.until_done);
try testing.expect(canceled);
try testing.expect(c_timer.state() == .dead);
try testing.expect(c_cancel.state() == .dead);
}
test "timer cancel" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var timer = try init();
defer timer.deinit();
// Add the timer
var canceled = false;
var c1: xev.Completion = undefined;
timer.run(&loop, &c1, 100_000, bool, &canceled, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: RunError!void,
) xev.CallbackAction {
ud.?.* = if (r) false else |err| err == error.Canceled;
return .disarm;
}
}).callback);
// Cancel
var cancel_confirm = false;
var c2: xev.Completion = undefined;
timer.cancel(&loop, &c1, &c2, bool, &cancel_confirm, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: CancelError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
// Wait
try loop.run(.until_done);
try testing.expect(canceled);
try testing.expect(cancel_confirm);
}
};
}

770
deps/libxev/src/watcher/udp.zig vendored Normal file
View File

@ -0,0 +1,770 @@
const std = @import("std");
const assert = std.debug.assert;
const posix = std.posix;
const stream = @import("stream.zig");
const common = @import("common.zig");
/// UDP client and server.
///
/// This is a "higher-level abstraction" in libxev. The goal of higher-level
/// abstractions in libxev are to make it easier to use specific functionality
/// with the event loop, but does not promise perfect flexibility or optimal
/// performance. In almost all cases, the abstraction is good enough. But,
/// if you have specific needs or want to push for the most optimal performance,
/// use the platform-specific Loop directly.
pub fn UDP(comptime xev: type) type {
return switch (xev.backend) {
// Supported, uses sendmsg/recvmsg exclusively
.io_uring,
.epoll,
=> UDPSendMsg(xev),
// Supported, uses sendto/recvfrom
.kqueue => UDPSendto(xev),
// Supported with tweaks
.iocp => UDPSendtoIOCP(xev),
// Noop
.wasi_poll => struct {},
};
}
/// UDP implementation that uses sendto/recvfrom.
fn UDPSendto(comptime xev: type) type {
return struct {
const Self = @This();
fd: posix.socket_t,
/// See UDPSendMsg.State
pub const State = struct {
userdata: ?*anyopaque,
};
pub usingnamespace stream.Stream(xev, Self, .{
.close = true,
.read = .none,
.write = .none,
});
/// Initialize a new UDP with the family from the given address. Only
/// the family is used, the actual address has no impact on the created
/// resource.
pub fn init(addr: std.net.Address) !Self {
return .{
.fd = try posix.socket(
addr.any.family,
posix.SOCK.NONBLOCK | posix.SOCK.DGRAM | posix.SOCK.CLOEXEC,
0,
),
};
}
/// Initialize a UDP socket from a file descriptor.
pub fn initFd(fd: posix.socket_t) Self {
return .{
.fd = fd,
};
}
/// Bind the address to the socket.
pub fn bind(self: Self, addr: std.net.Address) !void {
try posix.setsockopt(self.fd, posix.SOL.SOCKET, posix.SO.REUSEPORT, &std.mem.toBytes(@as(c_int, 1)));
try posix.setsockopt(self.fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(self.fd, &addr.any, addr.getOsSockLen());
}
/// Read from the socket. This performs a single read. The callback must
/// requeue the read if additional reads want to be performed. Additional
/// reads simultaneously can be queued by calling this multiple times. Note
/// that depending on the backend, the reads can happen out of order.
pub fn read(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
s: *State,
buf: xev.ReadBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: *State,
addr: std.net.Address,
s: Self,
b: xev.ReadBuffer,
r: ReadError!usize,
) xev.CallbackAction,
) void {
s.* = .{
.userdata = userdata,
};
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = .{
.recvfrom = .{
.fd = self.fd,
.buffer = buf,
},
},
.userdata = s,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const s_inner = @as(?*State, @ptrCast(@alignCast(ud))).?;
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, s_inner.userdata),
l_inner,
c_inner,
s_inner,
std.net.Address.initPosix(@alignCast(&c_inner.op.recvfrom.addr)),
initFd(c_inner.op.recvfrom.fd),
c_inner.op.recvfrom.buffer,
r.recvfrom,
});
}
}).callback,
};
loop.add(c);
},
}
}
/// Write to the socket. This performs a single write. Additional writes
/// can be queued by calling this multiple times. Note that depending on the
/// backend, writes can happen out of order.
pub fn write(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
s: *State,
addr: std.net.Address,
buf: xev.WriteBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: *State,
s: Self,
b: xev.WriteBuffer,
r: WriteError!usize,
) xev.CallbackAction,
) void {
s.* = .{
.userdata = userdata,
};
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = .{
.sendto = .{
.fd = self.fd,
.buffer = buf,
.addr = addr,
},
},
.userdata = s,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const s_inner = @as(?*State, @ptrCast(@alignCast(ud))).?;
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, s_inner.userdata),
l_inner,
c_inner,
s_inner,
initFd(c_inner.op.sendto.fd),
c_inner.op.sendto.buffer,
r.sendto,
});
}
}).callback,
};
loop.add(c);
},
}
}
pub const ReadError = xev.ReadError;
pub const WriteError = xev.WriteError;
/// Common tests
pub usingnamespace UDPTests(xev, Self);
};
}
/// UDP implementation that uses sendto/recvfrom.
fn UDPSendtoIOCP(comptime xev: type) type {
return struct {
const Self = @This();
const windows = std.os.windows;
fd: windows.HANDLE,
/// See UDPSendMsg.State
pub const State = struct {
userdata: ?*anyopaque,
};
pub usingnamespace stream.Stream(xev, Self, .{
.close = true,
.read = .none,
.write = .none,
});
/// Initialize a new UDP with the family from the given address. Only
/// the family is used, the actual address has no impact on the created
/// resource.
pub fn init(addr: std.net.Address) !Self {
const socket = try windows.WSASocketW(addr.any.family, posix.SOCK.DGRAM, 0, null, 0, windows.ws2_32.WSA_FLAG_OVERLAPPED);
return .{
.fd = socket,
};
}
/// Initialize a UDP socket from a file descriptor.
pub fn initFd(fd: windows.HANDLE) Self {
return .{
.fd = fd,
};
}
/// Bind the address to the socket.
pub fn bind(self: Self, addr: std.net.Address) !void {
const socket = @as(windows.ws2_32.SOCKET, @ptrCast(self.fd));
try posix.setsockopt(socket, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(socket, &addr.any, addr.getOsSockLen());
}
/// Read from the socket. This performs a single read. The callback must
/// requeue the read if additional reads want to be performed. Additional
/// reads simultaneously can be queued by calling this multiple times. Note
/// that depending on the backend, the reads can happen out of order.
///
/// TODO(mitchellh): a way to receive the remote addr
pub fn read(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
s: *State,
buf: xev.ReadBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: *State,
addr: std.net.Address,
s: Self,
b: xev.ReadBuffer,
r: ReadError!usize,
) xev.CallbackAction,
) void {
s.* = .{
.userdata = userdata,
};
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = .{
.recvfrom = .{
.fd = self.fd,
.buffer = buf,
},
},
.userdata = s,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const s_inner: *State = @ptrCast(@alignCast(ud.?));
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, s_inner.userdata),
l_inner,
c_inner,
s_inner,
std.net.Address.initPosix(@alignCast(&c_inner.op.recvfrom.addr)),
initFd(c_inner.op.recvfrom.fd),
c_inner.op.recvfrom.buffer,
r.recvfrom,
});
}
}).callback,
};
loop.add(c);
},
}
}
/// Write to the socket. This performs a single write. Additional writes
/// can be queued by calling this multiple times. Note that depending on the
/// backend, writes can happen out of order.
pub fn write(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
s: *State,
addr: std.net.Address,
buf: xev.WriteBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: *State,
s: Self,
b: xev.WriteBuffer,
r: WriteError!usize,
) xev.CallbackAction,
) void {
s.* = .{
.userdata = userdata,
};
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = .{
.sendto = .{
.fd = self.fd,
.buffer = buf,
.addr = addr,
},
},
.userdata = s,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const s_inner: *State = @ptrCast(@alignCast(ud.?));
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, s_inner.userdata),
l_inner,
c_inner,
s_inner,
initFd(c_inner.op.sendto.fd),
c_inner.op.sendto.buffer,
r.sendto,
});
}
}).callback,
};
loop.add(c);
},
}
}
pub const ReadError = xev.ReadError;
pub const WriteError = xev.WriteError;
/// Common tests
pub usingnamespace UDPTests(xev, Self);
};
}
/// UDP implementation that uses sendmsg/recvmsg
fn UDPSendMsg(comptime xev: type) type {
return struct {
const Self = @This();
fd: posix.socket_t,
/// UDP requires some extra state to perform operations. The state is
/// opaque. This isn't part of xev.Completion because it is relatively
/// large and would force ALL operations (not just UDP) to have a relatively
/// large structure size and we didn't want to pay that cost.
pub const State = struct {
userdata: ?*anyopaque = null,
op: union {
recv: struct {
buf: xev.ReadBuffer,
addr_buffer: std.posix.sockaddr.storage = undefined,
msghdr: std.posix.msghdr,
iov: [1]std.posix.iovec,
},
send: struct {
buf: xev.WriteBuffer,
addr: std.net.Address,
msghdr: std.posix.msghdr_const,
iov: [1]std.posix.iovec_const,
},
},
};
pub usingnamespace stream.Stream(xev, Self, .{
.close = true,
.read = .none,
.write = .none,
});
/// Initialize a new UDP with the family from the given address. Only
/// the family is used, the actual address has no impact on the created
/// resource.
pub fn init(addr: std.net.Address) !Self {
// On io_uring we don't use non-blocking sockets because we may
// just get EAGAIN over and over from completions.
const flags = flags: {
var flags: u32 = posix.SOCK.DGRAM | posix.SOCK.CLOEXEC;
if (xev.backend != .io_uring) flags |= posix.SOCK.NONBLOCK;
break :flags flags;
};
return .{
.fd = try posix.socket(addr.any.family, flags, 0),
};
}
/// Initialize a UDP socket from a file descriptor.
pub fn initFd(fd: posix.socket_t) Self {
return .{
.fd = fd,
};
}
/// Bind the address to the socket.
pub fn bind(self: Self, addr: std.net.Address) !void {
try posix.setsockopt(self.fd, posix.SOL.SOCKET, posix.SO.REUSEPORT, &std.mem.toBytes(@as(c_int, 1)));
try posix.setsockopt(self.fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(self.fd, &addr.any, addr.getOsSockLen());
}
/// Read from the socket. This performs a single read. The callback must
/// requeue the read if additional reads want to be performed. Additional
/// reads simultaneously can be queued by calling this multiple times. Note
/// that depending on the backend, the reads can happen out of order.
pub fn read(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
s: *State,
buf: xev.ReadBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: *State,
addr: std.net.Address,
s: Self,
b: xev.ReadBuffer,
r: ReadError!usize,
) xev.CallbackAction,
) void {
s.op = .{ .recv = undefined };
s.* = .{
.userdata = userdata,
.op = .{
.recv = .{
.buf = buf,
.msghdr = .{
.name = @ptrCast(&s.op.recv.addr_buffer),
.namelen = @sizeOf(@TypeOf(s.op.recv.addr_buffer)),
.iov = &s.op.recv.iov,
.iovlen = 1,
.control = null,
.controllen = 0,
.flags = 0,
},
.iov = undefined,
},
},
};
switch (s.op.recv.buf) {
.slice => |v| {
s.op.recv.iov[0] = .{
.base = v.ptr,
.len = v.len,
};
},
.array => |*arr| {
s.op.recv.iov[0] = .{
.base = arr,
.len = arr.len,
};
},
}
c.* = .{
.op = .{
.recvmsg = .{
.fd = self.fd,
.msghdr = &s.op.recv.msghdr,
},
},
.userdata = s,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const s_inner = @as(?*State, @ptrCast(@alignCast(ud))).?;
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, s_inner.userdata),
l_inner,
c_inner,
s_inner,
std.net.Address.initPosix(@ptrCast(&s_inner.op.recv.addr_buffer)),
initFd(c_inner.op.recvmsg.fd),
s_inner.op.recv.buf,
if (r.recvmsg) |v| v else |err| err,
});
}
}).callback,
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.kqueue,
.wasi_poll,
.iocp,
=> {},
.epoll => c.flags.dup = true,
}
loop.add(c);
}
/// Write to the socket. This performs a single write. Additional writes
/// can be queued by calling this multiple times. Note that depending on the
/// backend, writes can happen out of order.
pub fn write(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
s: *State,
addr: std.net.Address,
buf: xev.WriteBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: *State,
s: Self,
b: xev.WriteBuffer,
r: WriteError!usize,
) xev.CallbackAction,
) void {
// Set the active field for runtime safety
s.op = .{ .send = undefined };
s.* = .{
.userdata = userdata,
.op = .{
.send = .{
.addr = addr,
.buf = buf,
.msghdr = .{
.name = &s.op.send.addr.any,
.namelen = addr.getOsSockLen(),
.iov = &s.op.send.iov,
.iovlen = 1,
.control = null,
.controllen = 0,
.flags = 0,
},
.iov = undefined,
},
},
};
switch (s.op.send.buf) {
.slice => |v| {
s.op.send.iov[0] = .{
.base = v.ptr,
.len = v.len,
};
},
.array => |*arr| {
s.op.send.iov[0] = .{
.base = &arr.array,
.len = arr.len,
};
},
}
// On backends like epoll, you watch file descriptors for
// specific events. Our implementation doesn't merge multiple
// completions for a single fd, so we have to dup the fd. This
// means we use more fds than we could optimally. This isn't a
// problem with io_uring.
c.* = .{
.op = .{
.sendmsg = .{
.fd = self.fd,
.msghdr = &s.op.send.msghdr,
},
},
.userdata = s,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const s_inner = @as(?*State, @ptrCast(@alignCast(ud))).?;
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, s_inner.userdata),
l_inner,
c_inner,
s_inner,
initFd(c_inner.op.sendmsg.fd),
s_inner.op.send.buf,
if (r.sendmsg) |v| v else |err| err,
});
}
}).callback,
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.kqueue,
.wasi_poll,
.iocp,
=> {},
.epoll => c.flags.dup = true,
}
loop.add(c);
}
pub const ReadError = xev.ReadError;
pub const WriteError = xev.WriteError;
/// Common tests
pub usingnamespace UDPTests(xev, Self);
};
}
fn UDPTests(comptime xev: type, comptime Impl: type) type {
return struct {
test "UDP: read/write" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
const address = try std.net.Address.parseIp4("127.0.0.1", 3132);
const server = try Impl.init(address);
const client = try Impl.init(address);
// Bind / Recv
try server.bind(address);
var c_read: xev.Completion = undefined;
var s_read: Impl.State = undefined;
var recv_buf: [128]u8 = undefined;
var recv_len: usize = 0;
server.read(&loop, &c_read, &s_read, .{ .slice = &recv_buf }, usize, &recv_len, (struct {
fn callback(
ud: ?*usize,
_: *xev.Loop,
_: *xev.Completion,
_: *Impl.State,
_: std.net.Address,
_: Impl,
_: xev.ReadBuffer,
r: Impl.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Send
var send_buf = [_]u8{ 1, 1, 2, 3, 5, 8, 13 };
var c_write: xev.Completion = undefined;
var s_write: Impl.State = undefined;
client.write(&loop, &c_write, &s_write, address, .{ .slice = &send_buf }, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: *Impl.State,
_: Impl,
_: xev.WriteBuffer,
r: Impl.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback);
// Wait for the send/receive
try loop.run(.until_done);
try testing.expect(recv_len > 0);
try testing.expectEqualSlices(u8, &send_buf, recv_buf[0..recv_len]);
// Close
server.close(&loop, &c_read, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Impl,
r: Impl.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback);
client.close(&loop, &c_write, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Impl,
r: Impl.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback);
try loop.run(.until_done);
}
};
}

220
deps/libxev/src/windows.zig vendored Normal file
View File

@ -0,0 +1,220 @@
const std = @import("std");
const windows = std.os.windows;
const posix = std.posix;
pub usingnamespace std.os.windows;
/// Namespace containing missing utils from std
pub const exp = struct {
pub const STATUS_PENDING = 0x00000103;
pub const STILL_ACTIVE = STATUS_PENDING;
pub const JOBOBJECT_ASSOCIATE_COMPLETION_PORT = extern struct {
CompletionKey: windows.ULONG_PTR,
CompletionPort: windows.HANDLE,
};
pub const JOBOBJECT_BASIC_LIMIT_INFORMATION = extern struct {
PerProcessUserTimeLimit: windows.LARGE_INTEGER,
PerJobUserTimeLimit: windows.LARGE_INTEGER,
LimitFlags: windows.DWORD,
MinimumWorkingSetSize: windows.SIZE_T,
MaximumWorkingSetSize: windows.SIZE_T,
ActiveProcessLimit: windows.DWORD,
Affinity: windows.ULONG_PTR,
PriorityClass: windows.DWORD,
SchedulingClass: windows.DWORD,
};
pub const IO_COUNTERS = extern struct {
ReadOperationCount: windows.ULONGLONG,
WriteOperationCount: windows.ULONGLONG,
OtherOperationCount: windows.ULONGLONG,
ReadTransferCount: windows.ULONGLONG,
WriteTransferCount: windows.ULONGLONG,
OtherTransferCount: windows.ULONGLONG,
};
pub const JOBOBJECT_EXTENDED_LIMIT_INFORMATION = extern struct {
BasicLimitInformation: JOBOBJECT_BASIC_LIMIT_INFORMATION,
IoInfo: IO_COUNTERS,
ProcessMemoryLimit: windows.SIZE_T,
JobMemoryLimit: windows.SIZE_T,
PeakProcessMemoryUsed: windows.SIZE_T,
PeakJobMemoryUsed: windows.SIZE_T,
};
pub const JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x00000008;
pub const JOB_OBJECT_LIMIT_AFFINITY = 0x00000010;
pub const JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800;
pub const JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x00000400;
pub const JOB_OBJECT_LIMIT_JOB_MEMORY = 0x00000200;
pub const JOB_OBJECT_LIMIT_JOB_TIME = 0x00000004;
pub const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
pub const JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME = 0x00000004;
pub const JOB_OBJECT_LIMIT_PRIORITY_CLASS = 0x00000020;
pub const JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x00000100;
pub const JOB_OBJECT_LIMIT_PROCESS_TIME = 0x00000002;
pub const JOB_OBJECT_LIMIT_SCHEDULING_CLASS = 0x00000080;
pub const JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x00001000;
pub const JOB_OBJECT_LIMIT_SUBSET_AFFINITY = 0x00004000;
pub const JOB_OBJECT_LIMIT_WORKINGSET = 0x00000001;
pub const JOBOBJECT_INFORMATION_CLASS = enum(c_int) {
JobObjectAssociateCompletionPortInformation = 7,
JobObjectBasicLimitInformation = 2,
JobObjectBasicUIRestrictions = 4,
JobObjectCpuRateControlInformation = 15,
JobObjectEndOfJobTimeInformation = 6,
JobObjectExtendedLimitInformation = 9,
JobObjectGroupInformation = 11,
JobObjectGroupInformationEx = 14,
JobObjectLimitViolationInformation2 = 34,
JobObjectNetRateControlInformation = 32,
JobObjectNotificationLimitInformation = 12,
JobObjectNotificationLimitInformation2 = 33,
JobObjectSecurityLimitInformation = 5,
};
pub const JOB_OBJECT_MSG_TYPE = enum(windows.DWORD) {
JOB_OBJECT_MSG_END_OF_JOB_TIME = 1,
JOB_OBJECT_MSG_END_OF_PROCESS_TIME = 2,
JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT = 3,
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO = 4,
JOB_OBJECT_MSG_NEW_PROCESS = 6,
JOB_OBJECT_MSG_EXIT_PROCESS = 7,
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS = 8,
JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT = 9,
JOB_OBJECT_MSG_JOB_MEMORY_LIMIT = 10,
JOB_OBJECT_MSG_NOTIFICATION_LIMIT = 11,
JOB_OBJECT_MSG_JOB_CYCLE_TIME_LIMIT = 12,
JOB_OBJECT_MSG_SILO_TERMINATED = 13,
_,
};
pub const kernel32 = struct {
pub extern "kernel32" fn GetProcessId(Process: windows.HANDLE) callconv(windows.WINAPI) windows.DWORD;
pub extern "kernel32" fn CreateJobObjectA(lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES, lpName: ?windows.LPCSTR) callconv(windows.WINAPI) windows.HANDLE;
pub extern "kernel32" fn AssignProcessToJobObject(hJob: windows.HANDLE, hProcess: windows.HANDLE) callconv(windows.WINAPI) windows.BOOL;
pub extern "kernel32" fn SetInformationJobObject(
hJob: windows.HANDLE,
JobObjectInformationClass: JOBOBJECT_INFORMATION_CLASS,
lpJobObjectInformation: windows.LPVOID,
cbJobObjectInformationLength: windows.DWORD,
) callconv(windows.WINAPI) windows.BOOL;
};
pub const CreateFileError = error{} || posix.UnexpectedError;
pub fn CreateFile(
lpFileName: [*:0]const u16,
dwDesiredAccess: windows.DWORD,
dwShareMode: windows.DWORD,
lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES,
dwCreationDisposition: windows.DWORD,
dwFlagsAndAttributes: windows.DWORD,
hTemplateFile: ?windows.HANDLE,
) CreateFileError!windows.HANDLE {
const handle = windows.kernel32.CreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
if (handle == windows.INVALID_HANDLE_VALUE) {
const err = windows.kernel32.GetLastError();
return switch (err) {
else => windows.unexpectedError(err),
};
}
return handle;
}
pub fn ReadFile(
handle: windows.HANDLE,
buffer: []u8,
overlapped: ?*windows.OVERLAPPED,
) windows.ReadFileError!?usize {
var read: windows.DWORD = 0;
const result: windows.BOOL = windows.kernel32.ReadFile(handle, buffer.ptr, @as(windows.DWORD, @intCast(buffer.len)), &read, overlapped);
if (result == windows.FALSE) {
const err = windows.kernel32.GetLastError();
return switch (err) {
windows.Win32Error.IO_PENDING => null,
else => windows.unexpectedError(err),
};
}
return @as(usize, @intCast(read));
}
pub fn WriteFile(
handle: windows.HANDLE,
buffer: []const u8,
overlapped: ?*windows.OVERLAPPED,
) windows.WriteFileError!?usize {
var written: windows.DWORD = 0;
const result: windows.BOOL = windows.kernel32.WriteFile(handle, buffer.ptr, @as(windows.DWORD, @intCast(buffer.len)), &written, overlapped);
if (result == windows.FALSE) {
const err = windows.kernel32.GetLastError();
return switch (err) {
windows.Win32Error.IO_PENDING => null,
else => windows.unexpectedError(err),
};
}
return @as(usize, @intCast(written));
}
pub const DeleteFileError = error{} || posix.UnexpectedError;
pub fn DeleteFile(name: [*:0]const u16) DeleteFileError!void {
const result: windows.BOOL = windows.kernel32.DeleteFileW(name);
if (result == windows.FALSE) {
const err = windows.kernel32.GetLastError();
return switch (err) {
else => windows.unexpectedError(err),
};
}
}
pub const CreateJobObjectError = error{AlreadyExists} || posix.UnexpectedError;
pub fn CreateJobObject(
lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES,
lpName: ?windows.LPCSTR,
) !windows.HANDLE {
const handle = kernel32.CreateJobObjectA(lpSecurityAttributes, lpName);
return switch (windows.kernel32.GetLastError()) {
.SUCCESS => handle,
.ALREADY_EXISTS => CreateJobObjectError.AlreadyExists,
else => |err| windows.unexpectedError(err),
};
}
pub fn AssignProcessToJobObject(hJob: windows.HANDLE, hProcess: windows.HANDLE) posix.UnexpectedError!void {
const result: windows.BOOL = kernel32.AssignProcessToJobObject(hJob, hProcess);
if (result == windows.FALSE) {
const err = windows.kernel32.GetLastError();
return switch (err) {
else => windows.unexpectedError(err),
};
}
}
pub fn SetInformationJobObject(
hJob: windows.HANDLE,
JobObjectInformationClass: JOBOBJECT_INFORMATION_CLASS,
lpJobObjectInformation: windows.LPVOID,
cbJobObjectInformationLength: windows.DWORD,
) posix.UnexpectedError!void {
const result: windows.BOOL = kernel32.SetInformationJobObject(
hJob,
JobObjectInformationClass,
lpJobObjectInformation,
cbJobObjectInformationLength,
);
if (result == windows.FALSE) {
const err = windows.kernel32.GetLastError();
return switch (err) {
else => windows.unexpectedError(err),
};
}
}
};

4
deps/provenance vendored Normal file
View File

@ -0,0 +1,4 @@
- GapBuffer: https://github.com/ryleelyman/GapBuffer.zig#commit=9039708e09fc3eb5f698ab5694a436afe503c6a6
- libvaxis: https://github.com/rockorager/libvaxis#commit=a8baf9ce371b89a84383130c82549bb91401d15a
- libxev: https://github.com/mitchellh/libxev#commit=43c7e4b3308f359e5b758db2d824d7c447f4ed3f
- zg: https://codeberg.org/dude_the_builder/zg.git#commit=7ddce488e074c3f052949ba513a340446cea86e9

21
deps/zg/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Jose Colon Rodriguez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

538
deps/zg/README.md vendored Normal file
View File

@ -0,0 +1,538 @@
# zg
zg provides Unicode text processing for Zig projects.
## Unicode Version
The Unicode version supported by zg is 15.1.0.
## Zig Version
The minimum Zig version required is 0.13.0 stable.
## Integrating zg into your Zig Project
You first need to add zg as a dependency in your `build.zig.zon` file. In your
Zig project's root directory, run:
```plain
zig fetch --save https://codeberg.org/dude_the_builder/zg/archive/v0.13.2.tar.gz
```
Then instantiate the dependency in your `build.zig`:
```zig
const zg = b.dependency("zg", .{});
```
## A Modular Approach
zg is a modular library. This approach minimizes binary file size and memory
requirements by only including the Unicode data required for the specified module.
The following sections describe the various modules and their specific use case.
## Code Points
In the `code_point` module, you'll find a data structure representing a single code
point, `CodePoint`, and an `Iterator` to iterate over the code points in a string.
In your `build.zig`:
```zig
exe.root_module.addImport("code_point", zg.module("code_point"));
```
In your code:
```zig
const code_point = @import("code_point");
test "Code point iterator" {
const str = "Hi 😊";
var iter = code_point.Iterator{ .bytes = str };
var i: usize = 0;
while (iter.next()) |cp| : (i += 1) {
// The `code` field is the actual code point scalar as a `u21`.
if (i == 0) try expect(cp.code == 'H');
if (i == 1) try expect(cp.code == 'i');
if (i == 2) try expect(cp.code == ' ');
if (i == 3) {
try expect(cp.code == '😊');
// The `offset` field is the byte offset in the
// source string.
try expect(cp.offset == 3);
// The `len` field is the length in bytes of the
// code point in the source string.
try expect(cp.len == 4);
}
}
}
```
## Grapheme Clusters
Many characters are composed from more than one code point. These are known as
Grapheme Clusters and the `grapheme` module has a data structure to represent
them, `Grapheme`, and an `Iterator` to iterate over them in a string.
In your `build.zig`:
```zig
exe.root_module.addImport("grapheme", zg.module("grapheme"));
```
In your code:
```zig
const grapheme = @import("grapheme");
test "Grapheme cluster iterator" {
// we need some Unicode data to process Grapheme Clusters.
const gd = try grapheme.GraphemeData.init(allocator);
defer gd.deinit();
const str = "He\u{301}"; // Hé
var iter = grapheme.Iterator.init(str, &gd);
var i: usize = 0;
while (iter.next()) |gc| : (i += 1) {
// The `len` field is the length in bytes of the
// grapheme cluster in the source string.
if (i == 0) try expect(gc.len == 1);
if (i == 1) {
try expect(gc.len == 3);
// The `offset` in bytes of the grapheme cluster
// in the source string.
try expect(gc.offset == 1);
// The `bytes` method returns the slice of bytes
// that comprise this grapheme cluster in the
// source string `str`.
try expectEqualStrings("e\u{301}", gc.bytes(str));
}
}
}
```
## Unicode General Categories
To detect the general category for a code point, use the `GenCatData` module.
In your `build.zig`:
```zig
exe.root_module.addImport("GenCatData", zg.module("GenCatData"));
```
In your code:
```zig
const GenCatData = @import("GenCatData");
test "General Category" {
const gcd = try GenCatData.init(allocator);
defer gcd.deinit();
// The `gc` method returns the abbreviated General Category.
// These abbreviations and descriptive comments can be found
// in the source file `src/GenCatData.zig` as en enum.
try expect(gcd.gc('A') == .Lu); // Lu: uppercase letter
try expect(gcd.gc('3') == .Nd); // Nd: decimal number
// The following are convenience methods for groups of General
// Categories. For example, all letter categories start with `L`:
// Lu, Ll, Lt, Lo.
try expect(gcd.isControl(0));
try expect(gcd.isLetter('z'));
try expect(gcd.isMark('\u{301}'));
try expect(gcd.isNumber('3'));
try expect(gcd.isPunctuation('['));
try expect(gcd.isSeparator(' '));
try expect(gcd.isSymbol('©'));
}
```
## Unicode Properties
You can detect common properties of a code point with the `PropsData` module.
In your `build.zig`:
```zig
exe.root_module.addImport("PropsData", zg.module("PropsData"));
```
In your code:
```zig
const PropsData = @import("PropsData");
test "Properties" {
const pd = try PropsData.init(allocator);
defer pd.deinit();
// Mathematical symbols and letters.
try expect(pd.isMath('+'));
// Alphabetic only code points.
try expect(pd.isAlphabetic('Z'));
// Space, tab, and other separators.
try expect(pd.isWhitespace(' '));
// Hexadecimal digits and variations thereof.
try expect(pd.isHexDigit('f'));
try expect(!pd.isHexDigit('z'));
// Accents, dieresis, and other combining marks.
try expect(pd.isDiacritic('\u{301}'));
// Unicode has a specification for valid identifiers like
// the ones used in programming and regular expressions.
try expect(pd.isIdStart('Z')); // Identifier start character
try expect(!pd.isIdStart('1'));
try expect(pd.isIdContinue('1'));
// The `X` versions add some code points that can appear after
// normalizing a string.
try expect(pd.isXidStart('\u{b33}')); // Extended identifier start character
try expect(pd.isXidContinue('\u{e33}'));
try expect(!pd.isXidStart('1'));
// Note surprising Unicode numeric type properties!
try expect(pd.isNumeric('\u{277f}'));
try expect(!pd.isNumeric('3')); // 3 is not numeric!
try expect(pd.isDigit('\u{2070}'));
try expect(!pd.isDigit('3')); // 3 is not a digit!
try expect(pd.isDecimal('3')); // 3 is a decimal digit
}
```
## Letter Case Detection and Conversion
To detect and convert to and from different letter cases, use the `CaseData`
module.
In your `build.zig`:
```zig
exe.root_module.addImport("CaseData", zg.module("CaseData"));
```
In your code:
```zig
const CaseData = @import("CaseData");
test "Case" {
const cd = try CaseData.init(allocator);
defer cd.deinit();
// Upper and lower case.
try expect(cd.isUpper('A'));
try expect('A' == cd.toUpper('a'));
try expect(cd.isLower('a'));
try expect('a' == cd.toLower('A'));
// Code points that have case.
try expect(cd.isCased('É'));
try expect(!cd.isCased('3'));
// Case detection and conversion for strings.
try expect(cd.isUpperStr("HELLO 123!"));
const ucased = try cd.toUpperStr(allocator, "hello 123");
defer allocator.free(ucased);
try expectEqualStrings("HELLO 123", ucased);
try expect(cd.isLowerStr("hello 123!"));
const lcased = try cd.toLowerStr(allocator, "HELLO 123");
defer allocator.free(lcased);
try expectEqualStrings("hello 123", lcased);
}
```
## Normalization
Unicode normalization is the process of converting a string into a uniform
representation that can guarantee a known structure by following a strict set
of rules. There are four normalization forms:
Canonical Composition (NFC)
: The most compact representation obtained by first
decomposing to Canonical Decomposition and then composing to NFC.
Compatibility Composition (NFKC)
: The most comprehensive composition obtained
by first decomposing to Compatibility Decomposition and then composing to NFKC.
Canonical Decomposition (NFD)
: Only code points with canonical decompositions
are decomposed. This is a more compact and faster decomposition but will not
provide the most comprehensive normalization possible.
Compatibility Decomposition (NFKD)
: The most comprehensive decomposition method
where both canonical and compatibility decompositions are performed recursively.
zg has methods to produce all four normalization forms in the `Normalize` module.
In your `build.zig`:
```zig
exe.root_module.addImport("Normalize", zg.module("Normalize"));
```
In your code:
```zig
const Normalize = @import("Normalize");
test "Normalization" {
// We need lots of Unicode dta for normalization.
var norm_data: Normalize.NormData = undefined;
try Normalize.NormData.init(&norm_data, allocator);
defer norm_data.deinit();
// The `Normalize` structure takes a pointer to the data.
const n = Normalize{ .norm_data = &norm_data };
// NFC: Canonical composition
const nfc_result = try n.nfc(allocator, "Complex char: \u{3D2}\u{301}");
defer nfc_result.deinit();
try expectEqualStrings("Complex char: \u{3D3}", nfc_result.slice);
// NFKC: Compatibility composition
const nfkc_result = try n.nfkc(allocator, "Complex char: \u{03A5}\u{0301}");
defer nfkc_result.deinit();
try expectEqualStrings("Complex char: \u{038E}", nfkc_result.slice);
// NFD: Canonical decomposition
const nfd_result = try n.nfd(allocator, "Héllo World! \u{3d3}");
defer nfd_result.deinit();
try expectEqualStrings("He\u{301}llo World! \u{3d2}\u{301}", nfd_result.slice);
// NFKD: Compatibility decomposition
const nfkd_result = try n.nfkd(allocator, "Héllo World! \u{3d3}");
defer nfkd_result.deinit();
try expectEqualStrings("He\u{301}llo World! \u{3a5}\u{301}", nfkd_result.slice);
// Test for equality of two strings after normalizing to NFC.
try expect(try n.eql(allocator, "foé", "foe\u{0301}"));
try expect(try n.eql(allocator, "foϓ", "fo\u{03D2}\u{0301}"));
}
```
## Caseless Matching via Case Folding
Unicode provides a more efficient way of comparing strings while ignoring letter
case differences: case folding. When you case fold a string, it's converted into a
normalized case form suitable for efficient matching. Use the `CaseFold` module
for this.
In your `build.zig`:
```zig
exe.root_module.addImport("Normalize", zg.module("Normalize"));
exe.root_module.addImport("CaseFold", zg.module("CaseFold"));
```
In your code:
```zig
const Normalize = @import("Normalize");
const CaseFold = @import("CaseFold");
test "Caseless matching" {
// We need to normalize during the matching process.
var norm_data: Normalize.NormData = undefined;
try Normalize.NormData.init(&norm_data, allocator);
defer norm_data.deinit();
const n = Normalize{ .norm_data = &norm_data };
// We need Unicode case fold data.
const cfd = try CaseFold.FoldData.init(allocator);
defer cfd.deinit();
// The `CaseFold` structure takes a pointer to the data.
const cf = CaseFold{ .fold_data = &cfd };
// `compatCaselessMatch` provides the deepest level of caseless
// matching because it decomposes fully to NFKD.
const a = "Héllo World! \u{3d3}";
const b = "He\u{301}llo World! \u{3a5}\u{301}";
try expect(try cf.compatCaselessMatch(allocator, &n, a, b));
const c = "He\u{301}llo World! \u{3d2}\u{301}";
try expect(try cf.compatCaselessMatch(allocator, &n, a, c));
// `canonCaselessMatch` isn't as comprehensive as `compatCaselessMatch`
// because it only decomposes to NFD. Naturally, it's faster because of this.
try expect(!try cf.canonCaselessMatch(allocator, &n, a, b));
try expect(try cf.canonCaselessMatch(allocator, &n, a, c));
}
```
## Display Width of Characters and Strings
When displaying text with a fixed-width font on a terminal screen, it's very
important to know exactly how many columns or cells each character should take.
Most characters will use one column, but there are many, like emoji and East-
Asian ideographs that need more space. The `DisplayWidth` module provides
methods for this purpose. It also has methods that use the display width calculation
to `center`, `padLeft`, `padRight`, and `wrap` text.
In your `build.zig`:
```zig
exe.root_module.addImport("DisplayWidth", zg.module("DisplayWidth"));
```
In your code:
```zig
const DisplayWidth = @import("DisplayWidth");
test "Display width" {
// We need Unicode data for display width calculation.
const dwd = try DisplayWidth.DisplayWidthData.init(allocator);
defer dwd.deinit();
// The `DisplayWidth` structure takes a pointer to the data.
const dw = DisplayWidth{ .data = &dwd };
// String display width
try expectEqual(@as(usize, 5), dw.strWidth("Hello\r\n"));
try expectEqual(@as(usize, 8), dw.strWidth("Hello 😊"));
try expectEqual(@as(usize, 8), dw.strWidth("Héllo 😊"));
try expectEqual(@as(usize, 9), dw.strWidth("Ẓ̌á̲l͔̝̞̄̑͌g̖̘̘̔̔͢͞͝o̪̔T̢̙̫̈̍͞e̬͈͕͌̏͑x̺̍ṭ̓̓ͅ"));
try expectEqual(@as(usize, 17), dw.strWidth("슬라바 우크라이나"));
// Centering text
const centered = try dw.center(allocator, "w😊w", 10, "-");
defer allocator.free(centered);
try expectEqualStrings("---w😊w---", centered);
// Pad left
const right_aligned = try dw.padLeft(allocator, "abc", 9, "*");
defer allocator.free(right_aligned);
try expectEqualStrings("******abc", right_aligned);
// Pad right
const left_aligned = try dw.padRight(allocator, "abc", 9, "*");
defer allocator.free(left_aligned);
try expectEqualStrings("abc******", left_aligned);
// Wrap text
const input = "The quick brown fox\r\njumped over the lazy dog!";
const wrapped = try dw.wrap(allocator, input, 10, 3);
defer allocator.free(wrapped);
const want =
\\The quick
\\brown fox
\\jumped
\\over the
\\lazy dog!
;
try expectEqualStrings(want, wrapped);
}
```
## Scripts
Unicode categorizes code points by the Script in which they belong. A Script
collects letters and other symbols that belong to a particular writing system.
You can detect the Script for a code point with the `ScriptsData` module.
In your `build.zig`:
```zig
exe.root_module.addImport("ScriptsData", zg.module("ScriptsData"));
```
In your code:
```zig
const ScriptsData = @import("ScriptsData");
test "Scripts" {
const sd = try ScriptsData.init(allocator);
defer sd.deinit();
// To see the full list of Scripts, look at the
// `src/ScriptsData.zig` file. They are list in an enum.
try expect(sd.script('A') == .Latin);
try expect(sd.script('Ω') == .Greek);
try expect(sd.script('צ') == .Hebrew);
}
```
## Relation to Ziglyph
zg is a total re-write of some of the components of Ziglyph. The idea was to
reduce binary size and improve performance. These goals were achieved by using
trie-like data structures (inspired by [Ghostty's implementation](https://mitchellh.com/writing/ghostty-devlog-006))
instead of generated functions. Where Ziglyph uses a function call, zg uses an
array lookup, which is quite faster. In addition, all these data structures in
zg are loaded at runtime from compressed versions in the binary. This allows
for smaller binary sizes at the expense of increased memory
footprint at runtime.
Benchmarks demonstrate the above stated goals have been met:
```plain
Binary sizes =======
149K ziglyph_case
87K zg_case
275K ziglyph_caseless
168K zg_caseless
68K ziglyph_codepoint
68K zg_codepoint
101K ziglyph_grapheme
86K zg_grapheme
185K ziglyph_normalizer
152K zg_normalize
101K ziglyph_width
86K zg_width
Benchmarks ==========
Ziglyph toUpperStr/toLowerStr: result: 7911596, took: 80
Ziglyph isUpperStr/isLowerStr: result: 110959, took: 17
zg toUpperStr/toLowerStr: result: 7911596, took: 62
zg isUpperStr/isLowerStr: result: 110959, took: 7
Ziglyph Normalizer.eqlCaseless: result: 625, took: 500
zg CaseFold.canonCaselessMatch: result: 625, took: 385
zg CaseFold.compatCaselessMatch: result: 625, took: 593
Ziglyph CodePointIterator: result: 3769314, took: 2
zg CodePointIterator: result: 3769314, took: 3
Ziglyph GraphemeIterator: result: 3691806, took: 48
zg GraphemeIterator: result: 3691806, took: 16
Ziglyph Normalizer.nfkc: result: 3934162, took: 416
zg Normalize.nfkc: result: 3934162, took: 182
Ziglyph Normalizer.nfc: result: 3955798, took: 57
zg Normalize.nfc: result: 3955798, took: 28
Ziglyph Normalizer.nfkd: result: 4006398, took: 172
zg Normalize.nfkd: result: 4006398, took: 104
Ziglyph Normalizer.nfd: result: 4028034, took: 169
zg Normalize.nfd: result: 4028034, took: 104
Ziglyph Normalizer.eql: result: 625, took: 337
Zg Normalize.eql: result: 625, took: 53
Ziglyph display_width.strWidth: result: 3700914, took: 71
zg DisplayWidth.strWidth: result: 3700914, took: 24
```
These results were obtained on an M1 Mac with 16 GiB of RAM.
In contrast to Ziglyph, zg does not have:
- Word segmentation
- Sentence segmentation
- Collation
It's possible that any missing functionality will be added in future versions,
but only if enough demand is present in the community.

1
deps/zg/UNICODE_VERSION.txt vendored Normal file
View File

@ -0,0 +1 @@
This software is compatible with Unicode version 15.1.0

337
deps/zg/build.zig vendored Normal file
View File

@ -0,0 +1,337 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Code generation
// Grapheme break
const gbp_gen_exe = b.addExecutable(.{
.name = "gbp",
.root_source_file = b.path("codegen/gbp.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_gbp_gen_exe = b.addRunArtifact(gbp_gen_exe);
const gbp_gen_out = run_gbp_gen_exe.addOutputFileArg("gbp.bin.z");
// Display width
const cjk = b.option(bool, "cjk", "Ambiguouse code points are wide (display width: 2).") orelse false;
const options = b.addOptions();
options.addOption(bool, "cjk", cjk);
const dwp_gen_exe = b.addExecutable(.{
.name = "dwp",
.root_source_file = b.path("codegen/dwp.zig"),
.target = b.host,
.optimize = .Debug,
});
dwp_gen_exe.root_module.addOptions("options", options);
const run_dwp_gen_exe = b.addRunArtifact(dwp_gen_exe);
const dwp_gen_out = run_dwp_gen_exe.addOutputFileArg("dwp.bin.z");
// Normalization properties
const canon_gen_exe = b.addExecutable(.{
.name = "canon",
.root_source_file = b.path("codegen/canon.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_canon_gen_exe = b.addRunArtifact(canon_gen_exe);
const canon_gen_out = run_canon_gen_exe.addOutputFileArg("canon.bin.z");
const compat_gen_exe = b.addExecutable(.{
.name = "compat",
.root_source_file = b.path("codegen/compat.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_compat_gen_exe = b.addRunArtifact(compat_gen_exe);
const compat_gen_out = run_compat_gen_exe.addOutputFileArg("compat.bin.z");
const hangul_gen_exe = b.addExecutable(.{
.name = "hangul",
.root_source_file = b.path("codegen/hangul.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_hangul_gen_exe = b.addRunArtifact(hangul_gen_exe);
const hangul_gen_out = run_hangul_gen_exe.addOutputFileArg("hangul.bin.z");
const normp_gen_exe = b.addExecutable(.{
.name = "normp",
.root_source_file = b.path("codegen/normp.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_normp_gen_exe = b.addRunArtifact(normp_gen_exe);
const normp_gen_out = run_normp_gen_exe.addOutputFileArg("normp.bin.z");
const ccc_gen_exe = b.addExecutable(.{
.name = "ccc",
.root_source_file = b.path("codegen/ccc.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_ccc_gen_exe = b.addRunArtifact(ccc_gen_exe);
const ccc_gen_out = run_ccc_gen_exe.addOutputFileArg("ccc.bin.z");
const gencat_gen_exe = b.addExecutable(.{
.name = "gencat",
.root_source_file = b.path("codegen/gencat.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_gencat_gen_exe = b.addRunArtifact(gencat_gen_exe);
const gencat_gen_out = run_gencat_gen_exe.addOutputFileArg("gencat.bin.z");
const fold_gen_exe = b.addExecutable(.{
.name = "fold",
.root_source_file = b.path("codegen/fold.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_fold_gen_exe = b.addRunArtifact(fold_gen_exe);
const fold_gen_out = run_fold_gen_exe.addOutputFileArg("fold.bin.z");
// Numeric types
const num_gen_exe = b.addExecutable(.{
.name = "numeric",
.root_source_file = b.path("codegen/numeric.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_num_gen_exe = b.addRunArtifact(num_gen_exe);
const num_gen_out = run_num_gen_exe.addOutputFileArg("numeric.bin.z");
// Letter case properties
const case_prop_gen_exe = b.addExecutable(.{
.name = "case_prop",
.root_source_file = b.path("codegen/case_prop.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_case_prop_gen_exe = b.addRunArtifact(case_prop_gen_exe);
const case_prop_gen_out = run_case_prop_gen_exe.addOutputFileArg("case_prop.bin.z");
// Uppercase mappings
const upper_gen_exe = b.addExecutable(.{
.name = "upper",
.root_source_file = b.path("codegen/upper.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_upper_gen_exe = b.addRunArtifact(upper_gen_exe);
const upper_gen_out = run_upper_gen_exe.addOutputFileArg("upper.bin.z");
// Lowercase mappings
const lower_gen_exe = b.addExecutable(.{
.name = "lower",
.root_source_file = b.path("codegen/lower.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_lower_gen_exe = b.addRunArtifact(lower_gen_exe);
const lower_gen_out = run_lower_gen_exe.addOutputFileArg("lower.bin.z");
const scripts_gen_exe = b.addExecutable(.{
.name = "scripts",
.root_source_file = b.path("codegen/scripts.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_scripts_gen_exe = b.addRunArtifact(scripts_gen_exe);
const scripts_gen_out = run_scripts_gen_exe.addOutputFileArg("scripts.bin.z");
const core_gen_exe = b.addExecutable(.{
.name = "core",
.root_source_file = b.path("codegen/core_props.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_core_gen_exe = b.addRunArtifact(core_gen_exe);
const core_gen_out = run_core_gen_exe.addOutputFileArg("core_props.bin.z");
const props_gen_exe = b.addExecutable(.{
.name = "props",
.root_source_file = b.path("codegen/props.zig"),
.target = b.host,
.optimize = .Debug,
});
const run_props_gen_exe = b.addRunArtifact(props_gen_exe);
const props_gen_out = run_props_gen_exe.addOutputFileArg("props.bin.z");
// Modules we provide
// Code points
const code_point = b.addModule("code_point", .{
.root_source_file = b.path("src/code_point.zig"),
.target = target,
.optimize = optimize,
});
// Grapheme clusters
const grapheme_data = b.createModule(.{
.root_source_file = b.path("src/GraphemeData.zig"),
.target = target,
.optimize = optimize,
});
grapheme_data.addAnonymousImport("gbp", .{ .root_source_file = gbp_gen_out });
const grapheme = b.addModule("grapheme", .{
.root_source_file = b.path("src/grapheme.zig"),
.target = target,
.optimize = optimize,
});
grapheme.addImport("code_point", code_point);
grapheme.addImport("GraphemeData", grapheme_data);
// ASCII utilities
const ascii = b.addModule("ascii", .{
.root_source_file = b.path("src/ascii.zig"),
.target = target,
.optimize = optimize,
});
// Fixed pitch font display width
const width_data = b.createModule(.{
.root_source_file = b.path("src/WidthData.zig"),
.target = target,
.optimize = optimize,
});
width_data.addAnonymousImport("dwp", .{ .root_source_file = dwp_gen_out });
width_data.addImport("GraphemeData", grapheme_data);
const display_width = b.addModule("DisplayWidth", .{
.root_source_file = b.path("src/DisplayWidth.zig"),
.target = target,
.optimize = optimize,
});
display_width.addImport("ascii", ascii);
display_width.addImport("code_point", code_point);
display_width.addImport("grapheme", grapheme);
display_width.addImport("DisplayWidthData", width_data);
// Normalization
const ccc_data = b.createModule(.{
.root_source_file = b.path("src/CombiningData.zig"),
.target = target,
.optimize = optimize,
});
ccc_data.addAnonymousImport("ccc", .{ .root_source_file = ccc_gen_out });
const canon_data = b.createModule(.{
.root_source_file = b.path("src/CanonData.zig"),
.target = target,
.optimize = optimize,
});
canon_data.addAnonymousImport("canon", .{ .root_source_file = canon_gen_out });
const compat_data = b.createModule(.{
.root_source_file = b.path("src/CompatData.zig"),
.target = target,
.optimize = optimize,
});
compat_data.addAnonymousImport("compat", .{ .root_source_file = compat_gen_out });
const hangul_data = b.createModule(.{
.root_source_file = b.path("src/HangulData.zig"),
.target = target,
.optimize = optimize,
});
hangul_data.addAnonymousImport("hangul", .{ .root_source_file = hangul_gen_out });
const normp_data = b.createModule(.{
.root_source_file = b.path("src/NormPropsData.zig"),
.target = target,
.optimize = optimize,
});
normp_data.addAnonymousImport("normp", .{ .root_source_file = normp_gen_out });
const norm_data = b.createModule(.{
.root_source_file = b.path("src/NormData.zig"),
.target = target,
.optimize = optimize,
});
norm_data.addImport("CanonData", canon_data);
norm_data.addImport("CombiningData", ccc_data);
norm_data.addImport("CompatData", compat_data);
norm_data.addImport("HangulData", hangul_data);
norm_data.addImport("NormPropsData", normp_data);
const norm = b.addModule("Normalize", .{
.root_source_file = b.path("src/Normalize.zig"),
.target = target,
.optimize = optimize,
});
norm.addImport("ascii", ascii);
norm.addImport("code_point", code_point);
norm.addImport("NormData", norm_data);
// General Category
const gencat_data = b.addModule("GenCatData", .{
.root_source_file = b.path("src/GenCatData.zig"),
.target = target,
.optimize = optimize,
});
gencat_data.addAnonymousImport("gencat", .{ .root_source_file = gencat_gen_out });
// Case folding
const fold_data = b.createModule(.{
.root_source_file = b.path("src/FoldData.zig"),
.target = target,
.optimize = optimize,
});
fold_data.addAnonymousImport("fold", .{ .root_source_file = fold_gen_out });
const case_fold = b.addModule("CaseFold", .{
.root_source_file = b.path("src/CaseFold.zig"),
.target = target,
.optimize = optimize,
});
case_fold.addImport("ascii", ascii);
case_fold.addImport("FoldData", fold_data);
case_fold.addImport("Normalize", norm);
// Letter case
const case_data = b.addModule("CaseData", .{
.root_source_file = b.path("src/CaseData.zig"),
.target = target,
.optimize = optimize,
});
case_data.addImport("code_point", code_point);
case_data.addAnonymousImport("case_prop", .{ .root_source_file = case_prop_gen_out });
case_data.addAnonymousImport("upper", .{ .root_source_file = upper_gen_out });
case_data.addAnonymousImport("lower", .{ .root_source_file = lower_gen_out });
// Scripts
const scripts_data = b.addModule("ScriptsData", .{
.root_source_file = b.path("src/ScriptsData.zig"),
.target = target,
.optimize = optimize,
});
scripts_data.addAnonymousImport("scripts", .{ .root_source_file = scripts_gen_out });
// Properties
const props_data = b.addModule("PropsData", .{
.root_source_file = b.path("src/PropsData.zig"),
.target = target,
.optimize = optimize,
});
props_data.addAnonymousImport("core_props", .{ .root_source_file = core_gen_out });
props_data.addAnonymousImport("props", .{ .root_source_file = props_gen_out });
props_data.addAnonymousImport("numeric", .{ .root_source_file = num_gen_out });
// Unicode Tests
const unicode_tests = b.addTest(.{
.root_source_file = b.path("src/unicode_tests.zig"),
.target = target,
.optimize = optimize,
});
unicode_tests.root_module.addImport("grapheme", grapheme);
unicode_tests.root_module.addImport("Normalize", norm);
const run_unicode_tests = b.addRunArtifact(unicode_tests);
const unicode_test_step = b.step("unicode-test", "Run Unicode tests");
unicode_test_step.dependOn(&run_unicode_tests.step);
}

17
deps/zg/build.zig.zon vendored Normal file
View File

@ -0,0 +1,17 @@
.{
.name = "zg",
.version = "0.13.1",
.minimum_zig_version = "0.13.0",
.paths = .{
"build.zig",
"build.zig.zon",
"codegen",
"data",
"LICENSE",
"README.md",
"src",
"unicode_license",
"UNICODE_VERSION.txt",
},
}

67
deps/zg/codegen/canon.zig vendored Normal file
View File

@ -0,0 +1,67 @@
const std = @import("std");
const builtin = @import("builtin");
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Process UnicodeData.txt
var in_file = try std.fs.cwd().openFile("data/unicode/UnicodeData.txt", .{});
defer in_file.close();
var in_buf = std.io.bufferedReader(in_file.reader());
const in_reader = in_buf.reader();
var args_iter = try std.process.argsWithAllocator(allocator);
defer args_iter.deinit();
_ = args_iter.skip();
const output_path = args_iter.next() orelse @panic("No output file arg!");
const compressor = std.compress.flate.deflate.compressor;
var out_file = try std.fs.cwd().createFile(output_path, .{});
defer out_file.close();
var out_comp = try compressor(.raw, out_file.writer(), .{ .level = .best });
const writer = out_comp.writer();
const endian = builtin.cpu.arch.endian();
var line_buf: [4096]u8 = undefined;
lines: while (try in_reader.readUntilDelimiterOrEof(&line_buf, '\n')) |line| {
if (line.len == 0) continue;
var field_iter = std.mem.splitScalar(u8, line, ';');
var cps: [3]u24 = undefined;
var len: u8 = 2;
var i: usize = 0;
while (field_iter.next()) |field| : (i += 1) {
switch (i) {
0 => cps[0] = try std.fmt.parseInt(u24, field, 16),
5 => {
// Not canonical.
if (field.len == 0 or field[0] == '<') continue :lines;
if (std.mem.indexOfScalar(u8, field, ' ')) |space| {
// Canonical
len = 3;
cps[1] = try std.fmt.parseInt(u24, field[0..space], 16);
cps[2] = try std.fmt.parseInt(u24, field[space + 1 ..], 16);
} else {
// Singleton
cps[1] = try std.fmt.parseInt(u24, field, 16);
}
},
2 => if (line[0] == '<') continue :lines,
else => {},
}
}
try writer.writeInt(u8, @intCast(len), endian);
for (cps[0..len]) |cp| try writer.writeInt(u24, cp, endian);
}
try writer.writeInt(u16, 0, endian);
try out_comp.flush();
}

135
deps/zg/codegen/case_prop.zig vendored Normal file
View File

@ -0,0 +1,135 @@
const std = @import("std");
const builtin = @import("builtin");
const mem = std.mem;
const block_size = 256;
const Block = [block_size]u8;
const BlockMap = std.HashMap(
Block,
u16,
struct {
pub fn hash(_: @This(), k: Block) u64 {
var hasher = std.hash.Wyhash.init(0);
std.hash.autoHashStrat(&hasher, k, .DeepRecursive);
return hasher.final();
}
pub fn eql(_: @This(), a: Block, b: Block) bool {
return mem.eql(u8, &a, &b);
}
},
std.hash_map.default_max_load_percentage,
);
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var flat_map = std.AutoHashMap(u21, u8).init(allocator);
defer flat_map.deinit();
var line_buf: [4096]u8 = undefined;
// Process DerivedCoreProperties.txt
var in_file = try std.fs.cwd().openFile("data/unicode/DerivedCoreProperties.txt", .{});
defer in_file.close();
var in_buf = std.io.bufferedReader(in_file.reader());
const in_reader = in_buf.reader();
while (try in_reader.readUntilDelimiterOrEof(&line_buf, '\n')) |line| {
if (line.len == 0 or line[0] == '#') continue;
const no_comment = if (mem.indexOfScalar(u8, line, '#')) |octo| line[0..octo] else line;
var field_iter = mem.tokenizeAny(u8, no_comment, "; ");
var current_code: [2]u21 = undefined;
var i: usize = 0;
while (field_iter.next()) |field| : (i += 1) {
switch (i) {
0 => {
// Code point(s)
if (mem.indexOf(u8, field, "..")) |dots| {
current_code = .{
try std.fmt.parseInt(u21, field[0..dots], 16),
try std.fmt.parseInt(u21, field[dots + 2 ..], 16),
};
} else {
const code = try std.fmt.parseInt(u21, field, 16);
current_code = .{ code, code };
}
},
1 => {
// Props
var bit: u8 = 0;
if (mem.eql(u8, field, "Lowercase")) bit = 1;
if (mem.eql(u8, field, "Uppercase")) bit = 2;
if (mem.eql(u8, field, "Cased")) bit = 4;
if (bit != 0) {
for (current_code[0]..current_code[1] + 1) |cp| {
const gop = try flat_map.getOrPut(@intCast(cp));
if (!gop.found_existing) gop.value_ptr.* = 0;
gop.value_ptr.* |= bit;
}
}
},
else => {},
}
}
}
var blocks_map = BlockMap.init(allocator);
defer blocks_map.deinit();
var stage1 = std.ArrayList(u16).init(allocator);
defer stage1.deinit();
var stage2 = std.ArrayList(u8).init(allocator);
defer stage2.deinit();
var block: Block = [_]u8{0} ** block_size;
var block_len: u16 = 0;
for (0..0x110000) |i| {
const cp: u21 = @intCast(i);
const prop = flat_map.get(cp) orelse 0;
// Process block
block[block_len] = prop;
block_len += 1;
if (block_len < block_size and cp != 0x10ffff) continue;
const gop = try blocks_map.getOrPut(block);
if (!gop.found_existing) {
gop.value_ptr.* = @intCast(stage2.items.len);
try stage2.appendSlice(&block);
}
try stage1.append(gop.value_ptr.*);
block_len = 0;
}
var args_iter = try std.process.argsWithAllocator(allocator);
defer args_iter.deinit();
_ = args_iter.skip();
const output_path = args_iter.next() orelse @panic("No output file arg!");
const compressor = std.compress.flate.deflate.compressor;
var out_file = try std.fs.cwd().createFile(output_path, .{});
defer out_file.close();
var out_comp = try compressor(.raw, out_file.writer(), .{ .level = .best });
const writer = out_comp.writer();
const endian = builtin.cpu.arch.endian();
try writer.writeInt(u16, @intCast(stage1.items.len), endian);
for (stage1.items) |i| try writer.writeInt(u16, i, endian);
try writer.writeInt(u16, @intCast(stage2.items.len), endian);
try writer.writeAll(stage2.items);
try out_comp.flush();
}

Some files were not shown because too many files have changed in this diff Show More