summaryrefslogtreecommitdiff
path: root/main.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-01 09:08:52 -0300
committerGabriel Schneider <[email protected]>2026-07-01 09:09:18 -0300
commit6570dc04d6343cbbf31c0eb2363f9bec817aa4db (patch)
tree5d11e6227ca3e101deeda2ee170912037826c1f8 /main.zig
parentf6089186cfe1517073fafc8979a7546ec773d6c1 (diff)
downloadpardes-6570dc04d6343cbbf31c0eb2363f9bec817aa4db.tar.gz
pardes-6570dc04d6343cbbf31c0eb2363f9bec817aa4db.zip
added yank and tty scrolling w keyboard
Diffstat (limited to 'main.zig')
-rw-r--r--main.zig77
1 files changed, 68 insertions, 9 deletions
diff --git a/main.zig b/main.zig
index 4c82d862..3bb0cfda 100644
--- a/main.zig
+++ b/main.zig
@@ -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.