aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/input/paste.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/input/paste.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/input/paste.zig')
-rw-r--r--vendor/libghostty-vt/src/input/paste.zig228
1 files changed, 228 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/input/paste.zig b/vendor/libghostty-vt/src/input/paste.zig
new file mode 100644
index 00000000..16b6266b
--- /dev/null
+++ b/vendor/libghostty-vt/src/input/paste.zig
@@ -0,0 +1,228 @@
+const std = @import("std");
+const Terminal = @import("../terminal/Terminal.zig");
+
+pub const Options = struct {
+ /// True if bracketed paste mode is on.
+ bracketed: bool,
+
+ /// Return the encoding options based on the current terminal state.
+ pub fn fromTerminal(t: *const Terminal) Options {
+ return .{
+ .bracketed = t.modes.get(.bracketed_paste),
+ };
+ }
+};
+
+/// Encode the given data for pasting. The resulting value can be written
+/// to the pty to perform a paste of the input data.
+///
+/// The data can be either a `[]u8` or a `[]const u8`. If the data
+/// type is const then `EncodeError` may be returned. If the data type
+/// is mutable then this function can't return an error.
+///
+/// This slightly complex calling style allows for initially const
+/// data to be passed in without an allocation, since it is rare in normal
+/// use cases that the data will need to be modified. In the unlikely case
+/// data does need to be modified, the caller can make a mutable copy
+/// after seeing the error.
+///
+/// The data is returned as a set of slices to limit allocations. The caller
+/// can combine the slices into a single buffer if desired.
+///
+/// WARNING: The input data is not checked for safety. See the `isSafe`
+/// function to check if the data is safe to paste.
+pub fn encode(
+ data: anytype,
+ opts: Options,
+) switch (@TypeOf(data)) {
+ []u8 => [3][]const u8,
+ []const u8 => Error![3][]const u8,
+ else => unreachable,
+} {
+ // These are the set of byte values that are always replaced by
+ // a space (per xterm's behavior) for any text insertion method e.g.
+ // a paste, drag and drop, etc. These are copied directly from xterm's
+ // source.
+ const strip: []const u8 = &.{
+ 0x00, // NUL
+ 0x08, // BS
+ 0x05, // ENQ
+ 0x04, // EOT
+ 0x1B, // ESC
+ 0x7F, // DEL
+
+ // These can be overridden by the running terminal program
+ // via tcsetattr, so they aren't totally safe to hardcode like
+ // this. In practice, I haven't seen modern programs change these
+ // and its a much bigger architectural change to pass these through
+ // so for now they're hardcoded.
+ 0x03, // VINTR (Ctrl+C)
+ 0x1C, // VQUIT (Ctrl+\)
+ 0x15, // VKILL (Ctrl+U)
+ 0x1A, // VSUSP (Ctrl+Z)
+ 0x11, // VSTART (Ctrl+Q)
+ 0x13, // VSTOP (Ctrl+S)
+ 0x17, // VWERASE (Ctrl+W)
+ 0x16, // VLNEXT (Ctrl+V)
+ 0x12, // VREPRINT (Ctrl+R)
+ 0x0F, // VDISCARD (Ctrl+O)
+ };
+
+ const mutable = @TypeOf(data) == []u8;
+
+ var result: [3][]const u8 = .{ "", data, "" };
+
+ // If we have any of the strip values, then we need to replace them
+ // with spaces. This is what xterm does and it does it regardless
+ // of bracketed paste mode. This is a security measure to prevent pastes
+ // from containing bytes that could be used to inject commands.
+ if (std.mem.indexOfAny(u8, data, strip) != null) {
+ if (comptime !mutable) return Error.MutableRequired;
+ var offset: usize = 0;
+ while (std.mem.indexOfAny(
+ u8,
+ data[offset..],
+ strip,
+ )) |idx| {
+ offset += idx;
+ data[offset] = ' ';
+ offset += 1;
+ }
+ }
+
+ // Bracketed paste mode (mode 2004) wraps pasted data in
+ // fenceposts so that the terminal can ignore things like newlines.
+ if (opts.bracketed) {
+ result[0] = "\x1b[200~";
+ result[2] = "\x1b[201~";
+ return result;
+ }
+
+ // Non-bracketed. We have to replace newline with `\r`. This matches
+ // the behavior of xterm and other terminals. For `\r\n` this will
+ // result in `\r\r` which does match xterm.
+ if (comptime mutable) {
+ std.mem.replaceScalar(u8, data, '\n', '\r');
+ } else if (std.mem.indexOfScalar(u8, data, '\n') != null) {
+ return Error.MutableRequired;
+ }
+
+ return result;
+}
+
+pub const Error = error{
+ /// Returned if encoding requires a mutable copy of the data. This
+ /// can only be returned if the input data type is const.
+ MutableRequired,
+};
+
+/// Returns true if the data looks safe to paste. Data is considered
+/// unsafe if it contains any of the following:
+///
+/// - `\n`: Newlines can be used to inject commands.
+/// - `\x1b[201~`: This is the end of a bracketed paste. This cane be used
+/// to exit a bracketed paste and inject commands.
+///
+/// We consider any scenario unsafe regardless of current terminal state.
+/// For example, even if bracketed paste mode is not active, we still
+/// consider `\x1b[201~` unsafe. The existence of these types of bytes
+/// should raise suspicion that the producer of the paste data is
+/// acting strangely.
+pub fn isSafe(data: []const u8) bool {
+ return std.mem.indexOf(u8, data, "\n") == null and
+ std.mem.indexOf(u8, data, "\x1b[201~") == null;
+}
+
+test isSafe {
+ const testing = std.testing;
+ try testing.expect(isSafe("hello"));
+ try testing.expect(!isSafe("hello\n"));
+ try testing.expect(!isSafe("hello\nworld"));
+ try testing.expect(!isSafe("he\x1b[201~llo"));
+}
+
+test "encode bracketed" {
+ const testing = std.testing;
+ const result = try encode(
+ @as([]const u8, "hello"),
+ .{ .bracketed = true },
+ );
+ try testing.expectEqualStrings("\x1b[200~", result[0]);
+ try testing.expectEqualStrings("hello", result[1]);
+ try testing.expectEqualStrings("\x1b[201~", result[2]);
+}
+
+test "encode unbracketed no newlines" {
+ const testing = std.testing;
+ const result = try encode(
+ @as([]const u8, "hello"),
+ .{ .bracketed = false },
+ );
+ try testing.expectEqualStrings("", result[0]);
+ try testing.expectEqualStrings("hello", result[1]);
+ try testing.expectEqualStrings("", result[2]);
+}
+
+test "encode unbracketed newlines const" {
+ const testing = std.testing;
+ try testing.expectError(Error.MutableRequired, encode(
+ @as([]const u8, "hello\nworld"),
+ .{ .bracketed = false },
+ ));
+}
+
+test "encode unbracketed newlines" {
+ const testing = std.testing;
+ const data: []u8 = try testing.allocator.dupe(u8, "hello\nworld");
+ defer testing.allocator.free(data);
+ const result = encode(data, .{ .bracketed = false });
+ try testing.expectEqualStrings("", result[0]);
+ try testing.expectEqualStrings("hello\rworld", result[1]);
+ try testing.expectEqualStrings("", result[2]);
+}
+
+test "encode unbracketed windows-stye newline" {
+ const testing = std.testing;
+ const data: []u8 = try testing.allocator.dupe(u8, "hello\r\nworld");
+ defer testing.allocator.free(data);
+ const result = encode(data, .{ .bracketed = false });
+ try testing.expectEqualStrings("", result[0]);
+ try testing.expectEqualStrings("hello\r\rworld", result[1]);
+ try testing.expectEqualStrings("", result[2]);
+}
+
+test "encode strip unsafe bytes const" {
+ const testing = std.testing;
+ try testing.expectError(Error.MutableRequired, encode(
+ @as([]const u8, "hello\x00world"),
+ .{ .bracketed = true },
+ ));
+}
+
+test "encode strip unsafe bytes mutable bracketed" {
+ const testing = std.testing;
+ const data: []u8 = try testing.allocator.dupe(u8, "hel\x1blo\x00world");
+ defer testing.allocator.free(data);
+ const result = encode(data, .{ .bracketed = true });
+ try testing.expectEqualStrings("\x1b[200~", result[0]);
+ try testing.expectEqualStrings("hel lo world", result[1]);
+ try testing.expectEqualStrings("\x1b[201~", result[2]);
+}
+
+test "encode strip unsafe bytes mutable unbracketed" {
+ const testing = std.testing;
+ const data: []u8 = try testing.allocator.dupe(u8, "hel\x03lo");
+ defer testing.allocator.free(data);
+ const result = encode(data, .{ .bracketed = false });
+ try testing.expectEqualStrings("", result[0]);
+ try testing.expectEqualStrings("hel lo", result[1]);
+ try testing.expectEqualStrings("", result[2]);
+}
+
+test "encode strip multiple unsafe bytes" {
+ const testing = std.testing;
+ const data: []u8 = try testing.allocator.dupe(u8, "\x00\x08\x7f");
+ defer testing.allocator.free(data);
+ const result = encode(data, .{ .bracketed = true });
+ try testing.expectEqualStrings(" ", result[1]);
+}