diff --git a/examples/demo.zig b/examples/demo.zig index 88f987b..30603c0 100644 --- a/examples/demo.zig +++ b/examples/demo.zig @@ -79,54 +79,29 @@ pub fn main() !void { }, quit_text.element()); try container.append(try App.Container.init(allocator, .{}, scrollable.element())); - var nested_container: App.Container = try .init(allocator, .{ - .layout = .{ - .direction = .vertical, - .separator = .{ - .enabled = true, - }, - }, - }, .{}); - var inner_container: App.Container = try .init(allocator, .{ - .layout = .{ - .direction = .vertical, + var environment = try std.process.getEnvMap(allocator); + defer environment.deinit(); + + var editor: App.Exec = .init( + allocator, + &.{ + "tty", }, + &environment, + &app.queue, + ); + defer editor.deinit(); + + try container.append(try App.Container.init(allocator, .{ .border = .{ .color = .light_blue, .sides = .all, }, - }, .{}); - try inner_container.append(try .init(allocator, .{ - .rectangle = .{ - .fill = .blue, - }, .size = .{ - .grow = .horizontal, - .dim = .{ .y = 5 }, + .dim = .{ .x = 100 }, }, - }, .{})); - try inner_container.append(try .init(allocator, .{ - .rectangle = .{ - .fill = .red, - }, - .size = .{ - .grow = .horizontal, - .dim = .{ .y = 5 }, - }, - }, .{})); - try inner_container.append(try .init(allocator, .{ - .rectangle = .{ - .fill = .green, - }, - }, .{})); - try nested_container.append(inner_container); - try nested_container.append(try .init(allocator, .{ - .size = .{ - .grow = .horizontal, - .dim = .{ .y = 1 }, - }, - }, .{})); - try container.append(nested_container); + }, editor.element())); + try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .blue }, .size = .{ @@ -138,6 +113,11 @@ pub fn main() !void { try app.start(); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); + var process_in: std.ArrayListUnmanaged(u8) = .empty; + defer process_in.deinit(allocator); + var process_out: std.ArrayListUnmanaged(u8) = .empty; + defer process_out.deinit(allocator); + // event loop while (true) { const event = app.nextEvent(); @@ -145,23 +125,7 @@ pub fn main() !void { // pre event handling switch (event) { - .key => |key| { - if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(); - - if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) { - try app.interrupt(); - renderer.size = .{}; // reset size, such that next resize will cause a full re-draw! - defer app.start() catch @panic("could not start app event loop"); - var child = std.process.Child.init(&.{"hx"}, allocator); - _ = child.spawnAndWait() catch |err| app.postEvent(.{ - .err = .{ - .err = err, - .msg = "Spawning $EDITOR failed", - }, - }); - continue; - } - }, + .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(), // NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), else => {}, diff --git a/src/app.zig b/src/app.zig index e9c000a..efd7656 100644 --- a/src/app.zig +++ b/src/app.zig @@ -414,6 +414,7 @@ pub fn App(comptime E: type) type { pub const Element = element.Element(Event); pub const Alignment = element.Alignment(Event); pub const Button = element.Button(Event, Queue); + pub const Exec = element.Exec(Event, Queue); pub const Input = element.Input(Event, Queue); pub const Progress = element.Progress(Event, Queue); pub const Scrollable = element.Scrollable(Event); diff --git a/src/element.zig b/src/element.zig index b575d97..f224480 100644 --- a/src/element.zig +++ b/src/element.zig @@ -747,6 +747,240 @@ pub fn Progress(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { return progress_struct.progress_fn; } +pub fn Exec(Event: type, Queue: type) type { + // TODO + // - wrap own process into command structure + // https://github.com/rockorager/libvaxis/blob/main/src/widgets/terminal/Command.zig + // - create tty / pty and setup process to use that tty / pty + // https://github.com/rockorager/libvaxis/blob/main/src/tty.zig + // https://github.com/rockorager/libvaxis/blob/main/src/widgets/terminal/Pty.zig + // - read & write contents from & to tty / pty for outputs & inputs + return struct { + allocator: std.mem.Allocator, + command: Command, + pty: Pty, + queue: *Queue, + + pub const Pty = struct { + pty: std.posix.fd_t, + tty: std.posix.fd_t, + + pub fn init() !@This() { + const p = try std.posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0); + errdefer std.posix.close(p); + + // unlockpt + var n: c_uint = 0; + if (std.posix.system.ioctl(p, std.posix.T.IOCSPTLCK, @intFromPtr(&n)) != 0) return error.IoctlError; + + // ptsname + if (std.posix.system.ioctl(p, std.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.debug("pts: {s}", .{sname}); + + const t = try std.posix.open(sname, .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0); + + return .{ + .pty = p, + .tty = t, + }; + } + + pub fn deinit(this: *@This()) void { + std.posix.close(this.pty); + std.posix.close(this.tty); + } + + pub fn resize(this: *@This(), size: Point) !void { + const ws: std.posix.winsize = .{ + .col = size.x, + .row = size.y, + .xpixel = 0, + .ypixel = 0, + }; + if (std.posix.system.ioctl(this.pty, std.posix.T.IOCSWINSZ, @intFromPtr(&ws)) != 0) return error.SetWinsizeError; + } + }; + + pub const Command = struct { + argv: []const []const u8, + working_directory: ?[]const u8, + pid: ?std.posix.pid_t = null, + environment: *const std.process.EnvMap, + pty: Pty, + queue: *Queue, + handler: bool = false, + + const SignalHandler = struct { + context: *anyopaque, + callback: *const fn (ctx: *anyopaque, pid: std.posix.pid_t) void, + }; + + pub fn spawn(this: *@This(), allocator: std.mem.Allocator) !void { + const argv = try allocator.allocSentinel(?[*:0]const u8, this.argv.len, null); + for (this.argv, 0..) |arg, i| argv[i] = (try allocator.dupeZ(u8, arg)).ptr; + + // FIX environment is leaking memory + const environment = blk: { + const count: usize = this.environment.count(); + const buffer = try allocator.allocSentinel(?[*:0]u8, count, null); + var i: usize = 0; + var it = this.environment.iterator(); + while (it.next()) |pair| { + buffer[i] = try std.fmt.allocPrintZ(allocator, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* }); + i += 1; + } + std.debug.assert(i == count); + break :blk buffer; + }; + const pid = try std.posix.fork(); + + if (pid == 0) { + // child + _ = std.os.linux.setsid(); + // if (std.posix.system.ioctl(this.pty.tty, std.posix.T.IOCSCTTY, std.posix.STDIN_FILENO) != 0) return error.IoctlError; + + // TODO user input / output should not be stdin / stdout, but instead a different abstraction in between, allowing + // for simultan input handling of inner commands and `Container`'s and `Element`'s + + // set up io + try std.posix.dup2(this.pty.tty, std.posix.STDIN_FILENO); + try std.posix.dup2(this.pty.tty, std.posix.STDOUT_FILENO); + try std.posix.dup2(this.pty.tty, std.posix.STDERR_FILENO); + + std.posix.close(this.pty.tty); + if (this.pty.pty > 2) std.posix.close(this.pty.pty); + + if (this.working_directory) |wd| try std.posix.chdir(wd); + + const err = std.posix.execvpeZ(argv.ptr[0].?, argv.ptr, environment); + _ = err catch {}; + // free resources (no longer required) + allocator.free(environment); + } + + // parent + this.pid = @intCast(pid); + + // register handler for sig child + if (!this.handler) { + defer this.handler = true; + + var winch_act = std.posix.Sigaction{ + .handler = .{ .handler = @This().handleSigChild }, + .mask = std.posix.sigemptyset(), + .flags = 0, + }; + std.posix.sigaction(std.posix.SIG.CHLD, &winch_act, null); + + try registerSigChild(.{ + .context = this, + .callback = @This().exitCallback, + }); + } + } + + var chld_handler: ?SignalHandler = null; + + fn registerSigChild(handler: SignalHandler) !void { + if (chld_handler) |_| @panic("Cannot register another CHLD handler."); + chld_handler = handler; + } + + fn handleSigChild(_: c_int) callconv(.C) void { + const result = std.posix.waitpid(-1, 0); + if (chld_handler) |handler| handler.callback(handler.context, result.pid); + } + + fn exitCallback(ctx: *anyopaque, pid: std.posix.pid_t) void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + // fire exited event with corresponding pid to free + if (this.pid) |command_pid| if (pid == command_pid) this.queue.push(.{ .exited = pid }); + } + + pub fn kill(this: *@This()) void { + if (this.pid) |pid| { + std.posix.kill(pid, std.posix.SIG.TERM) catch {}; + this.pid = null; + } + } + }; + + pub fn init(allocator: std.mem.Allocator, argv: []const []const u8, environment: *const std.process.EnvMap, queue: *Queue) @This() { + const pty = Pty.init() catch @panic("failed to initialize PTY"); + return .{ + .allocator = allocator, + .command = .{ + .argv = argv, + .pty = pty, + .environment = environment, + .queue = queue, + .working_directory = null, + }, + .pty = pty, + .queue = queue, + }; + } + + pub fn deinit(this: *@This()) void { + this.command.kill(); + this.pty.deinit(); + } + + pub fn element(this: *@This()) Element(Event) { + return .{ + .ptr = this, + .vtable = &.{ + .resize = resize, + .handle = handle, + .content = content, + }, + }; + } + + fn resize(ctx: *anyopaque, size: Point) void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + this.pty.resize(size) catch {}; + } + + fn handle(ctx: *anyopaque, event: Event) !void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + + // TODO pass through inputs accordingly + // - key events + // - mouse events + switch (event) { + .init => try this.command.spawn(this.allocator), + .exited => |pid| if (this.command.pid) |command_pid| { + if (command_pid == pid) { + // this command has completed! + std.log.debug("command finished: {any}", .{pid}); + } + }, + else => {}, + } + } + + fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); + + var buf: [8 * 1024]u8 = undefined; + const length = try std.posix.read(this.pty.pty, &buf); + for (1.., buf[0..length]) |idx, c| cells[idx + size.x].cp = c; + + // TODO support tui applications which render raw! + // -> directly apply the raw ansi codes + // -> or translate them into the `Cell` structure + + // TODO map contents of tty / pty to cells + // - content (Cell.cp) + // - style (Cell.style) + } + }; +} + const std = @import("std"); const assert = std.debug.assert; const meta = std.meta; diff --git a/src/event.zig b/src/event.zig index 05fd6dd..b29b230 100644 --- a/src/event.zig +++ b/src/event.zig @@ -24,6 +24,8 @@ pub const SystemEvent = union(enum) { /// Focus event for mouse interaction /// TODO this should instead be a union with a `Size` to derive which container / element the focus meant for focus: bool, + /// Exit event for a completed `Exec` with the associated pid. Fired and handled by `Exec` `Element`s. + exited: std.posix.pid_t, }; pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {