diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-01 09:08:52 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-07-01 09:09:18 -0300 |
| commit | 6570dc04d6343cbbf31c0eb2363f9bec817aa4db (patch) | |
| tree | 5d11e6227ca3e101deeda2ee170912037826c1f8 /main.zig | |
| parent | f6089186cfe1517073fafc8979a7546ec773d6c1 (diff) | |
| download | pardes-6570dc04d6343cbbf31c0eb2363f9bec817aa4db.tar.gz pardes-6570dc04d6343cbbf31c0eb2363f9bec817aa4db.zip | |
added yank and tty scrolling w keyboard
Diffstat (limited to 'main.zig')
| -rw-r--r-- | main.zig | 77 |
1 files changed, 68 insertions, 9 deletions
@@ -78,12 +78,16 @@ pub const Command = struct { pty_eof: usize, winsize: vaxis.Winsize, mouse: vaxis.Mouse, + paste: []const u8, // OSC 52 system-clipboard read reply (the loop decodes it) } = .nop; }; // Modal yank register: gpa-owned, freed on overwrite / app exit. Helix-style -// `y` yanks the (line) selection into here; `p` pastes it after the cursor. +// `y` yanks the (line) selection into here; `p` pastes the system clipboard. +// A yank also mirrors out to the system clipboard (OSC 52): setYank raises +// yank_dirty and the main loop emits the copy where the tty writer is in scope. var yank_reg: ?[]u8 = null; +var yank_dirty: bool = false; fn freeYank(gpa: std.mem.Allocator) void { if (yank_reg) |y| gpa.free(y); yank_reg = null; @@ -91,6 +95,7 @@ fn freeYank(gpa: std.mem.Allocator) void { fn setYank(gpa: std.mem.Allocator, text: []const u8) void { freeYank(gpa); yank_reg = gpa.dupe(u8, text) catch null; + yank_dirty = true; } const Loop = vaxis.Loop(@TypeOf(Command.value)); @@ -1214,8 +1219,11 @@ const PaneLines = struct { body: ?[]u8, // terminal only: bodyText temp; arena-owned, freed by caller }; -// The lines the cursor moves over. File: all content lines (row0 = 0). Terminal: -// the viewport rows (row0 = scroll offset). `alloc` is per-frame scratch. +// The lines the cursor moves over, always with row0 = 0 (absolute rows). File: all +// content lines. Terminal: the WHOLE history+active grid, not just the viewport, so +// h/j/k/l/w/b/e/gg/G and the page motions ride the scrollback and ensureCursorVisible +// scrolls the viewport to follow — exactly like a file pane. `alloc` is per-frame +// scratch. fn paneCursorLines(alloc: std.mem.Allocator, t: *Term) !PaneLines { if (t.file) |f| { var ls: std.ArrayList([]const u8) = .empty; @@ -1223,11 +1231,29 @@ fn paneCursorLines(alloc: std.mem.Allocator, t: *Term) !PaneLines { while (it.next()) |ln| try ls.append(alloc, ln); return .{ .lines = try ls.toOwnedSlice(alloc), .row0 = 0, .body = null }; } - const body = try bodyText(alloc, t); + // Same prompt-hide + edit-splice transform as bodyText, but over every screen row + // (dump .screen, not .viewport) so the visible window still lines up with what's + // rendered while off-screen scrollback becomes navigable. paneCursorLines only runs + // in normal/insert (never tty), so prompt rows are always hidden here. + // ponytail: O(scrollback) dump+scan per keystroke — fine for normal sessions; window + // it around the viewport if a multi-MB scrollback ever makes navigation lag. + const full = try t.term.screens.active.dumpStringAlloc(alloc, .{ .screen = .{} }); + defer alloc.free(full); + var out: std.ArrayList(u8) = .empty; + errdefer out.deinit(alloc); + var it = std.mem.splitAny(u8, full, "\n"); + var pit = t.term.screens.active.pages.rowIterator(.right_down, .{ .screen = .{} }, null); + var i: usize = 0; + while (it.next()) |raw| : (i += 1) { + if (i > 0) try out.append(alloc, '\n'); + const is_prompt = if (pit.next()) |pin| pin.rowAndCell().row.semantic_prompt != .none else false; + try spliceRow(alloc, &out, t, @intCast(i), if (is_prompt) "" else raw); + } + const body = try out.toOwnedSlice(alloc); var ls: std.ArrayList([]const u8) = .empty; - var it = std.mem.splitAny(u8, body, "\n"); - while (it.next()) |ln| try ls.append(alloc, ln); - return .{ .lines = try ls.toOwnedSlice(alloc), .row0 = paneScroll(t), .body = body }; + var lit = std.mem.splitScalar(u8, body, '\n'); + while (lit.next()) |ln| try ls.append(alloc, ln); + return .{ .lines = try ls.toOwnedSlice(alloc), .row0 = 0, .body = body }; } fn freePaneLines(alloc: std.mem.Allocator, pl: PaneLines) void { @@ -1969,7 +1995,8 @@ fn handleNormal(gpa: std.mem.Allocator, talloc: std.mem.Allocator, t: *Term, key if (key.matches('d', .{})) return normalDelete(gpa, talloc, t); if (key.matches('c', .{})) return normalChange(gpa, talloc, t); if (key.matches('y', .{})) return normalYank(gpa, talloc, t); - if (key.matches('p', .{})) return normalPaste(gpa, t); + // `p` is intercepted in the main loop: it pastes the SYSTEM clipboard, which + // requires an OSC 52 round-trip (async .paste event), so it can't be done here. if (key.matches('u', .{})) return doUndo(gpa, t); if (key.matches('U', .{})) return doRedo(gpa, t); } @@ -2416,7 +2443,10 @@ pub fn main(init: std.process.Init) !void { var tty_buf: [0x10000]u8 = undefined; var tty = try vaxis.Tty.init(io, &tty_buf); - var vx = try vaxis.init(io, gpa, init.environ_map, .{}); + // system_clipboard_allocator lets the loop's reader thread decode OSC 52 paste + // replies (delivered as .paste events). gpa is already used cross-thread by the + // pty readers, so it's safe here too. + var vx = try vaxis.init(io, gpa, init.environ_map, .{ .system_clipboard_allocator = gpa }); defer vx.deinit(gpa, tty.writer()); try vx.setMouseMode(tty.writer(), true); @@ -2811,6 +2841,14 @@ pub fn main(init: std.process.Init) !void { break :kpress; } + // `p` (normal mode) pastes the SYSTEM clipboard: request it now; + // the host replies with an OSC 52 sequence the loop surfaces as a + // .paste event, which inserts the text at the cursor. + if (at.mode == .normal and key.matches('p', .{})) { + vx.requestSystemClipboard(tty.writer()) catch {}; + break :kpress; + } + // per-mode dispatch. Esc: insert -> normal (normal Esc is a // no-op, like helix). Ctrl-b toggles raw tty on terminals (above). // tty forwards every key to the pty. @@ -2858,10 +2896,31 @@ pub fn main(init: std.process.Init) !void { screen_h = ws.rows; try vx.resize(gpa, tty.writer(), ws); }, + .paste => |text| { + // OSC 52 clipboard reply to our `p` request: load it into the yank + // register and paste it at the active pane's cursor, reusing the + // normal paste path. The register came FROM the clipboard, so don't + // echo it straight back out. + defer gpa.free(text); + if (terms[active]) |t| { + setYank(gpa, text); + yank_dirty = false; + normalPaste(gpa, t); + } + }, .quit => break, else => {}, } + // a fresh yank (y/d/c) mirrors out to the system clipboard via OSC 52. Emit + // here where the tty writer + vx are in scope; the vx.render() below flushes. + // ponytail: OSC 52 has a per-terminal size cap, so a huge yank may be + // truncated by the host — the in-app register still holds the full text. + if (yank_dirty) { + yank_dirty = false; + if (yank_reg) |y| vx.copyToSystemClipboard(tty.writer(), y, gpa) catch {}; + } + // recompute geometry from the (possibly mutated) layout, then push any // grid-size change to each emulator + pty. Weights only change on a drag // *release*, so mid-drag the rects are unchanged and nothing resizes. |
