aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/paste.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/terminal/paste.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/paste.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/paste.zig247
1 files changed, 247 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/paste.zig b/vendor/libghostty-vt/src/terminal/paste.zig
new file mode 100644
index 00000000..05e088ee
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/paste.zig
@@ -0,0 +1,247 @@
+//! Pasting into a terminal.
+//!
+//! This is the single place that turns "the user pasted" into bytes for
+//! the pty, applying the terminal's current state:
+//!
+//! * Mode 5522 (Kitty clipboard protocol paste events) set, a
+//! user-initiated clipboard paste, and the embedder able to serve
+//! the program's follow-up clipboard read: send a paste event
+//! listing the clipboard's MIME types with a fresh one-time password
+//! and record a one-time read grant for it. No data is read.
+//! * Otherwise: write the first text representation, with unsafe bytes
+//! replaced (xterm behavior), framed per mode 2004 (bracketed paste)
+//! or with newlines converted to carriage returns if not.
+//!
+//! The precedence (5522 event, else 2004 framing, else plain) and the
+//! safety rule live only here so every embedder of the terminal shares
+//! one implementation.
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const clipboard = @import("clipboard.zig");
+const kitty_clipboard = @import("kitty/clipboard.zig");
+const input_paste = @import("../input/paste.zig");
+const Terminal = @import("Terminal.zig");
+
+/// Why a paste happened. Only clipboard pastes may become paste
+/// events, which is why only they carry a location: it's meaningless
+/// for text insertion.
+///
+/// C: GhosttyPasteSource, flattened next to the location since C has
+/// no tagged unions.
+pub const Source = union(enum) {
+ /// The user pasted from a clipboard: keybind, menu, middle click.
+ /// The payload is the clipboard the contents came from.
+ clipboard: clipboard.Location,
+
+ /// Text inserted some other way: IME commit, drag and drop,
+ /// scripted input.
+ text,
+};
+
+/// A paste of clipboard contents into the terminal. What actually gets
+/// written depends on terminal state; see `paste`.
+pub const Request = struct {
+ /// Why this paste happened, and for a clipboard paste, from which
+ /// clipboard. Only a user-initiated clipboard paste may become a
+ /// paste event; text insertion always writes text.
+ source: Source = .{ .clipboard = .standard },
+
+ /// The representations available. Borrowed only during the
+ /// duration of the paste function call.
+ contents: Contents,
+
+ /// Write data that could inject commands (see `paste`). The usual
+ /// flow is to call with false, confirm with the user on
+ /// error.UnsafePaste, and call again with true.
+ allow_unsafe: bool = false,
+};
+
+/// The representations available for a paste, in the embedder's
+/// preferred order.
+pub const Contents = union(enum) {
+ /// Every representation already in memory. For text the embedder
+ /// holds anyway (an IME commit, dropped text) or small clipboards.
+ memory: []const clipboard.Content,
+
+ /// Representations read on demand, so nothing is loaded that isn't
+ /// pasted. This is the form for a real clipboard, whose non-text
+ /// items may be huge.
+ reader: Reader,
+
+ /// The on-demand form: the MIME types available plus the reader
+ /// that produces the data of any one of them.
+ pub const Reader = struct {
+ /// The MIME types available, in preferred order.
+ mimes: []const []const u8,
+
+ /// Produces the data of any entry of `mimes`, which is passed
+ /// through to it as is. A paste reads at most once: the text
+ /// representation being pasted, never anything else and never
+ /// anything for a paste event. There is no requirement across
+ /// paste calls, so a source that changes between an unsafe
+ /// refusal and the embedder's confirmed retry simply pastes
+ /// its current contents.
+ read: clipboard.MimeReader,
+ };
+
+ /// The number of representations.
+ pub fn len(self: Contents) usize {
+ return switch (self) {
+ .memory => |v| v.len,
+ .reader => |v| v.mimes.len,
+ };
+ }
+
+ /// The MIME type of representation `index`.
+ pub fn mime(self: Contents, index: usize) []const u8 {
+ return switch (self) {
+ .memory => |v| v[index].mime,
+ .reader => |v| v.mimes[index],
+ };
+ }
+};
+
+/// What a caller supplies to `paste`: the terminal state the decision
+/// depends on, the session state an event records into, and the sink.
+pub const Context = struct {
+ /// The terminal whose modes decide the encoding.
+ terminal: *const Terminal,
+
+ /// Allocator for transient state: the buffered read and a paste
+ /// event's grant. Must be the allocator `kitty_clipboard.grants`
+ /// is freed with.
+ alloc: Allocator,
+
+ /// Receives the encoded text in chunks, or the event. Must be
+ /// buffered (the encoder works in its buffer) and is not flushed
+ /// by `paste`; the caller flushes once it returns.
+ writer: *std.Io.Writer,
+
+ /// Kitty clipboard protocol session state, or null if the embedder
+ /// does not serve Kitty clipboard reads (`clipboard.Read`). A
+ /// paste event is that protocol: it is only useful if the
+ /// program's follow-up read can be answered, since otherwise the
+ /// read would be refused and the user's paste would vanish. With
+ /// null, `paste` always writes text.
+ kitty_clipboard: ?KittyClipboard,
+
+ pub const KittyClipboard = struct {
+ /// Session grants. A paste event records its one-time password
+ /// here so the program's follow-up read is served without a
+ /// prompt.
+ grants: *kitty_clipboard.Grants,
+
+ /// Secure entropy for one-time passwords. See generateOtp for
+ /// why there is no fallback when this has none.
+ io: std.Io,
+ };
+};
+
+pub const Error = Allocator.Error ||
+ std.Io.RandomSecureError ||
+ clipboard.MimeReader.Error ||
+ error{
+ /// The data could inject commands and allow_unsafe was false.
+ UnsafePaste,
+ };
+
+/// Paste into the terminal, applying the terminal's current state as
+/// described in the module docs. Returns true if anything was written
+/// to `ctx.writer`: the encoded text or a paste event. False means
+/// there was nothing to paste (no non-empty text representation).
+///
+/// The safety rule for a text paste (`input.paste.isSafeWith`): a
+/// bracketed paste is unsafe only if it contains the bracket terminator
+/// (CSI 201~); an unbracketed paste is unsafe if it contains a newline
+/// or the terminator. Embedders wanting a stricter rule check
+/// `input.paste.isSafe` themselves before calling. A paste event never
+/// puts the data on the input stream, so the rule doesn't apply to it.
+///
+/// The contents are read at most once per call and buffered whole, so
+/// the source needs no stability across reads. Nothing reaches the
+/// writer until the read completed and the text passed the rule:
+/// every error writes nothing, and a failed event records no grant.
+pub fn paste(ctx: Context, req: Request) Error!bool {
+ // If the source is a clipboard and mode 5522 is enabled and
+ // the caller can handle kitty events, then do a kitty event.
+ if (req.source == .clipboard and
+ ctx.terminal.modes.get(.kitty_paste_events))
+ {
+ if (ctx.kitty_clipboard) |kitty| {
+ try pasteKittyEvent(ctx, kitty, req);
+ return true;
+ }
+ }
+
+ // For non-Kitty paste events we can only accept text content.
+ const index: usize = for (0..req.contents.len()) |i| {
+ if (clipboard.isTextMime(req.contents.mime(i))) break i;
+ } else return false;
+
+ const opts: input_paste.Options = .fromTerminal(ctx.terminal);
+
+ // In-memory contents are used directly to avoid a double copy but
+ // reader-based contents are read into memory so we can do the unsafe
+ // scan.
+ var aw: std.Io.Writer.Allocating = .init(ctx.alloc);
+ defer aw.deinit();
+ const text: []const u8 = switch (req.contents) {
+ .memory => |v| v[index].data,
+ .reader => |v| text: {
+ v.read.read(v.mimes[index], &aw.writer) catch |err| switch (err) {
+ // An allocating writer only fails to allocate.
+ error.WriteFailed => return error.OutOfMemory,
+ error.ReadFailed => |e| return e,
+ };
+ break :text aw.writer.buffered();
+ },
+ };
+ if (text.len == 0) return false;
+ if (!req.allow_unsafe and !input_paste.isSafeWith(
+ text,
+ opts,
+ )) return error.UnsafePaste;
+
+ // The text is copied exactly once per chunk, into the writer,
+ // where the encoder strips and converts it in place.
+ try input_paste.encodeWriter(ctx.writer, text, opts);
+ return true;
+}
+
+fn pasteKittyEvent(
+ ctx: Context,
+ kitty: Context.KittyClipboard,
+ req: Request,
+) Error!void {
+ const otp = try kitty_clipboard.generateOtp(kitty.io);
+
+ // Every representation is listed, never read. The listing is
+ // bounded; a clipboard with more types than that is not a thing.
+ var mimes_buf: [kitty_clipboard.max_listing_mimes][]const u8 = undefined;
+ const mimes_len = @min(req.contents.len(), mimes_buf.len);
+ for (mimes_buf[0..mimes_len], 0..) |*mime, i| mime.* = req.contents.mime(i);
+
+ // The grant is recorded before the event can reach the program,
+ // and revoked if the event fails to be written so a failure never
+ // leaves a grant for an event that was never sent. Using a
+ // one-time grant consumes it, which is the revocation.
+ try kitty.grants.grant(ctx.alloc, &otp, .read, true);
+ errdefer _ = kitty.grants.use(ctx.alloc, &otp, .read);
+
+ try (kitty_clipboard.PasteEvent{
+ // The protocol only distinguishes the clipboard from the
+ // primary selection, so both non-standard locations report as
+ // primary. Only a clipboard paste gets here; see `paste`.
+ .primary = req.source.clipboard != .standard,
+ .pw = &otp,
+ .available = mimes_buf[0..mimes_len],
+ }).encode(ctx.writer);
+}
+
+test {
+ // The behavior is tested end to end through the stream handler
+ // (stream_terminal.zig), which is the primary caller.
+ std.testing.refAllDecls(@This());
+}