a modern tui library written in zig
1const Command = @This();
2
3const std = @import("std");
4const builtin = @import("builtin");
5const Pty = @import("Pty.zig");
6const Terminal = @import("Terminal.zig");
7
8const posix = std.posix;
9
10argv: []const []const u8,
11
12working_directory: ?[]const u8,
13
14// Set after spawn()
15pid: ?std.posix.pid_t = null,
16
17env_map: *const std.process.EnvMap,
18
19pty: Pty,
20
21pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void {
22 var arena_allocator = std.heap.ArenaAllocator.init(allocator);
23 defer arena_allocator.deinit();
24
25 const arena = arena_allocator.allocator();
26
27 const argv_buf = try arena.allocSentinel(?[*:0]const u8, self.argv.len, null);
28 for (self.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr;
29
30 const envp = try createEnvironFromMap(arena, self.env_map);
31
32 const pid = try std.posix.fork();
33 if (pid == 0) {
34 // we are the child
35 _ = std.os.linux.setsid();
36
37 // set the controlling terminal
38 var u: c_uint = std.posix.STDIN_FILENO;
39 if (posix.system.ioctl(self.pty.tty.handle, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError;
40
41 // set up io
42 try posix.dup2(self.pty.tty.handle, std.posix.STDIN_FILENO);
43 try posix.dup2(self.pty.tty.handle, std.posix.STDOUT_FILENO);
44 try posix.dup2(self.pty.tty.handle, std.posix.STDERR_FILENO);
45
46 self.pty.tty.close();
47 if (self.pty.pty.handle > 2) self.pty.pty.close();
48
49 if (self.working_directory) |wd| {
50 try std.posix.chdir(wd);
51 }
52
53 // exec
54 const err = std.posix.execvpeZ(argv_buf.ptr[0].?, argv_buf.ptr, envp);
55 _ = err catch {};
56 }
57
58 // we are the parent
59 self.pid = @intCast(pid);
60
61 if (!Terminal.global_sigchild_installed) {
62 Terminal.global_sigchild_installed = true;
63 var act = posix.Sigaction{
64 .handler = .{ .handler = handleSigChild },
65 .mask = switch (builtin.os.tag) {
66 .macos => 0,
67 .linux => posix.sigemptyset(),
68 else => @compileError("os not supported"),
69 },
70 .flags = 0,
71 };
72 posix.sigaction(posix.SIG.CHLD, &act, null);
73 }
74
75 return;
76}
77
78fn handleSigChild(_: c_int) callconv(.c) void {
79 const result = std.posix.waitpid(-1, 0);
80
81 Terminal.global_vt_mutex.lock();
82 defer Terminal.global_vt_mutex.unlock();
83 if (Terminal.global_vts) |vts| {
84 var vt = vts.get(result.pid) orelse return;
85 vt.event_queue.push(.exited);
86 }
87}
88
89pub fn kill(self: *Command) void {
90 if (self.pid) |pid| {
91 std.posix.kill(pid, std.posix.SIG.TERM) catch {};
92 self.pid = null;
93 }
94}
95
96/// Creates a null-deliminated environment variable block in the format expected by POSIX, from a
97/// hash map plus options.
98fn createEnvironFromMap(
99 arena: std.mem.Allocator,
100 map: *const std.process.EnvMap,
101) ![:null]?[*:0]u8 {
102 const envp_count: usize = map.count();
103
104 const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null);
105 var i: usize = 0;
106
107 {
108 var it = map.iterator();
109 while (it.next()) |pair| {
110 envp_buf[i] = try std.fmt.allocPrintSentinel(arena, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* }, 0);
111 i += 1;
112 }
113 }
114
115 std.debug.assert(i == envp_count);
116 return envp_buf;
117}