aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/config/command.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/config/command.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/config/command.zig')
-rw-r--r--vendor/libghostty-vt/src/config/command.zig330
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;
+}