diff options
| author | tsne <tsne.dev@outlook.com> | 2025-01-01 14:58:28 +0100 |
|---|---|---|
| committer | tsne <tsne.dev@outlook.com> | 2025-10-30 08:32:49 +0100 |
| commit | 44e5ad763794a438ecfd50c8b7f6ea760ea82da5 (patch) | |
| tree | 575a1fc72ceb1d7f052cf582abc1e038e27f69a3 /src/shell.zig | |
| download | porteur-main.tar.gz | |
Diffstat (limited to 'src/shell.zig')
| -rw-r--r-- | src/shell.zig | 191 |
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] }); +} |