diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 14:46:38 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1 (patch) | |
| tree | 642150be8703f1d9a0766ff18516f06bfdc8801a /src/detached | |
| parent | 379f4fe55c7c9c7c8cc7b97dc921c83e2c24698b (diff) | |
| download | pardes-40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1.tar.gz pardes-40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1.zip | |
Add optional compact tagline styling for source context
Diffstat (limited to 'src/detached')
| -rw-r--r-- | src/detached/client.zig | 9 | ||||
| -rw-r--r-- | src/detached/server.zig | 6 | ||||
| -rw-r--r-- | src/detached/wire.zig | 164 |
3 files changed, 172 insertions, 7 deletions
diff --git a/src/detached/client.zig b/src/detached/client.zig index affa070e..42404bca 100644 --- a/src/detached/client.zig +++ b/src/detached/client.zig @@ -102,6 +102,7 @@ pub const Client = struct { grid: std.ArrayListUnmanaged(pardes.Cell) = .empty, cursor: ?wire.Cursor = null, pointer_shape: pardes.Surface.PointerShape = .arrow, + body_layers: [pardes.MAX_PANES]pardes.BodyLayer = @splat(.{}), in: std.ArrayListUnmanaged(u8) = .empty, out: std.ArrayListUnmanaged(u8) = .empty, /// Bytes of `in` belonging to the message `next` returned last. Compacted at @@ -174,6 +175,10 @@ pub const Client = struct { if (c.fd >= 0) _ = libc.close(c.fd); c.fd = -1; c.grid.deinit(c.gpa); + for (&c.body_layers) |*layer| { + c.gpa.free(layer.cells); + layer.* = .{}; + } c.in.deinit(c.gpa); c.out.deinit(c.gpa); } @@ -311,6 +316,10 @@ pub const Client = struct { try c.reshape(f.cols, f.rows); } try f.apply(c.grid.items); + f.applyLayers(c.gpa, &c.body_layers) catch |err| return switch (err) { + error.OutOfMemory => error.Closed, + else => @as(wire.Error, @errorCast(err)), + }; c.cursor = f.cursor; c.pointer_shape = f.pointer_shape; }, diff --git a/src/detached/server.zig b/src/detached/server.zig index 9d83d3eb..82abc432 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -707,7 +707,7 @@ pub const Session = struct { fn sendFrame(s: *Session, c: *Client, surface: *const pardes.Surface) void { const cells = surface.cells; - const want = wire.frameBound(surface.cols, surface.rows); + const want = wire.frameBound(surface.cols, surface.rows) + wire.layersBound(&surface.body_layers); s.scratch.ensureTotalCapacity(s.gpa, want) catch return s.close(c, .oom); const prev: []const pardes.Cell = if (c.need_full or c.mirror.items.len != cells.len) &.{} @@ -717,7 +717,7 @@ pub const Session = struct { .{ .x = cur.x, .y = cur.y, .bar = cur.bar } else null; - const bytes = wire.encodeFramePointer( + const bytes = wire.encodeFrameLayers( s.scratch.allocatedSlice()[0..want], surface.cols, surface.rows, @@ -725,6 +725,7 @@ pub const Session = struct { surface.pointer_shape, cells, prev, + &surface.body_layers, ) catch |err| { log.debug("frame {d}x{d} not encodable: {t}", .{ surface.cols, surface.rows, err }); return; @@ -987,6 +988,7 @@ pub const Session = struct { .resize => |r| { c.cols = r.cols; c.rows = r.rows; + if (r.row_metrics) |metrics| s.core.row_metrics = metrics; }, else => { s.origin = slot; diff --git a/src/detached/wire.zig b/src/detached/wire.zig index 5087cfcd..5d52e2df 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 = 3; +pub const version: u16 = 4; pub const Error = error{ Truncated, Overlong, BadTag, BadValue, Trailing, NoSpace }; @@ -15,7 +15,7 @@ pub const header_len = 5; // tag:u8, payload length:u32le const cell_max = 1 + 1 + 7 + 4 + 4 + 1 + 1 + 1; const run_header = 4 + 2; // start:u32, count:u16 const run_max = std.math.maxInt(u16); -const frame_head = 1 + 2 + 2 + 6 + 4; +const frame_head = 1 + 2 + 2 + 6 + 4 + 4; // Worst case: every cell changed, each in its own run. pub fn frameBound(cols: u16, rows: u16) usize { @@ -154,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 { + layers: []const u8 = &.{}, pointer_shape: pardes.Surface.PointerShape = .arrow, kind: FrameKind, cols: u16, @@ -162,6 +163,38 @@ pub const Frame = struct { nruns: u32, runs: []const u8, + pub fn applyLayers(f: Frame, gpa: std.mem.Allocator, layers: *[pardes.MAX_PANES]pardes.BodyLayer) !void { + for (layers) |*layer| { + gpa.free(layer.cells); + layer.* = .{}; + } + errdefer for (layers) |*layer| { + gpa.free(layer.cells); + layer.* = .{}; + }; + if (f.layers.len == 0) return; + var r: Reader = .init(f.layers); + const count = try r.getU16(); + if (count > layers.len) return error.Overlong; + for (layers[0..count]) |*layer| { + layer.pane = try r.getU16(); + layer.serial = try r.getU32(); + layer.viewport = .{ .x = try r.getU16(), .y = try r.getU16(), .w = try r.getU16(), .h = try r.getU16() }; + layer.cols = try r.getU16(); + layer.rows = try r.getU16(); + layer.context_rows = try r.getU16(); + if (layer.pane >= pardes.MAX_PANES or layer.cols == 0 or layer.cols > f.cols or + layer.rows == 0 or layer.rows > 2 * max_rows or layer.context_rows > layer.rows or + layer.viewport.x > f.cols or layer.viewport.w > f.cols - layer.viewport.x or + layer.viewport.y > f.rows or layer.viewport.h > f.rows - layer.viewport.y) return error.BadValue; + const cursor = try getCursor(&r, layer.cols, layer.rows); + layer.cursor = if (cursor) |c| .{ .x = c.x, .y = c.y, .bar = c.bar } else null; + layer.cells = try gpa.alloc(pardes.Cell, @as(usize, layer.cols) * layer.rows); + for (layer.cells) |*cell| cell.* = try decodeCell(&r); + } + try r.end(); + } + /// Paint this frame into `grid`, which must be exactly `cols * rows` cells /// — the receiver resizes on a geometry change before applying, and a grid /// of the wrong size is a receiver bug, not a wire condition. @@ -601,6 +634,18 @@ pub fn encodeFramePointer( cells: []const pardes.Cell, prev: []const pardes.Cell, ) Error![]const u8 { + return encodeFrameLayers(out, cols, rows, cursor, pointer_shape, cells, prev, &.{}); +} + +pub fn layersBound(layers: []const pardes.BodyLayer) usize { + var n: usize = 2; + for (layers) |layer| if (layer.rows != 0) { + n += 26 + layer.cells.len * cell_max; + }; + return n; +} + +pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, pointer_shape: pardes.Surface.PointerShape, cells: []const pardes.Cell, prev: []const pardes.Cell, layers: []const pardes.BodyLayer) 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 @@ -619,6 +664,31 @@ pub fn encodeFramePointer( try w.putU16(cols); try w.putU16(rows); try putCursor(&w, cursor, cols, rows); + const layers_len_at = w.n; + try w.putU32(0); + const layers_start = w.n; + if (layers.len != 0) { + var count: u16 = 0; + for (layers) |layer| if (layer.rows != 0) { + count += 1; + }; + try w.putU16(count); + for (layers) |layer| { + if (layer.rows == 0) continue; + try w.putU16(layer.pane); + try w.putU32(layer.serial); + try w.putU16(layer.viewport.x); + try w.putU16(layer.viewport.y); + try w.putU16(layer.viewport.w); + try w.putU16(layer.viewport.h); + try w.putU16(layer.cols); + try w.putU16(layer.rows); + try w.putU16(layer.context_rows); + try putCursor(&w, if (layer.cursor) |c| .{ .x = c.x, .y = c.y, .bar = c.bar } else null, layer.cols, layer.rows); + for (layer.cells) |*cell| try putCell(&w, cell); + } + } + std.mem.writeInt(u32, w.buf[layers_len_at..][0..4], @intCast(w.n - layers_start), .little); const nruns_at = w.n; try w.putU32(0); @@ -698,6 +768,42 @@ fn clientTag(msg: ClientMsg) ClientTag { /// WIRE is not. const has_cell_pixels = @hasField(pardes.CellPixels, "w"); +fn putMetrics(w: *Writer, metrics: ?pardes.RowMetrics) Error!void { + try w.putBool(metrics != null); + if (metrics) |m| { + try w.putU16(m.body_w); + try w.putU16(m.body_h); + try w.putU16(m.tagline_w); + try w.putU16(m.tagline_h); + } +} + +fn getMetrics(r: *Reader) Error!?pardes.RowMetrics { + if (!try r.getBool()) return null; + const m: pardes.RowMetrics = .{ .body_w = try r.getU16(), .body_h = try r.getU16(), .tagline_w = try r.getU16(), .tagline_h = try r.getU16() }; + if (m.body_w == 0 or m.body_h == 0 or m.tagline_w == 0 or m.tagline_h == 0) return error.BadValue; + return m; +} + +fn putBodyHit(w: *Writer, hit: ?pardes.Mouse.BodyHit) Error!void { + try w.putBool(hit != null); + if (hit) |h| { + try w.putU32(h.serial); + try w.putU16(h.row); + try w.putU16(h.col); + try w.putF32(h.pixel_x); + try w.putF32(h.pixel_y); + try putMetrics(w, h.metrics); + } +} + +fn getBodyHit(r: *Reader) Error!?pardes.Mouse.BodyHit { + if (!try r.getBool()) return null; + const hit: pardes.Mouse.BodyHit = .{ .serial = try r.getU32(), .row = try r.getU16(), .col = try r.getU16(), .pixel_x = try r.getF32(), .pixel_y = try r.getF32(), .metrics = (try getMetrics(r)) orelse return error.BadValue }; + if (!std.math.isFinite(hit.pixel_x) or !std.math.isFinite(hit.pixel_y)) return error.BadValue; + return hit; +} + pub fn encodeClient(out: []u8, msg: ClientMsg) Error![]const u8 { var w: Writer = .init(out); const at = try beginMessage(&w, @intFromEnum(clientTag(msg))); @@ -726,6 +832,7 @@ pub fn encodeClient(out: []u8, msg: ClientMsg) Error![]const u8 { try w.putU16(m.col); try w.putU16(m.row); try w.putBool(m.ctrl); + try putBodyHit(&w, m.body_hit); }, .resize => |rs| { try w.putU16(rs.cols); @@ -735,6 +842,7 @@ pub fn encodeClient(out: []u8, msg: ClientMsg) Error![]const u8 { // carries where it exists. try w.putU16(if (comptime has_cell_pixels) rs.cell_pixels.w else 8); try w.putU16(if (comptime has_cell_pixels) rs.cell_pixels.h else 16); + try putMetrics(&w, rs.row_metrics); }, .paste => |b| try w.putSlice32(b), .command => |line| try w.putSlice16(line), @@ -786,6 +894,7 @@ pub fn decodeClient(tag: u8, payload: []const u8) Error!ClientMsg { .col = try r.getU16(), .row = try r.getU16(), .ctrl = try r.getBool(), + .body_hit = try getBodyHit(&r), } } }, .resize => blk: { var ev: pardes.Event = .{ .resize = .{ .cols = try r.getCols(), .rows = try r.getRows() } }; @@ -794,6 +903,7 @@ pub fn decodeClient(tag: u8, payload: []const u8) Error!ClientMsg { // Read either way — the bytes are on the wire — and kept only by a // build that has somewhere to keep them. if (comptime has_cell_pixels) ev.resize.cell_pixels = .{ .w = px_w, .h = px_h }; + ev.resize.row_metrics = try getMetrics(&r); break :blk .{ .event = ev }; }, .paste => .{ .event = .{ .paste = try r.getSlice32() } }, @@ -849,7 +959,7 @@ pub fn encodeServer(out: []u8, msg: ServerMsg) Error![]const u8 { /// field-by-field count: a bound that is 32 bytes generous costs one `memcpy` /// worth of nothing, and a bound that is one byte tight is a bug that only /// shows up on the message nobody tested. -const msg_slack = header_len + 32; +const msg_slack = header_len + 64; /// An upper bound on `encodeServer`'s output, so a caller sizes its buffer /// once instead of guessing and retrying. @@ -897,6 +1007,7 @@ pub fn decodeServer(tag: u8, payload: []const u8) Error!ServerMsg { const cols = try r.getCols(); const rows = try r.getRows(); const cursor = try getCursor(&r, cols, rows); + const layers = try r.getSlice32(); const nruns = try r.getU32(); // One run per cell is the worst an encoder emits, so a bigger // count cannot be describing this grid. Checked HERE rather than @@ -906,6 +1017,7 @@ pub fn decodeServer(tag: u8, payload: []const u8) Error!ServerMsg { const runs = r.bytes[r.i..]; r.i = r.bytes.len; break :blk .{ .frame = .{ + .layers = layers, .kind = kind, .pointer_shape = if (header == .full_link or header == .diff_link) .link else .arrow, .cols = cols, @@ -1287,7 +1399,7 @@ test "detached wire: the diff is worth having, in bytes, on the board's grid" { for (cells) |*c| c.* = .{ .text = "a".* ++ @as([6]u8, @splat(0)), .len = 1, .default = false }; const full = (try encodeFrame(buf, cols, rows, null, cells, &.{})).len; try testing.expectEqual(@as(usize, header_len + frame_head + run_header + n * 8), full); - try testing.expectEqual(@as(usize, 6298), full); + try testing.expectEqual(@as(usize, 6302), full); const before = try gpa.dupe(pardes.Cell, cells); defer gpa.free(before); @@ -1298,7 +1410,7 @@ test "detached wire: the diff is worth having, in bytes, on the board's grid" { cells[500] = .{ .text = "d".* ++ @as([6]u8, @splat(0)), .len = 1, .default = false }; const diff = (try encodeFrame(buf, cols, rows, null, cells, before)).len; try testing.expectEqual(@as(usize, header_len + frame_head + 2 * run_header + 3 * 8), diff); - try testing.expectEqual(@as(usize, 56), diff); + try testing.expectEqual(@as(usize, 60), diff); // The whole reason this codec exists rather than shipping the Surface. try testing.expect(full / diff > 100); } @@ -1443,6 +1555,7 @@ test "detached wire: a frame that lies about its runs cannot walk out of the gri try w.putU16(cols); try w.putU16(rows); try putCursor(&w, null, cols, rows); + try w.putU32(0); // no body layers try w.putU32(9); try testing.expectError(error.Overlong, decodeServer(@intFromEnum(ServerTag.frame), w.written())); @@ -1508,6 +1621,7 @@ test "detached wire: a cursor outside the grid is refused, not painted" { try w.putU16(cols); try w.putU16(rows); try putCursor(&w, .{ .x = cols - 1, .y = rows - 1, .bar = true }, cols, rows); + try w.putU32(0); // no body layers try w.putU32(0); const got = (try decodeServer(@intFromEnum(ServerTag.frame), w.written())).frame; try testing.expectEqual(@as(u16, cols - 1), got.cursor.?.x); @@ -1607,3 +1721,43 @@ test "detached wire: max_payload bounds every message this protocol can build" { var small: [8]u8 = undefined; try testing.expectError(error.NoSpace, encodeClient(&small, .{ .event = .{ .paste = "0123456789" } })); } + +test "detached wire carries compact body rows and exact pointer metrics" { + const gpa = std.testing.allocator; + var cells: [8]pardes.Cell = @splat(.{}); + var logical: [12]pardes.Cell = @splat(.{}); + logical[10] = .{ .text = "z".* ++ @as([6]u8, @splat(0)), .len = 1, .default = false }; + const layer: pardes.BodyLayer = .{ .pane = 0, .serial = 42, .viewport = .{ .x = 0, .y = 0, .w = 2, .h = 4 }, .cols = 2, .rows = 6, .context_rows = 2, .cells = &logical, .cursor = .{ .x = 0, .y = 5, .bar = true } }; + const out = try gpa.alloc(u8, frameBound(2, 4) + layersBound(&.{layer})); + defer gpa.free(out); + const encoded = try encodeFrameLayers(out, 2, 4, null, .link, &cells, &cells, &.{layer}); + const packet = (try framed(encoded)).?; + const frame = (try decodeServer(packet.tag, packet.payload)).frame; + var decoded: [pardes.MAX_PANES]pardes.BodyLayer = @splat(.{}); + defer for (&decoded) |*item| gpa.free(item.cells); + try frame.apply(&cells); + try frame.applyLayers(gpa, &decoded); + try std.testing.expectEqual(@as(u16, 6), decoded[0].rows); + try std.testing.expectEqualStrings("z", decoded[0].cells[10].grapheme()); + try std.testing.expectEqual(@as(u16, 5), decoded[0].cursor.?.y); + const pointer = layer.hitAt(4, 27, 8, 16, 6, 8).?; + const message: ClientMsg = .{ .event = .{ .mouse = .{ .col = 0, .row = 1, .button = .left, .kind = .press, .body_hit = pointer } } }; + var mouse_bytes: [256]u8 = undefined; + const mouse_encoded = try encodeClient(&mouse_bytes, message); + const mouse_packet = (try framed(mouse_encoded)).?; + const mouse = (try decodeClient(mouse_packet.tag, mouse_packet.payload)).event.mouse; + try std.testing.expectEqualDeep(pointer, mouse.body_hit.?); + try std.testing.expect(mouse_encoded.len <= clientBound(message)); + var truncated = frame; + truncated.layers = frame.layers[0 .. frame.layers.len - 1]; + try std.testing.expectError(error.Truncated, truncated.applyLayers(gpa, &decoded)); + for (decoded) |item| { + try std.testing.expectEqual(@as(u16, 0), item.rows); + try std.testing.expectEqual(@as(usize, 0), item.cells.len); + } + const no_layers = try encodeFramePointer(out, 2, 4, null, .arrow, &cells, &cells); + const cleared_packet = (try framed(no_layers)).?; + const cleared = (try decodeServer(cleared_packet.tag, cleared_packet.payload)).frame; + try cleared.applyLayers(gpa, &decoded); + try std.testing.expectEqual(@as(u16, 0), decoded[0].rows); +} |
