diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 14:37:29 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | d88e381793c6505bf3f2af43705bd2fa32f55ef6 (patch) | |
| tree | d4f3966c4eacf7fee43b6f18fc83f136676f835d /src/detached | |
| parent | 7284b72396398213356577345151bd79e497d27c (diff) | |
| download | pardes-d88e381793c6505bf3f2af43705bd2fa32f55ef6.tar.gz pardes-d88e381793c6505bf3f2af43705bd2fa32f55ef6.zip | |
One Layer for tags, notices, headers and bodies; a taller tag is one layer of rows; wire v8
src/Layer.zig merges TagLayer and BodyLayer. `rows` (0 = no layer) and a
cursor at {x, y}. A tag of N rows is ONE layer of N grid rows: tagHit answers
the row as `line`, bodyHit keeps its meaning, and the per-line layer bases
(TAG_LINE_LAYER_BASE, HEADER_LINE_LAYER_BASE) are gone, not aliased.
Wire v8, the one bump: tag layers carry rows and cursor y, and the frame
carries the placed region list. v7 and v9 peers are refused in both
directions (server test over both, a new frontend test over both).
web: tag_layer_value 11 = rows, 12 = cursor y; app.mjs lays every row.
macOS: the Zig side compiles against Layer; pardes.h still sees one row
per tag layer (accepted, the macOS shell is ignored for now).
No visual change: snapshot goldens and the 17 GUI goldens byte-identical.
Shared files touched: pardes.zig, Messages.zig, gui.zig, macos.zig,
detached/client.zig, detached/server.zig, detached/wire.zig (plus web.zig,
app.mjs, edit.zig, look.zig). Not touched: mouse.zig, tagline.zig,
colors.zig, tty.zig, dump.zig, exec.zig, host_io.zig, panes.zig.
Diffstat (limited to 'src/detached')
| -rw-r--r-- | src/detached/client.zig | 30 | ||||
| -rw-r--r-- | src/detached/server.zig | 3 | ||||
| -rw-r--r-- | src/detached/wire.zig | 118 |
3 files changed, 110 insertions, 41 deletions
diff --git a/src/detached/client.zig b/src/detached/client.zig index 4468c961..3347f609 100644 --- a/src/detached/client.zig +++ b/src/detached/client.zig @@ -103,8 +103,10 @@ 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(.{}), - tag_layers: [pardes.MAX_TAG_LAYERS]pardes.TagLayer = @splat(.{}), + body_layers: [pardes.MAX_PANES]pardes.Layer = @splat(.{}), + tag_layers: [pardes.MAX_TAG_LAYERS]pardes.Layer = @splat(.{}), + /// The session's placed regions, as its last frame gave them. + regions: std.ArrayListUnmanaged(pardes.Region) = .empty, in: std.ArrayListUnmanaged(u8) = .empty, out: std.ArrayListUnmanaged(u8) = .empty, /// Bytes of `in` belonging to the message `next` returned last. Compacted at @@ -182,6 +184,7 @@ pub const Client = struct { layer.* = .{}; } for (&c.tag_layers) |*layer| c.gpa.free(layer.cells); + c.regions.deinit(c.gpa); c.in.deinit(c.gpa); c.out.deinit(c.gpa); } @@ -319,7 +322,7 @@ pub const Client = struct { try c.reshape(f.cols, f.rows); } try f.apply(c.grid.items); - f.applyLayers(c.gpa, &c.body_layers, &c.tag_layers) catch |err| return switch (err) { + f.applyLayers(c.gpa, &c.body_layers, &c.tag_layers, &c.regions) catch |err| return switch (err) { error.OutOfMemory => error.Closed, else => @as(wire.Error, @errorCast(err)), }; @@ -995,6 +998,12 @@ test "detached session: a frontend that dies takes nothing with it" { } test "detached session: a frontend speaking another protocol is refused, loudly" { + // The protocol before this one (v7 carried one-row tags and no regions) + // and the one after. + for ([_]u16{ wire.version - 1, wire.version + 1 }) |claimed| try refusedHello(claimed); +} + +fn refusedHello(claimed: u16) !void { var h: Harness = undefined; try h.init(60, 16); defer h.deinit(); @@ -1006,7 +1015,7 @@ test "detached session: a frontend speaking another protocol is refused, loudly" defer _ = libc.close(fd); var buf: [64]u8 = undefined; const hello = try wire.encodeClient(&buf, .{ - .hello = .{ .version = wire.version + 1, .cols = 60, .rows = 16 }, + .hello = .{ .version = claimed, .cols = 60, .rows = 16 }, }); try testing.expectEqual(@as(isize, @intCast(hello.len)), libc.send(fd, hello.ptr, hello.len, nosignal)); @@ -1024,6 +1033,17 @@ test "detached session: a frontend speaking another protocol is refused, loudly" try testing.expect(!h.core.quit); } +test "detached frontend: a session speaking another protocol is not attached to" { + for ([_]u16{ wire.version - 1, wire.version + 1 }) |claimed| { + var c: Client = .{ .gpa = testing.allocator }; + defer c.deinit(); + var buf: [64]u8 = undefined; + try c.in.appendSlice(testing.allocator, try wire.encodeServer(&buf, .{ .welcome = .{ .version = claimed, .slot = 0, .cols = 60, .rows = 16 } })); + try testing.expectError(error.Ungreeted, c.next()); + try testing.expect(!c.attached()); + } +} + test "detached session: a peer that sends garbage is dropped, not obeyed" { var h: Harness = undefined; try h.init(60, 16); @@ -1281,7 +1301,7 @@ fn pumpUntilNotice(h: *Harness, c: *Client, want: []const u8) !bool { try c.wait(5); while (try c.next()) |_| {} for (&c.tag_layers) |*layer| { - if (layer.cols == 0 or layer.kind != .notice) continue; + if (layer.rows == 0 or layer.kind != .notice) continue; var n: usize = 0; for (layer.cells[0..layer.cols]) |cell| { const g = cell.grapheme(); diff --git a/src/detached/server.zig b/src/detached/server.zig index 212c9a3d..46fb8fcf 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -739,7 +739,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) + wire.layersBound(&surface.body_layers, &surface.tag_layers); + const want = wire.frameBound(surface.cols, surface.rows) + wire.layersBound(&surface.body_layers, &surface.tag_layers, surface.regionList()); 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) &.{} @@ -759,6 +759,7 @@ pub const Session = struct { prev, &surface.body_layers, &surface.tag_layers, + surface.regionList(), ) catch |err| { log.debug("frame {d}x{d} not encodable: {t}", .{ surface.cols, surface.rows, err }); return; diff --git a/src/detached/wire.zig b/src/detached/wire.zig index 94bd7f0b..684c869b 100644 --- a/src/detached/wire.zig +++ b/src/detached/wire.zig @@ -7,7 +7,9 @@ const limits = @import("../memory.zig").limits; /// 7: tag layers gained the `.notice` kind, so a frame can carry a tag value /// an older frontend would reject mid-session. The greeting is where that has /// to be refused, which is what this number is for. -pub const version: u16 = 7; +/// 8: one layer type: a tag band carries all its rows (not a layer per +/// line), and a frame carries the core's placed regions after its layers. +pub const version: u16 = 8; pub const Error = error{ Truncated, Overlong, BadTag, BadValue, Trailing, NoSpace }; @@ -167,7 +169,8 @@ pub const Frame = struct { nruns: u32, runs: []const u8, - pub fn applyLayers(f: Frame, gpa: std.mem.Allocator, layers: *[pardes.MAX_PANES]pardes.BodyLayer, tags: *[pardes.MAX_TAG_LAYERS]pardes.TagLayer) !void { + pub fn applyLayers(f: Frame, gpa: std.mem.Allocator, layers: *[pardes.MAX_PANES]pardes.Layer, tags: *[pardes.MAX_TAG_LAYERS]pardes.Layer, regions: *std.ArrayListUnmanaged(pardes.Region)) !void { + regions.clearRetainingCapacity(); for (layers) |*layer| { gpa.free(layer.cells); layer.* = .{}; @@ -191,13 +194,14 @@ pub const Frame = struct { const count = try r.getU16(); if (count > layers.len) return error.Overlong; for (layers[0..count]) |*layer| { - layer.pane = try r.getU16(); + layer.kind = .body; + layer.id = 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 + if (layer.id >= 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.context_rows > limits.wrap_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; @@ -217,19 +221,40 @@ pub const Frame = struct { 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(); for (&layer.bg) |*channel| channel.* = try r.getByte(); - if (layer.cols == 0 or layer.viewport.w == 0 or layer.viewport.h != 1 or + if (layer.cols == 0 or layer.viewport.w == 0 or layer.rows == 0 or layer.rows > pardes.MAX_TAG_ROWS or + layer.viewport.h != layer.rows or layer.viewport.x > f.cols or layer.viewport.w > f.cols - layer.viewport.x or - layer.viewport.y >= f.rows or (switch (layer.kind) { + layer.viewport.y >= f.rows or layer.viewport.h > f.rows - layer.viewport.y or (switch (layer.kind) { .workspace => layer.id != 0, .column => layer.id >= pardes.MAX_COLS, .pane, .notice => layer.id >= pardes.MAX_PANES, + .body => true, })) return error.BadValue; - const cursor = try getCursor(&r, layer.cols, 1); - layer.cursor = if (cursor) |c| .{ .col = c.x, .bar = c.bar } else null; - layer.cells = try gpa.alloc(pardes.Cell, layer.cols); + 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); } + const region_count = try r.getU16(); + if (region_count > pardes.MAX_REGIONS) return error.Overlong; + try regions.ensureTotalCapacity(gpa, region_count); + for (0..region_count) |_| { + var region: pardes.Region = .{ .kind = try r.getTag(pardes.Region.Kind), .rect = undefined }; + region.owner = try r.getByte(); + region.serial = try r.getU32(); + region.rect = .{ .x = try r.getU16(), .y = try r.getU16(), .w = try r.getU16(), .h = try r.getU16() }; + region.active = try r.getBool(); + region.thumb_y = try r.getU16(); + region.thumb_h = try r.getU16(); + region.notice = try r.getByte(); + region.clip = try r.getU16(); + if (region.rect.x > f.cols or region.rect.w > f.cols - region.rect.x or + region.rect.y > f.rows or region.rect.h > f.rows - region.rect.y or + region.clip > region.rect.w or region.thumb_y > region.rect.h or region.thumb_h > region.rect.h - region.thumb_y) return error.BadValue; + regions.appendAssumeCapacity(region); + } try r.end(); } @@ -672,21 +697,23 @@ pub fn encodeFramePointer( cells: []const pardes.Cell, prev: []const pardes.Cell, ) Error![]const u8 { - return encodeFrameLayers(out, cols, rows, cursor, pointer_shape, cells, prev, &.{}, &.{}); + return encodeFrameLayers(out, cols, rows, cursor, pointer_shape, cells, prev, &.{}, &.{}, &.{}); } -pub fn layersBound(layers: []const pardes.BodyLayer, tags: []const pardes.TagLayer) usize { - var n: usize = 4; +pub fn layersBound(layers: []const pardes.Layer, tags: []const pardes.Layer, regions: []const pardes.Region) usize { + var n: usize = 6; for (layers) |layer| if (layer.rows != 0) { n += 26 + layer.context_rows + layer.cells.len * cell_max; }; - for (tags) |layer| if (layer.cols != 0) { - n += 26 + layer.cells.len * cell_max; + for (tags) |layer| if (layer.rows != 0) { + n += 28 + layer.cells.len * cell_max; }; - return n; + return n + regions.len * region_len; } -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, tags: []const pardes.TagLayer) Error![]const u8 { +const region_len = 1 + 1 + 4 + 8 + 1 + 2 + 2 + 1 + 2; + +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.Layer, tags: []const pardes.Layer, regions: []const pardes.Region) 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 @@ -708,7 +735,7 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point const layers_len_at = w.n; try w.putU32(0); const layers_start = w.n; - if (layers.len != 0 or tags.len != 0) { + if (layers.len != 0 or tags.len != 0 or regions.len != 0) { var count: u16 = 0; for (layers) |layer| if (layer.rows != 0) { count += 1; @@ -717,7 +744,7 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point for (layers) |layer| { if (layer.rows == 0) continue; if (layer.context_rows > limits.wrap_rows or layer.context_rows > layer.rows) return error.BadValue; - try w.putU16(layer.pane); + try w.putU16(layer.id); try w.putU32(layer.serial); try w.putU16(layer.viewport.x); try w.putU16(layer.viewport.y); @@ -731,13 +758,13 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point for (layer.cells) |*cell| try putCell(&w, cell); } var tag_count: u16 = 0; - for (tags) |layer| if (layer.cols != 0) { + for (tags) |layer| if (layer.rows != 0) { tag_count += 1; }; try w.putU16(tag_count); for (tags) |layer| { - if (layer.cols == 0) continue; - if (layer.cells.len != layer.cols) return error.BadValue; + if (layer.rows == 0) continue; + if (layer.cells.len != @as(usize, layer.cols) * layer.rows) return error.BadValue; try w.putByte(@intFromEnum(layer.kind)); try w.putU16(layer.id); try w.putU32(layer.serial); @@ -746,10 +773,26 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point try w.putU16(layer.viewport.w); try w.putU16(layer.viewport.h); try w.putU16(layer.cols); + try w.putU16(layer.rows); for (layer.bg) |channel| try w.putByte(channel); - try putCursor(&w, if (layer.cursor) |c| .{ .x = c.col, .y = 0, .bar = c.bar } else null, layer.cols, 1); + 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); } + try w.putU16(@intCast(regions.len)); + for (regions) |region| { + try w.putByte(@intFromEnum(region.kind)); + try w.putByte(region.owner); + try w.putU32(region.serial); + try w.putU16(region.rect.x); + try w.putU16(region.rect.y); + try w.putU16(region.rect.w); + try w.putU16(region.rect.h); + try w.putBool(region.active); + try w.putU16(region.thumb_y); + try w.putU16(region.thumb_h); + try w.putByte(region.notice); + try w.putU16(region.clip); + } } std.mem.writeInt(u32, w.buf[layers_len_at..][0..4], @intCast(w.n - layers_start), .little); const nruns_at = w.n; @@ -1820,22 +1863,26 @@ test "detached wire carries compact body rows and exact pointer metrics" { 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 }; - var 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 } }; + var layer: pardes.Layer = .{ .id = 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 } }; layer.context_separators.set(0); var tag_cells: [3]pardes.Cell = @splat(.{}); tag_cells[2] = logical[10]; - const tag: pardes.TagLayer = .{ .kind = .column, .id = 1, .viewport = .{ .x = 0, .y = 0, .w = 2, .h = 1 }, .cols = 3, .bg = .{ 11, 22, 33 }, .cells = &tag_cells, .cursor = .{ .col = 2, .bar = true } }; - const out = try gpa.alloc(u8, frameBound(2, 4) + layersBound(&.{layer}, &.{tag})); + const tag: pardes.Layer = .{ .kind = .column, .id = 1, .viewport = .{ .x = 0, .y = 0, .w = 2, .h = 1 }, .cols = 3, .rows = 1, .bg = .{ 11, 22, 33 }, .cells = &tag_cells, .cursor = .{ .x = 2, .y = 0, .bar = true } }; + const placed = [_]pardes.Region{.{ .kind = .rail, .owner = 3, .serial = 42, .rect = .{ .x = 0, .y = 1, .w = 2, .h = 3 }, .active = true, .thumb_y = 1, .thumb_h = 2, .clip = 1 }}; + const out = try gpa.alloc(u8, frameBound(2, 4) + layersBound(&.{layer}, &.{tag}, &placed)); defer gpa.free(out); - const encoded = try encodeFrameLayers(out, 2, 4, null, .link, &cells, &cells, &.{layer}, &.{tag}); + const encoded = try encodeFrameLayers(out, 2, 4, null, .link, &cells, &cells, &.{layer}, &.{tag}, &placed); const packet = (try framed(encoded)).?; const frame = (try decodeServer(packet.tag, packet.payload)).frame; - var decoded: [pardes.MAX_PANES]pardes.BodyLayer = @splat(.{}); - var tags: [pardes.MAX_TAG_LAYERS]pardes.TagLayer = @splat(.{}); + var decoded: [pardes.MAX_PANES]pardes.Layer = @splat(.{}); + var tags: [pardes.MAX_TAG_LAYERS]pardes.Layer = @splat(.{}); defer for (&tags) |*item| gpa.free(item.cells); defer for (&decoded) |*item| gpa.free(item.cells); try frame.apply(&cells); - try frame.applyLayers(gpa, &decoded, &tags); + var regions: std.ArrayListUnmanaged(pardes.Region) = .empty; + defer regions.deinit(gpa); + try frame.applyLayers(gpa, &decoded, &tags, ®ions); + try std.testing.expectEqualDeep(placed[0..], regions.items); try std.testing.expectEqual(@as(u16, 6), decoded[0].rows); try std.testing.expect(decoded[0].context_separators.isSet(0)); try std.testing.expect(!decoded[0].context_separators.isSet(1)); @@ -1847,10 +1894,10 @@ test "detached wire carries compact body rows and exact pointer metrics" { try std.testing.expectEqual(tag.id, tags[0].id); try std.testing.expectEqual(tag.bg, tags[0].bg); try std.testing.expectEqual(tag.cols, tags[0].cols); - try std.testing.expectEqual(@as(u16, 2), tags[0].cursor.?.col); + try std.testing.expectEqual(@as(u16, 2), tags[0].cursor.?.x); try std.testing.expectEqualStrings("z", tags[0].cells[2].grapheme()); - const pointer = layer.hitAt(4, 27, 8, 16, 6, 8).?; - const tag_pointer = tag.hitAt(11, 8, 8, 16, 4, 8).?; + const pointer = layer.bodyHit(4, 27, 8, 16, 6, 8).?; + const tag_pointer = tag.tagHit(11, 8, 8, 16, 4, 8).?; const message: ClientMsg = .{ .event = .{ .mouse = .{ .col = 0, .row = 1, .button = .left, .kind = .press, .body_hit = pointer, .tag_hit = tag_pointer } } }; var mouse_bytes: [256]u8 = undefined; const mouse_encoded = try encodeClient(&mouse_bytes, message); @@ -1861,19 +1908,20 @@ test "detached wire carries compact body rows and exact pointer metrics" { 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, &tags)); + try std.testing.expectError(error.Truncated, truncated.applyLayers(gpa, &decoded, &tags, ®ions)); for (decoded) |item| { try std.testing.expectEqual(@as(u16, 0), item.rows); try std.testing.expectEqual(@as(usize, 0), item.cells.len); try std.testing.expectEqual(@as(usize, 0), item.context_separators.count()); } for (tags) |item| { - try std.testing.expectEqual(@as(u16, 0), item.cols); + try std.testing.expectEqual(@as(u16, 0), item.rows); try std.testing.expectEqual(@as(usize, 0), item.cells.len); } + try std.testing.expectEqual(@as(usize, 0), regions.items.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, &tags); + try cleared.applyLayers(gpa, &decoded, &tags, ®ions); try std.testing.expectEqual(@as(u16, 0), decoded[0].rows); } |
