519410b1
fix: the run header is a delta against the previous run, as SGR is
a73x 2026-09-04 17:00
Commit message
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -8906,31 +8906,53 @@ four 80x24 screens fed real output shapes, comparing `dumpVtRow` against | |||
| 8906 | `encodeViewportRow` over the whole viewport: | 8906 | `encodeViewportRow` over the whole viewport: |
| 8907 | 8907 | ||
| 8908 | ``` | 8908 | ``` |
| 8909 | cells-measure prose: vt=988 cells=1009 ratio=1.02 | ||
| 8910 | cells-measure vim: vt=3132 cells=2164 ratio=0.69 | ||
| 8911 | cells-measure htop: vt=4167 cells=3130 ratio=0.75 | ||
| 8912 | cells-measure shell: vt=128 cells=88 ratio=0.69 | ||
| 8913 | ``` | ||
| 8914 | |||
| 8915 | **Gate MET, with room.** The gate asked for prose <= 1.5 and vim/htop <= 1.2. | ||
| 8916 | Prose is 1.02 and the two style-dense screens are 0.69 and 0.75 — cells are | ||
| 8917 | SMALLER than the VT they replace on every screen but prose, where they are | ||
| 8918 | level. Cells go on the wire for terminal clients too; the spec's alternative | ||
| 8919 | (VT frames for terminal clients, cells for native and browser) is not needed. | ||
| 8920 | |||
| 8921 | ### First cut, 16-byte absolute header (refused) | ||
| 8922 | |||
| 8923 | ``` | ||
| 8909 | cells-measure prose: vt=988 cells=1308 ratio=1.32 | 8924 | cells-measure prose: vt=988 cells=1308 ratio=1.32 |
| 8910 | cells-measure vim: vt=3132 cells=4533 ratio=1.45 | 8925 | cells-measure vim: vt=3132 cells=4533 ratio=1.45 |
| 8911 | cells-measure htop: vt=4167 cells=6488 ratio=1.56 | 8926 | cells-measure htop: vt=4167 cells=6488 ratio=1.56 |
| 8912 | cells-measure shell: vt=128 cells=127 ratio=0.99 | 8927 | cells-measure shell: vt=128 cells=127 ratio=0.99 |
| 8913 | ``` | 8928 | ``` |
| 8914 | 8929 | ||
| 8915 | **Gate verdict: NOT met.** The gate asked for prose <= 1.5 and vim/htop | 8930 | The first run header was absolute: 16 bytes of `flags, fg, bg, ul` on every |
| 8916 | <= 1.2. Prose passes at 1.32 and the shell screen is free at 0.99, but the | 8931 | run whether or not the run changed them. That failed the gate on both |
| 8917 | two style-dense screens are 1.45 and 1.56, well over their 1.2. The cost is | 8932 | style-dense screens. A syntax-highlighted source line or an htop row changes |
| 8918 | the run header: 16 bytes of style per run, and a syntax-highlighted source | 8933 | style eight to ten times, so such a row paid 130 to 160 bytes of header |
| 8919 | line or an htop row changes style eight to ten times, so a row pays 130-160 | 8934 | against roughly 70 bytes of text, where VT spends four to seven bytes on the |
| 8920 | bytes of header for 70 bytes of text. VT spends four to seven bytes on the | 8935 | same change — because SGR is a DELTA against the previous style and that |
| 8921 | same style change because SGR is a delta against the previous style and a | 8936 | header was not. |
| 8922 | run header is absolute. Whether that is worth paying for is the design's | 8937 | |
| 8923 | call, not the encoder's — the alternative the spec names is VT frames for | 8938 | So the header became a delta too: `u16 count` ++ `u8 mask` ++ only the fields |
| 8924 | terminal clients and cells for native and browser ones. | 8939 | the mask names. A run that turns bold on and keeps its colour spends three |
| 8925 | 8940 | bytes on style rather than twelve, and a colour spends a tag byte plus only | |
| 8926 | The first measurement of the same encoder read 2.08 / 1.67 / 1.80 / 1.17. | 8941 | the bytes that tag needs — none, a palette index, or three. The row is still |
| 8927 | The difference was one line: the encoder sent a space cell as the EMPTY text | 8942 | the unit: every row starts again from the default style, so a row remains |
| 8928 | a blank cell carries, on the reasoning that both paint the same and the empty | 8943 | self-contained and no reader needs the row before it. That one change is the |
| 8929 | form is shorter. It is shorter for one cell and much longer for the row. | 8944 | whole difference between the two tables above. |
| 8930 | Only a one-byte narrow cell qualifies for the run's ascii form, in which a | 8945 | |
| 8931 | cell is its single byte and nothing else, so one empty cell drops the whole | 8946 | ### The other measured line: a space is a space |
| 8932 | run out of that form and every remaining cell then pays a head byte as well | 8947 | |
| 8933 | as its text. Prose is a third spaces, so blanking them cost 36% of the | 8948 | An earlier cut of the encoder sent a space cell as the EMPTY text a blank |
| 8934 | screen. Sending the space as a space is also the better answer for copy: | 8949 | cell carries, on the reasoning that both paint the same and the empty form is |
| 8935 | an interior space is a space. Trailing blanks were never the question — | 8950 | shorter. It is shorter for one cell and much longer for the row. Only a |
| 8936 | the row already ends at the last cell that is not one. | 8951 | one-byte narrow cell qualifies for the run's ascii form, in which a cell is |
| 8952 | its single byte and nothing else, so one empty cell drops the whole run out | ||
| 8953 | of that form and every remaining cell then pays a head byte as well as its | ||
| 8954 | text. Prose is a third spaces, so blanking them cost 36% of the screen | ||
| 8955 | (2.08 rather than 1.32 under the absolute header). Sending the space as a | ||
| 8956 | space is also the better answer for copy: an interior space is a space. | ||
| 8957 | Trailing blanks were never the question — the row already ends at the last | ||
| 8958 | cell that is not one. | ||
src/engine/engine.zig
| Old | New | ||
|---|---|---|---|
| @@ -1847,7 +1847,7 @@ test "encodeViewportRow: an interior space keeps the row in one ascii run" { | |||
| 1847 | // form and cost a head byte on every cell of it — 36% of a prose screen. | 1847 | // form and cost a head byte on every cell of it — 36% of a prose screen. |
| 1848 | try std.testing.expectEqualSlices(u8, &[_]u8{ | 1848 | try std.testing.expectEqualSlices(u8, &[_]u8{ |
| 1849 | 3, 0, // ncells | 1849 | 3, 0, // ncells |
| 1850 | 3, 0, 0, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // run header, ascii | 1850 | 3, 0, 0x80, // run header: default style, ascii |
| 1851 | 'a', ' ', 'b', | 1851 | 'a', ' ', 'b', |
| 1852 | }, row); | 1852 | }, row); |
| 1853 | } | 1853 | } |
src/engine/protocol.zig
| Old | New | ||
|---|---|---|---|
| @@ -1298,10 +1298,29 @@ pub const CellStyle = struct { | |||
| 1298 | } | 1298 | } |
| 1299 | }; | 1299 | }; |
| 1300 | 1300 | ||
| 1301 | /// Run flag bit: every cell in the run is one byte 0x20..0x7E, narrow, and | 1301 | /// A run header is `u16 count` ++ `u8 mask` ++ the fields the mask names, in |
| 1302 | /// carries no head byte. Dense text is what a wire pays for most. | 1302 | /// bit order. The mask says what CHANGED since the previous run of the same |
| 1303 | pub const run_ascii: u16 = 1 << 15; | 1303 | /// row, the way SGR is a delta against the previous style: a run that only |
| 1304 | pub const run_header_len = 16; | 1304 | /// turns on bold spends three bytes on style, not twelve. Every row starts |
| 1305 | /// again from the default style, so a row stays self-contained and no reader | ||
| 1306 | /// needs the row before it. | ||
| 1307 | pub const mask_flags: u8 = 1 << 0; | ||
| 1308 | pub const mask_fg: u8 = 1 << 1; | ||
| 1309 | pub const mask_bg: u8 = 1 << 2; | ||
| 1310 | pub const mask_ul: u8 = 1 << 3; | ||
| 1311 | /// Every cell in the run is one byte 0x20..0x7E, narrow, and carries no head | ||
| 1312 | /// byte. Dense text is what a wire pays for most. | ||
| 1313 | pub const mask_ascii: u8 = 1 << 7; | ||
| 1314 | /// Unassigned. A writer that sets one is speaking a format this reader does | ||
| 1315 | /// not know, so a set bit is refused rather than ignored. | ||
| 1316 | const mask_reserved: u8 = 0b0111_0000; | ||
| 1317 | /// Bit 15 of the style flags. ghostty's `Style.Flags` reaches bit 10 and pads | ||
| 1318 | /// the rest, and the wire writes the flags as a whole u16 — so nothing may | ||
| 1319 | /// set it, and `CellRowWriter.cell` asserts as much. | ||
| 1320 | pub const flags_reserved: u16 = 1 << 15; | ||
| 1321 | |||
| 1322 | /// `count` and `mask`; the style fields after them are what the mask names. | ||
| 1323 | pub const run_header_min_len = 3; | ||
| 1305 | pub const cell_row_prefix_len = 2; | 1324 | pub const cell_row_prefix_len = 2; |
| 1306 | pub const cell_text_max = 63; | 1325 | pub const cell_text_max = 63; |
| 1307 | 1326 | ||
| @@ -1327,6 +1346,9 @@ pub const CellRowWriter = struct { | |||
| 1327 | /// The open run's cell texts, back to back. | 1346 | /// The open run's cell texts, back to back. |
| 1328 | text: std.ArrayListUnmanaged(u8) = .empty, | 1347 | text: std.ArrayListUnmanaged(u8) = .empty, |
| 1329 | run_style: CellStyle = .{}, | 1348 | run_style: CellStyle = .{}, |
| 1349 | /// The style the last run WRITTEN for this row carried, which the next | ||
| 1350 | /// run's mask is a delta against. A row opens at the default style. | ||
| 1351 | prev_style: CellStyle = .{}, | ||
| 1330 | 1352 | ||
| 1331 | pub fn begin(list: *std.ArrayList(u8), alloc: std.mem.Allocator) !CellRowWriter { | 1353 | pub fn begin(list: *std.ArrayList(u8), alloc: std.mem.Allocator) !CellRowWriter { |
| 1332 | const at = list.items.len; | 1354 | const at = list.items.len; |
| @@ -1336,9 +1358,7 @@ pub const CellRowWriter = struct { | |||
| 1336 | 1358 | ||
| 1337 | pub fn cell(self: *CellRowWriter, style: CellStyle, wide: Wide, text: []const u8) !void { | 1359 | pub fn cell(self: *CellRowWriter, style: CellStyle, wide: Wide, text: []const u8) !void { |
| 1338 | std.debug.assert(text.len <= cell_text_max); | 1360 | std.debug.assert(text.len <= cell_text_max); |
| 1339 | // Bit 15 is the run's own ascii marker on the wire, so a style may not | 1361 | std.debug.assert(style.flags & flags_reserved == 0); |
| 1340 | // carry it: ghostty's Style.Flags only reaches bit 10. | ||
| 1341 | std.debug.assert(style.flags & run_ascii == 0); | ||
| 1342 | if (self.run.items.len > 0 and !style.eql(self.run_style)) try self.flush(); | 1362 | if (self.run.items.len > 0 and !style.eql(self.run_style)) try self.flush(); |
| 1343 | if (self.run.items.len == 0) self.run_style = style; | 1363 | if (self.run.items.len == 0) self.run_style = style; |
| 1344 | const off: u32 = @intCast(self.text.items.len); | 1364 | const off: u32 = @intCast(self.text.items.len); |
| @@ -1351,6 +1371,27 @@ pub const CellRowWriter = struct { | |||
| 1351 | return self.text.items[c.text_off..][0..c.text_len]; | 1371 | return self.text.items[c.text_off..][0..c.text_len]; |
| 1352 | } | 1372 | } |
| 1353 | 1373 | ||
| 1374 | /// A colour on the wire is its tag byte and only the bytes that tag | ||
| 1375 | /// needs: nothing for none, an index for a palette entry, three for RGB. | ||
| 1376 | /// The packed u32 the rest of mux carries has the tag in its top byte | ||
| 1377 | /// already, which is what makes the shift below the whole conversion. | ||
| 1378 | fn writeColor(self: *CellRowWriter, col: u32) !void { | ||
| 1379 | const code: u8 = @intCast(col >> 24); | ||
| 1380 | try self.list.append(self.alloc, code); | ||
| 1381 | switch (code) { | ||
| 1382 | 0 => {}, | ||
| 1383 | 1 => try self.list.append(self.alloc, @truncate(col)), | ||
| 1384 | 2 => try self.list.appendSlice(self.alloc, &[_]u8{ | ||
| 1385 | @truncate(col >> 16), | ||
| 1386 | @truncate(col >> 8), | ||
| 1387 | @truncate(col), | ||
| 1388 | }), | ||
| 1389 | // colorPalette and colorRgb mint every colour mux holds, and | ||
| 1390 | // color_none is zero: there is no fourth tag to write. | ||
| 1391 | else => unreachable, | ||
| 1392 | } | ||
| 1393 | } | ||
| 1394 | |||
| 1354 | fn flush(self: *CellRowWriter) !void { | 1395 | fn flush(self: *CellRowWriter) !void { |
| 1355 | const cells = self.run.items; | 1396 | const cells = self.run.items; |
| 1356 | if (cells.len == 0) return; | 1397 | if (cells.len == 0) return; |
| @@ -1361,13 +1402,29 @@ pub const CellRowWriter = struct { | |||
| 1361 | break; | 1402 | break; |
| 1362 | } | 1403 | } |
| 1363 | } | 1404 | } |
| 1364 | var hdr: [run_header_len]u8 = undefined; | 1405 | const s = self.run_style; |
| 1406 | const prev = self.prev_style; | ||
| 1407 | var mask: u8 = 0; | ||
| 1408 | if (s.flags != prev.flags) mask |= mask_flags; | ||
| 1409 | if (s.fg != prev.fg) mask |= mask_fg; | ||
| 1410 | if (s.bg != prev.bg) mask |= mask_bg; | ||
| 1411 | if (s.ul != prev.ul) mask |= mask_ul; | ||
| 1412 | if (ascii) mask |= mask_ascii; | ||
| 1413 | |||
| 1414 | var hdr: [run_header_min_len]u8 = undefined; | ||
| 1365 | std.mem.writeInt(u16, hdr[0..2], @intCast(cells.len), .little); | 1415 | std.mem.writeInt(u16, hdr[0..2], @intCast(cells.len), .little); |
| 1366 | std.mem.writeInt(u16, hdr[2..4], self.run_style.flags | (if (ascii) run_ascii else 0), .little); | 1416 | hdr[2] = mask; |
| 1367 | std.mem.writeInt(u32, hdr[4..8], self.run_style.fg, .little); | ||
| 1368 | std.mem.writeInt(u32, hdr[8..12], self.run_style.bg, .little); | ||
| 1369 | std.mem.writeInt(u32, hdr[12..16], self.run_style.ul, .little); | ||
| 1370 | try self.list.appendSlice(self.alloc, &hdr); | 1417 | try self.list.appendSlice(self.alloc, &hdr); |
| 1418 | if (mask & mask_flags != 0) { | ||
| 1419 | var fb: [2]u8 = undefined; | ||
| 1420 | std.mem.writeInt(u16, &fb, s.flags, .little); | ||
| 1421 | try self.list.appendSlice(self.alloc, &fb); | ||
| 1422 | } | ||
| 1423 | if (mask & mask_fg != 0) try self.writeColor(s.fg); | ||
| 1424 | if (mask & mask_bg != 0) try self.writeColor(s.bg); | ||
| 1425 | if (mask & mask_ul != 0) try self.writeColor(s.ul); | ||
| 1426 | self.prev_style = s; | ||
| 1427 | |||
| 1371 | for (cells) |c| { | 1428 | for (cells) |c| { |
| 1372 | const text = self.cellText(c); | 1429 | const text = self.cellText(c); |
| 1373 | if (ascii) { | 1430 | if (ascii) { |
| @@ -1406,7 +1463,9 @@ pub const CellRowReader = struct { | |||
| 1406 | ncells: u16, | 1463 | ncells: u16, |
| 1407 | read: u16 = 0, | 1464 | read: u16 = 0, |
| 1408 | run_left: u16 = 0, | 1465 | run_left: u16 = 0, |
| 1409 | run_style: CellStyle = .{}, | 1466 | /// The style in force, carried across runs of this row: a header names |
| 1467 | /// only the fields that changed, so the rest stand. | ||
| 1468 | cur: CellStyle = .{}, | ||
| 1410 | run_ascii: bool = false, | 1469 | run_ascii: bool = false, |
| 1411 | 1470 | ||
| 1412 | pub fn init(bytes: []const u8) !CellRowReader { | 1471 | pub fn init(bytes: []const u8) !CellRowReader { |
| @@ -1417,23 +1476,51 @@ pub const CellRowReader = struct { | |||
| 1417 | }; | 1476 | }; |
| 1418 | } | 1477 | } |
| 1419 | 1478 | ||
| 1479 | /// The inverse of `CellRowWriter.writeColor`: a tag byte and only the | ||
| 1480 | /// bytes that tag needs. An unknown tag is refused, not skipped — its | ||
| 1481 | /// length is exactly what this reader would not know. | ||
| 1482 | fn readColor(self: *CellRowReader) !u32 { | ||
| 1483 | if (self.rest.len < 1) return error.BadPayload; | ||
| 1484 | const code = self.rest[0]; | ||
| 1485 | self.rest = self.rest[1..]; | ||
| 1486 | switch (code) { | ||
| 1487 | 0 => return color_none, | ||
| 1488 | 1 => { | ||
| 1489 | if (self.rest.len < 1) return error.BadPayload; | ||
| 1490 | const idx = self.rest[0]; | ||
| 1491 | self.rest = self.rest[1..]; | ||
| 1492 | return colorPalette(idx); | ||
| 1493 | }, | ||
| 1494 | 2 => { | ||
| 1495 | if (self.rest.len < 3) return error.BadPayload; | ||
| 1496 | const col = colorRgb(self.rest[0], self.rest[1], self.rest[2]); | ||
| 1497 | self.rest = self.rest[3..]; | ||
| 1498 | return col; | ||
| 1499 | }, | ||
| 1500 | else => return error.BadPayload, | ||
| 1501 | } | ||
| 1502 | } | ||
| 1503 | |||
| 1420 | pub fn next(self: *CellRowReader) !?DecodedCell { | 1504 | pub fn next(self: *CellRowReader) !?DecodedCell { |
| 1421 | if (self.read == self.ncells) return null; | 1505 | if (self.read == self.ncells) return null; |
| 1422 | if (self.run_left == 0) { | 1506 | if (self.run_left == 0) { |
| 1423 | if (self.rest.len < run_header_len) return error.BadPayload; | 1507 | if (self.rest.len < run_header_min_len) return error.BadPayload; |
| 1424 | const count = std.mem.readInt(u16, self.rest[0..2], .little); | 1508 | const count = std.mem.readInt(u16, self.rest[0..2], .little); |
| 1425 | const flags = std.mem.readInt(u16, self.rest[2..4], .little); | 1509 | const mask = self.rest[2]; |
| 1426 | // A run may not claim cells the row does not have. | 1510 | // A run may not claim cells the row does not have. |
| 1427 | if (count == 0 or count > self.ncells - self.read) return error.BadPayload; | 1511 | if (count == 0 or count > self.ncells - self.read) return error.BadPayload; |
| 1512 | if (mask & mask_reserved != 0) return error.BadPayload; | ||
| 1513 | self.rest = self.rest[run_header_min_len..]; | ||
| 1514 | if (mask & mask_flags != 0) { | ||
| 1515 | if (self.rest.len < 2) return error.BadPayload; | ||
| 1516 | self.cur.flags = std.mem.readInt(u16, self.rest[0..2], .little); | ||
| 1517 | self.rest = self.rest[2..]; | ||
| 1518 | } | ||
| 1519 | if (mask & mask_fg != 0) self.cur.fg = try self.readColor(); | ||
| 1520 | if (mask & mask_bg != 0) self.cur.bg = try self.readColor(); | ||
| 1521 | if (mask & mask_ul != 0) self.cur.ul = try self.readColor(); | ||
| 1428 | self.run_left = count; | 1522 | self.run_left = count; |
| 1429 | self.run_ascii = flags & run_ascii != 0; | 1523 | self.run_ascii = mask & mask_ascii != 0; |
| 1430 | self.run_style = .{ | ||
| 1431 | .flags = flags & ~run_ascii, | ||
| 1432 | .fg = std.mem.readInt(u32, self.rest[4..8], .little), | ||
| 1433 | .bg = std.mem.readInt(u32, self.rest[8..12], .little), | ||
| 1434 | .ul = std.mem.readInt(u32, self.rest[12..16], .little), | ||
| 1435 | }; | ||
| 1436 | self.rest = self.rest[run_header_len..]; | ||
| 1437 | } | 1524 | } |
| 1438 | self.run_left -= 1; | 1525 | self.run_left -= 1; |
| 1439 | self.read += 1; | 1526 | self.read += 1; |
| @@ -1441,7 +1528,7 @@ pub const CellRowReader = struct { | |||
| 1441 | if (self.rest.len < 1) return error.BadPayload; | 1528 | if (self.rest.len < 1) return error.BadPayload; |
| 1442 | const text = self.rest[0..1]; | 1529 | const text = self.rest[0..1]; |
| 1443 | self.rest = self.rest[1..]; | 1530 | self.rest = self.rest[1..]; |
| 1444 | return .{ .style = self.run_style, .wide = .narrow, .text = text }; | 1531 | return .{ .style = self.cur, .wide = .narrow, .text = text }; |
| 1445 | } | 1532 | } |
| 1446 | if (self.rest.len < 1) return error.BadPayload; | 1533 | if (self.rest.len < 1) return error.BadPayload; |
| 1447 | const head = self.rest[0]; | 1534 | const head = self.rest[0]; |
| @@ -1449,7 +1536,7 @@ pub const CellRowReader = struct { | |||
| 1449 | if (self.rest.len < 1 + len) return error.BadPayload; | 1536 | if (self.rest.len < 1 + len) return error.BadPayload; |
| 1450 | const text = self.rest[1 .. 1 + len]; | 1537 | const text = self.rest[1 .. 1 + len]; |
| 1451 | self.rest = self.rest[1 + len ..]; | 1538 | self.rest = self.rest[1 + len ..]; |
| 1452 | return .{ .style = self.run_style, .wide = @enumFromInt(head >> 6), .text = text }; | 1539 | return .{ .style = self.cur, .wide = @enumFromInt(head >> 6), .text = text }; |
| 1453 | } | 1540 | } |
| 1454 | 1541 | ||
| 1455 | /// The bytes after this row — the next row of a dense run, or nothing. | 1542 | /// The bytes after this row — the next row of a dense run, or nothing. |
| @@ -2813,10 +2900,11 @@ test "cellrow: two default ascii cells are one ascii run" { | |||
| 2813 | try w.cell(.{}, .narrow, "a"); | 2900 | try w.cell(.{}, .narrow, "a"); |
| 2814 | try w.cell(.{}, .narrow, "b"); | 2901 | try w.cell(.{}, .narrow, "b"); |
| 2815 | w.finish(); | 2902 | w.finish(); |
| 2816 | // ncells=2; run: count=2, flags=ascii, fg=bg=ul=0; then "ab". | 2903 | // ncells=2; run: count=2, mask=ascii and nothing else, since the row |
| 2904 | // opens at the default style and this run is it; then "ab". | ||
| 2817 | try std.testing.expectEqualSlices(u8, &[_]u8{ | 2905 | try std.testing.expectEqualSlices(u8, &[_]u8{ |
| 2818 | 2, 0, // ncells | 2906 | 2, 0, // ncells |
| 2819 | 2, 0, 0x00, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // run header | 2907 | 2, 0, 0x80, // run header |
| 2820 | 'a', 'b', | 2908 | 'a', 'b', |
| 2821 | }, list.items); | 2909 | }, list.items); |
| 2822 | } | 2910 | } |
| @@ -2882,12 +2970,55 @@ test "cellrow: an ascii run is only taken when every cell qualifies" { | |||
| 2882 | try w.cell(.{}, .narrow, "a"); | 2970 | try w.cell(.{}, .narrow, "a"); |
| 2883 | try w.cell(.{}, .narrow, "é"); // 2 bytes: breaks the ascii form for the whole run | 2971 | try w.cell(.{}, .narrow, "é"); // 2 bytes: breaks the ascii form for the whole run |
| 2884 | w.finish(); | 2972 | w.finish(); |
| 2885 | // ncells=2; run: count=2, flags=0; cells: head 1 'a', head 2 0xC3 0xA9 | 2973 | // ncells=2; run: count=2, mask=0 (default style, not ascii); cells: |
| 2974 | // head 1 'a', head 2 0xC3 0xA9 | ||
| 2886 | try std.testing.expectEqualSlices(u8, &[_]u8{ | 2975 | try std.testing.expectEqualSlices(u8, &[_]u8{ |
| 2887 | 2, 0, // ncells | 2976 | 2, 0, // ncells |
| 2888 | 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // run header | 2977 | 2, 0, 0, // run header |
| 2889 | 1, 'a', 2, 0xC3, 0xA9, | 2978 | 1, 'a', 2, |
| 2979 | 0xC3, 0xA9, | ||
| 2980 | }, list.items); | ||
| 2981 | } | ||
| 2982 | |||
| 2983 | test "cellrow: a run header carries only what changed since the run before it" { | ||
| 2984 | const alloc = std.testing.allocator; | ||
| 2985 | var list: std.ArrayList(u8) = .empty; | ||
| 2986 | defer list.deinit(alloc); | ||
| 2987 | const red: CellStyle = .{ .fg = colorPalette(1) }; | ||
| 2988 | const bold_red: CellStyle = .{ .fg = colorPalette(1), .flags = 1 }; | ||
| 2989 | const on_rgb: CellStyle = .{ .fg = colorPalette(1), .flags = 1, .bg = colorRgb(1, 2, 3) }; | ||
| 2990 | var w = try CellRowWriter.begin(&list, alloc); | ||
| 2991 | try w.cell(.{}, .narrow, "a"); | ||
| 2992 | try w.cell(.{}, .narrow, "b"); | ||
| 2993 | try w.cell(red, .narrow, "c"); | ||
| 2994 | try w.cell(bold_red, .narrow, "d"); | ||
| 2995 | try w.cell(on_rgb, .narrow, "e"); | ||
| 2996 | w.finish(); | ||
| 2997 | // The third run turns bold on and keeps the red: its header names the | ||
| 2998 | // flags and NOT the foreground, which is the whole point of the mask. | ||
| 2999 | // The fourth adds a background and repeats neither of them. | ||
| 3000 | try std.testing.expectEqualSlices(u8, &[_]u8{ | ||
| 3001 | 5, 0, // ncells | ||
| 3002 | 2, 0, 0x80, // run 1: default style, ascii | ||
| 3003 | 'a', 'b', | ||
| 3004 | 1, 0, 0x82, // run 2: ascii, fg changed | ||
| 3005 | 0x01, 0x01, // fg: palette, index 1 | ||
| 3006 | 'c', | ||
| 3007 | 1, 0, 0x81, // run 3: ascii, flags changed — fg is unsaid | ||
| 3008 | 0x01, 0x00, // flags: bold | ||
| 3009 | 'd', | ||
| 3010 | 1, 0, 0x84, // run 4: ascii, bg changed | ||
| 3011 | 0x02, 0x01, 0x02, 0x03, // bg: rgb 1,2,3 | ||
| 3012 | 'e', | ||
| 2890 | }, list.items); | 3013 | }, list.items); |
| 3014 | |||
| 3015 | var r = try CellRowReader.init(list.items); | ||
| 3016 | _ = try r.next(); | ||
| 3017 | _ = try r.next(); | ||
| 3018 | try std.testing.expect((try r.next()).?.style.eql(red)); | ||
| 3019 | // The reader carried the red across the header that never repeated it. | ||
| 3020 | try std.testing.expect((try r.next()).?.style.eql(bold_red)); | ||
| 3021 | try std.testing.expect((try r.next()).?.style.eql(on_rgb)); | ||
| 2891 | } | 3022 | } |
| 2892 | 3023 | ||
| 2893 | test "cellrow: the reader leaves the bytes after the row alone" { | 3024 | test "cellrow: the reader leaves the bytes after the row alone" { |
| @@ -2907,14 +3038,24 @@ test "cellrow: the reader leaves the bytes after the row alone" { | |||
| 2907 | test "cellrow: malformed rows are BadPayload, never a read past the end" { | 3038 | test "cellrow: malformed rows are BadPayload, never a read past the end" { |
| 2908 | // A run that claims more cells than the row's ncells: refused at the | 3039 | // A run that claims more cells than the row's ncells: refused at the |
| 2909 | // run header, before a cell is read. | 3040 | // run header, before a cell is read. |
| 2910 | var over = try CellRowReader.init(&[_]u8{ 1, 0, 2, 0, 0x00, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'a', 'b' }); | 3041 | var over = try CellRowReader.init(&[_]u8{ 1, 0, 2, 0, 0x80, 'a', 'b' }); |
| 2911 | try std.testing.expectError(error.BadPayload, over.next()); | 3042 | try std.testing.expectError(error.BadPayload, over.next()); |
| 2912 | // A cell whose text_len runs past the payload. | 3043 | // A cell whose text_len runs past the payload. |
| 2913 | var short_text = try CellRowReader.init(&[_]u8{ 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 'a' }); | 3044 | var short_text = try CellRowReader.init(&[_]u8{ 1, 0, 1, 0, 0x00, 5, 'a' }); |
| 2914 | try std.testing.expectError(error.BadPayload, short_text.next()); | 3045 | try std.testing.expectError(error.BadPayload, short_text.next()); |
| 2915 | // A row shorter than its own prefix. | 3046 | // A row shorter than its own prefix. |
| 2916 | try std.testing.expectError(error.BadPayload, CellRowReader.init(&[_]u8{1})); | 3047 | try std.testing.expectError(error.BadPayload, CellRowReader.init(&[_]u8{1})); |
| 2917 | // A row that ends mid-run header. | 3048 | // A row that ends mid-run header. |
| 2918 | var mid_header = try CellRowReader.init(&[_]u8{ 1, 0, 1, 0, 0 }); | 3049 | var mid_header = try CellRowReader.init(&[_]u8{ 1, 0, 1, 0 }); |
| 2919 | try std.testing.expectError(error.BadPayload, mid_header.next()); | 3050 | try std.testing.expectError(error.BadPayload, mid_header.next()); |
| 3051 | // A mask bit this format has not assigned: the fields it would name are | ||
| 3052 | // of a length this reader cannot know, so it stops rather than guesses. | ||
| 3053 | var reserved = try CellRowReader.init(&[_]u8{ 1, 0, 1, 0, 0x10, 'a' }); | ||
| 3054 | try std.testing.expectError(error.BadPayload, reserved.next()); | ||
| 3055 | // A colour tag past RGB, for the same reason. | ||
| 3056 | var bad_color = try CellRowReader.init(&[_]u8{ 1, 0, 1, 0, 0x02, 3, 0, 0, 0, 'a' }); | ||
| 3057 | try std.testing.expectError(error.BadPayload, bad_color.next()); | ||
| 3058 | // A header whose mask names a colour the payload does not carry. | ||
| 3059 | var truncated_color = try CellRowReader.init(&[_]u8{ 1, 0, 1, 0, 0x02, 1 }); | ||
| 3060 | try std.testing.expectError(error.BadPayload, truncated_color.next()); | ||
| 2920 | } | 3061 | } |