init
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:
203
deps/libvaxis/src/Cell.zig
vendored
Normal file
203
deps/libvaxis/src/Cell.zig
vendored
Normal 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
20
deps/libvaxis/src/GraphemeCache.zig
vendored
Normal 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
188
deps/libvaxis/src/Image.zig
vendored
Normal 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
123
deps/libvaxis/src/InternalScreen.zig
vendored
Normal 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
434
deps/libvaxis/src/Key.zig
vendored
Normal 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
308
deps/libvaxis/src/Loop.zig
vendored
Normal 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
50
deps/libvaxis/src/Mouse.zig
vendored
Normal 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
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
78
deps/libvaxis/src/Screen.zig
vendored
Normal 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
28
deps/libvaxis/src/Unicode.zig
vendored
Normal 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
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
854
deps/libvaxis/src/Window.zig
vendored
Normal 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
207
deps/libvaxis/src/aio.zig
vendored
Normal 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
137
deps/libvaxis/src/ctlseqs.zig
vendored
Normal 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
28
deps/libvaxis/src/event.zig
vendored
Normal 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
80
deps/libvaxis/src/gwidth.zig
vendored
Normal 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
92
deps/libvaxis/src/main.zig
vendored
Normal 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
178
deps/libvaxis/src/posix/Tty.zig
vendored
Normal 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
325
deps/libvaxis/src/queue.zig
vendored
Normal 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
17
deps/libvaxis/src/widgets.zig
vendored
Normal 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
112
deps/libvaxis/src/widgets/CodeView.zig
vendored
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
deps/libvaxis/src/widgets/LineNumbers.zig
vendored
Normal file
54
deps/libvaxis/src/widgets/LineNumbers.zig
vendored
Normal 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
128
deps/libvaxis/src/widgets/ScrollView.zig
vendored
Normal 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
33
deps/libvaxis/src/widgets/Scrollbar.zig
vendored
Normal 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
163
deps/libvaxis/src/widgets/Table.zig
vendored
Normal 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
346
deps/libvaxis/src/widgets/TextInput.zig
vendored
Normal 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
191
deps/libvaxis/src/widgets/TextView.zig
vendored
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
7
deps/libvaxis/src/widgets/alignment.zig
vendored
Normal file
7
deps/libvaxis/src/widgets/alignment.zig
vendored
Normal 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
52
deps/libvaxis/src/widgets/border.zig
vendored
Normal 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 });
|
||||
}
|
||||
117
deps/libvaxis/src/widgets/terminal/Command.zig
vendored
Normal file
117
deps/libvaxis/src/widgets/terminal/Command.zig
vendored
Normal 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;
|
||||
}
|
||||
184
deps/libvaxis/src/widgets/terminal/Parser.zig
vendored
Normal file
184
deps/libvaxis/src/widgets/terminal/Parser.zig
vendored
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
59
deps/libvaxis/src/widgets/terminal/Pty.zig
vendored
Normal file
59
deps/libvaxis/src/widgets/terminal/Pty.zig
vendored
Normal 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,
|
||||
};
|
||||
}
|
||||
513
deps/libvaxis/src/widgets/terminal/Screen.zig
vendored
Normal file
513
deps/libvaxis/src/widgets/terminal/Screen.zig
vendored
Normal 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);
|
||||
}
|
||||
794
deps/libvaxis/src/widgets/terminal/Terminal.zig
vendored
Normal file
794
deps/libvaxis/src/widgets/terminal/Terminal.zig
vendored
Normal 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);
|
||||
}
|
||||
143
deps/libvaxis/src/widgets/terminal/ansi.zig
vendored
Normal file
143
deps/libvaxis/src/widgets/terminal/ansi.zig
vendored
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
172
deps/libvaxis/src/widgets/terminal/key.zig
vendored
Normal file
172
deps/libvaxis/src/widgets/terminal/key.zig
vendored
Normal 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
497
deps/libvaxis/src/windows/Tty.zig
vendored
Normal 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
271
deps/libvaxis/src/xev.zig
vendored
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user