Merge remote-tracking branch 'upstream/main' into ligature-detect

This commit is contained in:
Jacob Sandlund
2026-01-13 09:48:53 -05:00
13 changed files with 331 additions and 99 deletions

View File

@@ -64,7 +64,7 @@ jobs:
mkdir blob
mv appcast.xml blob/appcast.xml
- name: Upload Appcast to R2
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with:
r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}

23
flake.lock generated
View File

@@ -41,27 +41,26 @@
]
},
"locked": {
"lastModified": 1755776884,
"narHash": "sha256-CPM7zm6csUx7vSfKvzMDIjepEJv1u/usmaT7zydzbuI=",
"lastModified": 1768068402,
"narHash": "sha256-bAXnnJZKJiF7Xr6eNW6+PhBf1lg2P1aFUO9+xgWkXfA=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "4fb695d10890e9fc6a19deadf85ff79ffb78da86",
"rev": "8bc5473b6bc2b6e1529a9c4040411e1199c43b4c",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-25.05",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1763191728,
"narHash": "sha256-gI9PpaoX4/f28HkjcTbFVpFhtOxSDtOEdFaHZrdETe0=",
"rev": "1d4c88323ac36805d09657d13a5273aea1b34f0c",
"lastModified": 1768032153,
"narHash": "sha256-zvxtwlM8ZlulmZKyYCQAPpkm5dngSEnnHjmjV7Teloc=",
"rev": "3146c6aa9995e7351a398e17470e15305e6e18ff",
"type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre896415.1d4c88323ac3/nixexprs.tar.xz"
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre925418.3146c6aa9995/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
@@ -126,17 +125,17 @@
]
},
"locked": {
"lastModified": 1758405547,
"narHash": "sha256-WgaDgvIZMPvlZcZrpPMjkaalTBnGF2lTG+62znXctWM=",
"lastModified": 1768231828,
"narHash": "sha256-wL/8Iij4T2OLkhHcc4NieOjf7YeJffaUYbCiCqKv/+0=",
"owner": "jcollie",
"repo": "zon2nix",
"rev": "bf983aa90ff169372b9fa8c02e57ea75e0b42245",
"rev": "c28e93f3ba133d4c1b1d65224e2eebede61fd071",
"type": "github"
},
"original": {
"owner": "jcollie",
"repo": "zon2nix",
"rev": "bf983aa90ff169372b9fa8c02e57ea75e0b42245",
"rev": "c28e93f3ba133d4c1b1d65224e2eebede61fd071",
"type": "github"
}
}

View File

@@ -28,14 +28,14 @@
};
zon2nix = {
url = "github:jcollie/zon2nix?rev=bf983aa90ff169372b9fa8c02e57ea75e0b42245";
url = "github:jcollie/zon2nix?rev=c28e93f3ba133d4c1b1d65224e2eebede61fd071";
inputs = {
nixpkgs.follows = "nixpkgs";
};
};
home-manager = {
url = "github:nix-community/home-manager?ref=release-25.05";
url = "github:nix-community/home-manager";
inputs = {
nixpkgs.follows = "nixpkgs";
};
@@ -117,7 +117,6 @@
wayland-gnome = runVM ./nix/vm/wayland-gnome.nix;
wayland-plasma6 = runVM ./nix/vm/wayland-plasma6.nix;
x11-cinnamon = runVM ./nix/vm/x11-cinnamon.nix;
x11-gnome = runVM ./nix/vm/x11-gnome.nix;
x11-plasma6 = runVM ./nix/vm/x11-plasma6.nix;
x11-xfce = runVM ./nix/vm/x11-xfce.nix;
};

View File

@@ -26,6 +26,7 @@
wasmtime,
wraptest,
zig,
zig_0_15,
zip,
llvmPackages_latest,
bzip2,

View File

