1 Commits

Author SHA1 Message Date
efb11d5c0c 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).
2025-04-01 21:56:10 +02:00
3 changed files with 267 additions and 13 deletions

View File

@@ -87,6 +87,19 @@ pub fn main() !void {
}, quit_text.element()); }, quit_text.element());
try container.append(try App.Container.init(allocator, .{}, scrollable.element())); try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
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, .{ try container.append(try App.Container.init(allocator, .{
.border = .{ .border = .{
.color = .light_blue, .color = .light_blue,
@@ -95,7 +108,7 @@ pub fn main() !void {
.size = .{ .size = .{
.dim = .{ .x = 100 }, .dim = .{ .x = 100 },
}, },
}, .{})); }, editor.element()));
try container.append(try App.Container.init(allocator, .{ try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue }, .rectangle = .{ .fill = .blue },
.size = .{ .size = .{
@@ -107,6 +120,11 @@ pub fn main() !void {
try app.start(); try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); 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 // event loop
while (true) { while (true) {
const event = app.nextEvent(); const event = app.nextEvent();
@@ -117,18 +135,18 @@ pub fn main() !void {
.key => |key| { .key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(); if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) { // if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.interrupt(); // try app.interrupt();
defer app.start() catch @panic("could not start app event loop"); // defer renderer.clear() catch {};
var child = std.process.Child.init(&.{"hx"}, allocator); // defer app.start() catch @panic("could not start app event loop");
_ = child.spawnAndWait() catch |err| app.postEvent(.{ // var child = std.process.Child.init(&.{"hx"}, allocator);
.err = .{ // _ = child.spawnAndWait() catch |err| app.postEvent(.{
.err = err, // .err = .{
.msg = "Spawning $EDITOR failed", // .err = err,
}, // .msg = "Spawning $EDITOR failed",
}); // },
continue; // });
} // }
}, },
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback // 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 }), .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),

View File

@@ -346,3 +346,237 @@ test "scrollable horizontal" {
try renderer.render(Container(event.SystemEvent), &container); try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
} }
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.empty_sigset,
.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)
}
};
}

View File

@@ -28,6 +28,8 @@ pub const SystemEvent = union(enum) {
/// Focus event for mouse interaction /// Focus event for mouse interaction
/// TODO this should instead be a union with a `Size` to derive which container / element the focus meant for /// TODO this should instead be a union with a `Size` to derive which container / element the focus meant for
focus: bool, 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 { pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {