summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/edit.zig39
-rw-r--r--src/gui/gui.zig52
-rw-r--r--src/terminal.zig15
3 files changed, 95 insertions, 11 deletions
diff --git a/src/edit.zig b/src/edit.zig
index 98c60efb..1305d66f 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -111,19 +111,12 @@ pub fn clipRequest(p: *Pardes, id: usize, mode: @FieldType(ClipRequest, "mode"))
p.emit(.read_clipboard);
}
+/// Text pasted, yanked or sent into the program at `id`, as xterm pastes
+/// (terminal.encodePaste).
pub fn typeToTty(p: *Pardes, id: usize, pane: *const Pane, text: []const u8) void {
if (text.len == 0) return;
- if (panes.terminal.bracketedPaste(pane)) {
- p.emitWrite(id, "\x1b[200~");
- p.emitWrite(id, text);
- p.emitWrite(id, "\x1b[201~");
- return;
- }
const cp = p.scratch.allocator().dupe(u8, text) catch return;
- for (cp) |*ch| {
- if (ch.* == '\n') ch.* = '\r';
- }
- p.emitWrite(id, cp);
+ for (panes.terminal.encodePaste(pane, cp)) |part| if (part.len > 0) p.emitWrite(id, part);
}
pub fn applyPaste(p: *Pardes, bytes: []const u8) void {
@@ -2405,3 +2398,29 @@ test "a typed edit's known end is where the texts' comparison would find it" {
try std.testing.expectEqual(by_scan.col, t.last_edit.?.col);
}
}
+
+test "a paste into a terminal is xterm's: bracketed when the program asked, control bytes spaces, so no ESC[201~ ends the brackets early" {
+ if (comptime !panes.terminal.enabled) return error.SkipZigTest;
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .cols = 60, .rows = 16, .tty_only = true });
+ defer p.deinit();
+ var out: std.ArrayList(u8) = .empty;
+ defer out.deinit(gpa);
+ const take = struct {
+ fn f(pp: *Pardes, o: *std.ArrayList(u8)) ![]const u8 {
+ o.clearRetainingCapacity();
+ while (pp.nextEffect()) |effect| {
+ if (effect == .write and effect.write.pane == 0) try o.appendSlice(pp.gpa, effect.write.bytes.slice());
+ }
+ return o.items;
+ }
+ }.f;
+ _ = try take(p, &out);
+ try std.testing.expectEqual(.tty, p.panes[0].?.body.mode);
+ p.update(.{ .paste = "echo a\nb\x03\tc" });
+ try std.testing.expectEqualStrings("echo a\rb \tc", try take(p, &out));
+ p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?2004h" } });
+ _ = try take(p, &out);
+ p.update(.{ .paste = "x\x1b[201~y\n" });
+ try std.testing.expectEqualStrings("\x1b[200~x [201~y\n\x1b[201~", try take(p, &out));
+}
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
diff --git a/src/terminal.zig b/src/terminal.zig
index af540202..4cdfa52d 100644
--- a/src/terminal.zig
+++ b/src/terminal.zig
@@ -380,6 +380,21 @@ pub fn reportsMouse(pane: *const Pane) bool {
return state.vt.flags.mouse_event != .none;
}
+/// A paste as xterm makes one (ghostty's encoder): the control bytes a
+/// paste could smuggle a command in by (ESC, so no ESC[201~ can end
+/// 2004's brackets early, NUL, ^C and the rest of xterm's list) become
+/// spaces, then the text goes inside the brackets when the program set
+/// 2004, else with its newlines as Returns. `text` is rewritten in place.
+pub fn encodePaste(pane: *const Pane, text: []u8) [3][]const u8 {
+ if (comptime !enabled) {
+ for (text) |*ch| if (ch.* == '\n') {
+ ch.* = '\r';
+ };
+ return .{ "", text, "" };
+ }
+ return ghostty_vt.input.encodePaste(text, .{ .bracketed = bracketedPaste(pane) });
+}
+
/// DECSET 1004: the program wants to hear when it gains and loses focus.
pub fn reportsFocus(pane: *const Pane) bool {
if (comptime !enabled) return false;