Merge remote-tracking branch 'upstream/main' into add-serbian-translation

This commit is contained in:
Марко М. Костић (Marko M. Kostić)
2026-08-31 20:41:03 +02:00
19 changed files with 494 additions and 280 deletions

6
.github/VOUCHED.td vendored
View File

@@ -46,6 +46,7 @@ alexfeijoo44
alexjuca
alosarjos
amadeus
and-rs
andrejdaskalov
anhthang
anmitalidev
@@ -160,6 +161,7 @@ j-c-m
j0hnm4r5
jacobsandlund
jake-stewart
jakeriksen
-jamesarch Bad AI etiquette
jamylak
jarred-sumner
@@ -183,6 +185,7 @@ khipp
kierancanter
kirwiisp
kjvdven
kleshzz
kloneets
knu
-kody-w
@@ -211,6 +214,7 @@ marrocco-simone
masterflitzer
matkotiric
mattn
mgsloan
micaeljarniac
michielvk
miguelelgallo
@@ -301,6 +305,7 @@ simonbcn
slsrepo
steven-tk
sunshine-syz
svector-anu
svmhdvn
tacherasasi
-tangivis
@@ -309,6 +314,7 @@ tbrundige
tdgroot
tdslot
thirstycrow
-thomas-trijindev No human in the loop
thoutbeckers
ticclick
tnagatomi

View File

@@ -160,7 +160,7 @@ jobs:
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.9.0
SPARKLE_VERSION: 2.9.6
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -328,7 +328,7 @@ jobs:
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.9.0
SPARKLE_VERSION: 2.9.6
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@@ -591,7 +591,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.9.0
SPARKLE_VERSION: 2.9.6
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -847,7 +847,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.9.0
SPARKLE_VERSION: 2.9.6
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -1044,7 +1044,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.9.0
SPARKLE_VERSION: 2.9.6
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@@ -125,8 +125,8 @@
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.android_ndk = .{ .path = "./pkg/android-ndk" },
.iterm2_themes = .{
.url = "https://deps.files.ghostty.org/ghostty-themes-release-20260810-152212-0173c3c.tgz",
.hash = "N-V-__8AAPpcBAD4_75xLBbLiYqdojOwQP74eoWmpL3jPrBl",
.url = "https://deps.files.ghostty.org/ghostty-themes-release-20260824-153547-75c93ee.tgz",
.hash = "N-V-__8AAGZkBACH0haGC9R-hfNPYWxu16hr-ydEsolad9GV",
.lazy = true,
},
},

6
build.zig.zon.json generated
View File

@@ -59,10 +59,10 @@
"url": "https://deps.files.ghostty.org/N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI.tar.gz",
"hash": "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs="
},
"N-V-__8AAPpcBAD4_75xLBbLiYqdojOwQP74eoWmpL3jPrBl": {
"N-V-__8AAGZkBACH0haGC9R-hfNPYWxu16hr-ydEsolad9GV": {
"name": "iterm2_themes",
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260810-152212-0173c3c.tgz",
"hash": "sha256-6ph4RxQg7lsS5/L/SACZyVTqUOVzob34P0PhBcm+Y/A="
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260824-153547-75c93ee.tgz",
"hash": "sha256-sf0fMQV/l8EnTbA8bORJYIN6GkH1InXLxkruLm40654="
},
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
"name": "jetbrains_mono",

6
build.zig.zon.nix generated
View File

@@ -206,11 +206,11 @@ in
};
}
{
name = "N-V-__8AAPpcBAD4_75xLBbLiYqdojOwQP74eoWmpL3jPrBl";
name = "N-V-__8AAGZkBACH0haGC9R-hfNPYWxu16hr-ydEsolad9GV";
path = fetchZigArtifact {
name = "iterm2_themes";
url = "https://deps.files.ghostty.org/ghostty-themes-release-20260810-152212-0173c3c.tgz";
hash = "sha256-6ph4RxQg7lsS5/L/SACZyVTqUOVzob34P0PhBcm+Y/A=";
url = "https://deps.files.ghostty.org/ghostty-themes-release-20260824-153547-75c93ee.tgz";
hash = "sha256-sf0fMQV/l8EnTbA8bORJYIN6GkH1InXLxkruLm40654=";
unpack = false;
};
}

