Compare commits

..

10 commits

Author SHA1 Message Date
54c43bab5d
wip: macos 2024-07-04 14:53:40 +02:00
CJ van den Berg
c213919849 tty(windows): fix resize event screen size translation from srWindow 2024-06-27 15:24:09 -07:00
Jari Vetoniemi
b84f9e58a6 vaxis: add aio event loop and example
disabled by default, aio/coro might not be that mature yet.
however, appreciated if people give it a go and report issues.
2024-06-27 08:32:21 -07:00
Tim Culverhouse
935c5a54bc window(wrap): refactor word wrap
Commit 74fb130797 "fix: `Window.printSegments` correctly prints all
non-trailing whitespace" fixed some bugs with word wrapping, but also
introduced a bug with printing leading whitespace in a segment.

Refactor the entire word wrap logic to use a custom LineIterator and a
tokenizer which gives whitespace tokens as well.
2024-06-26 12:40:50 -05:00
Tim Culverhouse
7c03077177 window: use saturating sub in border calcs 2024-06-25 09:07:33 -05:00
Jari Vetoniemi
40e5b673b6 scrollbar: use divCeil to calculate correct height 2024-06-25 06:57:52 -07:00
Rylee Lyman
f7cbd42ed5 fix: pass tests 2024-06-25 06:55:44 -07:00
Rylee Lyman
74fb130797 fix: Window.printSegments correctly prints all non-trailing whitespace
I'm told it's not unusual in the web world for multiple whitespace
characters to be consumed as a single space. If that is the intention
for `.word` wrap, that's fine with me! If not, this PR correctly
prints all non-trailing whitespace.

I also chose to consume a `\r\n` sequence as intending a single newline,
while all other sequences of `\r` and `\n` should produce multiple newlines.
2024-06-25 06:55:44 -07:00
Tim Culverhouse
fcb705adaa mouse: remove translateMouse debug log 2024-06-24 12:47:45 -05:00
Rylee Lyman
b2151349ad feat: add col_offset to Window.print
this commit adds convenience to `Window.print` by allowing it to
start printing at a nonzero column.
2024-06-21 02:57:58 -07:00
15 changed files with 944 additions and 102 deletions

View file

