aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/os/shell.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/os/shell.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/os/shell.zig')
-rw-r--r--vendor/libghostty-vt/src/os/shell.zig196
1 files changed, 196 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/os/shell.zig b/vendor/libghostty-vt/src/os/shell.zig
new file mode 100644
index 00000000..fe8f1b2f
--- /dev/null
+++ b/vendor/libghostty-vt/src/os/shell.zig
@@ -0,0 +1,196 @@
+const std = @import("std");
+const testing = std.testing;
+const Allocator = std.mem.Allocator;
+const Writer = std.Io.Writer;
+
+/// Builder for constructing space-separated shell command strings.
+/// Uses a caller-provided allocator (typically with stackFallback).
+pub const ShellCommandBuilder = struct {
+ buffer: std.Io.Writer.Allocating,
+
+ pub fn init(allocator: Allocator) ShellCommandBuilder {
+ return .{ .buffer = .init(allocator) };
+ }
+
+ pub fn deinit(self: *ShellCommandBuilder) void {
+ self.buffer.deinit();
+ }
+
+ /// Append an argument to the command with automatic space separation.
+ pub fn appendArg(self: *ShellCommandBuilder, arg: []const u8) (Allocator.Error || Writer.Error)!void {
+ if (arg.len == 0) return;
+ if (self.buffer.written().len > 0) {
+ try self.buffer.writer.writeByte(' ');
+ }
+ try self.buffer.writer.writeAll(arg);
+ }
+
+ /// Get the final null-terminated command string, transferring ownership to caller.
+ /// Calling deinit() after this is safe but unnecessary.
+ pub fn toOwnedSlice(self: *ShellCommandBuilder) Allocator.Error![:0]const u8 {
+ return try self.buffer.toOwnedSliceSentinel(0);
+ }
+};
+
+test ShellCommandBuilder {
+ // Empty command
+ {
+ var cmd = ShellCommandBuilder.init(testing.allocator);
+ defer cmd.deinit();
+ try testing.expectEqualStrings("", cmd.buffer.written());
+ }
+
+ // Single arg
+ {
+ var cmd = ShellCommandBuilder.init(testing.allocator);
+ defer cmd.deinit();
+ try cmd.appendArg("bash");
+ try testing.expectEqualStrings("bash", cmd.buffer.written());
+ }
+
+ // Multiple args
+ {
+ var cmd = ShellCommandBuilder.init(testing.allocator);
+ defer cmd.deinit();
+ try cmd.appendArg("bash");
+ try cmd.appendArg("--posix");
+ try cmd.appendArg("-l");
+ try testing.expectEqualStrings("bash --posix -l", cmd.buffer.written());
+ }
+
+ // Empty arg
+ {
+ var cmd = ShellCommandBuilder.init(testing.allocator);
+ defer cmd.deinit();
+ try cmd.appendArg("bash");
+ try cmd.appendArg("");
+ try testing.expectEqualStrings("bash", cmd.buffer.written());
+ }
+
+ // toOwnedSlice
+ {
+ var cmd = ShellCommandBuilder.init(testing.allocator);
+ try cmd.appendArg("bash");
+ try cmd.appendArg("--posix");
+ const result = try cmd.toOwnedSlice();
+ defer testing.allocator.free(result);
+ try testing.expectEqualStrings("bash --posix", result);
+ try testing.expectEqual(@as(u8, 0), result[result.len]);
+ }
+}
+
+/// Writer that escapes characters that shells treat specially to reduce the
+/// risk of injection attacks or other such weirdness. Specifically excludes
+/// linefeeds so that they can be used to delineate lists of file paths.
+pub const ShellEscapeWriter = struct {
+ writer: Writer,
+ child: *Writer,
+
+ pub fn init(child: *Writer) ShellEscapeWriter {
+ return .{
+ .writer = .{
+ // TODO: Actually use a buffer here
+ .buffer = &.{},
+ .vtable = &.{ .drain = ShellEscapeWriter.drain },
+ },
+ .child = child,
+ };
+ }
+
+ fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
+ const self: *ShellEscapeWriter = @fieldParentPtr("writer", w);
+
+ // TODO: This is a very naive implementation and does not really make
+ // full use of the post-Writergate API. However, since we know that
+ // this is going into an Allocating writer anyways, we can be a bit
+ // less strict here.
+
+ var count: usize = 0;
+ for (data[0 .. data.len - 1]) |chunk| try self.writeEscaped(chunk, &count);
+
+ for (0..splat) |_| try self.writeEscaped(data[data.len - 1], &count);
+ return count;
+ }
+
+ fn writeEscaped(
+ self: *ShellEscapeWriter,
+ s: []const u8,
+ count: *usize,
+ ) Writer.Error!void {
+ for (s) |byte| {
+ const buf = switch (byte) {
+ '\\',
+ '"',
+ '\'',
+ '$',
+ '`',
+ '*',
+ '?',
+ ' ',
+ '|',
+ '(',
+ ')',
+ => &[_]u8{ '\\', byte },
+ else => &[_]u8{byte},
+ };
+ try self.child.writeAll(buf);
+ count.* += 1;
+ }
+ }
+};
+
+test "shell escape 1" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("abc");
+ try testing.expectEqualStrings("abc", writer.buffered());
+}
+
+test "shell escape 2" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("a c");
+ try testing.expectEqualStrings("a\\ c", writer.buffered());
+}
+
+test "shell escape 3" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("a?c");
+ try testing.expectEqualStrings("a\\?c", writer.buffered());
+}
+
+test "shell escape 4" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("a\\c");
+ try testing.expectEqualStrings("a\\\\c", writer.buffered());
+}
+
+test "shell escape 5" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("a|c");
+ try testing.expectEqualStrings("a\\|c", writer.buffered());
+}
+
+test "shell escape 6" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("a\"c");
+ try testing.expectEqualStrings("a\\\"c", writer.buffered());
+}
+
+test "shell escape 7" {
+ var buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ var shell: ShellEscapeWriter = .init(&writer);
+ try shell.writer.writeAll("a(1)");
+ try testing.expectEqualStrings("a\\(1\\)", writer.buffered());
+}