summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig14
-rw-r--r--main.zig141
-rw-r--r--tests.zig120
3 files changed, 243 insertions, 32 deletions
diff --git a/build.zig b/build.zig
index 9bc1d2b6..25520c27 100644
--- a/build.zig
+++ b/build.zig
@@ -12,6 +12,15 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("main.zig"),
}),
});
+ const tests = b.addExecutable(.{
+ .name = "pardes-e2e",
+ .root_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .root_source_file = b.path("tests.zig"),
+ .link_libc = true,
+ }),
+ });
// ghostty-vt and vaxis both depend on uucode, but Zig 0.16 forbids the
// same source file from belonging to two modules, so they can't each have
@@ -68,4 +77,9 @@ pub fn build(b: *std.Build) void {
const run = b.step("run", "descend to the pardes");
run.dependOn(&run_exe.step);
+
+ const run_tests = b.addRunArtifact(tests);
+ run_tests.addArtifactArg(exe);
+ const test_step = b.step("test", "run deterministic tests");
+ test_step.dependOn(&run_tests.step);
}
diff --git a/main.zig b/main.zig
index a6027b0c..60bc1290 100644
--- a/main.zig
+++ b/main.zig
@@ -14,6 +14,8 @@ extern "c" fn execvp(
argv: [*:null]const ?[*:0]const u8,
) c_int;
+extern "c" fn _exit(status: c_int) noreturn;
+
// basic file navigation
// basic shell io
// basic layouting: for now following acme and ad
@@ -32,6 +34,7 @@ pub const Command = struct {
quit,
tick,
key_press: vaxis.Key,
+ pty_read: []u8,
winsize: vaxis.Winsize,
mouse: vaxis.Mouse,
} = .nop;
@@ -51,8 +54,9 @@ fn fork_pty(argv: [:0]const u8, ws: std.posix.winsize) std.Io.File {
var master: c_int = undefined;
const pid = forkpty(&master, null, null, &ws);
if (pid == 0) {
- _ = execvp(argv, &.{});
- std.process.exit(0);
+ const child_argv = [_:null]?[*:0]const u8{ argv.ptr, null };
+ _ = execvp(argv.ptr, &child_argv);
+ _exit(127);
}
// not sure about the false here
@@ -60,8 +64,23 @@ fn fork_pty(argv: [:0]const u8, ws: std.posix.winsize) std.Io.File {
return f;
}
-const nrows = 16;
-const ncols = 80;
+fn readPty(io: std.Io, gpa: std.mem.Allocator, pty: std.Io.File, loop: *Loop) !void {
+ var read_buf: [0x1000]u8 = undefined;
+ var reader = pty.readerStreaming(io, &read_buf);
+ while (true) {
+ var buf: [0x1000]u8 = undefined;
+ var vec = [_][]u8{&buf};
+ const n = try reader.interface.readVec(&vec);
+ if (n == 0) break;
+ const bytes = try gpa.dupe(u8, buf[0..n]);
+ errdefer gpa.free(bytes);
+ try loop.postEvent(.{ .pty_read = bytes });
+ }
+ try loop.postEvent(.quit);
+}
+
+const nrows = 64;
+const ncols = 100;
pub fn main(init: std.process.Init) !void {
const io = init.io;
@@ -72,10 +91,8 @@ pub fn main(init: std.process.Init) !void {
.rows = nrows,
});
defer term.deinit(gpa);
- var tty_reader = term.vtStream();
- defer tty_reader.deinit();
-
- // const str = try term.plainString(gpa);
+ var term_stream = term.vtStream();
+ defer term_stream.deinit();
var tty_buf: [0x10000]u8 = undefined;
@@ -94,50 +111,110 @@ pub fn main(init: std.process.Init) !void {
var write_buf: [0x100]u8 = undefined;
var writer = pty_master.writer(io, &write_buf);
- std.debug.print("written {}\n", .{try writer.interface.write("ls -la /\r\n")});
+ std.debug.print("written {}\n", .{try writer.interface.write("ls /\r\n")});
try writer.flush();
- var read_buf: [0x400]u8 = undefined;
- var reader = pty_master.readerStreaming(io, &read_buf);
-
- try io.sleep(.fromMilliseconds(200), .real);
- const str = try reader.interface.readAlloc(gpa, 0x400);
-
- tty_reader.nextSlice(str);
-
- defer gpa.free(str);
-
- const tty_str = try term.plainString(gpa);
- defer gpa.free(tty_str);
-
- // std.debug.print("{s}", .{tty_str});
-
- // try vx.setMouseMode(tty.writer(), true);
+ try vx.setMouseMode(tty.writer(), true);
+ try vx.enterAltScreen(tty.writer());
+ defer vx.exitAltScreen(tty.writer()) catch {};
var loop: Loop = .init(io, &tty, &vx);
try loop.start();
defer loop.stop();
+ var pty_reader = try io.concurrent(readPty, .{ io, gpa, pty_master, &loop });
+ defer _ = pty_reader.cancel(io) catch {};
+ defer pty_master.close(io);
+ var dbg_print_buf: [0x1000]u8 = undefined;
+
while (true) {
const event = try loop.nextEvent();
const win = vx.window();
- const win_child = win.child(.{
- .border = .{ .where = .all },
- .height = nrows,
- .width = ncols,
- });
- _ = win_child.printSegment(.{ .text = tty_str }, .{});
-
switch (event) {
+ .mouse => |mouse| {
+ if (mouse.button == .wheel_down) {
+ term.screens.active.scroll(.{ .delta_row = 1 });
+ }
+
+ if (mouse.button == .wheel_up) {
+ term.screens.active.scroll(.{ .delta_row = -1 });
+ }
+ },
.key_press => |key| {
if (key.matches('c', .{ .ctrl = true })) {
break;
}
+
+ if (key.matches('k', .{ .ctrl = true })) {
+ win.clear();
+ }
+
+ // this is not what I expected; it's just blank up or down
+ // if (key.matches('d', .{ .ctrl = true })) {
+ // try term.scrollUp(1);
+ // }
+
+ if (key.matches('d', .{ .ctrl = true })) {
+ term.screens.active.scroll(.{ .delta_row = 15 });
+ }
+ if (key.matches('u', .{ .ctrl = true })) {
+ term.screens.active.scroll(.{ .delta_row = -15 });
+ }
+
+ if (key.text) |text| {
+ try writer.interface.writeAll(text);
+ } else switch (key.codepoint) {
+ vaxis.Key.enter => try writer.interface.writeAll("\r"),
+ vaxis.Key.backspace => try writer.interface.writeAll("\x7f"),
+ vaxis.Key.tab => try writer.interface.writeAll("\t"),
+ vaxis.Key.escape => try writer.interface.writeAll("\x1b"),
+ vaxis.Key.up => try writer.interface.writeAll("\x1b[A"),
+ vaxis.Key.down => try writer.interface.writeAll("\x1b[B"),
+ vaxis.Key.right => try writer.interface.writeAll("\x1b[C"),
+ vaxis.Key.left => try writer.interface.writeAll("\x1b[D"),
+ else => {},
+ }
+ try writer.flush();
+ },
+ .pty_read => |bytes| {
+ defer gpa.free(bytes);
+ term_stream.nextSlice(bytes);
},
.winsize => |ws| try vx.resize(gpa, tty.writer(), ws),
+ .quit => break,
else => {},
}
+ // term.scrollViewport(.bottom);
+ const tty_str = try term.plainString(gpa);
+ defer gpa.free(tty_str);
+
+ const win_child = win.child(.{
+ .border = .{ .where = .bottom },
+ .height = nrows + 2,
+ .width = ncols + 2,
+ });
+ const win_dbg = win.child(.{
+ .border = .{ .where = .all },
+ .x_off = ncols + 2,
+ .height = nrows + 2,
+ .width = ncols + 2,
+ });
+
+ win_child.clear();
+ _ = win_child.printSegment(.{ .text = tty_str }, .{});
+ win_child.showCursor(term.screens.active.cursor.x, term.screens.active.cursor.y);
+
+ const dbg_str = try std.fmt.bufPrintSentinel(
+ &dbg_print_buf,
+ "c {} {}\npty pid {}\nwow it's working! debugging things will show here",
+ .{
+ pty_master.handle, term.screens.active.cursor.x, term.screens.active.cursor.y,
+ },
+ 0,
+ );
+ _ = win_dbg.printSegment(.{ .text = dbg_str }, .{});
+
try vx.render(tty.writer());
}
}
diff --git a/tests.zig b/tests.zig
new file mode 100644
index 00000000..4cae1c26
--- /dev/null
+++ b/tests.zig
@@ -0,0 +1,120 @@
+const std = @import("std");
+const posix = std.posix;
+const linux = std.os.linux;
+
+extern "c" fn forkpty(
+ amaster: *c_int,
+ name: ?[*:0]u8,
+ termp: ?*const anyopaque,
+ winp: ?*const posix.winsize,
+) c_int;
+extern "c" fn execvp(file: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int;
+extern "c" fn _exit(status: c_int) noreturn;
+
+fn writeAll(fd: c_int, bytes: []const u8) !void {
+ var off: usize = 0;
+ while (off < bytes.len) {
+ const rc = linux.write(fd, bytes[off..].ptr, bytes.len - off);
+ switch (posix.errno(rc)) {
+ .SUCCESS => off += @intCast(rc),
+ .INTR => {},
+ else => |err| {
+ std.debug.print("write failed: {}\n", .{err});
+ return error.WriteFailed;
+ },
+ }
+ }
+}
+
+fn waitNoHang(pid: c_int) bool {
+ var status: u32 = 0;
+ const rc = linux.waitpid(pid, &status, linux.W.NOHANG);
+ return posix.errno(rc) == .SUCCESS and rc == @as(usize, @intCast(pid));
+}
+
+pub fn main(init: std.process.Init) !void {
+ const gpa = init.gpa;
+ const args = try init.minimal.args.toSlice(init.arena.allocator());
+ if (args.len != 2) {
+ std.debug.print("usage: {s} <pardes-exe>\n", .{args[0]});
+ return error.BadArgs;
+ }
+
+ const exe = args[1];
+ const marker = "PARDES_E2E";
+ const command = "echo " ++ marker ++ "\r";
+
+ var master: c_int = undefined;
+ const ws = posix.winsize{ .row = 24, .col = 80, .xpixel = 0, .ypixel = 0 };
+ const pid = forkpty(&master, null, null, &ws);
+ if (pid == 0) {
+ const child_argv = [_:null]?[*:0]const u8{ exe.ptr, null };
+ _ = execvp(exe.ptr, &child_argv);
+ _exit(127);
+ }
+ defer _ = linux.close(master);
+ defer posix.kill(pid, posix.SIG.KILL) catch {};
+
+ var output: std.ArrayList(u8) = .empty;
+ defer output.deinit(gpa);
+
+ var buf: [4096]u8 = undefined;
+ for (0..20) |_| {
+ var fds = [_]posix.pollfd{.{
+ .fd = master,
+ .events = posix.POLL.IN,
+ .revents = 0,
+ }};
+ _ = try posix.poll(&fds, 100);
+ if ((fds[0].revents & posix.POLL.IN) != 0) {
+ const n = posix.read(master, &buf) catch break;
+ if (n == 0) break;
+ try output.appendSlice(gpa, buf[0..n]);
+ break;
+ }
+ }
+
+ try writeAll(master, command);
+
+ var saw_marker = false;
+ for (0..80) |_| {
+ var fds = [_]posix.pollfd{.{
+ .fd = master,
+ .events = posix.POLL.IN,
+ .revents = 0,
+ }};
+ _ = try posix.poll(&fds, 100);
+ if ((fds[0].revents & (posix.POLL.IN | posix.POLL.HUP | posix.POLL.ERR)) != 0) {
+ const n = posix.read(master, &buf) catch break;
+ if (n == 0) break;
+ try output.appendSlice(gpa, buf[0..n]);
+ if (std.mem.containsAtLeast(u8, output.items, 1, marker)) {
+ saw_marker = true;
+ break;
+ }
+ }
+ if (waitNoHang(pid)) break;
+ }
+
+ try writeAll(master, "\x03");
+ for (0..20) |_| {
+ if (waitNoHang(pid)) break;
+ var fds = [_]posix.pollfd{.{
+ .fd = master,
+ .events = posix.POLL.IN,
+ .revents = 0,
+ }};
+ _ = posix.poll(&fds, 50) catch {};
+ if ((fds[0].revents & posix.POLL.IN) != 0) {
+ _ = posix.read(master, &buf) catch 0;
+ }
+ }
+
+ if (!saw_marker) {
+ std.debug.print("app did not render marker; captured {d} bytes:\n{s}\n", .{
+ output.items.len,
+ output.items[0..@min(output.items.len, 4096)],
+ });
+ return error.MarkerNotRendered;
+ }
+}