2
build.zig.zon.txt generated
View File

@@ -9,7 +9,7 @@ https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918
https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz
https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz
https://deps.files.ghostty.org/gettext-0.24.tar.gz
https://deps.files.ghostty.org/ghostty-themes-release-20260810-152212-0173c3c.tgz
https://deps.files.ghostty.org/ghostty-themes-release-20260824-153547-75c93ee.tgz
https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz
https://deps.files.ghostty.org/gobject-2026-07-28-36-1.tar.zst
https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz

View File

@@ -73,9 +73,9 @@
},
{
"type": "archive",
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260810-152212-0173c3c.tgz",
"dest": "vendor/p/N-V-__8AAPpcBAD4_75xLBbLiYqdojOwQP74eoWmpL3jPrBl",
"sha256": "ea9878471420ee5b12e7f2ff480099c954ea50e573a1bdf83f43e105c9be63f0"
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260824-153547-75c93ee.tgz",
"dest": "vendor/p/N-V-__8AAGZkBACH0haGC9R-hfNPYWxu16hr-ydEsolad9GV",
"sha256": "b1fd1f31057f97c1274db03c6ce44960837a1a41f52275cbc64aee2e6e34eb9e"
},
{
"type": "archive",

View File

@@ -1099,8 +1099,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sparkle-project/Sparkle";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.5.1;
kind = exactVersion;
version = 2.9.6;
};
};
/* End XCRemoteSwiftPackageReference section */

View File

@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
"revision" : "21d8df80440b1ca3b65fa82e40782f1e5a9e6ba2",
"version" : "2.9.0"
"revision" : "ac2def288cbff5cfc7df3ffef6abdf45b72bcb0a",
"version" : "2.9.6"
}
}
],

382
po/uk.po

File diff suppressed because it is too large Load Diff

View File

