aboutsummaryrefslogtreecommitdiff
path: root/src/shell.zig
diff options
context:
space:
mode:
authortsne <tsne.dev@outlook.com>2025-01-01 14:58:28 +0100
committertsne <tsne.dev@outlook.com>2025-10-30 08:32:49 +0100
commit44e5ad763794a438ecfd50c8b7f6ea760ea82da5 (patch)
tree575a1fc72ceb1d7f052cf582abc1e038e27f69a3 /src/shell.zig
downloadporteur-main.tar.gz
initial commitHEADmain
Diffstat (limited to 'src/shell.zig')
-rw-r--r--src/shell.zig191
1 files changed, 191 insertions, 0 deletions
diff --git a/src/shell.zig b/src/shell.zig
new file mode 100644
index 0000000..d361b09
--- /dev/null
+++ b/src/shell.zig
@@ -0,0 +1,191 @@
+const std = @import("std");
+const assert = std.debug.assert;
+
+const cli = @import("cli.zig");
+const fs = @import("fs.zig");
+const conf = @import("conf.zig");
+const Env = @import("env.zig");
+const fatal = @import("cli.zig").fatal;
+
+pub const Exec_Args = struct {
+ env: Env,
+ argv: []const []const u8,
+ cwd: ?fs.Path = null,
+};
+
+pub const Exec_Result = union(enum) {
+ success: []const u8,
+ failure: []const u8,
+};
+
+pub fn exec(args: Exec_Args) Exec_Result {
+ assert(args.argv.len > 0);
+
+ const res = std.process.Child.run(.{
+ .allocator = args.env.allocator,
+ .argv = args.argv,
+ .cwd = if (args.cwd) |cwd| cwd.name() else null,
+ .cwd_dir = if (args.cwd == null) std.fs.cwd() else null,
+ }) catch |err| {
+ fatal("error executing {s}: {s}", .{ args.argv[0], @errorName(err) });
+ };
+ switch (res.term) {
+ .Exited => |code| {
+ if (code == 0) {
+ args.env.dealloc(res.stderr);
+ return .{ .success = res.stdout };
+ } else if (res.stderr.len > 0) {
+ args.env.dealloc(res.stdout);
+ return .{ .failure = res.stderr };
+ } else {
+ args.env.dealloc(res.stderr);
+ return .{ .failure = res.stdout };
+ }
+ },
+ .Signal => fatal("signal received executing {s}", .{args.argv[0]}),
+ .Stopped => fatal("stopped executing {s}", .{args.argv[0]}),
+ .Unknown => fatal("unknown error executing {s}", .{args.argv[0]}),
+ }
+}
+
+pub fn run(args: Exec_Args) void {
+ assert(args.argv.len > 0);
+
+ var child = std.process.Child.init(args.argv, args.env.allocator);
+ if (args.cwd) |cwd| child.cwd = cwd.name();
+ child.cwd_dir = if (args.cwd == null) std.fs.cwd() else null;
+ child.spawn() catch |err| {
+ fatal("error executing {s}: {s}", .{ args.argv[0], @errorName(err) });
+ };
+ errdefer _ = child.kill() catch {};
+
+ const term = child.wait() catch |err| {
+ fatal("error waiting for {s}: {s}", .{ args.argv[0], @errorName(err) });
+ };
+ switch (term) {
+ .Exited => |code| if (code != 0) std.process.exit(code),
+ .Signal => fatal("signal received executing {s}", .{args.argv[0]}),
+ .Stopped => fatal("stopped executing {s}", .{args.argv[0]}),
+ .Unknown => fatal("unknown error executing {s}", .{args.argv[0]}),
+ }
+}
+
+pub fn edit_config_file(env: Env, file: fs.Path, comptime verify: fn (env: Env, vars: conf.Config) bool) !void {
+ const tmp_file = tmp_edit_file(env, file);
+
+ fs.cp_file(file, tmp_file) catch |err| {
+ switch (err) {
+ error.FileNotFound => try fs.touch(tmp_file),
+ else => return err,
+ }
+ };
+
+ edit: while (true) {
+ const vars: ?conf.Config = parse: while (true) {
+ try exec_editor(env, tmp_file);
+ const parsed = try conf.Config.from_file(env.allocator, file.name());
+ switch (parsed) {
+ .conf => |c| break :parse c,
+ .err => |e| {
+ env.err("{s} (line {d})", .{ e.msg, e.line });
+ switch (ask_edit_again()) {
+ .again => continue,
+ .cancel => break :parse null,
+ }
+ },
+ }
+ };
+
+ if (vars) |v| {
+ defer v.deinit();
+ if (verify(env, v)) {
+ try fs.mv_file(tmp_file, file);
+ return;
+ }
+ switch (ask_edit_again()) {
+ .again => continue,
+ .cancel => break :edit,
+ }
+ } else {
+ break :edit;
+ }
+ }
+ try fs.rm(tmp_file);
+}
+
+pub fn edit_config_file_noverify(env: Env, file: fs.Path) !void {
+ try edit_config_file(env, file, struct {
+ fn noverify(_: Env, _: conf.Config) bool {
+ return true;
+ }
+ }.noverify);
+}
+
+fn ask_edit_again() enum { again, cancel } {
+ while (true) {
+ var again_buf: [16]u8 = undefined;
+ const again = cli.ask("Edit again? (yes/no) [yes] ", .{}, &again_buf) orelse "yes";
+ for (again, 0..) |c, i| again_buf[i] = std.ascii.toLower(c);
+ if (std.mem.eql(u8, again, "y") or std.mem.eql(u8, again, "yes")) {
+ return .again;
+ }
+ if (std.mem.eql(u8, again, "n") or std.mem.eql(u8, again, "no")) {
+ cli.stderr.print_line("cancelled (changes discarded)", .{});
+ cli.stderr.flush();
+ return .cancel;
+ }
+ }
+}
+
+fn exec_editor(env: Env, file: fs.Path) !void {
+ const pid = std.posix.fork() catch |err| {
+ fatal("cannot fork editor: {s}", .{@errorName(err)});
+ };
+
+ if (pid == 0) {
+ // editor process
+ var editor_env: [512:null]?[*:0]const u8 = undefined;
+ var editor_envlen: usize = 0;
+ const environ = if (std.os.environ.len < 512) std.os.environ else std.os.environ[0..511];
+ for (environ) |e| {
+ editor_env[editor_envlen] = e;
+ editor_envlen += 1;
+ }
+ editor_env[editor_envlen] = null;
+
+ var configured_editor_buf: [std.fs.max_path_bytes]u8 = undefined;
+ var configured_editor: ?[:0]u8 = null;
+ if (env.editor_cmd) |cmd| {
+ @memcpy(configured_editor_buf[0..cmd.len], cmd);
+ configured_editor_buf[cmd.len] = 0;
+ configured_editor = configured_editor_buf[0..cmd.len :0];
+ }
+
+ const editor = configured_editor orelse std.posix.getenv("VISUAL") orelse std.posix.getenv("EDITOR") orelse "vi";
+ const editor_argv: [*:null]const ?[*:0]const u8 = &.{ editor, file.name(), null };
+ std.posix.execvpeZ(editor_argv[0].?, editor_argv, &editor_env) catch unreachable;
+ std.process.exit(1);
+ }
+
+ _ = std.posix.waitpid(pid, 0);
+}
+
+/// Derive the filename for the temporary file of `file`.
+/// The filename is "{relname}.{timestamp}", where `relname` is the relative
+/// part of file regarding to etc.
+fn tmp_edit_file(env: Env, file: fs.Path) fs.Path {
+ const relname = file.relname(env.etc_path(.{})).?;
+
+ var tmp_filename_buf: [std.fs.max_name_bytes]u8 = undefined;
+ var tmp_filename_len: usize = 0;
+
+ tmp_filename_len += std.fmt.printInt(tmp_filename_buf[tmp_filename_len..], std.time.timestamp(), 16, .lower, .{});
+ tmp_filename_buf[tmp_filename_len] = '.';
+ tmp_filename_len += 1;
+ for (relname) |c| {
+ tmp_filename_buf[tmp_filename_len] = if (c == std.fs.path.sep) '_' else c;
+ tmp_filename_len += 1;
+ }
+
+ return env.work_path(.{ "edit", tmp_filename_buf[0..tmp_filename_len] });
+}