5 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
bce134f052 rem: unnecessary signal handler
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 2m1s
No event-based re-sizing, instead each re-render resizes by default.
2025-04-01 21:53:52 +02:00
962a384ecf add(Scrollable): init function with corresponding usages
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 42s
2025-03-29 21:21:23 +01:00
0b7d032b11 tag: 0.2.0
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m15s
2025-03-27 21:43:02 +01:00
7e20dd73d9 mod: add missing inline function attribute
Correct example to use the actual `zterm.Error` type accordingly.
2025-03-27 21:41:18 +01:00
12 changed files with 292 additions and 67 deletions

View File

@@ -24,7 +24,7 @@
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.1.0",
.version = "0.2.0",
// Tracks the earliest Zig version that the package considers to be a
// supported use case.

View File

@@ -75,7 +75,7 @@ pub fn main() !void {
}, .{}));
defer box.deinit();
var scrollable: App.Scrollable = .{ .container = box };
var scrollable: App.Scrollable = .init(box);
var container = try App.Container.init(allocator, .{
.layout = .{
@@ -87,6 +87,19 @@ pub fn main() !void {
}, quit_text.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, .{
.border = .{
.color = .light_blue,
@@ -95,7 +108,7 @@ pub fn main() !void {
.size = .{
.dim = .{ .x = 100 },
},
}, .{}));
}, editor.element()));
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
.size = .{
@@ -107,6 +120,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();
@@ -117,18 +135,18 @@ pub fn main() !void {
.key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.interrupt();
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;
}
// if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
// try app.interrupt();
// defer renderer.clear() catch {};
// 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",
// },
// });
// }
},
// 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 }),

View File

@@ -156,10 +156,10 @@ pub fn main() !void {
defer container.deinit();
// place empty container containing the element of the scrollable Container.
var scrollable_top: App.Scrollable = .{ .container = top_box };
var scrollable_top: App.Scrollable = .init(top_box);
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
var scrollable_bottom: App.Scrollable = .{ .container = bottom_box };
var scrollable_bottom: App.Scrollable = .init(bottom_box);
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
try app.start();

View File

@@ -67,7 +67,7 @@ const ErrorNotification = struct {
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.key => |key| if (!key.isAscii()) return error.UnsupportedKey,
.key => |key| if (!key.isAscii()) return zterm.Error.TooSmall,
.err => |err| this.msg = err.msg,
else => {},
}

View File

@@ -63,7 +63,7 @@ pub fn main() !void {
if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip
try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{}));
}
var scrollable: App.Scrollable = .{ .container = box };
var scrollable: App.Scrollable = .init(box);
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start();

View File

@@ -118,7 +118,7 @@ pub fn main() !void {
}, text_styles.element());
defer box.deinit();
var scrollable: App.Scrollable = .{ .container = box };
var scrollable: App.Scrollable = .init(box);
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start();

View File

@@ -44,13 +44,13 @@ pub fn App(comptime E: type) type {
const element = @import("element.zig");
pub const Element = element.Element(Event);
pub const Scrollable = element.Scrollable(Event);
pub const Exec = element.Exec(Event, Queue);
pub const Queue = queue.Queue(Event, 256);
queue: Queue,
thread: ?std.Thread,
thread: ?std.Thread = null,
quit_event: std.Thread.ResetEvent,
termios: ?std.posix.termios = null,
attached_handler: bool = false,
pub const SignalHandler = struct {
context: *anyopaque,
@@ -59,32 +59,14 @@ pub fn App(comptime E: type) type {
pub const init: @This() = .{
.queue = .{},
.thread = null,
.quit_event = .{},
.termios = null,
.attached_handler = false,
};
pub fn start(this: *@This()) !void {
if (this.thread) |_| return;
if (!this.attached_handler) {
var winch_act = std.posix.Sigaction{
.handler = .{ .handler = @This().handleWinch },
.mask = std.posix.empty_sigset,
.flags = 0,
};
std.posix.sigaction(std.posix.SIG.WINCH, &winch_act, null);
try registerWinch(.{
.context = this,
.callback = @This().winsizeCallback,
});
this.attached_handler = true;
// post init event (as the very first element to be in the queue - event loop)
this.postEvent(.init);
}
this.quit_event.reset();
this.thread = try std.Thread.spawn(.{}, @This().run, .{this});
@@ -139,27 +121,6 @@ pub fn App(comptime E: type) type {
this.queue.push(e);
}
fn winsizeCallback(ptr: *anyopaque) void {
const this: *@This() = @ptrCast(@alignCast(ptr));
_ = this;
// this.postEvent(.{ .size = terminal.getTerminalSize() });
}
var winch_handler: ?SignalHandler = null;
fn registerWinch(handler: SignalHandler) !void {
if (winch_handler) |_| {
@panic("Cannot register another WINCH handler.");
}
winch_handler = handler;
}
fn handleWinch(_: c_int) callconv(.C) void {
if (winch_handler) |handler| {
handler.callback(handler.context);
}
}
fn run(this: *@This()) !void {
// thread to read user inputs
var buf: [256]u8 = undefined;

View File

@@ -20,6 +20,8 @@ pub const Color = enum(u8) {
white,
// TODO add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors
// TODO might be useful to use the std.ascii stuff!
pub inline fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void {
if (this == .default) {
switch (coloring) {

View File

@@ -20,13 +20,13 @@ pub fn Element(Event: type) type {
};
/// Resize the corresponding `Element` with the given *size*.
pub fn resize(this: @This(), size: Point) void {
pub inline fn resize(this: @This(), size: Point) void {
if (this.vtable.resize) |resize_fn|
resize_fn(this.ptr, size);
}
/// Reposition the corresponding `Element` with the given *origin*.
pub fn reposition(this: @This(), origin: Point) void {
pub inline fn reposition(this: @This(), origin: Point) void {
if (this.vtable.reposition) |reposition_fn|
reposition_fn(this.ptr, origin);
}
@@ -78,6 +78,10 @@ pub fn Scrollable(Event: type) type {
/// The actual `Container`, that is scrollable.
container: Container(Event),
pub fn init(container: Container(Event)) @This() {
return .{ .container = container };
}
pub fn element(this: *@This()) Element(Event) {
return .{
.ptr = this,
@@ -222,7 +226,7 @@ test "scrollable vertical" {
}, .{}));
defer box.deinit();
var scrollable: Scrollable(event.SystemEvent) = .{ .container = box };
var scrollable: Scrollable(event.SystemEvent) = .init(box);
var container: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
@@ -300,7 +304,7 @@ test "scrollable horizontal" {
}, .{}));
defer box.deinit();
var scrollable: Scrollable(event.SystemEvent) = .{ .container = box };
var scrollable: Scrollable(event.SystemEvent) = .init(box);
var container: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
@@ -342,3 +346,237 @@ test "scrollable horizontal" {
try renderer.render(Container(event.SystemEvent), &container);
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
/// 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 {

View File

@@ -68,6 +68,8 @@ pub const Key = packed struct {
return std.meta.eql(this, other);
}
// TODO might be useful to use the std.ascii stuff!
/// Determine if the `Key` is an ascii character that can be printed to
/// the screen. This means that the code point of the `Key` is an ascii
/// character between 32 - 255 (with the exception of 127 = Delete) and no

View File

@@ -44,6 +44,8 @@ pub fn eql(this: Style, other: Style) bool {
return std.meta.eql(this, other);
}
// TODO might be useful to use the std.ascii stuff!
pub fn value(this: Style, writer: anytype, cp: u21) !void {
var buffer: [4]u8 = undefined;
const bytes = try std.unicode.utf8Encode(cp, &buffer);