@@ -20,16 +20,6 @@
wayland-scanner,
pkgs,
}: let
# The Zig hook has no way to select the release type without actual
# overriding of the default flags.
#
# TODO: Once
# https://github.com/ziglang/zig/issues/14281#issuecomment-1624220653 is
# ultimately acted on and has made its way to a nixpkgs implementation, this
# can probably be removed in favor of that.
zig_hook = zig_0_15.hook.overrideAttrs {
zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off";
};
gi_typelib_path = import ./build-support/gi-typelib-path.nix {
inherit pkgs lib stdenv;
};
@@ -73,7 +63,7 @@ in
ncurses
pandoc
pkg-config
zig_hook
zig_0_15
gobject-introspection
wrapGAppsHook4
blueprint-compiler
@@ -92,12 +82,16 @@ in
GI_TYPELIB_PATH = gi_typelib_path;
dontSetZigDefaultFlags = true;
zigBuildFlags = [
"--system"
"${finalAttrs.deps}"
"-Dversion-string=${finalAttrs.version}-${revision}-nix"
"-Dgtk-x11=${lib.boolToString enableX11}"
"-Dgtk-wayland=${lib.boolToString enableWayland}"
"-Dcpu=baseline"
"-Doptimize=${optimize}"
"-Dstrip=${lib.boolToString strip}"
];

View File

