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

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