diff options
Diffstat (limited to 'vendor/libghostty-vt/src/config/command.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/config/command.zig | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/config/command.zig b/vendor/libghostty-vt/src/config/command.zig new file mode 100644 index 00000000..7cd70acb --- /dev/null +++ b/vendor/libghostty-vt/src/config/command.zig @@ -0,0 +1,330 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const formatterpkg = @import("formatter.zig"); + +/// A command to execute (argv0 and args). +/// +/// A command is specified as a simple string such as "nvim a b c". +/// By default, we expect the downstream to do some sort of shell expansion +/// on this string. +/// +/// If a command is already expanded and the user does NOT want to do +/// shell expansion (because this usually requires a round trip into +/// /bin/sh or equivalent), specify a `direct:`-prefix. e.g. +/// `direct:nvim a b c`. +/// +/// The whitespace before or around the prefix is ignored. For example, +/// ` direct:nvim a b c` and `direct: nvim a b c` are equivalent. +/// +/// If the command is not absolute, it'll be looked up via the PATH. +/// For the shell-expansion case, we let the shell do this. For the +/// direct case, we do this directly. +pub const Command = union(enum) { + const Self = @This(); + + /// Execute a command directly, e.g. via `exec`. The format here + /// is already structured to be ready to passed directly to `exec` + /// with index zero being the command to execute. + /// + /// Index zero is not guaranteed to be an absolute path, and may require + /// PATH lookup. It is up to the downstream to do this, usually via + /// delegation to something like `execvp`. + direct: []const [:0]const u8, + + /// Execute a command via shell expansion. This provides the command + /// as a single string that is expected to be expanded in some way + /// (up to the downstream). Usually `/bin/sh -c`. + shell: [:0]const u8, + + pub fn parseCLI( + self: *Self, + alloc: Allocator, + input_: ?[]const u8, + ) !void { + // Input is required. Whitespace on the edges isn't needed. + // Commands must be non-empty. + const input = input_ orelse return error.ValueRequired; + const trimmed = std.mem.trim(u8, input, " "); + if (trimmed.len == 0) return error.ValueRequired; + + // If we have a `:` then we MIGHT have a prefix to specify what + // tag we should use. + const tag: std.meta.Tag(Self), const str: []const u8 = tag: { + if (std.mem.indexOfScalar(u8, trimmed, ':')) |idx| { + const prefix = trimmed[0..idx]; + if (std.mem.eql(u8, prefix, "direct")) { + break :tag .{ .direct, trimmed[idx + 1 ..] }; + } else if (std.mem.eql(u8, prefix, "shell")) { + break :tag .{ .shell, trimmed[idx + 1 ..] }; + } + } + + break :tag .{ .shell, trimmed }; + }; + + switch (tag) { + .shell => { + // We have a shell command, so we can just dupe it. + const copy = try alloc.dupeZ(u8, std.mem.trim(u8, str, " ")); + self.* = .{ .shell = copy }; + }, + + .direct => { + // We're not shell expanding, so the arguments are naively + // split on spaces. + var builder: std.ArrayListUnmanaged([:0]const u8) = .empty; + var args = std.mem.splitScalar( + u8, + std.mem.trim(u8, str, " "), + ' ', + ); + while (args.next()) |arg| { + const copy = try alloc.dupeZ(u8, arg); + try builder.append(alloc, copy); + } + + self.* = .{ .direct = try builder.toOwnedSlice(alloc) }; + }, + } + } + + /// Creates a command as a single string, joining arguments as + /// necessary with spaces. Its not guaranteed that this is a valid + /// command; it is only meant to be human readable. + pub fn string( + self: *const Self, + alloc: Allocator, + ) Allocator.Error![:0]const u8 { + return switch (self.*) { + .shell => |v| try alloc.dupeZ(u8, v), + .direct => |v| try std.mem.joinZ(alloc, " ", v), + }; + } + + /// Get an iterator over the arguments array. This may allocate + /// depending on the active tag of the command. + /// + /// For direct commands, this is very cheap and just iterates over + /// the array. There is no allocation. + /// + /// For shell commands, this will use Zig's ArgIteratorGeneral as + /// a best effort shell string parser. This is not guaranteed to be + /// 100% accurate, but it works for common cases. This requires allocation. + pub fn argIterator( + self: *const Self, + alloc: Allocator, + ) Allocator.Error!ArgIterator { + return switch (self.*) { + .direct => |v| .{ .direct = .{ .args = v } }, + .shell => |v| .{ .shell = try .init(alloc, v) }, + }; + } + + /// Iterates over each argument in the command. + pub const ArgIterator = union(enum) { + shell: std.process.ArgIteratorGeneral(.{}), + direct: struct { + i: usize = 0, + args: []const [:0]const u8, + }, + + /// Return the next argument. This may or may not be a copy + /// depending on the active tag. If you want to ensure that every + /// argument is a copy, use the `clone` method first. + pub fn next(self: *ArgIterator) ?[:0]const u8 { + return switch (self.*) { + .shell => |*v| v.next(), + .direct => |*v| { + if (v.i >= v.args.len) return null; + defer v.i += 1; + return v.args[v.i]; + }, + }; + } + + pub fn deinit(self: *ArgIterator) void { + switch (self.*) { + .shell => |*v| v.deinit(), + .direct => {}, + } + } + }; + + pub fn clone( + self: *const Self, + alloc: Allocator, + ) Allocator.Error!Self { + return switch (self.*) { + .shell => |v| .{ .shell = try alloc.dupeZ(u8, v) }, + .direct => |v| direct: { + const copy = try alloc.alloc([:0]const u8, v.len); + for (v, 0..) |arg, i| copy[i] = try alloc.dupeZ(u8, arg); + break :direct .{ .direct = copy }; + }, + }; + } + + pub fn deinit(self: *const Self, alloc: Allocator) void { + switch (self.*) { + .shell => |v| alloc.free(v), + .direct => |l| { + for (l) |v| alloc.free(v); + alloc.free(l); + }, + } + } + + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { + switch (self) { + .shell => |v| try formatter.formatEntry([]const u8, v), + + .direct => |v| { + var buf: [4096]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + writer.writeAll("direct:") catch return error.OutOfMemory; + for (v) |arg| { + writer.writeAll(arg) catch return error.OutOfMemory; + writer.writeByte(' ') catch return error.OutOfMemory; + } + + const written = writer.buffered(); + try formatter.formatEntry( + []const u8, + written[0..@intCast(written.len - 1)], + ); + }, + } + } + + test "Command: parseCLI errors" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var v: Self = undefined; + try testing.expectError(error.ValueRequired, v.parseCLI(alloc, null)); + try testing.expectError(error.ValueRequired, v.parseCLI(alloc, "")); + try testing.expectError(error.ValueRequired, v.parseCLI(alloc, " ")); + } + + test "Command: parseCLI shell expanded" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var v: Self = undefined; + try v.parseCLI(alloc, "echo hello"); + try testing.expect(v == .shell); + try testing.expectEqualStrings(v.shell, "echo hello"); + + // Spaces are stripped + try v.parseCLI(alloc, " echo hello "); + try testing.expect(v == .shell); + try testing.expectEqualStrings(v.shell, "echo hello"); + } + + test "Command: parseCLI direct" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var v: Self = undefined; + try v.parseCLI(alloc, "direct:echo hello"); + try testing.expect(v == .direct); + try testing.expectEqual(v.direct.len, 2); + try testing.expectEqualStrings(v.direct[0], "echo"); + try testing.expectEqualStrings(v.direct[1], "hello"); + + // Spaces around the prefix + try v.parseCLI(alloc, " direct: echo hello"); + try testing.expect(v == .direct); + try testing.expectEqual(v.direct.len, 2); + try testing.expectEqualStrings(v.direct[0], "echo"); + try testing.expectEqualStrings(v.direct[1], "hello"); + } + + test "Command: argIterator shell" { + const testing = std.testing; + const alloc = testing.allocator; + + var v: Self = .{ .shell = "echo hello world" }; + var it = try v.argIterator(alloc); + defer it.deinit(); + + try testing.expectEqualStrings(it.next().?, "echo"); + try testing.expectEqualStrings(it.next().?, "hello"); + try testing.expectEqualStrings(it.next().?, "world"); + try testing.expect(it.next() == null); + } + + test "Command: argIterator direct" { + const testing = std.testing; + const alloc = testing.allocator; + + var v: Self = .{ .direct = &.{ "echo", "hello world" } }; + var it = try v.argIterator(alloc); + defer it.deinit(); + + try testing.expectEqualStrings(it.next().?, "echo"); + try testing.expectEqualStrings(it.next().?, "hello world"); + try testing.expect(it.next() == null); + } + + test "Command: string shell" { + const testing = std.testing; + const alloc = testing.allocator; + + var v: Self = .{ .shell = "echo hello world" }; + const str = try v.string(alloc); + defer alloc.free(str); + try testing.expectEqualStrings(str, "echo hello world"); + } + + test "Command: string direct" { + const testing = std.testing; + const alloc = testing.allocator; + + var v: Self = .{ .direct = &.{ "echo", "hello world" } }; + const str = try v.string(alloc); + defer alloc.free(str); + try testing.expectEqualStrings(str, "echo hello world"); + } + + test "Command: formatConfig shell" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + + var v: Self = undefined; + try v.parseCLI(alloc, "echo hello"); + try v.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = echo hello\n", buf.written()); + } + + test "Command: formatConfig direct" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + + var v: Self = undefined; + try v.parseCLI(alloc, "direct: echo hello"); + try v.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = direct:echo hello\n", buf.written()); + } +}; + +test { + _ = Command; +} |
