1 Commits

Author SHA1 Message Date
1ad7bf0d48 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-07-12 20:26:47 +02:00
4 changed files with 259 additions and 58 deletions

View File

@@ -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 => {},

View File

@@ -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);

View File

@@ -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;

View File

@@ -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 {