@ -4,11 +4,13 @@ pub fn build(b: *std.Build) void {
const include_libxev = b.option(bool, "libxev", "Enable support for libxev library (default: true)") orelse true; const include_libxev = b.option(bool, "libxev", "Enable support for libxev library (default: true)") orelse true;
const include_images = b.option(bool, "images", "Enable support for images (default: true)") orelse true; const include_images = b.option(bool, "images", "Enable support for images (default: true)") orelse true;
const include_text_input = b.option(bool, "text_input", "Enable support for the TextInput widget (default: true)") orelse true; const include_text_input = b.option(bool, "text_input", "Enable support for the TextInput widget (default: true)") orelse true;
const include_aio = b.option(bool, "aio", "Enable support for zig-aio library (default: false)") orelse false;
const options = b.addOptions(); const options = b.addOptions();
options.addOption(bool, "libxev", include_libxev); options.addOption(bool, "libxev", include_libxev);
options.addOption(bool, "images", include_images); options.addOption(bool, "images", include_images);
options.addOption(bool, "text_input", include_text_input); options.addOption(bool, "text_input", include_text_input);
options.addOption(bool, "aio", include_aio);
const options_mod = options.createModule(); const options_mod = options.createModule();
@ -33,6 +35,10 @@ pub fn build(b: *std.Build) void {
.optimize = optimize, .optimize = optimize,
.target = target, .target = target,
}) else null; }) else null;
const aio_dep = if (include_aio) b.lazyDependency("aio", .{
.optimize = optimize,
.target = target,
}) else null;
// Module // Module
const vaxis_mod = b.addModule("vaxis", .{ const vaxis_mod = b.addModule("vaxis", .{
@ -46,6 +52,8 @@ pub fn build(b: *std.Build) void {
if (zigimg_dep) |dep| vaxis_mod.addImport("zigimg", dep.module("zigimg")); if (zigimg_dep) |dep| vaxis_mod.addImport("zigimg", dep.module("zigimg"));
if (gap_buffer_dep) |dep| vaxis_mod.addImport("gap_buffer", dep.module("gap_buffer")); if (gap_buffer_dep) |dep| vaxis_mod.addImport("gap_buffer", dep.module("gap_buffer"));
if (xev_dep) |dep| vaxis_mod.addImport("xev", dep.module("xev")); if (xev_dep) |dep| vaxis_mod.addImport("xev", dep.module("xev"));
if (aio_dep) |dep| vaxis_mod.addImport("aio", dep.module("aio"));
if (aio_dep) |dep| vaxis_mod.addImport("coro", dep.module("coro"));
vaxis_mod.addImport("build_options", options_mod); vaxis_mod.addImport("build_options", options_mod);
// Examples // Examples
@ -59,6 +67,7 @@ pub fn build(b: *std.Build) void {
vaxis, vaxis,
vt, vt,
xev, xev,
aio,
}; };
const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input; const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input;
const example_step = b.step("example", "Run example"); const example_step = b.step("example", "Run example");
@ -73,6 +82,8 @@ pub fn build(b: *std.Build) void {
}); });
example.root_module.addImport("vaxis", vaxis_mod); example.root_module.addImport("vaxis", vaxis_mod);
if (xev_dep) |dep| example.root_module.addImport("xev", dep.module("xev")); if (xev_dep) |dep| example.root_module.addImport("xev", dep.module("xev"));
if (aio_dep) |dep| example.root_module.addImport("aio", dep.module("aio"));
if (aio_dep) |dep| example.root_module.addImport("coro", dep.module("coro"));
const example_run = b.addRunArtifact(example); const example_run = b.addRunArtifact(example);
example_step.dependOn(&example_run.step); example_step.dependOn(&example_run.step);

View file

@ -22,6 +22,11 @@
.hash = "12207b7a5b538ffb7fb18f954ae17d2f8490b6e3778a9e30564ad82c58ee8da52361", .hash = "12207b7a5b538ffb7fb18f954ae17d2f8490b6e3778a9e30564ad82c58ee8da52361",
.lazy = true, .lazy = true,
}, },
.aio = .{
.url = "git+https://github.com/Cloudef/zig-aio#be8e2b374bf223202090e282447fa4581029c2eb",
.hash = "122012a11b37a350395a32fdb514e57ff54a0f9d8d4ce09498b6c45ffb7211232920",
.lazy = true,
},
}, },
.paths = .{ .paths = .{
"LICENSE", "LICENSE",

171
examples/aio.zig Normal file
View file

@ -0,0 +1,171 @@
const builtin = @import("builtin");
const std = @import("std");
const vaxis = @import("vaxis");
const aio = @import("aio");
const coro = @import("coro");
pub const panic = vaxis.panic_handler;
const Event = union(enum) {
key_press: vaxis.Key,
winsize: vaxis.Winsize,
};
const Loop = vaxis.aio.Loop(Event);
const Video = enum { no_state, ready, end };
const Audio = enum { no_state, ready, end };
fn downloadTask(allocator: std.mem.Allocator, url: []const u8) ![]const u8 {
var client: std.http.Client = .{ .allocator = allocator };
defer client.deinit();
var body = std.ArrayList(u8).init(allocator);
_ = try client.fetch(.{
.location = .{ .url = url },
.response_storage = .{ .dynamic = &body },
.max_append_size = 1.6e+7,
});
return try body.toOwnedSlice();
}
fn audioTask(allocator: std.mem.Allocator) !void {
errdefer coro.yield(Audio.end) catch {};
// var child = std.process.Child.init(&.{ "aplay", "-Dplug:default", "-q", "-f", "S16_LE", "-r", "8000" }, allocator);
var child = std.process.Child.init(&.{ "mpv", "--audio-samplerate=16000", "--audio-channels=mono", "--audio-format=s16", "-" }, allocator);
child.stdin_behavior = .Pipe;
child.stdout_behavior = .Ignore;
child.stderr_behavior = .Ignore;
child.spawn() catch return; // no sound
defer _ = child.kill() catch {};
const sound = blk: {
var tpool: coro.ThreadPool = .{};
try tpool.start(allocator, 1);
defer tpool.deinit();
break :blk try tpool.yieldForCompletition(downloadTask, .{ allocator, "https://keroserene.net/lol/roll.s16" });
};
defer allocator.free(sound);
try coro.yield(Audio.ready);
var audio_off: usize = 0;
while (audio_off < sound.len) {
var written: usize = 0;
try coro.io.single(aio.Write{ .file = child.stdin.?, .buffer = sound[audio_off..], .out_written = &written });
audio_off += written;
}
// the audio is already fed to the player and the defer
// would kill the child stay here chilling
coro.yield(Audio.end) catch {};
}
fn videoTask(writer: std.io.AnyWriter) !void {
defer coro.yield(Video.end) catch {};
var socket: std.posix.socket_t = undefined;
try coro.io.single(aio.Socket{
.domain = std.posix.AF.INET,
.flags = std.posix.SOCK.STREAM | std.posix.SOCK.CLOEXEC,
.protocol = std.posix.IPPROTO.TCP,
.out_socket = &socket,
});
defer std.posix.close(socket);
const address = std.net.Address.initIp4(.{ 44, 224, 41, 160 }, 1987);
try coro.io.single(aio.Connect{
.socket = socket,
.addr = &address.any,
.addrlen = address.getOsSockLen(),
});
try coro.yield(Video.ready);
var buf: [1024]u8 = undefined;
while (true) {
var read: usize = 0;
try coro.io.single(aio.Recv{ .socket = socket, .buffer = &buf, .out_read = &read });
if (read == 0) break;
_ = try writer.write(buf[0..read]);
}
}
fn loadingTask(vx: *vaxis.Vaxis, writer: std.io.AnyWriter) !void {
var color_idx: u8 = 30;
var dir: enum { up, down } = .up;
while (true) {
try coro.io.single(aio.Timeout{ .ns = 8 * std.time.ns_per_ms });
const style: vaxis.Style = .{ .fg = .{ .rgb = [_]u8{ color_idx, color_idx, color_idx } } };
const segment: vaxis.Segment = .{ .text = vaxis.logo, .style = style };
const win = vx.window();
win.clear();
var loc = vaxis.widgets.alignment.center(win, 28, 4);
_ = try loc.printSegment(segment, .{ .wrap = .grapheme });
switch (dir) {
.up => {
color_idx += 1;
if (color_idx == 255) dir = .down;
},
.down => {
color_idx -= 1;
if (color_idx == 30) dir = .up;
},
}
try vx.render(writer);
}
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var tty = try vaxis.Tty.init();
defer tty.deinit();
var vx = try vaxis.init(allocator, .{});
defer vx.deinit(allocator, tty.anyWriter());
var scheduler = try coro.Scheduler.init(allocator, .{});
defer scheduler.deinit();
var loop = try Loop.init();
try loop.spawn(&scheduler, &vx, &tty, null, .{});
defer loop.deinit(&vx, &tty);
try vx.enterAltScreen(tty.anyWriter());
try vx.queryTerminalSend(tty.anyWriter());
var buffered_tty_writer = tty.bufferedWriter();
const loading = try scheduler.spawn(loadingTask, .{ &vx, buffered_tty_writer.writer().any() }, .{});
const audio = try scheduler.spawn(audioTask, .{allocator}, .{});
const video = try scheduler.spawn(videoTask, .{buffered_tty_writer.writer().any()}, .{});
main: while (try scheduler.tick(.blocking) > 0) {
while (try loop.popEvent()) |event| switch (event) {
.key_press => |key| {
if (key.matches('c', .{ .ctrl = true })) {
break :main;
}
},
.winsize => |ws| try vx.resize(allocator, buffered_tty_writer.writer().any(), ws),
};
if (audio.state(Video) == .ready and video.state(Audio) == .ready) {
loading.cancel();
audio.wakeup();
video.wakeup();
} else if (audio.state(Audio) == .end and video.state(Video) == .end) {
break :main;
}
try buffered_tty_writer.flush();
}
}

View file

@ -61,9 +61,11 @@ pub fn main() !void {
var redraw: bool = false; var redraw: bool = false;
while (true) { while (true) {
std.debug.print("inside while loop before resize\n", .{});
std.time.sleep(8 * std.time.ns_per_ms); std.time.sleep(8 * std.time.ns_per_ms);
// try vt events first // try vt events first
while (vt.tryEvent()) |event| { while (vt.tryEvent()) |event| {
std.debug.print("inside stryEventloop \n", .{});
redraw = true; redraw = true;
switch (event) { switch (event) {
.bell => {}, .bell => {},
@ -74,6 +76,7 @@ pub fn main() !void {
} }
} }
while (loop.tryEvent()) |event| { while (loop.tryEvent()) |event| {
std.debug.print("inside loop.tryEvent\n", .{});
redraw = true; redraw = true;
switch (event) { switch (event) {
.key_press => |key| { .key_press => |key| {

172
log.lg Normal file
View file

@ -0,0 +1,172 @@
info(loop): pixel mouse capability detected
info(loop): unicode capability detected
info(loop): color_scheme_updates capability detected
info(loop): kitty keyboard capability detected
info(loop): kitty graphics capability detected
slave name: /dev/ttys008
posix opened : /dev/ttys008
set size done
inside spawnwe are not child: 56928
inside while loop before resize
inside run
inside while loop
inside loop.tryEvent
debug(vaxis): resizing screen: width=81 height=49
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside loop.tryEvent
before switch event
inside print event
inside while loop
inside while loop before resize
inside stryEventloop
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside loop.tryEvent
before switch event
inside print event
inside while loop
inside while loop before resize
inside stryEventloop
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside loop.tryEvent
before switch event
inside handlec0
inside while loop
before switch event
inside handlec0
inside while loop
inside while loop before resize
inside stryEventloop
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside loop.tryEvent
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside while loop before resize
inside loop.tryEvent
before switch event
inside handlec0
debug(vaxis): total renders = 8
debug(vaxis): microseconds per render = 1393

View file

@ -704,18 +704,6 @@ pub fn translateMouse(self: Vaxis, mouse: Mouse) Mouse {
result.row = ypos / ycell; result.row = ypos / ycell;
result.xoffset = xpos % xcell; result.xoffset = xpos % xcell;
result.yoffset = ypos % ycell; result.yoffset = ypos % ycell;
log.debug("translateMouse x/ypos:{d}/{d} cell:{d}/{d} xtra:{d}/{d} col/rol:{d}/{d} x/y:{d}/{d}", .{
xpos, ypos,
xcell, ycell,
xextra, yextra,
result.col, result.row,
result.xoffset, result.yoffset,
});
} else {
log.debug("translateMouse col/rol:{d}/{d} x/y:{d}/{d}", .{
result.col, result.row,
result.xoffset, result.yoffset,
});
} }
return result; return result;
} }

View file

@ -182,8 +182,8 @@ pub fn child(self: Window, opts: ChildOptions) Window {
const y_off: usize = if (loc.top) 1 else 0; const y_off: usize = if (loc.top) 1 else 0;
const h_delt: usize = if (loc.bottom) 1 else 0; const h_delt: usize = if (loc.bottom) 1 else 0;
const w_delt: usize = if (loc.right) 1 else 0; const w_delt: usize = if (loc.right) 1 else 0;
const h_ch: usize = h - y_off - h_delt; const h_ch: usize = h -| y_off -| h_delt;
const w_ch: usize = w - x_off - w_delt; const w_ch: usize = w -| x_off -| w_delt;
return result.initChild(x_off, y_off, .{ .limit = w_ch }, .{ .limit = h_ch }); return result.initChild(x_off, y_off, .{ .limit = w_ch }, .{ .limit = h_ch });
} }
@ -256,6 +256,8 @@ pub fn setCursorShape(self: Window, shape: Cell.CursorShape) void {
pub const PrintOptions = struct { pub const PrintOptions = struct {
/// vertical offset to start printing at /// vertical offset to start printing at
row_offset: usize = 0, row_offset: usize = 0,
/// horizontal offset to start printing at
col_offset: usize = 0,
/// wrap behavior for printing /// wrap behavior for printing
wrap: enum { wrap: enum {
@ -285,7 +287,7 @@ pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) !Print
var row = opts.row_offset; var row = opts.row_offset;
switch (opts.wrap) { switch (opts.wrap) {
.grapheme => { .grapheme => {
var col: usize = 0; var col: usize = opts.col_offset;
const overflow: bool = blk: for (segments) |segment| { const overflow: bool = blk: for (segments) |segment| {
var iter = self.screen.unicode.graphemeIterator(segment.text); var iter = self.screen.unicode.graphemeIterator(segment.text);
while (iter.next()) |grapheme| { while (iter.next()) |grapheme| {
@ -324,58 +326,59 @@ pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) !Print
}; };
}, },
.word => { .word => {
var col: usize = 0; var col: usize = opts.col_offset;
var overflow: bool = false; var overflow: bool = false;
var soft_wrapped: bool = false; var soft_wrapped: bool = false;
for (segments) |segment| { outer: for (segments) |segment| {
var start: usize = 0; var line_iter: LineIterator = .{ .buf = segment.text };
var i: usize = 0; while (line_iter.next()) |line| {
while (i < segment.text.len) : (i += 1) { defer {
// for (segment.text, 0..) |b, i| { // We only set soft_wrapped to false if a segment actually contains a linebreak
const b = segment.text[i]; if (line_iter.has_break) {
const end = switch (b) { soft_wrapped = false;
' ',
'\r',
'\n',
=> i,
else => if (i != segment.text.len - 1) continue else i + 1,
};
const word = segment.text[start..end];
// find the start of the next word
start = while (i + 1 < segment.text.len) : (i += 1) {
if (segment.text[i + 1] == ' ') continue;
break i + 1;
} else i;
const width = self.gwidth(word);
const non_wsp_width: usize = for (word, 0..) |wb, wi| {
if (wb == '\r' or wb == '\n') {
row += 1; row += 1;
col = 0; col = 0;
break width -| wi -| 1;
} }
if (wb != ' ') break width - wi; }
} else 0; var iter: WhitespaceTokenizer = .{ .buf = line };
while (iter.next()) |token| {
if (width + col > self.width and non_wsp_width < self.width) { switch (token) {
// wrap .whitespace => |len| {
row += 1; if (soft_wrapped) continue;
for (0..len) |_| {
if (col >= self.width) {
col = 0; col = 0;
soft_wrapped = true; row += 1;
}
if (row >= self.height) {
overflow = true;
break; break;
} }
// if we are soft wrapped, (col == 0 and row > 0), then trim if (opts.commit) {
// leading spaces self.writeCell(col, row, .{
const printed_word = if (soft_wrapped) .char = .{
std.mem.trimLeft(u8, word, " ") .grapheme = " ",
else .width = 1,
word; },
defer soft_wrapped = false; .style = segment.style,
var iter = self.screen.unicode.graphemeIterator(printed_word); .link = segment.link,
while (iter.next()) |grapheme| { });
const s = grapheme.bytes(printed_word); }
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); const w = self.gwidth(s);
if (opts.commit) self.writeCell(col, row, .{ if (opts.commit) self.writeCell(col, row, .{
.char = .{ .char = .{
@ -389,29 +392,11 @@ pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) !Print
if (col >= self.width) { if (col >= self.width) {
row += 1; row += 1;
col = 0; col = 0;
soft_wrapped = true;
} }
} }
switch (b) {
' ' => {
if (col > 0) {
if (opts.commit) self.writeCell(col, row, .{
.char = .{
.grapheme = " ",
.width = 1,
}, },
.style = segment.style,
.link = segment.link,
});
col += 1;
} }
},
'\r',
'\n',
=> {
col = 0;
row += 1;
},
else => {},
} }
} }
} }
@ -423,7 +408,7 @@ pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) !Print
}; };
}, },
.none => { .none => {
var col: usize = 0; var col: usize = opts.col_offset;
const overflow: bool = blk: for (segments) |segment| { const overflow: bool = blk: for (segments) |segment| {
var iter = self.screen.unicode.graphemeIterator(segment.text); var iter = self.screen.unicode.graphemeIterator(segment.text);
while (iter.next()) |grapheme| { while (iter.next()) |grapheme| {
@ -636,6 +621,24 @@ test "print: word" {
try std.testing.expectEqual(0, result.row); try std.testing.expectEqual(0, result.row);
try std.testing.expectEqual(false, result.overflow); 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{ var segments = [_]Segment{
.{ .text = "a b" }, .{ .text = "a b" },
@ -749,4 +752,104 @@ test "print: word" {
try std.testing.expectEqual(1, result.row); try std.testing.expectEqual(1, result.row);
try std.testing.expectEqual(false, result.overflow); 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] };
},
}
}
};

268
src/aio.zig Normal file
View file

@ -0,0 +1,268 @@
const builtin = @import("builtin");
const std = @import("std");
const aio = @import("aio");
const coro = @import("coro");
const vaxis = @import("main.zig");
const log = std.log.scoped(.vaxis_aio);
comptime {
if (builtin.target.os.tag == .windows) {
@compileError("Windows is not supported right now");
}
}
const Yield = enum { no_state, took_event };
/// zig-aio based event loop
/// <https://github.com/Cloudef/zig-aio>
pub fn Loop(comptime T: 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 (@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 ttyReaderInner(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.Read{ .file = 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;
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);
}
try 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);
}
try self.postEvent(.{ .key_release = mut_key });
}
},
.mouse => |mouse| {
if (@hasField(Event, "mouse")) {
try self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
}
},
.focus_in => {
if (@hasField(Event, "focus_in")) {
try self.postEvent(.focus_in);
}
},
.focus_out => {
if (@hasField(Event, "focus_out")) {
try self.postEvent(.focus_out);
}
},
.paste_start => {
if (@hasField(Event, "paste_start")) {
try self.postEvent(.paste_start);
}
},
.paste_end => {
if (@hasField(Event, "paste_end")) {
try self.postEvent(.paste_end);
}
},
.paste => |text| {
if (@hasField(Event, "paste")) {
try self.postEvent(.{ .paste = text });
} else {
if (paste_allocator) |_|
paste_allocator.?.free(text);
}
},
.color_report => |report| {
if (@hasField(Event, "color_report")) {
try self.postEvent(.{ .color_report = report });
}
},
.color_scheme => |scheme| {
if (@hasField(Event, "color_scheme")) {
try 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 => unreachable, // handled elsewhere for posix
}
}
}
}
fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void {
self.ttyReaderInner(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();
}
};
}

View file

@ -6,6 +6,7 @@ pub const Vaxis = @import("Vaxis.zig");
pub const Loop = @import("Loop.zig").Loop; pub const Loop = @import("Loop.zig").Loop;
pub const xev = @import("xev.zig"); pub const xev = @import("xev.zig");
pub const aio = @import("aio.zig");
pub const Queue = @import("queue.zig").Queue; pub const Queue = @import("queue.zig").Queue;
pub const Key = @import("Key.zig"); pub const Key = @import("Key.zig");

View file

@ -25,8 +25,7 @@ pub fn draw(self: Scrollbar, win: vaxis.Window) void {
// don't draw when all items can be shown // don't draw when all items can be shown
if (self.view_size >= self.total) return; if (self.view_size >= self.total) return;
var bar_height = self.view_size * win.height / self.total; const bar_height = @max(std.math.divCeil(usize, self.view_size * win.height, self.total) catch unreachable, 1);
if (bar_height < 0) bar_height = 1;
const bar_top = self.top * win.height / self.total; const bar_top = self.top * win.height / self.total;
var i: usize = 0; var i: usize = 0;
while (i < bar_height) : (i += 1) while (i < bar_height) : (i += 1)

View file

@ -17,6 +17,22 @@ pid: ?std.posix.pid_t = null,
env_map: *const std.process.EnvMap, env_map: *const std.process.EnvMap,
pty: Pty, pty: Pty,
const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY;
const TIOCSWINSZ = if (builtin.os.tag == .macos) 2148037735 else c.TIOCSWINSZ;
const TIOCGWINSZ = if (builtin.os.tag == .macos) 1074295912 else c.TIOCGWINSZ;
extern "c" fn setsid() std.c.pid_t;
const c = struct {
usingnamespace switch (builtin.os.tag) {
.macos => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("util.h"); // openpty()
}),
else => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("pty.h");
}),
};
};
pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void { pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void {
var arena_allocator = std.heap.ArenaAllocator.init(allocator); var arena_allocator = std.heap.ArenaAllocator.init(allocator);
@ -33,10 +49,12 @@ pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void {
if (pid == 0) { if (pid == 0) {
// we are the child // we are the child
_ = std.os.linux.setsid(); _ = std.os.linux.setsid();
_ = setsid();
// if (setsid() < 0) return error.ProcessGroupFailed;
// set the controlling terminal // set the controlling terminal
var u: c_uint = std.posix.STDIN_FILENO; var u: c_uint = std.posix.STDIN_FILENO;
if (posix.system.ioctl(self.pty.tty, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError; if (c.ioctl(self.pty.tty, TIOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError;
// set up io // set up io
try posix.dup2(self.pty.tty, std.posix.STDIN_FILENO); try posix.dup2(self.pty.tty, std.posix.STDIN_FILENO);
@ -53,6 +71,10 @@ pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void {
// exec // exec
const err = std.posix.execvpeZ(argv_buf.ptr[0].?, argv_buf.ptr, envp); const err = std.posix.execvpeZ(argv_buf.ptr[0].?, argv_buf.ptr, envp);
_ = err catch {}; _ = err catch {};
} else {
std.debug.print("we are not child: {d}\n", .{pid});
// posix.close(self.pty.tty);
_ = posix.waitpid(pid, 0);
} }
// we are the parent // we are the parent

View file

@ -7,6 +7,25 @@ const Winsize = @import("../../main.zig").Winsize;
const posix = std.posix; const posix = std.posix;
extern "c" fn setsid() std.c.pid_t;
const c = struct {
usingnamespace switch (builtin.os.tag) {
.macos => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("util.h"); // openpty()
}),
else => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("pty.h");
}),
};
};
const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY;
const TIOCSWINSZ = if (builtin.os.tag == .macos) 2148037735 else c.TIOCSWINSZ;
const TIOCGWINSZ = if (builtin.os.tag == .macos) 1074295912 else c.TIOCGWINSZ;
pty: posix.fd_t, pty: posix.fd_t,
tty: posix.fd_t, tty: posix.fd_t,
@ -14,6 +33,7 @@ tty: posix.fd_t,
pub fn init() !Pty { pub fn init() !Pty {
switch (builtin.os.tag) { switch (builtin.os.tag) {
.linux => return openPtyLinux(), .linux => return openPtyLinux(),
.macos => return openPtyMacos2(),
else => @compileError("unsupported os"), else => @compileError("unsupported os"),
} }
} }
@ -32,10 +52,75 @@ pub fn setSize(self: Pty, ws: Winsize) !void {
.ws_xpixel = @truncate(ws.x_pixel), .ws_xpixel = @truncate(ws.x_pixel),
.ws_ypixel = @truncate(ws.y_pixel), .ws_ypixel = @truncate(ws.y_pixel),
}; };
if (posix.system.ioctl(self.pty, posix.T.IOCSWINSZ, @intFromPtr(&_ws)) != 0) if (c.ioctl(self.pty, TIOCSWINSZ, @intFromPtr(&_ws)) != 0)
return error.SetWinsizeError; return error.SetWinsizeError;
} }
fn openPtyMacos2() !Pty {
const p = try posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
errdefer posix.close(p);
// unlockpt
// var n: c_uint = 0;
if (c.ioctl(p, c.TIOCPTYUNLK) != 0) return error.IoctlError;
// ptsname
if (c.ioctl(p, c.TIOCPTYGRANT) != 0) return error.IoctlError;
var buf: [128]u8 = undefined;
// var buf2: [128]u8 = undefined;
if (c.ioctl(p, c.TIOCPTYGNAME, &buf) != 0) return error.IoctlError;
const sname = buf[0 .. 13 - 1];
// std.debug.print("sizeof buf: {d}", .{buf.len});
// const sname = try std.fmt.bufPrint(&buf2, "{s}", .{buf});
std.debug.print("slave name: {s}\n", .{sname});
// 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);
std.debug.print("posix opened : {s}\n", .{sname});
var attrs: c.termios = undefined;
if (c.tcgetattr(p, &attrs) != 0)
return error.OpenptyFailed;
attrs.c_iflag |= c.IUTF8;
if (c.tcsetattr(p, c.TCSANOW, &attrs) != 0)
return error.OpenptyFailed;
return .{
.pty = p,
.tty = t,
};
}
fn openPtyMacos() !Pty {
var master_fd: posix.fd_t = undefined;
var slave_fd: posix.fd_t = undefined;
if (c.openpty(
&master_fd,
&slave_fd,
null,
null,
null,
) < 0)
return error.OpenptyFailed;
errdefer {
_ = posix.system.close(master_fd);
_ = posix.system.close(slave_fd);
}
var attrs: c.termios = undefined;
if (c.tcgetattr(master_fd, &attrs) != 0)
return error.OpenptyFailed;
attrs.c_iflag |= c.IUTF8;
if (c.tcsetattr(master_fd, c.TCSANOW, &attrs) != 0)
return error.OpenptyFailed;
return .{
.pty = master_fd,
.tty = slave_fd,
};
}
fn openPtyLinux() !Pty { fn openPtyLinux() !Pty {
const p = try posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0); const p = try posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
errdefer posix.close(p); errdefer posix.close(p);

View file

@ -96,6 +96,7 @@ pub fn init(
) !Terminal { ) !Terminal {
const pty = try Pty.init(); const pty = try Pty.init();
try pty.setSize(opts.winsize); try pty.setSize(opts.winsize);
std.debug.print("set size done\n", .{});
const cmd: Command = .{ const cmd: Command = .{
.argv = argv, .argv = argv,
.env_map = env, .env_map = env,
@ -155,6 +156,7 @@ pub fn deinit(self: *Terminal) void {
} }
pub fn spawn(self: *Terminal) !void { pub fn spawn(self: *Terminal) !void {
std.debug.print("inside spawn", .{});
if (self.thread != null) return; if (self.thread != null) return;
self.back_screen = &self.back_screen_pri; self.back_screen = &self.back_screen_pri;
@ -259,6 +261,7 @@ fn anyReader(self: *const Terminal) std.io.AnyReader {
/// process the output from the command on the pty /// process the output from the command on the pty
fn run(self: *Terminal) !void { fn run(self: *Terminal) !void {
std.debug.print("inside run\n", .{});
var parser: Parser = .{ var parser: Parser = .{
.buf = try std.ArrayList(u8).initCapacity(self.allocator, 128), .buf = try std.ArrayList(u8).initCapacity(self.allocator, 128),
}; };
@ -268,6 +271,7 @@ fn run(self: *Terminal) !void {
var reader = std.io.bufferedReader(self.anyReader()); var reader = std.io.bufferedReader(self.anyReader());
while (!self.should_quit) { while (!self.should_quit) {
std.debug.print("inside while loop\n", .{});
const event = try parser.parseReader(&reader); const event = try parser.parseReader(&reader);
self.back_mutex.lock(); self.back_mutex.lock();
defer self.back_mutex.unlock(); defer self.back_mutex.unlock();
@ -275,8 +279,10 @@ fn run(self: *Terminal) !void {
if (!self.dirty and self.event_queue.tryPush(.redraw)) if (!self.dirty and self.event_queue.tryPush(.redraw))
self.dirty = true; self.dirty = true;
std.debug.print("before switch event\n", .{});
switch (event) { switch (event) {
.print => |str| { .print => |str| {
std.debug.print("inside print event\n", .{});
var iter = grapheme.Iterator.init(str, &self.unicode.grapheme_data); var iter = grapheme.Iterator.init(str, &self.unicode.grapheme_data);
while (iter.next()) |g| { while (iter.next()) |g| {
const gr = g.bytes(str); const gr = g.bytes(str);
@ -287,6 +293,7 @@ fn run(self: *Terminal) !void {
}, },
.c0 => |b| try self.handleC0(b), .c0 => |b| try self.handleC0(b),
.escape => |esc| { .escape => |esc| {
std.debug.print("inside escape event\n", .{});
const final = esc[esc.len - 1]; const final = esc[esc.len - 1];
switch (final) { switch (final) {
'B' => {}, // TODO: handle charsets 'B' => {}, // TODO: handle charsets
@ -700,6 +707,7 @@ fn run(self: *Terminal) !void {
} }
inline fn handleC0(self: *Terminal, b: ansi.C0) !void { inline fn handleC0(self: *Terminal, b: ansi.C0) !void {
std.debug.print("inside handlec0\n", .{});
switch (b) { switch (b) {
.NUL, .SOH, .STX => {}, .NUL, .SOH, .STX => {},
.EOT => {}, // we send EOT to quit the read thread .EOT => {}, // we send EOT to quit the read thread
@ -716,6 +724,7 @@ inline fn handleC0(self: *Terminal, b: ansi.C0) !void {
} }
pub fn setMode(self: *Terminal, mode: u16, val: bool) void { pub fn setMode(self: *Terminal, mode: u16, val: bool) void {
std.debug.print("inside setmode\n", .{});
switch (mode) { switch (mode) {
7 => self.mode.autowrap = val, 7 => self.mode.autowrap = val,
25 => self.mode.cursor = val, 25 => self.mode.cursor = val,

View file

@ -0,0 +1,5 @@
info(loop): pixel mouse capability detected
info(loop): unicode capability detected
info(loop): color_scheme_updates capability detected
info(loop): kitty keyboard capability detected
info(loop): kitty graphics capability detected

View file

@ -379,8 +379,8 @@ pub fn nextEvent(self: *Tty) !Event {
return windows.unexpectedError(windows.kernel32.GetLastError()); return windows.unexpectedError(windows.kernel32.GetLastError());
} }
const window_rect = console_info.srWindow; const window_rect = console_info.srWindow;
const width = window_rect.Right - window_rect.Left; const width = window_rect.Right - window_rect.Left + 1;
const height = window_rect.Bottom - window_rect.Top; const height = window_rect.Bottom - window_rect.Top + 1;
return .{ return .{
.winsize = .{ .winsize = .{
.cols = @intCast(width), .cols = @intCast(width),