mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-01-01 03:02:15 +00:00
This moves our paste logic to `src/input` in preparation for exposing this as part of libghostty-vt. This yields an immediate benefit of unit tests for paste encoding. Additionally, we were able to remove one allocation on every unbracketed paste path unless the input specifically contains a newline. Unlikely to be noticable, but nice. NOTE: This also includes one change in behavior: we no longer encode `\r\n` and a single `\r`, but as a duplicate `\r\r`. This matches xterm behavior and I don't think will result in any issues since duplicate carriage returns should do nothing in well-behaved terminals.
147 lines
5.1 KiB
Zig
147 lines
5.1 KiB
Zig
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
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,
|
|
} {
|
|
const mutable = @TypeOf(data) == []u8;
|
|
|
|
var result: [3][]const u8 = .{ "", data, "" };
|
|
|
|
// 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]);
|
|
}
|