@@ -274,7 +274,7 @@ in {
client.succeed("${su "${ghostty} +new-window"}")
client.wait_until_succeeds("${wm_class} | grep -q 'com.mitchellh.ghostty-debug'")
with subtest("SSH from client to server and verify that the Ghostty terminfo is copied.")
with subtest("SSH from client to server and verify that the Ghostty terminfo is copied."):
client.sleep(2)
client.send_chars("ssh ghostty@server\n")
server.wait_for_file("${user.home}/.terminfo/x/xterm-ghostty", timeout=30)

View File

@@ -8,7 +8,7 @@
./common.nix
];
services.xserver = {
services = {
displayManager = {
gdm = {
enable = true;

View File

@@ -1,9 +0,0 @@
{...}: {
imports = [
./common-gnome.nix
];
services.displayManager = {
defaultSession = "gnome-xorg";
};
}

View File

@@ -287,7 +287,7 @@ fn writeBashCompletions(writer: *std.Io.Writer) !void {
\\ else prev="${COMP_WORDS[COMP_CWORD-1]}"
\\ fi
\\
\\ # current completion is double quoted add a space so the curor progresses
\\ # current completion is double quoted add a space so the cursor progresses
\\ if [[ "$2" == \"*\" ]]; then
\\ COMPREPLY=( "$cur " );
\\ return;

View File

@@ -49,7 +49,12 @@ const Node = struct {
/// The memory pool we get page nodes from.
const NodePool = std.heap.MemoryPool(List.Node);
/// The standard page capacity that we use as a starting point for
/// all pages. This is chosen as a sane default that fits most terminal
/// usage to support using our pool.
const std_capacity = pagepkg.std_capacity;
/// The byte size required for a standard page.
const std_size = Page.layout(std_capacity).total_size;
/// The memory pool we use for page memory buffers. We use a separate pool
@@ -223,19 +228,30 @@ pub const Viewport = union(enum) {
/// But this gives us a nice fast heuristic for determining min/max size.
/// Therefore, if the page size is violated you should always also verify
/// that we have enough space for the active area.
fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) !usize {
fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) usize {
// Invariant required to ensure our divCeil below cannot overflow.
comptime {
const max_rows = std.math.maxInt(size.CellCountInt);
_ = std.math.divCeil(usize, max_rows, 1) catch unreachable;
}
// Get our capacity to fit our rows. If the cols are too big, it may
// force less rows than we want meaning we need more than one page to
// represent a viewport.
const cap = try std_capacity.adjust(.{ .cols = cols });
const cap = initialCapacity(cols);
// Calculate the number of standard sized pages we need to represent
// an active area.
const pages_exact = if (cap.rows >= rows) 1 else try std.math.divCeil(
const pages_exact = if (cap.rows >= rows) 1 else std.math.divCeil(
usize,
rows,
cap.rows,
);
) catch {
// Not possible:
// - initialCapacity guarantees at least 1 row
// - numerator/denominator can't overflow because of comptime check above
unreachable;
};
// We always need at least one page extra so that we
// can fit partial pages to spread our active area across two pages.
@@ -255,6 +271,49 @@ fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) !usize {
return PagePool.item_size * pages;
}
/// Calculates the initial capacity for a new page for a given column
/// count. This will attempt to fit within std_size at all times so we
/// can use our memory pool, but if cols is too big, this will return a
/// larger capacity.
///
/// The returned capacity is always guaranteed to layout properly (not
/// overflow). We are able to support capacities up to the maximum int
/// value of cols, so this will never overflow.
fn initialCapacity(cols: size.CellCountInt) Capacity {
// This is an important invariant that ensures that this function
// can never return an error. We verify here that our standard capacity
// when increased to maximum possible columns can always support at
// least one row in memory.
//
// IF THIS EVER FAILS: We probably need to modify our logic below
// to reduce other elements of the capacity (styles, graphemes, etc.).
// But, instead, I recommend taking a step back and re-evaluating
// life choices.
comptime {
var cap = std_capacity;
cap.cols = std.math.maxInt(size.CellCountInt);
_ = Page.layout(cap);
}
if (std_capacity.adjust(
.{ .cols = cols },
)) |cap| {
// If we can adjust our standard capacity, we fit within the
// standard size and we're good!
return cap;
} else |err| {
// Ensure our error set doesn't change.
comptime assert(@TypeOf(err) == error{OutOfMemory});
}
// This code path means that our standard capacity can't even
// accommodate our column count! The only solution is to increase
// our capacity and go non-standard.
var cap: Capacity = std_capacity;
cap.cols = cols;
return cap;
}
/// This is the page allocator we'll use for all our underlying
/// VM page allocations.
inline fn pageAllocator() Allocator {
@@ -310,7 +369,7 @@ pub fn init(
);
// Get our minimum max size, see doc comments for more details.
const min_max_size = try minMaxSize(cols, rows);
const min_max_size = minMaxSize(cols, rows);
// We always track our viewport pin to ensure this is never an allocation
const viewport_pin = try pool.pins.create();
@@ -344,17 +403,31 @@ fn initPages(
serial: *u64,
cols: size.CellCountInt,
rows: size.CellCountInt,
) !struct { List, usize } {
) Allocator.Error!struct { List, usize } {
var page_list: List = .{};
var page_size: usize = 0;
// Add pages as needed to create our initial viewport.
const cap = try std_capacity.adjust(.{ .cols = cols });
const cap = initialCapacity(cols);
const layout = Page.layout(cap);
const pooled = layout.total_size <= std_size;
const page_alloc = pool.pages.arena.child_allocator;
var rem = rows;
while (rem > 0) {
const node = try pool.nodes.create();
const page_buf = try pool.pages.create();
// no errdefer because the pool deinit will clean these up
const page_buf = if (pooled)
try pool.pages.create()
else
try page_alloc.alignedAlloc(
u8,
.fromByteUnits(std.heap.page_size_min),
layout.total_size,
);
errdefer if (pooled)
pool.pages.destroy(page_buf)
else
page_alloc.free(page_buf);
// In runtime safety modes we have to memset because the Zig allocator
// interface will always memset to 0xAA for undefined. In non-safe modes
@@ -364,10 +437,7 @@ fn initPages(
// Initialize the first set of pages to contain our viewport so that
// the top of the first page is always the active area.
node.* = .{
.data = .initBuf(
.init(page_buf),
Page.layout(cap),
),
.data = .initBuf(.init(page_buf), layout),
.serial = serial.*,
};
node.data.size.rows = @min(rem, node.data.capacity.rows);
@@ -533,10 +603,8 @@ pub fn reset(self: *PageList) void {
// We need enough pages/nodes to keep our active area. This should
// never fail since we by definition have allocated a page already
// that fits our size but I'm not confident to make that assertion.
const cap = std_capacity.adjust(
.{ .cols = self.cols },
) catch @panic("reset: std_capacity.adjust failed");
assert(cap.rows > 0); // adjust should never return 0 rows
const cap = initialCapacity(self.cols);
assert(cap.rows > 0);
// The number of pages we need is the number of rows in the active
// area divided by the row capacity of a page.
@@ -828,7 +896,7 @@ pub fn resize(self: *PageList, opts: Resize) !void {
// when increasing beyond our initial minimum max size or explicit max
// size to fit the active area.
const old_min_max_size = self.min_max_size;
self.min_max_size = try minMaxSize(
self.min_max_size = minMaxSize(
opts.cols orelse self.cols,
opts.rows orelse self.rows,
);
@@ -1592,7 +1660,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void {
// We only set the new min_max_size if we're not reflowing. If we are
// reflowing, then resize handles this for us.
const old_min_max_size = self.min_max_size;
self.min_max_size = if (!opts.reflow) try minMaxSize(
self.min_max_size = if (!opts.reflow) minMaxSize(
opts.cols orelse self.cols,
opts.rows orelse self.rows,
) else old_min_max_size;
@@ -4551,6 +4619,38 @@ test "PageList init rows across two pages" {
}, s.scrollbar());
}
test "PageList init more than max cols" {
const testing = std.testing;
const alloc = testing.allocator;
// Initialize with more columns than we can fit in our standard
// capacity. This is going to force us to go to a non-standard page
// immediately.
var s = try init(
alloc,
std_capacity.maxCols().? + 1,
80,
null,
);
defer s.deinit();
try testing.expect(s.viewport == .active);
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
// We expect a single, non-standard page
try testing.expect(s.pages.first != null);
try testing.expect(s.pages.first.?.data.memory.len > std_size);
// Initial total rows should be our row count
try testing.expectEqual(s.rows, s.total_rows);
// Scrollbar should be where we expect it
try testing.expectEqual(Scrollbar{
.total = s.rows,
.offset = 0,
.len = s.rows,
}, s.scrollbar());
}
test "PageList pointFromPin active no history" {
const testing = std.testing;
const alloc = testing.allocator;

View File

@@ -1602,9 +1602,6 @@ pub fn insertLines(self: *Terminal, count: usize) void {
const cur_rac = cur_p.rowAndCell();
const cur_row: *Row = cur_rac.row;
// Mark the row as dirty
cur_p.markDirty();
// If this is one of the lines we need to shift, do so
if (y > adjusted_count) {
const off_p = cur_p.up(adjusted_count).?;
@@ -1699,9 +1696,6 @@ pub fn insertLines(self: *Terminal, count: usize) void {
dst_row.* = src_row.*;
src_row.* = dst;
// Make sure the row is marked as dirty though.
dst_row.dirty = true;
// Ensure what we did didn't corrupt the page
cur_p.node.data.assertIntegrity();
} else {
@@ -1728,6 +1722,9 @@ pub fn insertLines(self: *Terminal, count: usize) void {
);
}
// Mark the row as dirty
cur_p.markDirty();
// We have successfully processed a line.
y -= 1;
// Move our pin up to the next row.
@@ -1805,9 +1802,6 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
const cur_rac = cur_p.rowAndCell();
const cur_row: *Row = cur_rac.row;
// Mark the row as dirty
cur_p.markDirty();
// If this is one of the lines we need to shift, do so
if (y < rem - adjusted_count) {
const off_p = cur_p.down(adjusted_count).?;
@@ -1897,9 +1891,6 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
dst_row.* = src_row.*;
src_row.* = dst;
// Make sure the row is marked as dirty though.
dst_row.dirty = true;
// Ensure what we did didn't corrupt the page
cur_p.node.data.assertIntegrity();
} else {
@@ -1926,6 +1917,9 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
);
}
// Mark the row as dirty
cur_p.markDirty();
// We have successfully processed a line.
y += 1;
// Move our pin down to the next row.
@@ -5419,6 +5413,52 @@ test "Terminal: insertLines top/bottom scroll region" {
}
}
test "Terminal: insertLines across page boundary marks all shifted rows dirty" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 });
defer t.deinit(alloc);
const first_page = t.screens.active.pages.pages.first.?;
const first_page_nrows = first_page.data.capacity.rows;
// Fill up the first page minus 3 rows
for (0..first_page_nrows - 3) |_| try t.linefeed();
// Add content that will cross a page boundary
try t.printString("1AAAA");
t.carriageReturn();
try t.linefeed();
try t.printString("2BBBB");
t.carriageReturn();
try t.linefeed();
try t.printString("3CCCC");
t.carriageReturn();
try t.linefeed();
try t.printString("4DDDD");
t.carriageReturn();
try t.linefeed();
try t.printString("5EEEE");
// Verify we now have a second page
try testing.expect(first_page.next != null);
t.setCursorPos(1, 1);
t.clearDirty();
t.insertLines(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n1AAAA\n2BBBB\n3CCCC\n4DDDD", str);
}
}
test "Terminal: insertLines (legacy test)" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
@@ -8042,6 +8082,52 @@ test "Terminal: deleteLines colors with bg color" {
}
}
test "Terminal: deleteLines across page boundary marks all shifted rows dirty" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 });
defer t.deinit(alloc);
const first_page = t.screens.active.pages.pages.first.?;
const first_page_nrows = first_page.data.capacity.rows;
// Fill up the first page minus 3 rows
for (0..first_page_nrows - 3) |_| try t.linefeed();
// Add content that will cross a page boundary
try t.printString("1AAAA");
t.carriageReturn();
try t.linefeed();
try t.printString("2BBBB");
t.carriageReturn();
try t.linefeed();
try t.printString("3CCCC");
t.carriageReturn();
try t.linefeed();
try t.printString("4DDDD");
t.carriageReturn();
try t.linefeed();
try t.printString("5EEEE");
// Verify we now have a second page
try testing.expect(first_page.next != null);
t.setCursorPos(1, 1);
t.clearDirty();
t.deleteLines(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("2BBBB\n3CCCC\n4DDDD\n5EEEE", str);
}
}
test "Terminal: deleteLines (legacy)" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 80, .rows = 80 });

View File

@@ -196,7 +196,8 @@ pub const Page = struct {
// We need to go through and initialize all the rows so that
// they point to a valid offset into the cells, since the rows
// zero-initialized aren't valid.
const cells_ptr = cells.ptr(buf)[0 .. cap.cols * cap.rows];
const cells_len = @as(usize, cap.cols) * @as(usize, cap.rows);
const cells_ptr = cells.ptr(buf)[0..cells_len];
for (rows.ptr(buf)[0..cap.rows], 0..) |*row, y| {
const start = y * cap.cols;
row.* = .{
@@ -1556,7 +1557,7 @@ pub const Page = struct {
const rows_start = 0;
const rows_end: usize = rows_start + (rows_count * @sizeOf(Row));
const cells_count: usize = @intCast(cap.cols * cap.rows);
const cells_count: usize = @as(usize, cap.cols) * @as(usize, cap.rows);
const cells_start = alignForward(usize, rows_end, @alignOf(Cell));
const cells_end = cells_start + (cells_count * @sizeOf(Cell));
@@ -1662,43 +1663,42 @@ pub const Capacity = struct {
cols: ?size.CellCountInt = null,
};
/// Returns the maximum number of columns that can be used with this
/// capacity while still fitting at least one row. Returns null if even
/// a single column cannot fit (which would indicate an unusable capacity).
///
/// Note that this is the maximum number of columns that never increases
/// the amount of memory the original capacity will take. If you modify
/// the original capacity to add rows, then you can fit more columns.
pub fn maxCols(self: Capacity) ?size.CellCountInt {
const available_bits = self.availableBitsForGrid();
// If we can't even fit the row metadata, return null
if (available_bits <= @bitSizeOf(Row)) return null;
// We do the math of how many columns we can fit in the remaining
// bits ignoring the metadata of a row.
const remaining_bits = available_bits - @bitSizeOf(Row);
const max_cols = remaining_bits / @bitSizeOf(Cell);
// Clamp to CellCountInt max
return @min(std.math.maxInt(size.CellCountInt), max_cols);
}
/// Adjust the capacity parameters while retaining the same total size.
///
/// Adjustments always happen by limiting the rows in the page. Everything
/// else can grow. If it is impossible to achieve the desired adjustment,
/// OutOfMemory is returned.
pub fn adjust(self: Capacity, req: Adjustment) Allocator.Error!Capacity {
var adjusted = self;
if (req.cols) |cols| {
// The math below only works if there is no alignment gap between
// the end of the rows array and the start of the cells array.
//
// To guarantee this, we assert that Row's size is a multiple of
// Cell's alignment, so that any length array of Rows will end on
// a valid alignment for the start of the Cell array.
assert(@sizeOf(Row) % @alignOf(Cell) == 0);
const layout = Page.layout(self);
// In order to determine the amount of space in the page available
// for rows & cells (which will allow us to calculate the number of
// rows we can fit at a certain column width) we need to layout the
// "meta" members of the page (i.e. everything else) from the end.
const hyperlink_map_start = alignBackward(usize, layout.total_size - layout.hyperlink_map_layout.total_size, hyperlink.Map.base_align.toByteUnits());
const hyperlink_set_start = alignBackward(usize, hyperlink_map_start - layout.hyperlink_set_layout.total_size, hyperlink.Set.base_align.toByteUnits());
const string_alloc_start = alignBackward(usize, hyperlink_set_start - layout.string_alloc_layout.total_size, StringAlloc.base_align.toByteUnits());
const grapheme_map_start = alignBackward(usize, string_alloc_start - layout.grapheme_map_layout.total_size, GraphemeMap.base_align.toByteUnits());
const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - layout.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align.toByteUnits());
const styles_start = alignBackward(usize, grapheme_alloc_start - layout.styles_layout.total_size, StyleSet.base_align.toByteUnits());
const available_bits = self.availableBitsForGrid();
// The size per row is:
// - The row metadata itself
// - The cells per row (n=cols)
const bits_per_row: usize = size: {
var bits: usize = @bitSizeOf(Row); // Row metadata
bits += @bitSizeOf(Cell) * @as(usize, @intCast(cols)); // Cells (n=cols)
break :size bits;
};
const available_bits: usize = styles_start * 8;
const bits_per_row: usize = @bitSizeOf(Row) + @bitSizeOf(Cell) * @as(usize, @intCast(cols));
const new_rows: usize = @divFloor(available_bits, bits_per_row);
// If our rows go to zero then we can't fit any row metadata
@@ -1711,6 +1711,34 @@ pub const Capacity = struct {
return adjusted;
}
/// Computes the number of bits available for rows and cells in the page.
///
/// This is done by laying out the "meta" members (styles, graphemes,
/// hyperlinks, strings) from the end of the page and finding where they
/// start, which gives us the space available for rows and cells.
fn availableBitsForGrid(self: Capacity) usize {
// The math below only works if there is no alignment gap between
// the end of the rows array and the start of the cells array.
//
// To guarantee this, we assert that Row's size is a multiple of
// Cell's alignment, so that any length array of Rows will end on
// a valid alignment for the start of the Cell array.
assert(@sizeOf(Row) % @alignOf(Cell) == 0);
const l = Page.layout(self);
// Layout meta members from the end to find styles_start
const hyperlink_map_start = alignBackward(usize, l.total_size - l.hyperlink_map_layout.total_size, hyperlink.Map.base_align.toByteUnits());
const hyperlink_set_start = alignBackward(usize, hyperlink_map_start - l.hyperlink_set_layout.total_size, hyperlink.Set.base_align.toByteUnits());
const string_alloc_start = alignBackward(usize, hyperlink_set_start - l.string_alloc_layout.total_size, StringAlloc.base_align.toByteUnits());
const grapheme_map_start = alignBackward(usize, string_alloc_start - l.grapheme_map_layout.total_size, GraphemeMap.base_align.toByteUnits());
const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - l.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align.toByteUnits());
const styles_start = alignBackward(usize, grapheme_alloc_start - l.styles_layout.total_size, StyleSet.base_align.toByteUnits());
// Multiply by 8 to convert bytes to bits
return styles_start * 8;
}
};
pub const Row = packed struct(u64) {
@@ -2070,6 +2098,40 @@ test "Page capacity adjust cols too high" {
);
}
test "Capacity maxCols basic" {
const cap = std_capacity;
const max = cap.maxCols().?;
// maxCols should be >= current cols (since current capacity is valid)
try testing.expect(max >= cap.cols);
// Adjusting to maxCols should succeed with at least 1 row
const adjusted = try cap.adjust(.{ .cols = max });
try testing.expect(adjusted.rows >= 1);
// Adjusting to maxCols + 1 should fail
try testing.expectError(
error.OutOfMemory,
cap.adjust(.{ .cols = max + 1 }),
);
}
test "Capacity maxCols preserves total size" {
const cap = std_capacity;
const original_size = Page.layout(cap).total_size;
const max = cap.maxCols().?;
const adjusted = try cap.adjust(.{ .cols = max });
const adjusted_size = Page.layout(adjusted).total_size;
try testing.expectEqual(original_size, adjusted_size);
}
test "Capacity maxCols with 1 row exactly" {
const cap = std_capacity;
const max = cap.maxCols().?;
const adjusted = try cap.adjust(.{ .cols = max });
try testing.expectEqual(@as(size.CellCountInt, 1), adjusted.rows);
}
test "Page init" {
var page = try Page.init(.{
.cols = 120,

View File

@@ -1641,7 +1641,7 @@ pub fn Stream(comptime Handler: type) type {
},
},
else => {
log.warn("invalid set curor style command: {f}", .{input});
log.warn("invalid set cursor style command: {f}", .{input});
return;
},
};