@@ -2378,31 +2378,29 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
fn setSelectionAndCopy(self: *Surface, sel: terminal.Selection) !void {
try self.setSelection(sel);
// If copy on select is false then exit early.
if (self.config.copy_on_select == .false) return;
switch (self.config.copy_on_select) {
.false => unreachable, // handled above with an early exit
.none => {},
// The selection clipboard is set if supported, otherwise nothing is copied.
.primary => try self.copySelectionToClipboards(
sel,
&.{.selection},
.mixed,
),
// Only the standard clipboard is set.
.clipboard => try self.copySelectionToClipboards(
sel,
&.{.standard},
.mixed,
),
// Both standard and selection clipboards are set.
.clipboard => try self.copySelectionToClipboards(
.both => try self.copySelectionToClipboards(
sel,
&.{ .standard, .selection },
.mixed,
),
// The selection clipboard is set if supported, otherwise the standard.
.true => {
const clipboard: apprt.Clipboard = if (self.rt_surface.supportsClipboard(.selection))
.selection
else
.standard;
try self.copySelectionToClipboards(
sel,
&.{clipboard},
.mixed,
);
},
}
}
@@ -3891,7 +3889,7 @@ pub fn mouseButtonCallback(
// The selection clipboard is only updated for left-click drag when
// the left button is released. This is to avoid the clipboard
// being updated on every mouse move which would be noisy.
if (self.config.copy_on_select != .false) {
if (self.config.copy_on_select != .none) {
const prev_ = self.io.terminal.screens.active.selection;
if (prev_) |prev| {
try self.setSelectionAndCopy(terminal.Selection.init(
@@ -4049,22 +4047,16 @@ pub fn mouseButtonCallback(
}
}
// Middle-click paste source follows copy-on-select: when copy-on-select
// targets the selection clipboard, middle-click reads from it; when
// copy-on-select targets the system clipboard, middle-click reads from
// that instead. Falls back to the standard clipboard on platforms that
// do not support the selection clipboard.
// Middle-click action, either ignore, or paste from clipboard or paste from the selection clipboard if supported.
if (button == .middle and action == .press) switch (self.config.middle_click_action) {
.ignore => {},
.@"clipboard-paste" => {
_ = try self.startClipboardRequest(.standard, .{ .paste = .standard });
},
.@"primary-paste" => {
const clipboard: apprt.Clipboard = switch (self.config.copy_on_select) {
.clipboard => .standard,
.true, .false => if (self.rt_surface.supportsClipboard(.selection))
.selection
else
.standard,
};
_ = try self.startClipboardRequest(clipboard, .{ .paste = clipboard });
if (self.rt_surface.supportsClipboard(.selection)) {
_ = try self.startClipboardRequest(.selection, .{ .paste = .selection });
}
},
};

View File

@@ -853,14 +853,24 @@ pub const Surface = extern struct {
pub fn bindIsSplit(self: *Self, tree: *SplitTree) void {
const priv = self.private();
if (priv.is_split_binding) |bind| bind.unbind();
if (priv.is_split_binding) |binding| {
binding.unbind();
binding.unref();
priv.is_split_binding = null;
}
priv.is_split_binding = tree.as(gobject.Object).bindProperty(
const binding = tree.as(gobject.Object).bindProperty(
"is-split",
self.as(gobject.Object),
"is-split",
.{ .sync_create = true },
);
// The ref created by bindProperty is owned by the binding itself.
// We need another ref to prevent the binding object from being
// freed if the source object (SplitTree) is finalized. Otherwise
// our pointer to the binding could become stale.
binding.ref();
priv.is_split_binding = binding;
}
/// Callback used to determine whether unfocused-split-fill / unfocused-split-opacity
@@ -1567,7 +1577,11 @@ pub const Surface = extern struct {
// https://gitlab.gnome.org/GNOME/libadwaita/-/commit/a7738a4d269bfdf4d8d5429ca73ccdd9b2450421
// https://gitlab.gnome.org/GNOME/libadwaita/-/commit/9759d3fd81129608dd78116001928f2aed974ead
if (gtk_xft_dpi <= 0) {
log.warn("gtk-xft-dpi has invalid value ({}), using default", .{gtk_xft_dpi});
// -1 is a valid value which specifies default scale.
// https://docs.gtk.org/gtk4/property.Settings.gtk-xft-dpi.html
if (gtk_xft_dpi != -1) {
log.warn("gtk-xft-dpi has invalid value ({}), using default", .{gtk_xft_dpi});
}
break :xft_scale 1.0;
}
@@ -1881,6 +1895,12 @@ pub const Surface = extern struct {
priv.config = null;
}
if (priv.is_split_binding) |binding| {
binding.unbind();
binding.unref();
priv.is_split_binding = null;
}
if (priv.vadj_signal_group) |group| {
group.setTarget(null);
group.as(gobject.Object).unref();

View File

@@ -89,7 +89,8 @@ pub const SurfaceScrolledWindow = extern struct {
const priv = self.private();
if (priv.config_binding) |binding| {
binding.as(gobject.Object).unref();
binding.unbind();
binding.unref();
priv.config_binding = null;
}
@@ -168,18 +169,23 @@ pub const SurfaceScrolledWindow = extern struct {
// Unbind old config binding if it exists
if (priv.config_binding) |binding| {
binding.as(gobject.Object).unref();
binding.unbind();
binding.unref();
priv.config_binding = null;
}
// Bind config from surface to our config property
if (priv.surface) |surface| {
priv.config_binding = surface.as(gobject.Object).bindProperty(
const binding = surface.as(gobject.Object).bindProperty(
properties.config.name,
self.as(gobject.Object),
properties.config.name,
.{ .sync_create = true },
);
// Keep another ref, otherwise the binding would be freed and
// our pointer become stale if the surface gets finalized.
binding.ref();
priv.config_binding = binding;
}
}

View File

@@ -100,6 +100,12 @@ pub const compatibility = std.StaticStringMap(
// Ghostty 1.4 renamed `scrollback-limit` to `scrollback-limit-bytes`
// when `scrollback-limit-lines` was added so the units are explicit.
.{ "scrollback-limit", cli.compatibilityRenamed(Config, "scrollback-limit-bytes") },
// Ghostty 1.4 updated "copy-on-select", allow copying to the selection
// clipboard (on supported operating systems), the system clipboard, or
// both. The semantics also changed but this is the correct mapping.
// See: https://github.com/ghostty-org/ghostty/pull/12604
.{ "copy-on-select", compatCopyOnSelect },
});
/// Set Ghostty's graphical user interface language to a language other than the
@@ -2517,25 +2523,28 @@ keybind: Keybinds = .{},
/// limit per surface is double.
@"image-storage-limit": u32 = 320 * 1000 * 1000,
/// Whether to automatically copy selected text to the clipboard. `true`
/// will prefer to copy to the selection clipboard, otherwise it will copy to
/// the system clipboard.
/// Whether to automatically copy selected text to the clipboard.
///
/// The value `clipboard` will always copy text to the selection clipboard
/// as well as the system clipboard.
/// Valid values:
///
/// Middle-click primary paste (see `middle-click-action`) is enabled by
/// default even if this is `false`. The clipboard it pastes from follows
/// this setting: with `true` (or `false`) it reads from the selection
/// clipboard (falling back to the system clipboard on platforms without a
/// selection clipboard); with `clipboard` it reads from the system
/// clipboard.
/// * `none` - Do not copy selected text automatically.
///
/// The default value is true on Linux and macOS.
/// * `primary` - On Linux, copy to the selection clipboard only. This has no
/// effect on macOS. (Available since: 1.4.0)
///
/// * `clipboard` - Copy text to the system clipboard only.
///
/// * `both` - Copy to both clipboards on Linux, and only the system clipboard
/// on macOS. (Available since: 1.4.0)
///
/// For backward compatibility and convenience, a value of `true` is the same as
/// `primary` on Linux and `clipboard` on macOS, and `false` is an alias for
/// `none`.
///
/// The default value is `primary` on Linux and `none` otherwise.
@"copy-on-select": CopyOnSelect = switch (builtin.os.tag) {
.linux => .true,
.macos => .true,
else => .false,
.linux => .primary,
else => .none,
},
/// The action to take when the user right-clicks on the terminal surface.
@@ -2554,8 +2563,9 @@ keybind: Keybinds = .{},
/// The action to take when the user middle-clicks on the terminal surface.
///
/// Valid values:
/// * `primary-paste` - Paste from the selection (or system) clipboard per
/// `copy-on-select`.
/// * `primary-paste` - Paste from the selection clipboard on Linux.
/// Does nothing on macOS.
/// * `clipboard-paste` - Paste from the system clipboard.
/// * `ignore` - Do nothing, ignore the middle click.
///
/// The default value is `primary-paste`.
@@ -5047,6 +5057,31 @@ fn compatMacOSDockDropBehavior(
return false;
}
fn compatCopyOnSelect(
self: *Config,
alloc: Allocator,
key: []const u8,
value: ?[]const u8,
) bool {
_ = alloc;
assert(std.mem.eql(u8, key, "copy-on-select"));
if (std.mem.eql(u8, value orelse "", "true")) {
self.@"copy-on-select" = switch (builtin.os.tag) {
.linux, .freebsd => .primary,
else => .clipboard,
};
return true;
}
if (std.mem.eql(u8, value orelse "", "false")) {
self.@"copy-on-select" = .none;
return true;
}
return false;
}
/// Add a diagnostic message to the config with the given string.
/// This is always added with a location of "none".
pub fn addDiagnosticFmt(
@@ -8728,16 +8763,20 @@ pub const RepeatableLink = struct {
/// Options for copy on select behavior.
pub const CopyOnSelect = enum {
/// Disables copy on select entirely.
false,
/// Disables copy on select entirely. This is the default on macOS.
none,
/// Copy on select is enabled, but goes to the selection clipboard.
/// This is not supported on platforms such as macOS. This is the default.
true,
/// This is not supported on platforms such as macOS. This is the default
/// on Linux.
primary,
/// Copy on select is enabled and goes to the system clipboard.
clipboard,
/// Copy on select is enabled and goes to both the system clipboard
/// and the selection clipboard (for Linux).
clipboard,
both,
};
/// Options for right-click actions.
@@ -8761,8 +8800,10 @@ pub const RightClickAction = enum {
/// Options for middle-click actions.
pub const MiddleClickAction = enum {
/// Paste from the selection/standard clipboard per `copy-on-select`.
/// Paste from the selection clipboard.
@"primary-paste",
/// Paste from the standard clipboard.
@"clipboard-paste",
/// No action is taken on middle click.
ignore,

View File

@@ -228,7 +228,7 @@ pub const std_options: std.Options = .{
.allow_stack_tracing = if (builtin.target.os.tag.isDarwin() and builtin.target.os.tag != .macos)
false
else
builtin.strip_debug_info,
!builtin.strip_debug_info,
};
test {

View File

@@ -1194,8 +1194,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const should_run =
// Non-visible windows never vsync
self.visible and
// Non-focused windows only render on-demand
self.focused and
// Only vsync if we have cell changes or animation
(self.cells_rebuilt or self.animationWake() != null);

View File

@@ -210,6 +210,15 @@ pub const LoadingImage = struct {
return error.UnsupportedMedium;
}
// POSIX shared memory names must begin with a slash, contain at
// least one character after it, contain no other slashes, and fit
// within NAME_MAX. Some shm_open implementations accept names
// without the leading slash, but the Kitty protocol does not.
if (!validSharedMemoryName(path, posix.NAME_MAX)) {
log.warn("invalid shared memory name", .{});
return error.InvalidData;
}
// Since we're only supporting posix then max_path_bytes should
// be enough to stack allocate the path.
var buf: [std.fs.max_path_bytes]u8 = undefined;
@@ -831,6 +840,16 @@ pub const Rect = struct {
}
};
/// Returns whether a name follows the POSIX shared memory name format.
fn validSharedMemoryName(name: []const u8, name_max: usize) bool {
if (name.len < 2 or name.len > name_max or name[0] != '/') return false;
for (name[1..]) |c| {
if (c == '/' or c == 0) return false;
}
return true;
}
/// Returns true if `path` is `dir` or is contained within it, requiring a
/// path-separator boundary so similarly prefixed directories do not match.
fn isPathInDir(dir: []const u8, path: []const u8) bool {
@@ -852,6 +871,53 @@ test "temporary file path must be inside directory" {
try testing.expect(!isPathInDir("/custom/tmp", "/custom/tmp-suffix/tty-graphics-protocol-image.data"));
}
test "shared memory names follow POSIX rules" {
const testing = std.testing;
try testing.expect(validSharedMemoryName("/kitty", 8));
try testing.expect(validSharedMemoryName("/1234567", 8));
try testing.expect(!validSharedMemoryName("", 8));
try testing.expect(!validSharedMemoryName("/", 8));
try testing.expect(!validSharedMemoryName("kitty", 8));
try testing.expect(!validSharedMemoryName("/kitty/image", 16));
try testing.expect(!validSharedMemoryName("/kitty\x00image", 16));
try testing.expect(!validSharedMemoryName("/12345678", 8));
}
test "image load rejects invalid POSIX shared memory names" {
if (comptime builtin.abi.isAndroid() or
builtin.target.os.tag == .windows or
!builtin.link_libc)
{
return error.SkipZigTest;
}
const testing = std.testing;
const alloc = testing.allocator;
var cmd: command.Command = .{
.control = .{ .transmit = .{
.format = .rgb,
.medium = .shared_memory,
.width = 1,
.height = 1,
.image_id = 31,
} },
.data = try alloc.dupe(u8, "kitty-without-leading-slash"),
};
defer cmd.deinit(alloc);
try testing.expectError(
error.InvalidData,
LoadingImage.init(testing.io, alloc, &cmd, .{
.file = false,
.temporary_file = .disabled,
.shared_memory = true,
}),
);
}
test "shared memory range with offset and size" {
const testing = std.testing;

View File

@@ -317,6 +317,19 @@ pub fn RefCountedSet(
if (id < self.next_id) {
if (items[id].meta.ref == 0) {
// Requested ID is dead, but if the value exists not under
// another ID then ref count that and increase the ID.
if (self.lookupContext(base, value, ctx)) |existing_id| {
// Notify the context that the value is "deleted"
// because we're reusing the existing value in the
// set. This allows callers to clean up any
// resources associated with the value.
if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value);
items[existing_id].meta.ref += 1;
return existing_id;
}
// See comment in `addContext` for details.
if (self.psl_stats[self.psl_stats.len - 1] > 0) {
@branchHint(.cold);
@@ -324,8 +337,7 @@ pub fn RefCountedSet(
}
self.deleteItem(base, id, ctx);
const added_id = self.upsert(base, value, id, ctx);
const added_id = self.insert(base, value, id, ctx);
items[added_id].meta.ref += 1;
@@ -589,24 +601,6 @@ pub fn RefCountedSet(
return null;
}
/// Find the provided value in the hash table, or add a new item
/// for it if not present. If a new item is added, `new_id` will
/// be used as the ID. If an existing item is found, the `new_id`
/// is ignored and the existing item's ID is returned.
fn upsert(self: *Self, base: anytype, value: T, new_id: Id, ctx: Context) Id {
// If the item already exists, return it.
if (self.lookupContext(base, value, ctx)) |id| {
// Notify the context that the value is "deleted" because
// we're reusing the existing value in the set. This allows
// callers to clean up any resources associated with the value.
if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value);
return id;
}
return self.insert(base, value, new_id, ctx);
}
/// Insert the given value into the hash table with the given ID.
///
/// If runtime safety is enabled, asserts that
@@ -764,6 +758,65 @@ pub fn RefCountedSet(
};
}
test "addWithId dead id resolving to an existing value" {
const alloc = testing.allocator;
const TestSet = RefCountedSet(
u32,
u16,
u16,
struct {
pub fn hash(_: *const @This(), value: u32) u64 {
return std.hash.int(value);
}
pub fn eql(_: *const @This(), a: u32, b: u32) bool {
return a == b;
}
},
);
const layout: TestSet.Layout = .init(8);
const buf = try alloc.alignedAlloc(
u8,
TestSet.base_align,
layout.total_size,
);
defer alloc.free(buf);
var set: TestSet = .init(.init(buf), layout, .{});
// Create a dead item between two live ones so that it can't
// be reaped by the trim loop in `add`, then release it. This
// mirrors a page style set after a styled run is erased.
const live = try set.add(buf, 11);
const released = try set.add(buf, 22);
const last = try set.add(buf, 33);
set.release(buf, released);
try testing.expectEqual(@as(usize, 2), set.count());
// Request the dead ID for a value that is already live under
// a different ID: we must resolve to the existing item and
// take a reference, without changing the living count.
const resolved = (try set.addWithId(buf, 11, released)).?;
try testing.expectEqual(live, resolved);
try testing.expectEqual(@as(u16, 2), set.refCount(buf, live));
try testing.expectEqual(@as(usize, 2), set.count());
// The living count must agree with the iterator.
var it = set.iterator(buf);
var iterated: usize = 0;
while (it.next()) |_| iterated += 1;
try testing.expectEqual(set.count(), iterated);
// The dead slot is still reusable for a value that is not
// in the set: the requested ID must be used (null return).
try testing.expectEqual(@as(?u16, null), try set.addWithId(buf, 44, released));
try testing.expectEqual(@as(u16, 1), set.refCount(buf, released));
try testing.expectEqual(@as(usize, 3), set.count());
_ = last;
}
test "iterator visits live entries in ID order" {
const alloc = testing.allocator;
const TestSet = RefCountedSet(