From 6d7b506c8a380e4dd97f337590ca079d810a360a Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 15 Sep 2026 13:46:50 -0300 Subject: Follow embedded PDF links through Look --- src/detached/client.zig | 3 +++ src/detached/server.zig | 3 ++- src/detached/wire.zig | 46 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 47 insertions(+), 5 deletions(-) (limited to 'src/detached') diff --git a/src/detached/client.zig b/src/detached/client.zig index 0350ecf7..affa070e 100644 --- a/src/detached/client.zig +++ b/src/detached/client.zig @@ -101,6 +101,7 @@ pub const Client = struct { /// `cols * rows` cells: what the session is showing right now. grid: std.ArrayListUnmanaged(pardes.Cell) = .empty, cursor: ?wire.Cursor = null, + pointer_shape: pardes.Surface.PointerShape = .arrow, in: std.ArrayListUnmanaged(u8) = .empty, out: std.ArrayListUnmanaged(u8) = .empty, /// Bytes of `in` belonging to the message `next` returned last. Compacted at @@ -311,6 +312,7 @@ pub const Client = struct { } try f.apply(c.grid.items); c.cursor = f.cursor; + c.pointer_shape = f.pointer_shape; }, // The session has ended. Left for the caller to act on, and the // descriptor stays open so `deinit` is the only place that closes. @@ -351,6 +353,7 @@ pub const Client = struct { c.cols = cols; c.rows = rows; c.cursor = null; + c.pointer_shape = .arrow; } /// Take everything the kernel is holding, not one chunk of it. The server diff --git a/src/detached/server.zig b/src/detached/server.zig index 9a47a562..9d83d3eb 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -717,11 +717,12 @@ pub const Session = struct { .{ .x = cur.x, .y = cur.y, .bar = cur.bar } else null; - const bytes = wire.encodeFrame( + const bytes = wire.encodeFramePointer( s.scratch.allocatedSlice()[0..want], surface.cols, surface.rows, cursor, + surface.pointer_shape, cells, prev, ) catch |err| { diff --git a/src/detached/wire.zig b/src/detached/wire.zig index 989991e0..5087cfcd 100644 --- a/src/detached/wire.zig +++ b/src/detached/wire.zig @@ -3,7 +3,7 @@ const std = @import("std"); const pardes = @import("../pardes.zig"); -pub const version: u16 = 2; +pub const version: u16 = 3; pub const Error = error{ Truncated, Overlong, BadTag, BadValue, Trailing, NoSpace }; @@ -91,6 +91,7 @@ const KindTag = enum(u8) { press = 0x00, release = 0x01, motion = 0x02, drag = 0 /// of semantics and one code path, and it is what makes a full frame of a /// mostly-empty grid cheap. pub const FrameKind = enum(u8) { full = 0x01, diff = 0x02 }; +const FrameHeader = enum(u8) { full = 0x01, diff = 0x02, full_link = 0x03, diff_link = 0x04 }; /// Bit per `CellStyle` bool, packed into one byte. Bit 7 is unassigned and a /// set bit 7 is a decode error: it is a byte this protocol cannot mean. @@ -153,6 +154,7 @@ pub const Cursor = struct { x: u16, y: u16, bar: bool }; /// union is passed by value, and the receiver already owns the grid the runs /// belong in. `apply` is the bounds-checked walk. pub const Frame = struct { + pointer_shape: pardes.Surface.PointerShape = .arrow, kind: FrameKind, cols: u16, rows: u16, @@ -586,6 +588,18 @@ pub fn encodeFrame( cursor: ?Cursor, cells: []const pardes.Cell, prev: []const pardes.Cell, +) Error![]const u8 { + return encodeFramePointer(out, cols, rows, cursor, .arrow, cells, prev); +} + +pub fn encodeFramePointer( + out: []u8, + cols: u16, + rows: u16, + cursor: ?Cursor, + pointer_shape: pardes.Surface.PointerShape, + cells: []const pardes.Cell, + prev: []const pardes.Cell, ) Error![]const u8 { // The protocol's ceiling, enforced by the ENCODER too, and BEFORE the // assert below so a caller can be told rather than tripped. `max_payload` @@ -597,7 +611,11 @@ pub fn encodeFrame( const full = prev.len != cells.len; var w: Writer = .init(out); const at = try beginMessage(&w, @intFromEnum(ServerTag.frame)); - try w.putByte(@intFromEnum(@as(FrameKind, if (full) .full else .diff))); + const header: FrameHeader = if (pointer_shape == .link) + (if (full) .full_link else .diff_link) + else + (if (full) .full else .diff); + try w.putByte(@intFromEnum(header)); try w.putU16(cols); try w.putU16(rows); try putCursor(&w, cursor, cols, rows); @@ -871,7 +889,11 @@ pub fn decodeServer(tag: u8, payload: []const u8) Error!ServerMsg { } }, .refuse => .{ .refuse = try r.getTag(Refusal) }, .frame => blk: { - const kind = try r.getTag(FrameKind); + const header = try r.getTag(FrameHeader); + const kind: FrameKind = switch (header) { + .full, .full_link => .full, + .diff, .diff_link => .diff, + }; const cols = try r.getCols(); const rows = try r.getRows(); const cursor = try getCursor(&r, cols, rows); @@ -885,6 +907,7 @@ pub fn decodeServer(tag: u8, payload: []const u8) Error!ServerMsg { r.i = r.bytes.len; break :blk .{ .frame = .{ .kind = kind, + .pointer_shape = if (header == .full_link or header == .diff_link) .link else .arrow, .cols = cols, .rows = rows, .cursor = cursor, @@ -1347,7 +1370,7 @@ test "detached wire: an unknown tag is refused, never guessed" { try testing.expectError(error.BadTag, decodeServer(@intFromEnum(ServerTag.refuse), &.{0x7f})); try testing.expectError(error.BadTag, decodeClient( @intFromEnum(ClientTag.mouse), - &.{ 0x09, 0x00, 0, 0, 0, 0, 0 }, + &.{ 0xff, 0x00, 0, 0, 0, 0, 0 }, )); } @@ -1436,6 +1459,21 @@ test "detached wire: a frame that lies about its runs cannot walk out of the gri }).apply(&small)); } +test "detached wire: link pointer survives full and unchanged grid frames" { + const cells = [_]pardes.Cell{.{}}; + var buffer: [256]u8 = undefined; + for ([_]pardes.Surface.PointerShape{ .link, .arrow, .link }) |shape| { + for ([_]bool{ true, false }) |full| { + const bytes = try encodeFramePointer(&buffer, 1, 1, null, shape, &cells, if (full) &.{} else &cells); + const packet = (try framed(bytes)).?; + const frame = (try decodeServer(packet.tag, packet.payload)).frame; + try std.testing.expectEqual(shape, frame.pointer_shape); + try std.testing.expectEqual(if (full) FrameKind.full else FrameKind.diff, frame.kind); + if (!full) try std.testing.expectEqual(@as(u32, 0), frame.nruns); + } + } +} + test "detached wire: a cursor outside the grid is refused, not painted" { // The one field of a frame a frontend indexes with rather than copies: // tty.zig moves the terminal's own cursor to `cursor.x`/`cursor.y`, and -- cgit v1.3