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).
This commit is contained in:
@@ -79,54 +79,29 @@ 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 nested_container: App.Container = try .init(allocator, .{
|
var environment = try std.process.getEnvMap(allocator);
|
||||||
.layout = .{
|
defer environment.deinit();
|
||||||
.direction = .vertical,
|
|
||||||
.separator = .{
|
var editor: App.Exec = .init(
|
||||||
.enabled = true,
|
allocator,
|
||||||
},
|
&.{
|
||||||
},
|
"tty",
|
||||||
}, .{});
|
|
||||||
var inner_container: App.Container = try .init(allocator, .{
|
|
||||||
.layout = .{
|
|
||||||
.direction = .vertical,
|
|
||||||
},
|
},
|
||||||
|
&environment,
|
||||||
|
&app.queue,
|
||||||
|
);
|
||||||
|
defer editor.deinit();
|
||||||
|
|
||||||
|
try container.append(try App.Container.init(allocator, .{
|
||||||
.border = .{
|
.border = .{
|
||||||
.color = .light_blue,
|
.color = .light_blue,
|
||||||
.sides = .all,
|
.sides = .all,
|
||||||
},
|
},
|
||||||
}, .{});
|
|
||||||
try inner_container.append(try .init(allocator, .{
|
|
||||||
.rectangle = .{
|
|
||||||
.fill = .blue,
|
|
||||||
},
|
|
||||||
.size = .{
|
.size = .{
|
||||||
.grow = .horizontal,
|
.dim = .{ .x = 100 },
|
||||||
.dim = .{ .y = 5 },
|
|
||||||
},
|
},
|
||||||
}, .{}));
|
}, editor.element()));
|
||||||
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);
|
|
||||||
try container.append(try App.Container.init(allocator, .{
|
try container.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .blue },
|
.rectangle = .{ .fill = .blue },
|
||||||
.size = .{
|
.size = .{
|
||||||
@@ -138,6 +113,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();
|
||||||
@@ -145,23 +125,7 @@ pub fn main() !void {
|
|||||||
|
|
||||||
// pre event handling
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.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 } })) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 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 }),
|
||||||
else => {},
|
else => {},
|
||||||
|
|||||||
@@ -414,6 +414,7 @@ pub fn App(comptime E: type) type {
|
|||||||
pub const Element = element.Element(Event);
|
pub const Element = element.Element(Event);
|
||||||
pub const Alignment = element.Alignment(Event);
|
pub const Alignment = element.Alignment(Event);
|
||||||
pub const Button = element.Button(Event, Queue);
|
pub const Button = element.Button(Event, Queue);
|
||||||
|
pub const Exec = element.Exec(Event, Queue);
|
||||||
pub const Input = element.Input(Event, Queue);
|
pub const Input = element.Input(Event, Queue);
|
||||||
pub const Progress = element.Progress(Event, Queue);
|
pub const Progress = element.Progress(Event, Queue);
|
||||||
pub const Scrollable = element.Scrollable(Event);
|
pub const Scrollable = element.Scrollable(Event);
|
||||||
|
|||||||
234
src/element.zig
234
src/element.zig
@@ -747,6 +747,240 @@ pub fn Progress(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type {
|
|||||||
return progress_struct.progress_fn;
|
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 std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const meta = std.meta;
|
const meta = std.meta;
|
||||||
|
|||||||
@@ -24,6 +24,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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user