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:
2024-08-09 17:32:06 -07:00
commit 7692cb4bc7
155 changed files with 206515 additions and 0 deletions

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;
}
};
}