diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 14:23:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 505d15e238e4e9c5020a8e567d700bdbf67a8528 (patch) | |
| tree | d0e136a29cc5f3416046157824f07c5e3bb1f67e /src/gui/gui.zig | |
| parent | 17c0d06618085de8d0bbf8152750ffa862db0080 (diff) | |
| download | pardes-505d15e238e4e9c5020a8e567d700bdbf67a8528.tar.gz pardes-505d15e238e4e9c5020a8e567d700bdbf67a8528.zip | |
A paste into a terminal is xterm's paste: bracketed when the program set 2004, and the control bytes a paste could smuggle a command in by, ESC first, become spaces, so no ESC[201~ in the text ends the brackets early
Every paste path goes through edit.typeToTty (the GUI's and the tty's
paste, p from a register, the acme chord's paste, a REPL's send), which
now takes ghostty's paste encoder. A 9P write to a terminal's body or
pty/data stays typed input, not a paste. The GUI test shell's stdin now
takes a bracketed paste as one paste, as the tty does, not as keys.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/gui/gui.zig')
| -rw-r--r-- | src/gui/gui.zig | 52 |
1 files changed, 51 insertions, 1 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 81b68afd..14205906 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -3097,6 +3097,9 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void { const StdinFeed = struct { parser: vaxis.Parser = .{}, + /// Inside the driver's bracketed paste: its keys are the paste's text. + in_paste: bool = false, + paste: std.ArrayList(u8) = .empty, cache: vaxis.GraphemeCache = .{}, buf: [1024]u8 = undefined, fill: usize = 0, @@ -3241,10 +3244,24 @@ const StdinFeed = struct { seq_start += result.n; const event = result.event orelse continue; switch (event) { - .key_press => |key| { + .key_press => |key| if (f.in_paste) { + // A newline reads as Ctrl-J, as the tty's paste has it. + const text = key.text orelse if (key.codepoint == vaxis.Key.enter or (key.mods.ctrl and key.codepoint == 'j')) "\n" else if (key.codepoint == vaxis.Key.tab) "\t" else ""; + f.paste.appendSlice(gpa, text) catch {}; + } else { postKey(in, key, if (key.text) |t| f.cache.put(t) else ""); out.n_events += 1; }, + .paste_start => { + f.in_paste = true; + f.paste.clearRetainingCapacity(); + }, + .paste_end => { + f.in_paste = false; + if (f.paste.items.len > 0) in.post(.{ .paste = f.paste.items }); + f.paste.clearAndFree(gpa); + out.n_events += 1; + }, .mouse => |m| { const button: ?pardes.Mouse.Button = switch (m.button) { .left => .left, @@ -4820,6 +4837,39 @@ test "the headless grid host round-trips a yank back as a paste" { try std.testing.expectEqual(before + yanked.len, pane.file.?.content.len); } +test "the clipboard pasted into a terminal reaches its pty as xterm pastes: bracketed when asked, no ESC[201~ smuggled through" { + if (comptime !pardes.panes.terminal.enabled) return error.SkipZigTest; + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .cols = 60, .rows = 16, .tty_only = true }); + defer core.deinit(); + var shell: Shell = undefined; + shell.core = core; + var sent: std.ArrayList(u8) = .empty; + defer sent.deinit(gpa); + const take = struct { + fn f(core_: *pardes.Pardes, out: *std.ArrayList(u8)) ![]const u8 { + out.clearRetainingCapacity(); + while (core_.nextEffect()) |effect| { + if (effect == .write and effect.write.pane == 0) try out.appendSlice(std.testing.allocator, effect.write.bytes.slice()); + } + return out.items; + } + }.f; + _ = try take(core, &sent); + // The clipboard is SDL's own with no display to hold one. + _ = c.SDL_SetHint(c.SDL_HINT_VIDEO_DRIVER, "dummy"); + defer _ = c.SDL_ResetHint(c.SDL_HINT_VIDEO_DRIVER); + if (!c.SDL_InitSubSystem(c.SDL_INIT_VIDEO)) return error.SkipZigTest; + defer c.SDL_QuitSubSystem(c.SDL_INIT_VIDEO); + putClipboard(gpa, "ls\x1b[201~; rm -rf ~\nx"); + readClipboard(&shell); + try std.testing.expectEqualStrings("ls [201~; rm -rf ~\rx", try take(core, &sent)); + core.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?2004h" } }); + _ = try take(core, &sent); + readClipboard(&shell); + try std.testing.expectEqualStrings("\x1b[200~ls [201~; rm -rf ~\nx\x1b[201~", try take(core, &sent)); +} + fn openLink(ctx: ?*anyopaque, url: []const u8) void { _ = ctx; look.openLink(url); // desktop browser |
