From 1ad7bf0d484f991790ed6b1b2e760d239a54957a Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Tue, 1 Apr 2025 21:56:10 +0200 Subject: [PATCH] WIP: exec element with initial implementation Problem is that the main application actually does not create a pty and instead uses the current one (for better compatability for ssh based hosting). The current problem is the fact that the child process should not take over (and never give back too) the input / output handling of the current pts. Such that both applications can receive inputs accordingly (in best case actually controlled by the main application). --- examples/demo.zig | 80 +++++----------- src/app.zig | 1 + src/element.zig | 234 ++++++++++++++++++++++++++++++++++++++++++++++ src/event.zig | 2 + 4 files changed, 259 insertions(+), 58 deletions(-) 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 {