summaryrefslogtreecommitdiff
path: root/src/Surface.zig
diff options
context:
space:
mode:
authorMitchell Hashimoto <m@mitchellh.com>2025-10-04 13:24:34 -0700
committerMitchell Hashimoto <m@mitchellh.com>2025-10-04 14:05:32 -0700
commitd4dcecb071a113f5a27ab2948c721384210114fc (patch)
tree3a9d3d03052482daf044a27b0c5f318d1a564cd2 /src/Surface.zig
parent18eee610bc715e181fcbf96a438b48edb0673090 (diff)
Move paste encoding to the input package, test, optimize away one alloc
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.
Diffstat (limited to 'src/Surface.zig')
-rw-r--r--src/Surface.zig74
1 files changed, 24 insertions, 50 deletions
diff --git a/src/Surface.zig b/src/Surface.zig
index 018c4206b..5aabb2b80 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -5270,13 +5270,10 @@ fn completeClipboardPaste(
) !void {
if (data.len == 0) return;
- const critical: struct {
- bracketed: bool,
- } = critical: {
+ const encode_opts: input.paste.Options = encode_opts: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
-
- const bracketed = self.io.terminal.modes.get(.bracketed_paste);
+ const opts: input.paste.Options = .fromTerminal(&self.io.terminal);
// If we have paste protection enabled, we detect unsafe pastes and return
// an error. The error approach allows apprt to attempt to complete the paste
@@ -5292,7 +5289,7 @@ fn completeClipboardPaste(
// This is set during confirmation usually.
if (allow_unsafe) break :unsafe false;
- if (bracketed) {
+ if (opts.bracketed) {
// If we're bracketed and the paste contains and ending
// bracket then something naughty might be going on and we
// never trust it.
@@ -5303,7 +5300,7 @@ fn completeClipboardPaste(
if (self.config.clipboard_paste_bracketed_safe) break :unsafe false;
}
- break :unsafe !terminal.isSafePaste(data);
+ break :unsafe !input.paste.isSafe(data);
};
if (unsafe) {
@@ -5317,55 +5314,32 @@ fn completeClipboardPaste(
log.warn("error scrolling to bottom err={}", .{err});
};
- break :critical .{
- .bracketed = bracketed,
- };
+ break :encode_opts opts;
};
- if (critical.bracketed) {
- // If we're bracketd we write the data as-is to the terminal with
- // the bracketed paste escape codes around it.
- self.io.queueMessage(.{
- .write_stable = "\x1B[200~",
- }, .unlocked);
- self.io.queueMessage(try termio.Message.writeReq(
- self.alloc,
- data,
- ), .unlocked);
- self.io.queueMessage(.{
- .write_stable = "\x1B[201~",
- }, .unlocked);
- } else {
- // If its not bracketed the input bytes are indistinguishable from
- // keystrokes, so we must be careful. For example, we must replace
- // any newlines with '\r'.
-
- // We just do a heap allocation here because its easy and I don't think
- // worth the optimization of using small messages.
- var buf = try self.alloc.alloc(u8, data.len);
- defer self.alloc.free(buf);
-
- // This is super, super suboptimal. We can easily make use of SIMD
- // here, but maybe LLVM in release mode is smart enough to figure
- // out something clever. Either way, large non-bracketed pastes are
- // increasingly rare for modern applications.
- var len: usize = 0;
- for (data, 0..) |ch, i| {
- const dch = switch (ch) {
- '\n' => '\r',
- '\r' => if (i + 1 < data.len and data[i + 1] == '\n') continue else ch,
- else => ch,
- };
-
- buf[len] = dch;
- len += 1;
- }
+ // Encode the data. In most cases this doesn't require any
+ // copies, so we optimize for that case.
+ var data_duped: ?[]u8 = null;
+ const vecs = input.paste.encode(data, encode_opts) catch |err| switch (err) {
+ error.MutableRequired => vecs: {
+ const buf: []u8 = try self.alloc.dupe(u8, data);
+ errdefer self.alloc.free(buf);
+ data_duped = buf;
+ break :vecs input.paste.encode(buf, encode_opts);
+ },
+ };
+ defer if (data_duped) |v| {
+ // This code path means the data did require a copy and mutation.
+ // We must free it.
+ self.alloc.free(v);
+ };
+ for (vecs) |vec| if (vec.len > 0) {
self.io.queueMessage(try termio.Message.writeReq(
self.alloc,
- buf[0..len],
+ vec,
), .unlocked);
- }
+ };
}
fn completeClipboardReadOSC52(