summaryrefslogtreecommitdiff
path: root/src/detached
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 13:46:50 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commit6d7b506c8a380e4dd97f337590ca079d810a360a (patch)
tree71075c8c4383df25c6ecd5140ef75063a897b444 /src/detached
parent13d0d107a06d7ac5fb5f8e86c747e54603f51b4e (diff)
downloadpardes-6d7b506c8a380e4dd97f337590ca079d810a360a.tar.gz
pardes-6d7b506c8a380e4dd97f337590ca079d810a360a.zip
Follow embedded PDF links through Look
Diffstat (limited to 'src/detached')
-rw-r--r--src/detached/client.zig3
-rw-r--r--src/detached/server.zig3
-rw-r--r--src/detached/wire.zig46
3 files changed, 47 insertions, 5 deletions
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,
@@ -587,6 +589,18 @@ pub fn encodeFrame(
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`
// is derived from these two, so a larger grid is a frame this decoder
@@ -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