aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/datastruct
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/datastruct
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/datastruct')
-rw-r--r--vendor/libghostty-vt/src/datastruct/array_list_collection.zig44
-rw-r--r--vendor/libghostty-vt/src/datastruct/blocking_queue.zig248
-rw-r--r--vendor/libghostty-vt/src/datastruct/cache_table.zig181
-rw-r--r--vendor/libghostty-vt/src/datastruct/circ_buf.zig866
-rw-r--r--vendor/libghostty-vt/src/datastruct/comparison.zig430
-rw-r--r--vendor/libghostty-vt/src/datastruct/intrusive_linked_list.zig181
-rw-r--r--vendor/libghostty-vt/src/datastruct/lru.zig349
-rw-r--r--vendor/libghostty-vt/src/datastruct/main.zig24
-rw-r--r--vendor/libghostty-vt/src/datastruct/message_data.zig124
-rw-r--r--vendor/libghostty-vt/src/datastruct/segmented_pool.zig95
-rw-r--r--vendor/libghostty-vt/src/datastruct/split_tree.zig2367
11 files changed, 4909 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/datastruct/array_list_collection.zig b/vendor/libghostty-vt/src/datastruct/array_list_collection.zig
new file mode 100644
index 00000000..d3fbddb1
--- /dev/null
+++ b/vendor/libghostty-vt/src/datastruct/array_list_collection.zig
@@ -0,0 +1,44 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+/// A collection of ArrayLists with methods for bulk operations.
+pub fn ArrayListCollection(comptime T: type) type {
+ return struct {
+ const Self = ArrayListCollection(T);
+ const ArrayListT = std.ArrayListUnmanaged(T);
+
+ // An array containing the lists that belong to this collection.
+ lists: []ArrayListT,
+
+ // The collection will be initialized with empty ArrayLists.
+ pub fn init(
+ alloc: Allocator,
+ list_count: usize,
+ initial_capacity: usize,
+ ) Allocator.Error!Self {
+ const self: Self = .{
+ .lists = try alloc.alloc(ArrayListT, list_count),
+ };
+
+ for (self.lists) |*list| {
+ list.* = try .initCapacity(alloc, initial_capacity);
+ }
+
+ return self;
+ }
+
+ pub fn deinit(self: *Self, alloc: Allocator) void {
+ for (self.lists) |*list| {
+ list.deinit(alloc);
+ }
+ alloc.free(self.lists);
+ }
+
+ /// Clear all lists in the collection, retaining capacity.
+ pub fn reset(self: *Self) void {
+ for (self.lists) |*list| {
+ list.clearRetainingCapacity();
+ }
+ }
+ };
+}
diff --git a/vendor/libghostty-vt/src/datastruct/blocking_queue.zig b/vendor/libghostty-vt/src/datastruct/blocking_queue.zig
new file mode 100644
index 00000000..3185d98d
--- /dev/null
+++ b/vendor/libghostty-vt/src/datastruct/blocking_queue.zig
@@ -0,0 +1,248 @@
+//! Blocking queue implementation aimed primarily for message passing
+//! between threads.
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+/// Returns a blocking queue implementation for type T.
+///
+/// This is tailor made for ghostty usage so it isn't meant to be maximally
+/// generic, but I'm happy to make it more generic over time. Traits of this
+/// queue that are specific to our usage:
+///
+/// - Fixed size. We expect our queue to quickly drain and also not be
+/// too large so we prefer a fixed size queue for now.
+/// - No blocking pop. We use an external event loop mechanism such as
+/// eventfd to notify our waiter that there is no data available so
+/// we don't need to implement a blocking pop.
+/// - Drain function. Most queues usually pop one at a time. We have
+/// a mechanism for draining since on every IO loop our TTY drains
+/// the full queue so we can get rid of the overhead of a ton of
+/// locks and bounds checking and do a one-time drain.
+///
+/// One key usage pattern is that our blocking queues are single producer
+/// single consumer (SPSC). This should let us do some interesting optimizations
+/// in the future. At the time of writing this, the blocking queue implementation
+/// is purposely naive to build something quickly, but we should benchmark
+/// and make this more optimized as necessary.
+pub fn BlockingQueue(
+ comptime T: type,
+ comptime capacity: usize,
+) type {
+ return struct {
+ const Self = @This();
+
+ // The type we use for queue size types. We can optimize this
+ // in the future to be the correct bit-size for our preallocated
+ // size for this queue.
+ pub const Size = u32;
+
+ // The bounds of this queue. We recast this to Size so we can do math.
+ const bounds: Size = @intCast(capacity);
+
+ /// Specifies the timeout for an operation.
+ pub const Timeout = union(enum) {
+ /// Fail instantly (non-blocking).
+ instant: void,
+
+ /// Run forever or until interrupted
+ forever: void,
+
+ /// Nanoseconds
+ ns: u64,
+ };
+
+ /// Our data. The values are undefined until they are written.
+ data: [bounds]T = undefined,
+
+ /// The next location to write (next empty loc) and next location
+ /// to read (next non-empty loc). The number of written elements.
+ write: Size = 0,
+ read: Size = 0,
+ len: Size = 0,
+
+ /// The big mutex that must be held to read/write.
+ mutex: std.Thread.Mutex = .{},
+
+ /// A CV for being notified when the queue is no longer full. This is
+ /// used for writing. Note we DON'T have a CV for waiting on the
+ /// queue not being EMPTY because we use external notifiers for that.
+ cond_not_full: std.Thread.Condition = .{},
+ not_full_waiters: usize = 0,
+
+ /// Allocate the blocking queue on the heap.
+ pub fn create(alloc: Allocator) Allocator.Error!*Self {
+ const ptr = try alloc.create(Self);
+ errdefer alloc.destroy(ptr);
+
+ ptr.* = .{
+ .data = undefined,
+ .len = 0,
+ .write = 0,
+ .read = 0,
+ .mutex = .{},
+ .cond_not_full = .{},
+ .not_full_waiters = 0,
+ };
+
+ return ptr;
+ }
+
+ /// Free all the resources for this queue. This should only be
+ /// called once all producers and consumers have quit.
+ pub fn destroy(self: *Self, alloc: Allocator) void {
+ self.* = undefined;
+ alloc.destroy(self);
+ }
+
+ /// Push a value to the queue. This returns the total size of the
+ /// queue (unread items) after the push. A return value of zero
+ /// means that the push failed.
+ pub fn push(self: *Self, value: T, timeout: Timeout) Size {
+ self.mutex.lock();
+ defer self.mutex.unlock();
+
+ // The
+ if (self.full()) {
+ switch (timeout) {
+ // If we're not waiting, then we failed to write.
+ .instant => return 0,
+
+ .forever => {
+ self.not_full_waiters += 1;
+ defer self.not_full_waiters -= 1;
+ self.cond_not_full.wait(&self.mutex);
+ },
+
+ .ns => |ns| {
+ self.not_full_waiters += 1;
+ defer self.not_full_waiters -= 1;
+ self.cond_not_full.timedWait(&self.mutex, ns) catch return 0;
+ },
+ }
+
+ // If we're still full, then we failed to write. This can
+ // happen in situations where we are interrupted.
+ if (self.full()) return 0;
+ }
+
+ // Add our data and update our accounting
+ self.data[self.write] = value;
+ self.write += 1;
+ if (self.write >= bounds) self.write -= bounds;
+ self.len += 1;
+
+ return self.len;
+ }
+
+ /// Pop a value from the queue without blocking.
+ pub fn pop(self: *Self) ?T {
+ self.mutex.lock();
+ defer self.mutex.unlock();
+
+ // If we're empty we have nothing
+ if (self.len == 0) return null;
+
+ // Get the index we're going to read data from and do some
+ // accounting. We don't copy the value here to avoid copying twice.
+ const n = self.read;
+ self.read += 1;
+ if (self.read >= bounds) self.read -= bounds;
+ self.len -= 1;
+
+ // If we have consumers waiting on a full queue, notify.
+ if (self.not_full_waiters > 0) self.cond_not_full.signal();
+
+ return self.data[n];
+ }
+
+ /// Pop all values from the queue. This will hold the big mutex
+ /// until `deinit` is called on the return value. This is used if
+ /// you know you're going to "pop" and utilize all the values
+ /// quickly to avoid many locks, bounds checks, and cv signals.
+ pub fn drain(self: *Self) DrainIterator {
+ self.mutex.lock();
+ return .{ .queue = self };
+ }
+
+ pub const DrainIterator = struct {
+ queue: *Self,
+
+ pub fn next(self: *DrainIterator) ?T {
+ if (self.queue.len == 0) return null;
+
+ // Read and account
+ const n = self.queue.read;
+ self.queue.read += 1;
+ if (self.queue.read >= bounds) self.queue.read -= bounds;
+ self.queue.len -= 1;
+
+ return self.queue.data[n];
+ }
+
+ pub fn deinit(self: *DrainIterator) void {
+ // If we have consumers waiting on a full queue, notify.
+ if (self.queue.not_full_waiters > 0) self.queue.cond_not_full.signal();
+
+ // Unlock
+ self.queue.mutex.unlock();
+ }
+ };
+
+ /// Returns true if the queue is full. This is not public because
+ /// it requires the lock to be held.
+ inline fn full(self: *Self) bool {
+ return self.len == bounds;
+ }
+ };
+}
+
+test "basic push and pop" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Q = BlockingQueue(u64, 4);
+ const q = try Q.create(alloc);
+ defer q.destroy(alloc);
+
+ // Should have no values
+ try testing.expect(q.pop() == null);
+
+ // Push until we're full
+ try testing.expectEqual(@as(Q.Size, 1), q.push(1, .{ .instant = {} }));
+ try testing.expectEqual(@as(Q.Size, 2), q.push(2, .{ .instant = {} }));
+ try testing.expectEqual(@as(Q.Size, 3), q.push(3, .{ .instant = {} }));
+ try testing.expectEqual(@as(Q.Size, 4), q.push(4, .{ .instant = {} }));
+ try testing.expectEqual(@as(Q.Size, 0), q.push(5, .{ .instant = {} }));
+
+ // Pop!
+ try testing.expect(q.pop().? == 1);
+ try testing.expect(q.pop().? == 2);
+ try testing.expect(q.pop().? == 3);
+ try testing.expect(q.pop().? == 4);
+ try testing.expect(q.pop() == null);
+
+ // Drain does nothing
+ var it = q.drain();
+ try testing.expect(it.next() == null);
+ it.deinit();
+
+ // Verify we can still push
+ try testing.expectEqual(@as(Q.Size, 1), q.push(1, .{ .instant = {} }));
+}
+
+test "timed push" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Q = BlockingQueue(u64, 1);
+ const q = try Q.create(alloc);
+ defer q.destroy(alloc);
+
+ // Push
+ try testing.expectEqual(@as(Q.Size, 1), q.push(1, .{ .instant = {} }));
+ try testing.expectEqual(@as(Q.Size, 0), q.push(2, .{ .instant = {} }));
+
+ // Timed push should fail
+ try testing.expectEqual(@as(Q.Size, 0), q.push(2, .{ .ns = 1000 }));
+}
diff --git a/vendor/libghostty-vt/src/datastruct/cache_table.zig b/vendor/libghostty-vt/src/datastruct/cache_table.zig
new file mode 100644
index 00000000..49172398
--- /dev/null
+++ b/vendor/libghostty-vt/src/datastruct/cache_table.zig
@@ -0,0 +1,181 @@
+const fastmem = @import("../fastmem.zig");
+
+const std = @import("std");
+const assert = @import("../quirks.zig").inlineAssert;
+
+/// An associative data structure used for efficiently storing and
+/// retrieving values which are able to be recomputed if necessary.
+///
+/// This structure is effectively a hash table with fixed-sized buckets.
+///
+/// When inserting an item in to a full bucket, the least recently used
+/// item is replaced.
+///
+/// To achieve this, when an item is accessed, it's moved to the end of
+/// the bucket, and the rest of the items are moved over to fill the gap.
+///
+/// This should provide very good query performance and keep frequently
+/// accessed items cached indefinitely.
+///
+/// Parameters:
+///
+/// `Context`
+/// A type containing methods to define CacheTable behaviors.
+/// - `fn hash(*Context, K) u64` - Return a hash for a key.
+/// - `fn eql(*Context, K, K) bool` - Check two keys for equality.
+///
+/// - `fn evicted(*Context, K, V) void` - [OPTIONAL] Eviction callback.
+/// If present, called whenever an item is evicted from the cache.
+///
+/// `bucket_count`
+/// Should ideally be close to the median number of important items that
+/// you expect to be cached at any given point. This is required to be a
+/// power of 2 since performance suffers if it's not and there's no good
+/// reason to allow it to be anything else.
+///
+/// `bucket_size`
+/// should be larger if you expect a large number of unimportant items to
+/// enter the cache at a time. Having larger buckets will avoid important
+/// items being dropped from the cache prematurely.
+///
+pub fn CacheTable(
+ comptime K: type,
+ comptime V: type,
+ comptime Context: type,
+ comptime bucket_count: usize,
+ comptime bucket_size: u8,
+) type {
+ return struct {
+ const Self = @This();
+
+ const KV = struct {
+ key: K,
+ value: V,
+ };
+
+ comptime {
+ assert(std.math.isPowerOfTwo(bucket_count));
+ assert(bucket_count <= std.math.maxInt(usize));
+ }
+
+ /// `bucket_count` buckets containing `bucket_size` KV pairs each.
+ ///
+ /// We don't need to initialize this memory because we don't use it
+ /// unless it's within a bucket's stored length, which will guarantee
+ /// that we put actual items there.
+ buckets: [bucket_count][bucket_size]KV = undefined,
+
+ /// We use this array to keep track of how many slots in each bucket
+ /// have actual items in them. Once all the buckets fill up this will
+ /// become a pointless check, but hopefully branch prediction picks
+ /// up on it at that point. The memory cost isn't too bad since it's
+ /// just bytes, so should be a fraction the size of the main table.
+ lengths: [bucket_count]u8 = @splat(0),
+
+ /// An instance of the context structure.
+ /// Must be initialized before calling any operations.
+ context: Context,
+
+ /// Adds an item to the cache table. If an old value was removed to
+ /// make room then it is returned in a struct with its key and value.
+ pub fn put(self: *Self, key: K, value: V) ?KV {
+ const kv: KV = .{ .key = key, .value = value };
+ const idx: usize = @intCast(self.context.hash(key) % bucket_count);
+
+ // If we have space available in the bucket then we just append
+ if (self.lengths[idx] < bucket_size) {
+ self.buckets[idx][self.lengths[idx]] = kv;
+ self.lengths[idx] += 1;
+ return null;
+ }
+ assert(self.lengths[idx] == bucket_size);
+
+ // Append our new item and return the oldest
+ const evicted = fastmem.rotateIn(KV, &self.buckets[idx], kv);
+
+ // The Context is allowed to register an eviction hook.
+ if (comptime @hasDecl(Context, "evicted")) self.context.evicted(
+ evicted.key,
+ evicted.value,
+ );
+
+ return evicted;
+ }
+
+ /// Retrieves an item from the cache table.
+ ///
+ /// Returns null if no item is found with the provided key.
+ pub fn get(self: *Self, key: K) ?V {
+ const idx: usize = @intCast(self.context.hash(key) % bucket_count);
+ const len = self.lengths[idx];
+ var i: usize = len;
+ while (i > 0) {
+ i -= 1;
+ if (self.context.eql(key, self.buckets[idx][i].key)) {
+ defer fastmem.rotateOnce(KV, self.buckets[idx][i..len]);
+ return self.buckets[idx][i].value;
+ }
+ }
+
+ return null;
+ }
+
+ /// Removes all items from the cache table.
+ ///
+ /// If your `Context` has an `evicted` method,
+ /// it will be called with all removed items.
+ pub fn clear(self: *Self) void {
+ if (comptime @hasDecl(Context, "evicted")) {
+ for (self.buckets, self.lengths) |b, l| {
+ for (b[0..l]) |kv| {
+ self.context.evicted(kv.key, kv.value);
+ }
+ }
+ }
+
+ @memset(&self.lengths, 0);
+ }
+ };
+}
+
+/// Creates a Context automatically for the given key type. This uses the
+/// same logic as std.hash_map.AutoContext today since the API matches.
+fn AutoContext(comptime K: type) type {
+ return std.hash_map.AutoContext(K);
+}
+
+test CacheTable {
+ const testing = std.testing;
+
+ // Construct a table that purposely has a predictable hash so we can
+ // test all edge cases.
+ const T = CacheTable(u32, u32, struct {
+ pub fn hash(self: *const @This(), key: u32) u64 {
+ _ = self;
+ return @intCast(key);
+ }
+
+ pub fn eql(self: *const @This(), a: u32, b: u32) bool {
+ _ = self;
+ return a == b;
+ }
+ }, 2, 2);
+ var t: T = .{ .context = .{} };
+
+ // Fill the table
+ try testing.expect(t.put(0, 0) == null);
+ try testing.expect(t.put(1, 0) == null);
+ try testing.expect(t.put(2, 0) == null);
+ try testing.expect(t.put(3, 0) == null);
+
+ // It should now be full, so any insert should evict the oldest item.
+ // NOTE: For the sake of this test, we're assuming that the first item
+ // is evicted but we don't need to promise this.
+ try testing.expectEqual(T.KV{
+ .key = 0,
+ .value = 0,
+ }, t.put(4, 0).?);
+
+ // The first item should now be gone
+ try testing.expect(t.get(0) == null);
+}
diff --git a/vendor/libghostty-vt/src/datastruct/circ_buf.zig b/vendor/libghostty-vt/src/datastruct/circ_buf.zig
new file mode 100644
index 00000000..3e373cb9
--- /dev/null
+++ b/vendor/libghostty-vt/src/datastruct/circ_buf.zig
@@ -0,0 +1,866 @@
+const std = @import("std");
+const assert = @import("../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const fastmem = @import("../fastmem.zig");
+
+/// Returns a circular buffer containing type T.
+pub fn CircBuf(comptime T: type, comptime default: T) type {
+ return struct {
+ const Self = @This();
+
+ // Implementation note: there's a lot of unsafe addition of usize
+ // here in this implementation that can technically overflow. If someone
+ // wants to fix this and make it overflow safe (use subtractions for
+ // checks prior to additions) then I welcome it. In reality, we'd
+ // have to be a really, really large terminal screen to even worry
+ // about this so I'm punting it.
+
+ storage: []T,
+ head: usize,
+ tail: usize,
+
+ // We could remove this and just use math with head/tail to figure
+ // it out, but our usage of circular buffers stores so much data that
+ // this minor overhead is not worth optimizing out.
+ full: bool,
+
+ pub const Iterator = struct {
+ buf: Self,
+ idx: usize,
+ direction: Direction,
+
+ pub const Direction = enum { forward, reverse };
+
+ pub fn next(self: *Iterator) ?*T {
+ if (self.idx >= self.buf.len()) return null;
+
+ // Get our index from the tail
+ const tail_idx = switch (self.direction) {
+ .forward => self.idx,
+ .reverse => self.buf.len() - self.idx - 1,
+ };
+
+ // Translate the tail index to a storage index
+ const storage_idx = (self.buf.tail + tail_idx) % self.buf.capacity();
+ self.idx += 1;
+ return &self.buf.storage[storage_idx];
+ }
+
+ /// Seek the iterator by a given amount. This will clamp
+ /// the values to the bounds of the buffer so overflows are
+ /// not possible.
+ pub fn seekBy(self: *Iterator, amount: isize) void {
+ if (amount > 0) {
+ self.idx +|= @intCast(amount);
+ } else {
+ self.idx -|= @intCast(@abs(amount));
+ }
+ }
+
+ /// Reset the iterator back to the first value.
+ pub fn reset(self: *Iterator) void {
+ self.idx = 0;
+ }
+ };
+
+ /// Initialize a new circular buffer that can store size elements.
+ pub fn init(alloc: Allocator, size: usize) Allocator.Error!Self {
+ const buf = try alloc.alloc(T, size);
+ @memset(buf, default);
+
+ return Self{
+ .storage = buf,
+ .head = 0,
+ .tail = 0,
+ .full = size == 0,
+ };
+ }
+
+ pub fn deinit(self: *Self, alloc: Allocator) void {
+ alloc.free(self.storage);
+ self.* = undefined;
+ }
+
+ /// Append a single value to the buffer. If the buffer is full,
+ /// an error will be returned.
+ pub fn append(self: *Self, v: T) Allocator.Error!void {
+ if (self.full) return error.OutOfMemory;
+ self.storage[self.head] = v;
+ self.head += 1;
+ if (self.head >= self.storage.len) self.head = 0;
+ self.full = self.head == self.tail;
+ }
+
+ /// Append a single value to the buffer, assuming there is capacity.
+ pub fn appendAssumeCapacity(self: *Self, v: T) void {
+ assert(!self.full);
+ self.storage[self.head] = v;
+ self.head += 1;
+ if (self.head >= self.storage.len) self.head = 0;
+ self.full = self.head == self.tail;
+ }
+
+ /// Append a slice to the buffer.
+ pub fn appendSliceAssumeCapacity(
+ self: *Self,
+ slice: []const T,
+ ) void {
+ const storage = self.getPtrSlice(
+ self.len(),
+ slice.len,
+ );
+ fastmem.copy(T, storage[0], slice[0..storage[0].len]);
+ fastmem.copy(T, storage[1], slice[storage[0].len..]);
+ }
+
+ /// Clear the buffer.
+ pub fn clear(self: *Self) void {
+ self.head = 0;
+ self.tail = 0;
+ self.full = false;
+ }
+
+ /// Iterate over the circular buffer.
+ pub fn iterator(self: Self, direction: Iterator.Direction) Iterator {
+ return Iterator{
+ .buf = self,
+ .idx = 0,
+ .direction = direction,
+ };
+ }
+
+ /// Get the first (oldest) value in the buffer.
+ pub fn first(self: Self) ?*T {
+ // Note: this can be more efficient by not using the
+ // iterator, but this was an easy way to implement it.
+ var it = self.iterator(.forward);
+ return it.next();
+ }
+
+ /// Get the last (newest) value in the buffer.
+ pub fn last(self: Self) ?*T {
+ // Note: this can be more efficient by not using the
+ // iterator, but this was an easy way to implement it.
+ var it = self.iterator(.reverse);
+ return it.next();
+ }
+
+ /// Ensures that there is enough capacity to store amount more
+ /// items via append.
+ pub fn ensureUnusedCapacity(
+ self: *Self,
+ alloc: Allocator,
+ amount: usize,
+ ) Allocator.Error!void {
+ const new_cap = self.len() + amount;
+ if (new_cap <= self.capacity()) return;
+ try self.resize(alloc, new_cap);
+ }
+
+ /// Resize the buffer to the given size (larger or smaller).
+ /// If larger, new values will be set to the default value.
+ pub fn resize(self: *Self, alloc: Allocator, size: usize) Allocator.Error!void {
+ // Rotate to zero so it is aligned.
+ try self.rotateToZero();
+
+ // Reallocate, this adds to the end so we're ready to go.
+ const prev_len = self.len();
+ const prev_cap = self.storage.len;
+ self.storage = try alloc.realloc(self.storage, size);
+
+ // If we grew, we need to set our new defaults. We can add it
+ // at the end since we rotated to start.
+ if (size > prev_cap) {
+ @memset(self.storage[prev_cap..], default);
+
+ // Fix up our head/tail
+ if (self.full) {
+ self.head = prev_len;
+ self.full = false;
+ }
+ }
+ }
+
+ /// Rotate the data so that it is zero-aligned.
+ fn rotateToZero(self: *Self) Allocator.Error!void {
+ // If we're already at zero then do nothing.
+ if (self.tail == 0) return;
+
+ // We use std.mem.rotate to rotate our storage in-place.
+ std.mem.rotate(T, self.storage, self.tail);
+
+ // Then fix up our head and tail.
+ self.head = self.len() % self.storage.len;
+ self.tail = 0;
+ }
+
+ /// Returns if the buffer is currently empty. To check if its
+ /// full, just check the "full" attribute.
+ pub fn empty(self: Self) bool {
+ return !self.full and self.head == self.tail;
+ }
+
+ /// Returns the total capacity allocated for this buffer.
+ pub fn capacity(self: Self) usize {
+ return self.storage.len;
+ }
+
+ /// Returns the length in elements that are used.
+ pub fn len(self: Self) usize {
+ if (self.full) return self.storage.len;
+ if (self.head >= self.tail) return self.head - self.tail;
+ return self.storage.len - (self.tail - self.head);
+ }
+
+ /// Delete the oldest n values from the buffer. If there are less
+ /// than n values in the buffer, it'll delete everything.
+ pub fn deleteOldest(self: *Self, n: usize) void {
+ assert(n <= self.storage.len);
+
+ // Special case n == 0 otherwise we will accidentally break
+ // our circular buffer.
+ if (n == 0) {
+ @branchHint(.cold);
+ return;
+ }
+
+ // Clear the values back to default
+ const slices = self.getPtrSlice(0, n);
+ inline for (slices) |slice| @memset(slice, default);
+
+ // If we're not full, we can just advance the tail. We know
+ // it'll be less than the length because otherwise we'd be full.
+ self.tail += @min(self.len(), n);
+ if (self.tail >= self.storage.len) self.tail -= self.storage.len;
+ self.full = false;
+ }
+
+ /// Returns a pointer to the value at offset with the given length,
+ /// and considers this full amount of data "written" if it is beyond
+ /// the end of our buffer. This never "rotates" the buffer because
+ /// the offset can only be within the size of the buffer.
+ pub fn getPtrSlice(self: *Self, offset: usize, slice_len: usize) [2][]T {
+ // Special case the empty slice fast-path.
+ if (slice_len == 0) {
+ @branchHint(.cold);
+ return .{ &.{}, &.{} };
+ }
+
+ // Note: this assertion is very important, it hints the compiler
+ // which generates ~10% faster code than without it.
+ assert(offset + slice_len <= self.capacity());
+
+ // End offset is the last offset (exclusive) for our slice.
+ // We use exclusive because it makes the math easier and it
+ // matches Zigs slicing parameterization.
+ const end_offset = offset + slice_len;
+
+ // If our slice can't fit it in our length, then we need to advance.
+ if (end_offset > self.len()) self.advance(end_offset - self.len());
+
+ // Our start and end indexes into the storage buffer
+ const start_idx = self.storageOffset(offset);
+ const end_idx = self.storageOffset(end_offset - 1);
+ // std.log.warn("A={} B={}", .{ start_idx, end_idx });
+
+ // Optimistically, our data fits in one slice
+ if (end_idx >= start_idx) {
+ return .{
+ self.storage[start_idx .. end_idx + 1],
+ self.storage[0..0], // So there is an empty slice
+ };
+ }
+
+ return .{
+ self.storage[start_idx..],
+ self.storage[0 .. end_idx + 1],
+ };
+ }
+
+ /// Advances the head/tail so that we can store amount.
+ fn advance(self: *Self, amount: usize) void {
+ assert(amount <= self.storage.len - self.len());
+
+ // Optimistically add our amount
+ self.head += amount;
+
+ // If we exceeded the length of the buffer, wrap around.
+ if (self.head >= self.storage.len) self.head = self.head - self.storage.len;
+
+ // If we're full, we have to keep tail lined up.
+ if (self.full) self.tail = self.head;
+
+ // We're full if the head reached the tail. The head can never
+ // pass the tail because advance asserts amount is only in
+ // available space left
+ self.full = self.head == self.tail;
+ }
+
+ /// For a given offset from zero, this returns the offset in the
+ /// storage buffer where this data can be found.
+ fn storageOffset(self: Self, offset: usize) usize {
+ assert(offset < self.storage.len);
+
+ // This should be subtraction ideally to avoid overflows but
+ // it would take a really, really, huge buffer to overflow.
+ const fits_offset = self.tail + offset;
+ if (fits_offset < self.storage.len) return fits_offset;
+ return fits_offset - self.storage.len;
+ }
+ };
+}
+
+test {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 12);
+ defer buf.deinit(alloc);
+
+ try testing.expect(buf.empty());
+ try testing.expectEqual(@as(usize, 0), buf.len());
+}
+
+test "CircBuf append" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 3);
+ defer buf.deinit(alloc);
+
+ try buf.append(1);
+ try buf.append(2);
+ try buf.append(3);
+ try testing.expectError(error.OutOfMemory, buf.append(4));
+ buf.deleteOldest(1);
+ try buf.append(4);
+ try testing.expectError(error.OutOfMemory, buf.append(5));
+}
+
+test "CircBuf forward iterator" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 3);
+ defer buf.deinit(alloc);
+
+ // Empty
+ {
+ var it = buf.iterator(.forward);
+ try testing.expect(it.next() == null);
+ }
+
+ // Partially full
+ try buf.append(1);
+ try buf.append(2);
+ {
+ var it = buf.iterator(.forward);
+ try testing.expect(it.next().?.* == 1);
+ try testing.expect(it.next().?.* == 2);
+ try testing.expect(it.next() == null);
+ }
+
+ // Full
+ try buf.append(3);
+ {
+ var it = buf.iterator(.forward);
+ try testing.expect(it.next().?.* == 1);
+ try testing.expect(it.next().?.* == 2);
+ try testing.expect(it.next().?.* == 3);
+ try testing.expect(it.next() == null);
+ }
+
+ // Delete and add
+ buf.deleteOldest(1);
+ try buf.append(4);
+ {
+ var it = buf.iterator(.forward);
+ try testing.expect(it.next().?.* == 2);
+ try testing.expect(it.next().?.* == 3);
+ try testing.expect(it.next().?.* == 4);
+ try testing.expect(it.next() == null);
+ }
+}
+
+test "CircBuf reverse iterator" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 3);
+ defer buf.deinit(alloc);
+
+ // Empty
+ {
+ var it = buf.iterator(.reverse);
+ try testing.expect(it.next() == null);
+ }
+
+ // Partially full
+ try buf.append(1);
+ try buf.append(2);
+ {
+ var it = buf.iterator(.reverse);
+ try testing.expect(it.next().?.* == 2);
+ try testing.expect(it.next().?.* == 1);
+ try testing.expect(it.next() == null);
+ }
+
+ // Full
+ try buf.append(3);
+ {
+ var it = buf.iterator(.reverse);
+ try testing.expect(it.next().?.* == 3);
+ try testing.expect(it.next().?.* == 2);
+ try testing.expect(it.next().?.* == 1);
+ try testing.expect(it.next() == null);
+ }
+
+ // Delete and add
+ buf.deleteOldest(1);
+ try buf.append(4);
+ {
+ var it = buf.iterator(.reverse);
+ try testing.expect(it.next().?.* == 4);
+ try testing.expect(it.next().?.* == 3);
+ try testing.expect(it.next().?.* == 2);
+ try testing.expect(it.next() == null);
+ }
+}
+
+test "CircBuf first/last" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 3);
+ defer buf.deinit(alloc);
+
+ try buf.append(1);
+ try buf.append(2);
+ try buf.append(3);
+ try testing.expectEqual(3, buf.last().?.*);
+ try testing.expectEqual(1, buf.first().?.*);
+}
+
+test "CircBuf first/last empty" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 0);
+ defer buf.deinit(alloc);
+
+ try testing.expect(buf.first() == null);
+ try testing.expect(buf.last() == null);
+}
+
+test "CircBuf first/last empty with cap" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 3);
+ defer buf.deinit(alloc);
+
+ try testing.expect(buf.first() == null);
+ try testing.expect(buf.last() == null);
+}
+
+test "CircBuf append slice" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 5);
+ defer buf.deinit(alloc);
+
+ buf.appendSliceAssumeCapacity("hello");
+ {
+ var it = buf.iterator(.forward);
+ try testing.expect(it.next().?.* == 'h');
+ try testing.expect(it.next().?.* == 'e');
+ try testing.expect(it.next().?.* == 'l');
+ try testing.expect(it.next().?.* == 'l');
+ try testing.expect(it.next().?.* == 'o');
+ try testing.expect(it.next() == null);
+ }
+}
+
+test "CircBuf append slice with wrap" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 4);
+ defer buf.deinit(alloc);
+
+ // Fill the buffer
+ _ = buf.getPtrSlice(0, buf.capacity());
+ try testing.expect(buf.full);
+ try testing.expectEqual(@as(usize, 4), buf.len());
+
+ // Delete
+ buf.deleteOldest(2);
+ try testing.expect(!buf.full);
+ try testing.expectEqual(@as(usize, 2), buf.len());
+
+ buf.appendSliceAssumeCapacity("AB");
+ {
+ var it = buf.iterator(.forward);
+ try testing.expect(it.next().?.* == 0);
+ try testing.expect(it.next().?.* == 0);
+ try testing.expect(it.next().?.* == 'A');
+ try testing.expect(it.next().?.* == 'B');
+ try testing.expect(it.next() == null);
+ }
+}
+
+test "CircBuf getPtrSlice fits" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 12);
+ defer buf.deinit(alloc);
+
+ const slices = buf.getPtrSlice(0, 11);
+ try testing.expectEqual(@as(usize, 11), slices[0].len);
+ try testing.expectEqual(@as(usize, 0), slices[1].len);
+ try testing.expectEqual(@as(usize, 11), buf.len());
+}
+
+test "CircBuf getPtrSlice wraps" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 4);
+ defer buf.deinit(alloc);
+
+ // Fill the buffer
+ _ = buf.getPtrSlice(0, buf.capacity());
+ try testing.expect(buf.full);
+ try testing.expectEqual(@as(usize, 4), buf.len());
+
+ // Delete
+ buf.deleteOldest(2);
+ try testing.expect(!buf.full);
+ try testing.expectEqual(@as(usize, 2), buf.len());
+
+ // Get a slice that doesn't grow
+ {
+ const slices = buf.getPtrSlice(0, 2);
+ try testing.expectEqual(@as(usize, 2), slices[0].len);
+ try testing.expectEqual(@as(usize, 0), slices[1].len);
+ try testing.expectEqual(@as(usize, 2), buf.len());
+ slices[0][0] = 1;
+ slices[0][1] = 2;
+ }
+
+ // Get a slice that does grow, and forces wrap
+ {
+ const slices = buf.getPtrSlice(2, 2);
+ try testing.expectEqual(@as(usize, 2), slices[0].len);
+ try testing.expectEqual(@as(usize, 0), slices[1].len);
+ try testing.expectEqual(@as(usize, 4), buf.len());
+
+ // should be empty
+ try testing.expectEqual(@as(u8, 0), slices[0][0]);
+ try testing.expectEqual(@as(u8, 0), slices[0][1]);
+ slices[0][0] = 3;
+ slices[0][1] = 4;
+ }
+
+ // Get a slice across boundaries
+ {
+ const slices = buf.getPtrSlice(0, 4);
+ try testing.expectEqual(@as(usize, 2), slices[0].len);
+ try testing.expectEqual(@as(usize, 2), slices[1].len);
+ try testing.expectEqual(@as(usize, 4), buf.len());
+
+ try testing.expectEqual(@as(u8, 1), slices[0][0]);
+ try testing.expectEqual(@as(u8, 2), slices[0][1]);
+ try testing.expectEqual(@as(u8, 3), slices[1][0]);
+ try testing.expectEqual(@as(u8, 4), slices[1][1]);
+ }
+}
+
+test "CircBuf rotateToZero" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 12);
+ defer buf.deinit(alloc);
+
+ _ = buf.getPtrSlice(0, 11);
+ try buf.rotateToZero();
+}
+
+test "CircBuf rotateToZero offset" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 4);
+ defer buf.deinit(alloc);
+
+ // Fill the buffer
+ _ = buf.getPtrSlice(0, 3);
+ try testing.expectEqual(@as(usize, 3), buf.len());
+
+ // Delete
+ buf.deleteOldest(2);
+ try testing.expect(!buf.full);
+ try testing.expectEqual(@as(usize, 1), buf.len());
+ try testing.expect(buf.tail > 0 and buf.head >= buf.tail);
+
+ // Rotate to zero
+ try buf.rotateToZero();
+ try testing.expectEqual(@as(usize, 0), buf.tail);
+ try testing.expectEqual(@as(usize, 1), buf.head);
+}
+
+test "CircBuf rotateToZero wraps" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 4);
+ defer buf.deinit(alloc);
+
+ // Fill the buffer
+ _ = buf.getPtrSlice(0, 3);
+ try testing.expectEqual(@as(usize, 3), buf.len());
+ try testing.expect(buf.tail == 0 and buf.head == 3);
+
+ // Delete all
+ buf.deleteOldest(3);
+ try testing.expectEqual(@as(usize, 0), buf.len());
+ try testing.expect(buf.tail == 3 and buf.head == 3);
+
+ // Refill to force a wrap
+ {
+ const slices = buf.getPtrSlice(0, 3);
+ slices[0][0] = 1;
+ slices[1][0] = 2;
+ slices[1][1] = 3;
+ try testing.expectEqual(@as(usize, 3), buf.len());
+ try testing.expect(buf.tail == 3 and buf.head == 2);
+ }
+
+ // Rotate to zero
+ try buf.rotateToZero();
+ try testing.expectEqual(@as(usize, 0), buf.tail);
+ try testing.expectEqual(@as(usize, 3), buf.head);
+ {
+ const slices = buf.getPtrSlice(0, 3);
+ try testing.expectEqual(@as(u8, 1), slices[0][0]);
+ try testing.expectEqual(@as(u8, 2), slices[0][1]);
+ try testing.expectEqual(@as(u8, 3), slices[0][2]);
+ }
+}
+
+test "CircBuf rotateToZero full no wrap" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 4);
+ defer buf.deinit(alloc);
+
+ // Fill the buffer
+ _ = buf.getPtrSlice(0, 3);
+
+ // Delete all
+ buf.deleteOldest(3);
+
+ // Refill to force a wrap
+ {
+ const slices = buf.getPtrSlice(0, 4);
+ try testing.expect(buf.full);
+ slices[0][0] = 1;
+ slices[1][0] = 2;
+ slices[1][1] = 3;
+ slices[1][2] = 4;
+ }
+
+ // Rotate to zero
+ try buf.rotateToZero();
+ try testing.expect(buf.full);
+ try testing.expectEqual(@as(usize, 0), buf.tail);
+ try testing.expectEqual(@as(usize, 0), buf.head);
+ {
+ const slices = buf.getPtrSlice(0, 4);
+ try testing.expectEqual(@as(u8, 1), slices[0][0]);
+ try testing.expectEqual(@as(u8, 2), slices[0][1]);
+ try testing.expectEqual(@as(u8, 3), slices[0][2]);
+ try testing.expectEqual(@as(u8, 4), slices[0][3]);
+ }
+}
+
+test "CircBuf resize grow from zero" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 0);
+ defer buf.deinit(alloc);
+ try testing.expect(buf.full);
+
+ // Resize
+ try buf.resize(alloc, 2);
+ try testing.expect(!buf.full);
+ try testing.expectEqual(@as(usize, 0), buf.len());
+ try testing.expectEqual(@as(usize, 2), buf.capacity());
+
+ try buf.append(1);
+ try buf.append(2);
+
+ {
+ const slices = buf.getPtrSlice(0, 2);
+ try testing.expectEqual(@as(u8, 1), slices[0][0]);
+ try testing.expectEqual(@as(u8, 2), slices[0][1]);
+ }
+}
+
+test "CircBuf resize grow" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 4);
+ defer buf.deinit(alloc);
+
+ // Fill and write
+ {
+ const slices = buf.getPtrSlice(0, 4);
+ try testing.expect(buf.full);
+ slices[0][0] = 1;
+ slices[0][1] = 2;
+ slices[0][2] = 3;
+ slices[0][3] = 4;
+ }
+
+ // Resize
+ try buf.resize(alloc, 6);
+ try testing.expect(!buf.full);
+ try testing.expectEqual(@as(usize, 4), buf.len());
+ try testing.expectEqual(@as(usize, 6), buf.capacity());
+
+ {
+ const slices = buf.getPtrSlice(0, 4);
+ try testing.expectEqual(@as(u8, 1), slices[0][0]);
+ try testing.expectEqual(@as(u8, 2), slices[0][1]);
+ try testing.expectEqual(@as(u8, 3), slices[0][2]);
+ try testing.expectEqual(@as(u8, 4), slices[0][3]);
+ }
+}
+
+test "CircBuf resize shrink" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 4);
+ defer buf.deinit(alloc);
+
+ // Fill and write
+ {
+ const slices = buf.getPtrSlice(0, 4);
+ try testing.expect(buf.full);
+ slices[0][0] = 1;
+ slices[0][1] = 2;
+ slices[0][2] = 3;
+ slices[0][3] = 4;
+ }
+
+ // Resize
+ try buf.resize(alloc, 3);
+ try testing.expect(buf.full);
+ try testing.expectEqual(@as(usize, 3), buf.len());
+ try testing.expectEqual(@as(usize, 3), buf.capacity());
+
+ {
+ const slices = buf.getPtrSlice(0, 3);
+ try testing.expectEqual(@as(u8, 1), slices[0][0]);
+ try testing.expectEqual(@as(u8, 2), slices[0][1]);
+ try testing.expectEqual(@as(u8, 3), slices[0][2]);
+ }
+}
+
+test "CircBuf append empty slice" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 5);
+ defer buf.deinit(alloc);
+
+ // Appending an empty slice to empty buffer should be a no-op
+ buf.appendSliceAssumeCapacity("");
+ try testing.expectEqual(@as(usize, 0), buf.len());
+ try testing.expect(!buf.full);
+
+ // Buffer should still work normally after appending empty slice
+ buf.appendSliceAssumeCapacity("hi");
+ try testing.expectEqual(@as(usize, 2), buf.len());
+
+ // Appending an empty slice to non-empty buffer should also be a no-op
+ buf.appendSliceAssumeCapacity("");
+ try testing.expectEqual(@as(usize, 2), buf.len());
+}
+
+test "CircBuf getPtrSlice zero length" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 5);
+ defer buf.deinit(alloc);
+
+ // getPtrSlice with zero length on empty buffer should return empty slices
+ const slices = buf.getPtrSlice(0, 0);
+ try testing.expectEqual(@as(usize, 0), slices[0].len);
+ try testing.expectEqual(@as(usize, 0), slices[1].len);
+ try testing.expectEqual(@as(usize, 0), buf.len());
+
+ // Fill buffer partially
+ buf.appendSliceAssumeCapacity("abc");
+ try testing.expectEqual(@as(usize, 3), buf.len());
+
+ // getPtrSlice with zero length on non-empty buffer should also work
+ const slices2 = buf.getPtrSlice(0, 0);
+ try testing.expectEqual(@as(usize, 0), slices2[0].len);
+ try testing.expectEqual(@as(usize, 0), slices2[1].len);
+ try testing.expectEqual(@as(usize, 3), buf.len());
+}
+
+test "CircBuf deleteOldest zero" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Buf = CircBuf(u8, 0);
+ var buf = try Buf.init(alloc, 5);
+ defer buf.deinit(alloc);
+
+ // deleteOldest(0) on empty buffer should be a no-op
+ buf.deleteOldest(0);
+ try testing.expectEqual(@as(usize, 0), buf.len());
+
+ // Fill buffer
+ buf.appendSliceAssumeCapacity("hello");
+ try testing.expectEqual(@as(usize, 5), buf.len());
+
+ // deleteOldest(0) on non-empty buffer should be a no-op
+ buf.deleteOldest(0);
+ try testing.expectEqual(@as(usize, 5), buf.len());
+
+ // Verify data is unchanged
+ var it = buf.iterator(.forward);
+ try testing.expectEqual(@as(u8, 'h'), it.next().?.*);
+}
diff --git a/vendor/libghostty-vt/src/datastruct/comparison.zig b/vendor/libghostty-vt/src/datastruct/comparison.zig
new file mode 100644
index 00000000..6a862bb9
--- /dev/null
+++ b/vendor/libghostty-vt/src/datastruct/comparison.zig
@@ -0,0 +1,430 @@
+// The contents of this file is largely based on testing.zig from the Zig 0.15.1
+// stdlib, distributed under the MIT license, copyright (c) Zig contributors
+const std = @import("std");
+const testing = std.testing;
+
+/// A deep equality comparison function that works for most types. We
+/// add types as necessary. It defers to `equal` decls on types that support
+/// decls.
+pub fn deepEqual(comptime T: type, old: T, new: T) bool {
+ // Do known named types first
+ switch (T) {
+ inline []const u8,
+ [:0]const u8,
+ => return std.mem.eql(u8, old, new),
+
+ []const [:0]const u8,
+ => {
+ if (old.len != new.len) return false;
+ for (old, new) |a, b| {
+ if (!std.mem.eql(u8, a, b)) return false;
+ }
+
+ return true;
+ },
+
+ else => {},
+ }
+
+ // Back into types of types
+ switch (@typeInfo(T)) {
+ .void => return true,
+
+ inline .bool,
+ .int,
+ .float,
+ .@"enum",
+ => return old == new,
+
+ .optional => |info| {
+ if (old == null and new == null) return true;
+ if (old == null or new == null) return false;
+ return deepEqual(info.child, old.?, new.?);
+ },
+
+ .array => |info| for (old, new) |old_elem, new_elem| {
+ if (!deepEqual(
+ info.child,
+ old_elem,
+ new_elem,
+ )) return false;
+ } else return true,
+
+ .@"struct" => |info| {
+ if (@hasDecl(T, "equal")) return old.equal(new);
+
+ // If a struct doesn't declare an "equal" function, we fall back
+ // to a recursive field-by-field compare.
+ inline for (info.fields) |field_info| {
+ if (!deepEqual(
+ field_info.type,
+ @field(old, field_info.name),
+ @field(new, field_info.name),
+ )) return false;
+ }
+ return true;
+ },
+
+ .@"union" => |info| {
+ if (@hasDecl(T, "equal")) return old.equal(new);
+
+ const tag_type = info.tag_type.?;
+ const old_tag = std.meta.activeTag(old);
+ const new_tag = std.meta.activeTag(new);
+ if (old_tag != new_tag) return false;
+
+ inline for (info.fields) |field_info| {
+ if (@field(tag_type, field_info.name) == old_tag) {
+ return deepEqual(
+ field_info.type,
+ @field(old, field_info.name),
+ @field(new, field_info.name),
+ );
+ }
+ }
+
+ unreachable;
+ },
+
+ else => {
+ @compileLog(T);
+ @compileError("unsupported field type");
+ },
+ }
+}
+
+/// Generic, recursive equality testing utility using approximate comparison for
+/// floats and equality for everything else
+///
+/// Based on `testing.expectEqual` and `testing.expectEqualSlices`.
+///
+/// The relative tolerance is currently hardcoded to `sqrt(eps(float_type))`.
+pub inline fn expectApproxEqual(expected: anytype, actual: anytype) !void {
+ const T = @TypeOf(expected, actual);
+ return expectApproxEqualInner(T, expected, actual);
+}
+
+fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void {
+ switch (@typeInfo(T)) {
+ // check approximate equality for floats
+ .float => {
+ const sqrt_eps = comptime std.math.sqrt(std.math.floatEps(T));
+ if (!std.math.approxEqRel(T, expected, actual, sqrt_eps)) {
+ print("expected approximately {any}, found {any}\n", .{ expected, actual });
+ return error.TestExpectedApproxEqual;
+ }
+ },
+
+ // recurse into containers
+ .array => {
+ const diff_index: usize = diff_index: {
+ const shortest = @min(expected.len, actual.len);
+ var index: usize = 0;
+ while (index < shortest) : (index += 1) {
+ expectApproxEqual(actual[index], expected[index]) catch break :diff_index index;
+ }
+ break :diff_index if (expected.len == actual.len) return else shortest;
+ };
+ print("slices not approximately equal. first significant difference occurs at index {d} (0x{X})\n", .{ diff_index, diff_index });
+ return error.TestExpectedApproxEqual;
+ },
+ .vector => |info| {
+ var i: usize = 0;
+ while (i < info.len) : (i += 1) {
+ expectApproxEqual(expected[i], actual[i]) catch {
+ print("index {d} incorrect. expected approximately {any}, found {any}\n", .{
+ i, expected[i], actual[i],
+ });
+ return error.TestExpectedApproxEqual;
+ };
+ }
+ },
+ .@"struct" => |structType| {
+ inline for (structType.fields) |field| {
+ try expectApproxEqual(@field(expected, field.name), @field(actual, field.name));
+ }
+ },
+
+ // unwrap unions, optionals, and error unions
+ .@"union" => |union_info| {
+ if (union_info.tag_type == null) {
+ // untagged unions can only be compared bitwise,
+ // so expectEqual is all we need
+ testing.expectEqual(expected, actual) catch {
+ return error.TestExpectedApproxEqual;
+ };
+ }
+
+ const Tag = std.meta.Tag(@TypeOf(expected));
+
+ const expectedTag = @as(Tag, expected);
+ const actualTag = @as(Tag, actual);
+
+ testing.expectEqual(expectedTag, actualTag) catch {
+ return error.TestExpectedApproxEqual;
+ };
+
+ // we only reach this switch if the tags are equal
+ switch (expected) {
+ inline else => |val, tag| try expectApproxEqual(val, @field(actual, @tagName(tag))),
+ }
+ },
+ .optional, .error_union => {
+ if (expected) |expected_payload| if (actual) |actual_payload| {
+ return expectApproxEqual(expected_payload, actual_payload);
+ };
+ // we only reach this point if there's at least one null or error,
+ // in which case expectEqual is all we need
+ testing.expectEqual(expected, actual) catch {
+ return error.TestExpectedApproxEqual;
+ };
+ },
+
+ // fall back to expectEqual for everything else
+ else => testing.expectEqual(expected, actual) catch {
+ return error.TestExpectedApproxEqual;
+ },
+ }
+}
+
+/// Copy of testing.print (not public)
+fn print(comptime fmt: []const u8, args: anytype) void {
+ if (@inComptime()) {
+ @compileError(std.fmt.comptimePrint(fmt, args));
+ } else if (testing.backend_can_print) {
+ std.debug.print(fmt, args);
+ }
+}
+
+// Tests based on the `expectEqual` tests in the Zig stdlib
+test "expectApproxEqual.union(enum)" {
+ const T = union(enum) {
+ a: i32,
+ b: f32,
+ };
+
+ const b10 = T{ .b = 10.0 };
+ const b10plus = T{ .b = 10.000001 };
+
+ try expectApproxEqual(b10, b10plus);
+}
+
+test "expectApproxEqual nested array" {
+ const a = [2][2]f32{
+ [_]f32{ 1.0, 0.0 },
+ [_]f32{ 0.0, 1.0 },
+ };
+
+ const b = [2][2]f32{
+ [_]f32{ 1.000001, 0.0 },
+ [_]f32{ 0.0, 0.999999 },
+ };
+
+ try expectApproxEqual(a, b);
+}
+
+test "expectApproxEqual vector" {
+ const a: @Vector(4, f32) = @splat(4.0);
+ const b: @Vector(4, f32) = @splat(4.000001);
+
+ try expectApproxEqual(a, b);
+}
+
+test "expectApproxEqual struct" {
+ const a = .{ 1, @as(f32, 1.0) };
+ const b = .{ 1, @as(f32, 0.999999) };
+
+ try expectApproxEqual(a, b);
+}
+
+test "deepEqual void" {
+ try testing.expect(deepEqual(void, {}, {}));
+}
+
+test "deepEqual bool" {
+ try testing.expect(deepEqual(bool, true, true));
+ try testing.expect(deepEqual(bool, false, false));
+ try testing.expect(!deepEqual(bool, true, false));
+ try testing.expect(!deepEqual(bool, false, true));
+}
+
+test "deepEqual int" {
+ try testing.expect(deepEqual(i32, 42, 42));
+ try testing.expect(deepEqual(i32, -100, -100));
+ try testing.expect(!deepEqual(i32, 42, 43));
+ try testing.expect(deepEqual(u64, 0, 0));
+ try testing.expect(!deepEqual(u64, 0, 1));
+}
+
+test "deepEqual float" {
+ try testing.expect(deepEqual(f32, 1.0, 1.0));
+ try testing.expect(!deepEqual(f32, 1.0, 1.1));
+ try testing.expect(deepEqual(f64, 3.14159, 3.14159));
+ try testing.expect(!deepEqual(f64, 3.14159, 3.14158));
+}
+
+test "deepEqual enum" {
+ const Color = enum { red, green, blue };
+ try testing.expect(deepEqual(Color, .red, .red));
+ try testing.expect(deepEqual(Color, .blue, .blue));
+ try testing.expect(!deepEqual(Color, .red, .green));
+ try testing.expect(!deepEqual(Color, .green, .blue));
+}
+
+test "deepEqual []const u8" {
+ try testing.expect(deepEqual([]const u8, "hello", "hello"));
+ try testing.expect(deepEqual([]const u8, "", ""));
+ try testing.expect(!deepEqual([]const u8, "hello", "world"));
+ try testing.expect(!deepEqual([]const u8, "hello", "hell"));
+ try testing.expect(!deepEqual([]const u8, "hello", "hello!"));
+}
+
+test "deepEqual [:0]const u8" {
+ try testing.expect(deepEqual([:0]const u8, "foo", "foo"));
+ try testing.expect(!deepEqual([:0]const u8, "foo", "bar"));
+ try testing.expect(!deepEqual([:0]const u8, "foo", "fo"));
+}
+
+test "deepEqual []const [:0]const u8" {
+ const a: []const [:0]const u8 = &.{ "one", "two", "three" };
+ const b: []const [:0]const u8 = &.{ "one", "two", "three" };
+ const c: []const [:0]const u8 = &.{ "one", "two" };
+ const d: []const [:0]const u8 = &.{ "one", "two", "four" };
+ const e: []const [:0]const u8 = &.{};
+
+ try testing.expect(deepEqual([]const [:0]const u8, a, b));
+ try testing.expect(!deepEqual([]const [:0]const u8, a, c));
+ try testing.expect(!deepEqual([]const [:0]const u8, a, d));
+ try testing.expect(deepEqual([]const [:0]const u8, e, e));
+ try testing.expect(!deepEqual([]const [:0]const u8, a, e));
+}
+
+test "deepEqual optional" {
+ try testing.expect(deepEqual(?i32, null, null));
+ try testing.expect(deepEqual(?i32, 42, 42));
+ try testing.expect(!deepEqual(?i32, null, 42));
+ try testing.expect(!deepEqual(?i32, 42, null));
+ try testing.expect(!deepEqual(?i32, 42, 43));
+}
+
+test "deepEqual optional nested" {
+ const Nested = struct { x: i32, y: i32 };
+ try testing.expect(deepEqual(?Nested, null, null));
+ try testing.expect(deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 2 }));
+ try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 3 }));
+ try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, null));
+}
+
+test "deepEqual array" {
+ try testing.expect(deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 3 }));
+ try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 4 }));
+ try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 0, 2, 3 }));
+ try testing.expect(deepEqual([0]i32, .{}, .{}));
+}
+
+test "deepEqual nested array" {
+ const a = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } };
+ const b = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } };
+ const c = [2][2]i32{ .{ 1, 2 }, .{ 3, 5 } };
+
+ try testing.expect(deepEqual([2][2]i32, a, b));
+ try testing.expect(!deepEqual([2][2]i32, a, c));
+}
+
+test "deepEqual struct" {
+ const Point = struct { x: i32, y: i32 };
+ try testing.expect(deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 20 }));
+ try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 21 }));
+ try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 11, .y = 20 }));
+}
+
+test "deepEqual struct nested" {
+ const Inner = struct { value: i32 };
+ const Outer = struct { a: Inner, b: Inner };
+
+ const x = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } };
+ const y = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } };
+ const z = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 3 } };
+
+ try testing.expect(deepEqual(Outer, x, y));
+ try testing.expect(!deepEqual(Outer, x, z));
+}
+
+test "deepEqual struct with equal decl" {
+ const Custom = struct {
+ value: i32,
+
+ pub fn equal(self: @This(), other: @This()) bool {
+ return @mod(self.value, 10) == @mod(other.value, 10);
+ }
+ };
+
+ try testing.expect(deepEqual(Custom, .{ .value = 5 }, .{ .value = 15 }));
+ try testing.expect(deepEqual(Custom, .{ .value = 100 }, .{ .value = 0 }));
+ try testing.expect(!deepEqual(Custom, .{ .value = 5 }, .{ .value = 6 }));
+}
+
+test "deepEqual union" {
+ const Value = union(enum) {
+ int: i32,
+ float: f32,
+ none,
+ };
+
+ try testing.expect(deepEqual(Value, .{ .int = 42 }, .{ .int = 42 }));
+ try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .int = 43 }));
+ try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .float = 42.0 }));
+ try testing.expect(deepEqual(Value, .none, .none));
+ try testing.expect(!deepEqual(Value, .none, .{ .int = 0 }));
+}
+
+test "deepEqual union with equal decl" {
+ const Value = union(enum) {
+ num: i32,
+ str: []const u8,
+
+ pub fn equal(self: @This(), other: @This()) bool {
+ return switch (self) {
+ .num => |n| switch (other) {
+ .num => |m| @mod(n, 10) == @mod(m, 10),
+ else => false,
+ },
+ .str => |s| switch (other) {
+ .str => |t| s.len == t.len,
+ else => false,
+ },
+ };
+ }
+ };
+
+ try testing.expect(deepEqual(Value, .{ .num = 5 }, .{ .num = 25 }));
+ try testing.expect(!deepEqual(Value, .{ .num = 5 }, .{ .num = 6 }));
+ try testing.expect(deepEqual(Value, .{ .str = "abc" }, .{ .str = "xyz" }));
+ try testing.expect(!deepEqual(Value, .{ .str = "abc" }, .{ .str = "ab" }));
+}
+
+test "deepEqual array of structs" {
+ const Item = struct { id: i32, name: []const u8 };
+ const a = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } };
+ const b = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } };
+ const c = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "TWO" } };
+
+ try testing.expect(deepEqual([2]Item, a, b));
+ try testing.expect(!deepEqual([2]Item, a, c));
+}
+
+test "deepEqual struct with optional field" {
+ const Config = struct { name: []const u8, port: ?u16 };
+
+ try testing.expect(deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8080 }));
+ try testing.expect(deepEqual(Config, .{ .name = "app", .port = null }, .{ .name = "app", .port = null }));
+ try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = null }));
+ try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8081 }));
+}
+
+test "deepEqual struct with array field" {
+ const Data = struct { values: [3]i32 };
+
+ try testing.expect(deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 3 } }));
+ try testing.expect(!deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 4 } }));
+}
diff --git a/vendor/libghostty-vt/src/datastruct/intrusive_linked_list.zig b/vendor/libghostty-vt/src/datastruct/intrusive_linked_list.zig
new file mode 100644
index 00000000..734b82ff
--- /dev/null
+++ b/vendor/libghostty-vt/src/datastruct/intrusive_linked_list.zig
@@ -0,0 +1,181 @@
+const std = @import("std");
+const testing = std.testing;
+
+/// An intrusive doubly-linked list. The type T must have a "next" and "prev"
+/// field pointing to itself.
+///
+/// This is an adaptation of the DoublyLinkedList from the Zig standard
+/// library, which is MIT licensed. I've removed functionality that I don't
+/// need.
+pub fn DoublyLinkedList(comptime T: type) type {
+ return struct {
+ const Self = @This();
+
+ /// The type of the node in the list. This makes it easy to get the
+ /// node type from the list type.
+ pub const Node = T;
+
+ first: ?*Node = null,
+ last: ?*Node = null,
+
+ /// Insert a new node after an existing one.
+ ///
+ /// Arguments:
+ /// node: Pointer to a node in the list.
+ /// new_node: Pointer to the new node to insert.
+ pub inline fn insertAfter(list: *Self, node: *Node, new_node: *Node) void {
+ new_node.prev = node;
+ if (node.next) |next_node| {
+ // Intermediate node.
+ new_node.next = next_node;
+ next_node.prev = new_node;
+ } else {
+ // Last element of the list.
+ new_node.next = null;
+ list.last = new_node;
+ }
+ node.next = new_node;
+ }
+
+ /// Insert a new node before an existing one.
+ ///
+ /// Arguments:
+ /// node: Pointer to a node in the list.
+ /// new_node: Pointer to the new node to insert.
+ pub inline fn insertBefore(list: *Self, node: *Node, new_node: *Node) void {
+ new_node.next = node;
+ if (node.prev) |prev_node| {
+ // Intermediate node.
+ new_node.prev = prev_node;
+ prev_node.next = new_node;
+ } else {
+ // First element of the list.
+ new_node.prev = null;
+ list.first = new_node;
+ }
+ node.prev = new_node;
+ }
+
+ /// Insert a new node at the end of the list.
+ ///
+ /// Arguments:
+ /// new_node: Pointer to the new node to insert.
+ pub inline fn append(list: *Self, new_node: *Node) void {
+ if (list.last) |last| {
+ // Insert after last.
+ list.insertAfter(last, new_node);
+ } else {
+ // Empty list.
+ list.prepend(new_node);
+ }
+ }
+
+ /// Insert a new node at the beginning of the list.
+ ///
+ /// Arguments:
+ /// new_node: Pointer to the new node to insert.
+ pub inline fn prepend(list: *Self, new_node: *Node) void {
+ if (list.first) |first| {
+ // Insert before first.
+ list.insertBefore(first, new_node);
+ } else {
+ // Empty list.
+ list.first = new_node;
+ list.last = new_node;
+ new_node.prev = null;
+ new_node.next = null;
+ }
+ }
+
+ /// Remove a node from the list.
+ ///
+ /// Arguments:
+ /// node: Pointer to the node to be removed.
+ pub inline fn remove(list: *Self, node: *Node) void {
+ if (node.prev) |prev_node| {
+ // Intermediate node.
+ prev_node.next = node.next;
+ } else {
+ // First element of the list.
+ list.first = node.next;
+ }
+
+ if (node.next) |next_node| {
+ // Intermediate node.
+ next_node.prev = node.prev;
+ } else {
+ // Last element of the list.
+ list.last = node.prev;
+ }
+ }
+
+ /// Remove and return the last node in the list.
+ ///
+ /// Returns:
+ /// A pointer to the last node in the list.
+ pub inline fn pop(list: *Self) ?*Node {
+ const last = list.last orelse return null;
+ list.remove(last);
+ return last;
+ }
+
+ /// Remove and return the first node in the list.
+ ///
+ /// Returns:
+ /// A pointer to the first node in the list.
+ pub inline fn popFirst(list: *Self) ?*Node {
+ const first = list.first orelse return null;
+ list.remove(first);
+ return first;
+ }
+ };
+}
+
+test "basic DoublyLinkedList test" {
+ const Node = struct {
+ data: u32,
+ prev: ?*@This() = null,
+ next: ?*@This() = null,
+ };
+ const L = DoublyLinkedList(Node);
+ var list: L = .{};
+
+ var one: Node = .{ .data = 1 };
+ var two: Node = .{ .data = 2 };
+ var three: Node = .{ .data = 3 };
+ var four: Node = .{ .data = 4 };
+ var five: Node = .{ .data = 5 };
+
+ list.append(&two); // {2}
+ list.append(&five); // {2, 5}
+ list.prepend(&one); // {1, 2, 5}
+ list.insertBefore(&five, &four); // {1, 2, 4, 5}
+ list.insertAfter(&two, &three); // {1, 2, 3, 4, 5}
+
+ // Traverse forwards.
+ {
+ var it = list.first;
+ var index: u32 = 1;
+ while (it) |node| : (it = node.next) {
+ try testing.expect(node.data == index);
+ index += 1;
+ }
+ }
+
+ // Traverse backwards.
+ {
+ var it = list.last;
+ var index: u32 = 1;
+ while (it) |node| : (it = node.prev) {
+ try testing.expect(node.data == (6 - index));
+ index += 1;
+ }
+ }
+
+ _ = list.popFirst(); // {2, 3, 4, 5}
+ _ = list.pop(); // {2, 3, 4}
+ list.remove(&three); // {2, 4}
+
+ try testing.expect(list.first.?.data == 2);
+ try testing.expect(list.last.?.data == 4);
+}
diff --git a/vendor/libghostty-vt/src/datastruct/lru.zig b/vendor/libghostty-vt/src/datastruct/lru.zig
new file mode 100644
index 00000000..83d2cf8e
--- /dev/null
+++ b/vendor/libghostty-vt/src/datastruct/lru.zig
@@ -0,0 +1,349 @@
+const std = @import("std");
+const assert = @import("../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+
+/// Create a HashMap for a key type that can be automatically hashed.
+/// If you want finer-grained control, use HashMap directly.
+pub fn AutoHashMap(comptime K: type, comptime V: type) type {
+ return HashMap(
+ K,
+ V,
+ std.hash_map.AutoContext(K),
+ std.hash_map.default_max_load_percentage,
+ );
+}
+
+/// HashMap implementation that supports least-recently-used eviction.
+///
+/// Beware of the Zig bug where a hashmap gets slower over time
+/// (https://github.com/ziglang/zig/issues/17851). This LRU uses a hashmap
+/// and evictions will cause this issue to appear. Callers should keep
+/// track of eviction counts and periodically reinitialize the LRU to
+/// avoid this issue. The LRU itself can't do this because it doesn't
+/// know how to free values.
+///
+/// Note: This is a really elementary CS101 version of an LRU right now.
+/// This is done initially to get something working. Once we have it working,
+/// we can benchmark and improve if this ends up being a source of slowness.
+pub fn HashMap(
+ comptime K: type,
+ comptime V: type,
+ comptime Context: type,
+ comptime max_load_percentage: u64,
+) type {
+ return struct {
+ const Self = @This();
+ const Queue = std.DoublyLinkedList;
+ const Map = std.HashMapUnmanaged(
+ K,
+ *Entry,
+ Context,
+ max_load_percentage,
+ );
+
+ /// Map to maintain our entries.
+ map: Map,
+
+ /// Queue to maintain LRU order.
+ queue: Queue,
+
+ /// The capacity of our map. If this capacity is reached, cache
+ /// misses will begin evicting entries.
+ capacity: Map.Size,
+
+ const Entry = struct {
+ data: KV,
+ node: Queue.Node,
+
+ fn fromNode(node: *Queue.Node) *Entry {
+ return @fieldParentPtr("node", node);
+ }
+ };
+
+ pub const KV = struct {
+ key: K,
+ value: V,
+ };
+
+ /// The result of a getOrPut operation.
+ pub const GetOrPutResult = struct {
+ /// The entry that was retrieved. If found_existing is false,
+ /// then this is a pointer to allocated space to store a V.
+ /// If found_existing is true, the pointer value is valid, but
+ /// can be overwritten.
+ value_ptr: *V,
+
+ /// Whether an existing value was found or not.
+ found_existing: bool,
+
+ /// If another entry had to be evicted to make space for this
+ /// put operation, then this is the value that was evicted.
+ evicted: ?KV,
+ };
+
+ pub fn init(capacity: Map.Size) Self {
+ return .{
+ .map = .{},
+ .queue = .{},
+ .capacity = capacity,
+ };
+ }
+
+ pub fn deinit(self: *Self, alloc: Allocator) void {
+ // Important: use our queue as a source of truth for dealloc
+ // because we might keep items in the queue around that aren't
+ // present in our LRU anymore to prevent future allocations.
+ var it = self.queue.first;
+ while (it) |node| {
+ it = node.next;
+ alloc.destroy(Entry.fromNode(node));
+ }
+
+ self.map.deinit(alloc);
+ self.* = undefined;
+ }
+
+ /// Get or put a value for a key. See GetOrPutResult on how to check
+ /// if an existing value was found, if an existing value was evicted,
+ /// etc.
+ pub fn getOrPut(self: *Self, allocator: Allocator, key: K) Allocator.Error!GetOrPutResult {
+ if (@sizeOf(Context) != 0)
+ @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call getOrPutContext instead.");
+ return self.getOrPutContext(allocator, key, undefined);
+ }
+
+ /// See getOrPut
+ pub fn getOrPutContext(
+ self: *Self,
+ alloc: Allocator,
+ key: K,
+ ctx: Context,
+ ) Allocator.Error!GetOrPutResult {
+ const map_gop = try self.map.getOrPutContext(alloc, key, ctx);
+ if (map_gop.found_existing) {
+ // Move to end to mark as most recently used
+ self.queue.remove(&map_gop.value_ptr.*.node);
+ self.queue.append(&map_gop.value_ptr.*.node);
+
+ return GetOrPutResult{
+ .found_existing = true,
+ .value_ptr = &map_gop.value_ptr.*.data.value,
+ .evicted = null,
+ };
+ }
+ errdefer _ = self.map.remove(key);
+
+ // We're evicting if our map insertion increased our capacity.
+ const evict = self.map.count() > self.capacity;
+
+ // Get our entry. If we're not evicting then we allocate a new
+ // entry. If we are evicting then we avoid allocation by just
+ // reusing the entry we would've evicted.
+ const entry: *Entry = if (!evict) try alloc.create(Entry) else entry: {
+ // Our first node is the least recently used.
+ const least_used_node = self.queue.popFirst().?;
+ const least_used_entry: *Entry = .fromNode(least_used_node);
+
+ // Remove the least used from the map
+ _ = self.map.remove(least_used_entry.data.key);
+
+ break :entry least_used_entry;
+ };
+ errdefer if (!evict) alloc.destroy(entry);
+
+ // Store our entry in the map.
+ map_gop.value_ptr.* = entry;
+
+ // Mark the entry as most recently used
+ self.queue.append(&entry.node);
+
+ // Set our key
+ entry.data.key = key;
+
+ return .{
+ .found_existing = map_gop.found_existing,
+ .value_ptr = &entry.data.value,
+ .evicted = if (!evict) null else entry.data,
+ };
+ }
+
+ /// Get a value for a key.
+ pub fn get(self: *const Self, key: K) ?V {
+ if (@sizeOf(Context) != 0) {
+ @compileError("getContext must be used.");
+ }
+ return self.getContext(key, undefined);
+ }
+
+ /// See get
+ pub fn getContext(self: *const Self, key: K, ctx: Context) ?V {
+ const node = self.map.getContext(key, ctx) orelse return null;
+ return node.data.value;
+ }
+
+ /// Resize the LRU. If this shrinks the LRU then LRU items will be
+ /// deallocated. The deallocated items are returned in the slice. This
+ /// slice must be freed by the caller.
+ pub fn resize(self: *Self, alloc: Allocator, capacity: Map.Size) Allocator.Error!?[]V {
+ // Fastest
+ if (capacity >= self.capacity) {
+ self.capacity = capacity;
+ return null;
+ }
+
+ // If we're shrinking but we're smaller than the new capacity,
+ // then we don't have to do anything.
+ if (self.map.count() <= capacity) {
+ self.capacity = capacity;
+ return null;
+ }
+
+ // We're shrinking and we have more items than the new capacity
+ const delta = self.map.count() - capacity;
+ var evicted = try alloc.alloc(V, delta);
+
+ var i: Map.Size = 0;
+ while (i < delta) : (i += 1) {
+ const node = self.queue.popFirst().?;
+ const entry: *Entry = .fromNode(node);
+ evicted[i] = entry.data.value;
+ self.queue.remove(node);
+ _ = self.map.remove(entry.data.key);
+ alloc.destroy(entry);
+ }
+
+ self.capacity = capacity;
+ assert(self.map.count() == capacity);
+
+ return evicted;
+ }
+ };
+}
+
+test "getOrPut" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Map = AutoHashMap(u32, u8);
+ var m = Map.init(2);
+ defer m.deinit(alloc);
+
+ // Insert cap values, should be hits
+ {
+ const gop = try m.getOrPut(alloc, 1);
+ try testing.expect(!gop.found_existing);
+ try testing.expect(gop.evicted == null);
+ gop.value_ptr.* = 1;
+ }
+ {
+ const gop = try m.getOrPut(alloc, 2);
+ try testing.expect(!gop.found_existing);
+ try testing.expect(gop.evicted == null);
+ gop.value_ptr.* = 2;
+ }
+
+ // 1 is LRU
+ try testing.expect((try m.getOrPut(alloc, 1)).found_existing);
+ try testing.expect((try m.getOrPut(alloc, 2)).found_existing);
+
+ // Next should evict
+ {
+ const gop = try m.getOrPut(alloc, 3);
+ try testing.expect(!gop.found_existing);
+ try testing.expect(gop.evicted != null);
+ try testing.expect(gop.evicted.?.value == 1);
+ gop.value_ptr.* = 3;
+ }
+
+ // Currently: 2 is LRU, let's make 3 LRU
+ try testing.expect((try m.getOrPut(alloc, 2)).found_existing);
+
+ // Next should evict
+ {
+ const gop = try m.getOrPut(alloc, 4);
+ try testing.expect(!gop.found_existing);
+ try testing.expect(gop.evicted != null);
+ try testing.expect(gop.evicted.?.value == 3);
+ gop.value_ptr.* = 4;
+ }
+}
+
+test "get" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Map = AutoHashMap(u32, u8);
+ var m = Map.init(2);
+ defer m.deinit(alloc);
+
+ // Insert cap values, should be hits
+ {
+ const gop = try m.getOrPut(alloc, 1);
+ try testing.expect(!gop.found_existing);
+ try testing.expect(gop.evicted == null);
+ gop.value_ptr.* = 1;
+ }
+
+ try testing.expect(m.get(1) != null);
+ try testing.expect(m.get(1).? == 1);
+ try testing.expect(m.get(2) == null);
+}
+
+test "resize shrink without removal" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Map = AutoHashMap(u32, u8);
+ var m = Map.init(2);
+ defer m.deinit(alloc);
+
+ // Insert cap values, LRU is 1
+ {
+ const gop = try m.getOrPut(alloc, 1);
+ try testing.expect(!gop.found_existing);
+ try testing.expect(gop.evicted == null);
+ gop.value_ptr.* = 1;
+ }
+
+ // Shrink
+ const evicted = try m.resize(alloc, 1);
+ try testing.expect(evicted == null);
+ {
+ const gop = try m.getOrPut(alloc, 1);
+ try testing.expect(gop.found_existing);
+ }
+}
+
+test "resize shrink and remove" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Map = AutoHashMap(u32, u8);
+ var m = Map.init(2);
+ defer m.deinit(alloc);
+
+ // Insert cap values, LRU is 1
+ {
+ const gop = try m.getOrPut(alloc, 1);
+ try testing.expect(!gop.found_existing);
+ try testing.expect(gop.evicted == null);
+ gop.value_ptr.* = 1;
+ }
+ {
+ const gop = try m.getOrPut(alloc, 2);
+ try testing.expect(!gop.found_existing);
+ try testing.expect(gop.evicted == null);
+ gop.value_ptr.* = 2;
+ }
+
+ // Shrink
+ const evicted = try m.resize(alloc, 1);
+ defer alloc.free(evicted.?);
+ try testing.expectEqual(@as(usize, 1), evicted.?.len);
+ {
+ const gop = try m.getOrPut(alloc, 1);
+ try testing.expect(!gop.found_existing);
+ try testing.expect(gop.evicted.?.value == 2);
+ gop.value_ptr.* = 1;
+ }
+}
diff --git a/vendor/libghostty-vt/src/datastruct/main.zig b/vendor/libghostty-vt/src/datastruct/main.zig
new file mode 100644
index 00000000..bfee2342
--- /dev/null
+++ b/vendor/libghostty-vt/src/datastruct/main.zig
@@ -0,0 +1,24 @@
+//! The datastruct package contains data structures or anything closely
+//! related to data structures.
+
+const blocking_queue = @import("blocking_queue.zig");
+const cache_table = @import("cache_table.zig");
+const circ_buf = @import("circ_buf.zig");
+const intrusive_linked_list = @import("intrusive_linked_list.zig");
+const segmented_pool = @import("segmented_pool.zig");
+const split_tree = @import("split_tree.zig");
+
+pub const lru = @import("lru.zig");
+pub const BlockingQueue = blocking_queue.BlockingQueue;
+pub const CacheTable = cache_table.CacheTable;
+pub const CircBuf = circ_buf.CircBuf;
+pub const IntrusiveDoublyLinkedList = intrusive_linked_list.DoublyLinkedList;
+pub const MessageData = @import("message_data.zig").MessageData;
+pub const SegmentedPool = segmented_pool.SegmentedPool;
+pub const SplitTree = split_tree.SplitTree;
+
+test {
+ @import("std").testing.refAllDecls(@This());
+
+ _ = @import("comparison.zig");
+}
diff --git a/vendor/libghostty-vt/src/datastruct/message_data.zig b/vendor/libghostty-vt/src/datastruct/message_data.zig
new file mode 100644
index 00000000..3e5cdae6
--- /dev/null
+++ b/vendor/libghostty-vt/src/datastruct/message_data.zig
@@ -0,0 +1,124 @@
+const std = @import("std");
+const assert = @import("../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+
+/// Creates a union that can be used to accommodate data that fit within an array,
+/// are a stable pointer, or require deallocation. This is helpful for thread
+/// messaging utilities.
+pub fn MessageData(comptime Elem: type, comptime small_size: comptime_int) type {
+ return union(enum) {
+ pub const Self = @This();
+
+ pub const Small = struct {
+ pub const Max = small_size;
+ pub const Array = [Max]Elem;
+ pub const Len = std.math.IntFittingRange(0, small_size);
+ data: Array = undefined,
+ len: Len = 0,
+ };
+
+ pub const Alloc = struct {
+ alloc: Allocator,
+ data: []Elem,
+ };
+
+ pub const Stable = []const Elem;
+
+ /// A small write where the data fits into this union size.
+ small: Small,
+
+ /// A stable pointer so we can just pass the slice directly through.
+ /// This is useful i.e. for const data.
+ stable: Stable,
+
+ /// Allocated and must be freed with the provided allocator. This
+ /// should be rarely used.
+ alloc: Alloc,
+
+ /// Initializes the union for a given data type. This will
+ /// attempt to fit into a small value if possible, otherwise
+ /// will allocate and put into alloc.
+ ///
+ /// This can't and will never detect stable pointers.
+ pub fn init(alloc: Allocator, data: anytype) !Self {
+ switch (@typeInfo(@TypeOf(data))) {
+ .pointer => |info| {
+ assert(info.size == .slice);
+ assert(info.child == Elem);
+
+ // If it fits in our small request, do that.
+ if (data.len <= Small.Max) {
+ var buf: Small.Array = undefined;
+ @memcpy(buf[0..data.len], data);
+ return Self{
+ .small = .{
+ .data = buf,
+ .len = @intCast(data.len),
+ },
+ };
+ }
+
+ // Otherwise, allocate
+ const buf = try alloc.dupe(Elem, data);
+ errdefer alloc.free(buf);
+ return Self{
+ .alloc = .{
+ .alloc = alloc,
+ .data = buf,
+ },
+ };
+ },
+
+ else => unreachable,
+ }
+ }
+
+ pub fn deinit(self: Self) void {
+ switch (self) {
+ .small, .stable => {},
+ .alloc => |v| v.alloc.free(v.data),
+ }
+ }
+
+ /// Returns a const slice of the data pointed to by this request.
+ pub fn slice(self: *const Self) []const Elem {
+ return switch (self.*) {
+ .small => |*v| v.data[0..v.len],
+ .stable => |v| v,
+ .alloc => |v| v.data,
+ };
+ }
+ };
+}
+
+test "MessageData init small" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Data = MessageData(u8, 10);
+ const input = "hello!";
+ const io = try Data.init(alloc, @as([]const u8, input));
+ try testing.expect(io == .small);
+}
+
+test "MessageData init alloc" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const Data = MessageData(u8, 10);
+ const input = "hello! " ** 100;
+ const io = try Data.init(alloc, @as([]const u8, input));
+ try testing.expect(io == .alloc);
+ io.alloc.alloc.free(io.alloc.data);
+}
+
+test "MessageData small fits non-u8 sized data" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const len = 500;
+ const Data = MessageData(u8, len);
+ const input: []const u8 = "X" ** len;
+ const io = try Data.init(alloc, input);
+ try testing.expect(io == .small);
+}
diff --git a/vendor/libghostty-vt/src/datastruct/segmented_pool.zig b/vendor/libghostty-vt/src/datastruct/segmented_pool.zig
new file mode 100644
index 00000000..328eb239
--- /dev/null
+++ b/vendor/libghostty-vt/src/datastruct/segmented_pool.zig
@@ -0,0 +1,95 @@
+const std = @import("std");
+const assert = @import("../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const testing = std.testing;
+
+/// A data structure where you can get stable (never copied) pointers to
+/// a type that automatically grows if necessary. The values can be "put back"
+/// but are expected to be put back IN ORDER.
+///
+/// This is implemented specifically for libuv write requests, since the
+/// write requests must have a stable pointer and are guaranteed to be processed
+/// in order for a single stream.
+///
+/// This is NOT thread safe.
+pub fn SegmentedPool(comptime T: type, comptime prealloc: usize) type {
+ return struct {
+ const Self = @This();
+
+ i: usize = 0,
+ available: usize = prealloc,
+ list: std.SegmentedList(T, prealloc) = .{ .len = prealloc },
+
+ pub fn deinit(self: *Self, alloc: Allocator) void {
+ self.list.deinit(alloc);
+ self.* = undefined;
+ }
+
+ /// Get the next available value out of the list. This will not
+ /// grow the list.
+ pub fn get(self: *Self) !*T {
+ // Error to not have any
+ if (self.available == 0) return error.OutOfValues;
+
+ // The index we grab is just i % len, so we wrap around to the front.
+ const i = @mod(self.i, self.list.len);
+ self.i +%= 1; // Wrapping addition to swe go back to 0
+ self.available -= 1;
+ return self.list.at(i);
+ }
+
+ /// Get the next available value out of the list and grow the list
+ /// if necessary.
+ pub fn getGrow(self: *Self, alloc: Allocator) !*T {
+ if (self.available == 0) try self.grow(alloc);
+ return try self.get();
+ }
+
+ fn grow(self: *Self, alloc: Allocator) !void {
+ try self.list.growCapacity(alloc, self.list.len * 2);
+ self.i = self.list.len;
+ self.available = self.list.len;
+ self.list.len *= 2;
+ }
+
+ /// Put a value back. The value put back is expected to be the
+ /// in order of get.
+ pub fn put(self: *Self) void {
+ self.available += 1;
+ assert(self.available <= self.list.len);
+ }
+ };
+}
+
+test "SegmentedPool" {
+ var list: SegmentedPool(u8, 2) = .{};
+ defer list.deinit(testing.allocator);
+ try testing.expectEqual(@as(usize, 2), list.available);
+
+ // Get to capacity
+ const v1 = try list.get();
+ const v2 = try list.get();
+ try testing.expect(v1 != v2);
+ try testing.expectError(error.OutOfValues, list.get());
+
+ // Test writing for later
+ v1.* = 42;
+
+ // Put a value back
+ list.put();
+ const temp = try list.get();
+ try testing.expect(v1 == temp);
+ try testing.expect(temp.* == 42);
+ try testing.expectError(error.OutOfValues, list.get());
+
+ // Grow
+ const v3 = try list.getGrow(testing.allocator);
+ try testing.expect(v1 != v3 and v2 != v3);
+ _ = try list.get();
+ try testing.expectError(error.OutOfValues, list.get());
+
+ // Put a value back
+ list.put();
+ try testing.expect(v1 == try list.get());
+ try testing.expectError(error.OutOfValues, list.get());
+}
diff --git a/vendor/libghostty-vt/src/datastruct/split_tree.zig b/vendor/libghostty-vt/src/datastruct/split_tree.zig
new file mode 100644
index 00000000..c7da8ad9
--- /dev/null
+++ b/vendor/libghostty-vt/src/datastruct/split_tree.zig
@@ -0,0 +1,2367 @@
+const std = @import("std");
+const assert = @import("../quirks.zig").inlineAssert;
+const build_config = @import("../build_config.zig");
+const ArenaAllocator = std.heap.ArenaAllocator;
+const Allocator = std.mem.Allocator;
+
+/// SplitTree represents a tree of view types that can be divided.
+///
+/// Concretely for Ghostty, it represents a tree of terminal views. In
+/// its basic state, there are no splits and it is a single full-sized
+/// terminal. However, it can be split arbitrarily many times among two
+/// axes (horizontal and vertical) to create a tree of terminal views.
+///
+/// This is an immutable tree structure, meaning all operations on it
+/// will return a new tree with the operation applied. This allows us to
+/// store versions of the tree in a history for easy undo/redo. To facilitate
+/// this, the stored View type must implement reference counting; this is left
+/// as an implementation detail of the View type.
+///
+/// The View type will be stored as a pointer within the tree and must
+/// implement a number of functions to work properly:
+///
+/// - `fn ref(*View, Allocator) Allocator.Error!*View` - Increase a
+/// reference count of the view. The Allocator will be the allocator provided
+/// to the tree operation. This is allowed to copy the value if it wants to;
+/// the returned value is expected to be a new reference (but that may
+/// just be a copy).
+///
+/// - `fn unref(*View, Allocator) void` - Decrease the reference count of a
+/// view. The Allocator will be the allocator provided to the tree
+/// operation.
+///
+/// - `fn eql(*const View, *const View) bool` - Check if two views are equal.
+///
+/// Optionally the following functions can also be implemented:
+///
+/// - `fn splitTreeLabel(*const View) []const u8` - Return a label that is used
+/// for the debug view. If this isn't specified then the node handle
+/// will be used.
+///
+/// Note: for both the ref and unref functions, the allocator is optional.
+/// If the functions take less arguments, then the allocator will not be
+/// passed.
+pub fn SplitTree(comptime V: type) type {
+ return struct {
+ const Self = @This();
+
+ /// The view that this tree contains.
+ pub const View = V;
+
+ /// The arena allocator used for all allocations in the tree.
+ /// Since the tree is an immutable structure, this lets us
+ /// cleanly free all memory when the tree is deinitialized.
+ arena: ArenaAllocator,
+
+ /// All the nodes in the tree. Node at index 0 is always the root.
+ nodes: []const Node,
+
+ /// The handle of the zoomed node. A "zoomed" node is one that is
+ /// expected to be made the full size of the split tree. Various
+ /// operations may unzoom (e.g. resize).
+ zoomed: ?Node.Handle,
+
+ /// An empty tree.
+ pub const empty: Self = .{
+ // Arena can be undefined because we have zero allocated nodes.
+ // If our nodes are empty our deinit function doesn't touch the
+ // arena.
+ .arena = undefined,
+ .nodes = &.{},
+ .zoomed = null,
+ };
+
+ pub const Node = union(enum) {
+ leaf: *View,
+ split: Split,
+
+ /// A handle into the nodes array. This lets us keep track of
+ /// nodes with 16-bit handles rather than full pointer-width
+ /// values.
+ pub const Handle = enum(Backing) {
+ root = 0,
+ _,
+
+ pub const Backing = u16;
+
+ pub inline fn idx(self: Handle) usize {
+ return @intFromEnum(self);
+ }
+
+ /// Offset the handle by a given amount.
+ pub fn offset(self: Handle, v: usize) Handle {
+ const self_usize: usize = @intCast(@intFromEnum(self));
+ const final = self_usize + v;
+ assert(final < std.math.maxInt(Backing));
+ return @enumFromInt(final);
+ }
+ };
+ };
+
+ pub const Split = struct {
+ layout: Layout,
+ ratio: f16,
+ left: Node.Handle,
+ right: Node.Handle,
+
+ pub const Layout = enum { horizontal, vertical };
+ pub const Direction = enum { left, right, down, up };
+ };
+
+ /// Initialize a new tree with a single view.
+ pub fn init(gpa: Allocator, view: *View) Allocator.Error!Self {
+ var arena = ArenaAllocator.init(gpa);
+ errdefer arena.deinit();
+ const alloc = arena.allocator();
+
+ const nodes = try alloc.alloc(Node, 1);
+ nodes[0] = .{ .leaf = try viewRef(view, gpa) };
+ errdefer viewUnref(view, gpa);
+
+ return .{
+ .arena = arena,
+ .nodes = nodes,
+ .zoomed = null,
+ };
+ }
+
+ pub fn deinit(self: *Self) void {
+ // Important: only free memory if we have memory to free,
+ // because we use an undefined arena for empty trees.
+ if (self.nodes.len > 0) {
+ // Unref all our views
+ const gpa: Allocator = self.arena.child_allocator;
+ for (self.nodes) |node| switch (node) {
+ .leaf => |view| viewUnref(view, gpa),
+ .split => {},
+ };
+ self.arena.deinit();
+ }
+
+ self.* = undefined;
+ }
+
+ /// Clone this tree, returning a new tree with the same nodes.
+ pub fn clone(self: *const Self, gpa: Allocator) Allocator.Error!Self {
+ // If we're empty then return an empty tree.
+ if (self.isEmpty()) return .empty;
+
+ // Create a new arena allocator for the clone.
+ var arena = ArenaAllocator.init(gpa);
+ errdefer arena.deinit();
+ const alloc = arena.allocator();
+
+ // Allocate a new nodes array and copy the existing nodes into it.
+ const nodes = try alloc.dupe(Node, self.nodes);
+
+ // Increase the reference count of all the views in the nodes.
+ try refNodes(gpa, nodes);
+
+ return .{
+ .arena = arena,
+ .nodes = nodes,
+ .zoomed = self.zoomed,
+ };
+ }
+
+ /// Returns true if this is an empty tree.
+ pub fn isEmpty(self: *const Self) bool {
+ // An empty tree has no nodes.
+ return self.nodes.len == 0;
+ }
+
+ /// Returns true if this tree has more than one split (i.e., the root
+ /// is a split node). This is useful for determining if actions like
+ /// resize_split or toggle_split_zoom are performable.
+ pub fn isSplit(self: *const Self) bool {
+ // An empty tree is not split.
+ if (self.isEmpty()) return false;
+ // The root node is at index 0. If it's a split, we have multiple splits.
+ return switch (self.nodes[0]) {
+ .split => true,
+ .leaf => false,
+ };
+ }
+
+ /// An iterator over all the views in the tree.
+ pub fn iterator(
+ self: *const Self,
+ ) Iterator {
+ return .{ .nodes = self.nodes };
+ }
+
+ pub const ViewEntry = struct {
+ handle: Node.Handle,
+ view: *View,
+ };
+
+ pub const Iterator = struct {
+ i: Node.Handle = .root,
+ nodes: []const Node,
+
+ pub fn next(self: *Iterator) ?ViewEntry {
+ // If we have no nodes, return null.
+ if (@intFromEnum(self.i) >= self.nodes.len) return null;
+
+ // Get the current node and increment the index.
+ const handle = self.i;
+ self.i = @enumFromInt(handle.idx() + 1);
+ const node = self.nodes[handle.idx()];
+
+ return switch (node) {
+ .leaf => |v| .{ .handle = handle, .view = v },
+ .split => self.next(),
+ };
+ }
+ };
+
+ /// Change the zoomed state to the given node. Assumes the handle
+ /// is valid.
+ pub fn zoom(self: *Self, handle: ?Node.Handle) void {
+ if (handle) |v| {
+ assert(@intFromEnum(v) >= 0);
+ assert(@intFromEnum(v) < self.nodes.len);
+ }
+ self.zoomed = handle;
+ }
+
+ pub const Goto = union(enum) {
+ /// Previous view, null if we're the first view.
+ previous,
+
+ /// Next view, null if we're the last view.
+ next,
+
+ /// Previous view, but wrapped around to the last view. May
+ /// return the same view if this is the first view.
+ previous_wrapped,
+
+ /// Next view, but wrapped around to the first view. May return
+ /// the same view if this is the last view.
+ next_wrapped,
+
+ /// A spatial direction. "Spatial" means that the direction is
+ /// based on the nearest surface in the given direction visually
+ /// as the surfaces are laid out on a 2D grid.
+ spatial: Spatial.Direction,
+ };
+
+ /// Goto a view from a certain point in the split tree. Returns null
+ /// if the direction results in no visitable view.
+ ///
+ /// Allocator is only used for temporary state for spatial navigation.
+ pub fn goto(
+ self: *const Self,
+ alloc: Allocator,
+ from: Node.Handle,
+ to: Goto,
+ ) Allocator.Error!?Node.Handle {
+ return switch (to) {
+ .previous => self.previous(from),
+ .next => self.next(from),
+ .previous_wrapped => self.previous(from) orelse self.deepest(.right, .root),
+ .next_wrapped => self.next(from) orelse self.deepest(.left, .root),
+ .spatial => |d| spatial: {
+ // Get our spatial representation.
+ var sp = try self.spatial(alloc);
+ defer sp.deinit(alloc);
+ break :spatial self.nearestWrapped(sp, from, d);
+ },
+ };
+ }
+
+ pub const Side = enum { left, right };
+
+ /// Returns the deepest view in the tree in the given direction.
+ /// This can be used to find the leftmost/rightmost surface within
+ /// a given split structure.
+ pub fn deepest(
+ self: *const Self,
+ side: Side,
+ from: Node.Handle,
+ ) Node.Handle {
+ var current: Node.Handle = from;
+ while (true) {
+ switch (self.nodes[current.idx()]) {
+ .leaf => return current,
+ .split => |s| current = switch (side) {
+ .left => s.left,
+ .right => s.right,
+ },
+ }
+ }
+ }
+
+ /// Returns the previous view from the given node handle (which itself
+ /// doesn't need to be a view). If there is no previous (this is the
+ /// most previous view) then this will return null.
+ ///
+ /// "Previous" is defined as the previous node in an in-order
+ /// traversal of the tree. This isn't a perfect definition and we
+ /// may want to change this to something that better matches a
+ /// spatial view of the tree later.
+ fn previous(self: *const Self, from: Node.Handle) ?Node.Handle {
+ return switch (self.previousBacktrack(from, .root)) {
+ .result => |v| v,
+ .backtrack, .deadend => null,
+ };
+ }
+
+ /// Same as `previous`, but returns the next view instead.
+ fn next(self: *const Self, from: Node.Handle) ?Node.Handle {
+ return switch (self.nextBacktrack(from, .root)) {
+ .result => |v| v,
+ .backtrack, .deadend => null,
+ };
+ }
+
+ // Design note: we use a recursive backtracking search because
+ // split trees are never that deep, so we can abuse the stack as
+ // a safe allocator (stack overflow unlikely unless the kernel is
+ // tuned in some really weird way).
+ const Backtrack = union(enum) {
+ deadend,
+ backtrack,
+ result: Node.Handle,
+ };
+
+ fn previousBacktrack(
+ self: *const Self,
+ from: Node.Handle,
+ current: Node.Handle,
+ ) Backtrack {
+ // If we reached the point that we're trying to find the previous
+ // value of, then we need to backtrack from here.
+ if (from == current) return .backtrack;
+
+ return switch (self.nodes[current.idx()]) {
+ // If we hit a leaf that isn't our target, then deadend.
+ .leaf => .deadend,
+
+ .split => |s| switch (self.previousBacktrack(from, s.left)) {
+ .result => |v| .{ .result = v },
+
+ // Backtrack from the left means we have to continue
+ // backtracking because we can't see what's before the left.
+ .backtrack => .backtrack,
+
+ // If we hit a deadend on the left then let's move right.
+ .deadend => switch (self.previousBacktrack(from, s.right)) {
+ .result => |v| .{ .result = v },
+
+ // Deadend means its not in this split at all since
+ // we already tracked the left.
+ .deadend => .deadend,
+
+ // Backtrack means that its in our left view because
+ // we can see the immediate previous and there MUST
+ // be leaves (we can't have split-only leaves).
+ .backtrack => .{ .result = self.deepest(.right, s.left) },
+ },
+ },
+ };
+ }
+
+ // See previousBacktrack for detailed comments. This is a mirror
+ // of that.
+ fn nextBacktrack(
+ self: *const Self,
+ from: Node.Handle,
+ current: Node.Handle,
+ ) Backtrack {
+ if (from == current) return .backtrack;
+ return switch (self.nodes[current.idx()]) {
+ .leaf => .deadend,
+ .split => |s| switch (self.nextBacktrack(from, s.right)) {
+ .result => |v| .{ .result = v },
+ .backtrack => .backtrack,
+ .deadend => switch (self.nextBacktrack(from, s.left)) {
+ .result => |v| .{ .result = v },
+ .deadend => .deadend,
+ .backtrack => .{ .result = self.deepest(.left, s.right) },
+ },
+ },
+ };
+ }
+
+ /// Returns the nearest leaf node (view) in the given direction.
+ /// This does not handle wrapping and will return null if there
+ /// is no node in that direction.
+ fn nearest(
+ self: *const Self,
+ sp: Spatial,
+ from: Node.Handle,
+ direction: Spatial.Direction,
+ target: Spatial.Slot,
+ ) ?Node.Handle {
+ var result: ?struct {
+ handle: Node.Handle,
+ distance: f16,
+ } = null;
+ for (sp.slots, 0..) |slot, handle| {
+ // Never match ourself
+ if (handle == from.idx()) continue;
+
+ // Only match leaves
+ switch (self.nodes[handle]) {
+ .leaf => {},
+ .split => continue,
+ }
+
+ // Ensure it is in the proper direction
+ if (!switch (direction) {
+ .left => slot.maxX() <= target.x,
+ .right => slot.x >= target.maxX(),
+ .up => slot.maxY() <= target.y,
+ .down => slot.y >= target.maxY(),
+ }) continue;
+
+ // Track our distance
+ const dx = slot.x - target.x;
+ const dy = slot.y - target.y;
+ const distance = @sqrt(dx * dx + dy * dy);
+
+ // If we have a nearest it must be closer.
+ if (result) |n| {
+ if (distance >= n.distance) continue;
+ }
+ result = .{
+ .handle = @enumFromInt(handle),
+ .distance = distance,
+ };
+ }
+
+ return if (result) |n| n.handle else null;
+ }
+
+ /// Same as nearest but supports wrapping.
+ fn nearestWrapped(
+ self: *const Self,
+ sp: Spatial,
+ from: Node.Handle,
+ direction: Spatial.Direction,
+ ) ?Node.Handle {
+ // If we can find a nearest value without wrapping, then
+ // use that.
+ var target = sp.slots[from.idx()];
+ if (self.nearest(
+ sp,
+ from,
+ direction,
+ target,
+ )) |v| return v;
+
+ // The spatial grid is normalized to 1x1, so wrapping is modeled
+ // by shifting the target slot by one full grid in the opposite
+ // direction and reusing the same nearest distance logic.
+ // We don't actually modify the grid or spatial representation,
+ // this just fakes it.
+ assert(target.x >= 0 and target.y >= 0);
+ assert(target.maxX() <= 1 and target.maxY() <= 1);
+ switch (direction) {
+ .left => target.x += 1,
+ .right => target.x -= 1,
+ .up => target.y += 1,
+ .down => target.y -= 1,
+ }
+
+ return self.nearest(
+ sp,
+ from,
+ direction,
+ target,
+ );
+ }
+
+ /// Resize the given node in place. The node MUST be a split (asserted).
+ ///
+ /// In general, this is an immutable data structure so this is
+ /// heavily discouraged. However, this is provided for convenience
+ /// and performance reasons where its very important for GUIs to
+ /// update the ratio during a live resize than to redraw the entire
+ /// widget tree.
+ pub fn resizeInPlace(
+ self: *Self,
+ at: Node.Handle,
+ ratio: f16,
+ ) void {
+ // Let's talk about this constCast. Our member are const but
+ // we actually always own their memory. We don't want consumers
+ // who directly access the nodes to be able to modify them
+ // (without nasty stuff like this), but given this is internal
+ // usage its perfectly fine to modify the node in-place.
+ const s: *Split = @constCast(&self.nodes[at.idx()].split);
+ s.ratio = ratio;
+ }
+
+ /// Insert another tree into this tree at the given node in the
+ /// specified direction. The other tree will be inserted in the
+ /// new direction. For example, if the direction is "right" then
+ /// `insert` is inserted right of the existing node.
+ ///
+ /// The allocator will be used for the newly created tree.
+ /// The previous trees will not be freed, but reference counts
+ /// for the views will be increased accordingly for the new tree.
+ pub fn split(
+ self: *const Self,
+ gpa: Allocator,
+ at: Node.Handle,
+ direction: Split.Direction,
+ ratio: f16,
+ insert: *const Self,
+ ) Allocator.Error!Self {
+ // The new arena for our new tree.
+ var arena = ArenaAllocator.init(gpa);
+ errdefer arena.deinit();
+ const alloc = arena.allocator();
+
+ // We know we're going to need the sum total of the nodes
+ // between the two trees plus one for the new split node.
+ const nodes = try alloc.alloc(Node, self.nodes.len + insert.nodes.len + 1);
+ if (nodes.len > std.math.maxInt(Node.Handle.Backing)) return error.OutOfMemory;
+
+ // We can copy our nodes exactly as they are, since they're
+ // mostly not changing (only `at` is changing).
+ @memcpy(nodes[0..self.nodes.len], self.nodes);
+
+ // We can copy the destination nodes as well directly next to
+ // the source nodes. We just have to go through and offset
+ // all the handles in the destination tree to account for
+ // the shift.
+ const nodes_inserted = nodes[self.nodes.len..][0..insert.nodes.len];
+ @memcpy(nodes_inserted, insert.nodes);
+ for (nodes_inserted) |*node| switch (node.*) {
+ .leaf => {},
+ .split => |*s| {
+ // We need to offset the handles in the split
+ s.left = s.left.offset(self.nodes.len);
+ s.right = s.right.offset(self.nodes.len);
+ },
+ };
+
+ // Determine our split layout and if we're on the left
+ const layout: Split.Layout, const left: bool = switch (direction) {
+ .left => .{ .horizontal, true },
+ .right => .{ .horizontal, false },
+ .up => .{ .vertical, true },
+ .down => .{ .vertical, false },
+ };
+
+ // Copy our previous value to the end of the nodes list and
+ // create our new split node.
+ nodes[nodes.len - 1] = nodes[at.idx()];
+ nodes[at.idx()] = .{ .split = .{
+ .layout = layout,
+ .ratio = ratio,
+ .left = @enumFromInt(if (left) self.nodes.len else nodes.len - 1),
+ .right = @enumFromInt(if (left) nodes.len - 1 else self.nodes.len),
+ } };
+
+ // We need to increase the reference count of all the nodes.
+ try refNodes(gpa, nodes);
+
+ return .{
+ .arena = arena,
+ .nodes = nodes,
+ // Splitting always resets zoom state.
+ .zoomed = null,
+ };
+ }
+
+ /// Remove a node from the tree.
+ pub fn remove(
+ self: *Self,
+ gpa: Allocator,
+ at: Node.Handle,
+ ) Allocator.Error!Self {
+ assert(at.idx() < self.nodes.len);
+
+ // If we're removing node zero then we're clearing the tree.
+ if (at == .root) return .empty;
+
+ // The new arena for our new tree.
+ var arena = ArenaAllocator.init(gpa);
+ errdefer arena.deinit();
+ const alloc = arena.allocator();
+
+ // Allocate our new nodes list with the number of nodes we'll
+ // need after the removal.
+ const nodes = try alloc.alloc(Node, self.countAfterRemoval(
+ .root,
+ at,
+ 0,
+ ));
+
+ var result: Self = .{
+ .arena = arena,
+ .nodes = nodes,
+ .zoomed = null,
+ };
+
+ // Traverse the tree and copy all our nodes into place.
+ assert(self.removeNode(
+ &result,
+ 0,
+ .root,
+ at,
+ ) != 0);
+
+ // Increase the reference count of all the nodes.
+ try refNodes(gpa, nodes);
+
+ return result;
+ }
+
+ fn removeNode(
+ old: *Self,
+ new: *Self,
+ new_offset: usize,
+ current: Node.Handle,
+ target: Node.Handle,
+ ) usize {
+ assert(current != target);
+
+ // If we have a zoomed node and this is it then we migrate it.
+ if (old.zoomed) |v| {
+ if (v == current) {
+ assert(new.zoomed == null);
+ new.zoomed = @enumFromInt(new_offset);
+ }
+ }
+
+ // Let's talk about this constCast. Our member are const but
+ // we actually always own their memory. We don't want consumers
+ // who directly access the nodes to be able to modify them
+ // (without nasty stuff like this), but given this is internal
+ // usage its perfectly fine to modify the node in-place.
+ const new_nodes: []Node = @constCast(new.nodes);
+
+ switch (old.nodes[current.idx()]) {
+ // Leaf is simple, just copy it over. We don't ref anything
+ // yet because it'd make undo (errdefer) harder. We do that
+ // all at once later.
+ .leaf => |view| {
+ new_nodes[new_offset] = .{ .leaf = view };
+ return 1;
+ },
+
+ .split => |s| {
+ // If we're removing one of the split node sides then
+ // we remove the split node itself as well and only add
+ // the other (non-removed) side.
+ if (s.left == target) return old.removeNode(
+ new,
+ new_offset,
+ s.right,
+ target,
+ );
+ if (s.right == target) return old.removeNode(
+ new,
+ new_offset,
+ s.left,
+ target,
+ );
+
+ // Neither side is being directly removed, so we traverse.
+ const left = old.removeNode(
+ new,
+ new_offset + 1,
+ s.left,
+ target,
+ );
+ assert(left != 0);
+ const right = old.removeNode(
+ new,
+ new_offset + left + 1,
+ s.right,
+ target,
+ );
+ assert(right != 0);
+ new_nodes[new_offset] = .{ .split = .{
+ .layout = s.layout,
+ .ratio = s.ratio,
+ .left = @enumFromInt(new_offset + 1),
+ .right = @enumFromInt(new_offset + 1 + left),
+ } };
+
+ return left + right + 1;
+ },
+ }
+ }
+
+ /// Returns the number of nodes that would be needed to store
+ /// the tree if the target node is removed.
+ fn countAfterRemoval(
+ self: *Self,
+ current: Node.Handle,
+ target: Node.Handle,
+ acc: usize,
+ ) usize {
+ assert(current != target);
+
+ return switch (self.nodes[current.idx()]) {
+ // Leaf is simple, always takes one node.
+ .leaf => acc + 1,
+
+ // Split is slightly more complicated. If either side is the
+ // target to remove, then we remove the split node as well
+ // so our count is just the count of the other side.
+ //
+ // If neither side is the target, then we count both sides
+ // and add one to account for the split node itself.
+ .split => |s| if (s.left == target) self.countAfterRemoval(
+ s.right,
+ target,
+ acc,
+ ) else if (s.right == target) self.countAfterRemoval(
+ s.left,
+ target,
+ acc,
+ ) else self.countAfterRemoval(
+ s.left,
+ target,
+ acc,
+ ) + self.countAfterRemoval(
+ s.right,
+ target,
+ acc,
+ ) + 1,
+ };
+ }
+
+ /// Reference all the nodes in the given slice, handling unref if
+ /// any fail. This should be called LAST so you don't have to undo
+ /// the refs at any further point after this.
+ fn refNodes(gpa: Allocator, nodes: []Node) Allocator.Error!void {
+ // We need to increase the reference count of all the nodes.
+ // Careful accounting here so that we properly unref on error
+ // only the nodes we referenced.
+ var reffed: usize = 0;
+ errdefer for (0..reffed) |i| {
+ switch (nodes[i]) {
+ .split => {},
+ .leaf => |view| viewUnref(view, gpa),
+ }
+ };
+ for (0..nodes.len) |i| {
+ switch (nodes[i]) {
+ .split => {},
+ .leaf => |view| nodes[i] = .{ .leaf = try viewRef(view, gpa) },
+ }
+ reffed = i;
+ }
+ assert(reffed == nodes.len - 1);
+ }
+
+ /// Equalize this node and all its children, returning a new node with splits
+ /// adjusted so that each split's ratio is based on the relative weight
+ /// (number of leaves) of its children.
+ pub fn equalize(
+ self: *const Self,
+ gpa: Allocator,
+ ) Allocator.Error!Self {
+ if (self.isEmpty()) return .empty;
+
+ // Create a new arena allocator for the clone.
+ var arena = ArenaAllocator.init(gpa);
+ errdefer arena.deinit();
+ const alloc = arena.allocator();
+
+ // Allocate a new nodes array and copy the existing nodes into it.
+ const nodes = try alloc.dupe(Node, self.nodes);
+
+ // Go through and equalize our ratios based on weights.
+ for (nodes) |*node| switch (node.*) {
+ .leaf => {},
+ .split => |*s| {
+ const weight_left = self.weight(s.left, s.layout, 0);
+ const weight_right = self.weight(s.right, s.layout, 0);
+ assert(weight_left > 0);
+ assert(weight_right > 0);
+ const total_f16: f16 = @floatFromInt(weight_left + weight_right);
+ const weight_left_f16: f16 = @floatFromInt(weight_left);
+ s.ratio = weight_left_f16 / total_f16;
+ },
+ };
+
+ // Increase the reference count of all the views in the nodes.
+ try refNodes(gpa, nodes);
+
+ return .{
+ .arena = arena,
+ .nodes = nodes,
+ .zoomed = self.zoomed,
+ };
+ }
+
+ fn weight(
+ self: *const Self,
+ from: Node.Handle,
+ layout: Split.Layout,
+ acc: usize,
+ ) usize {
+ return switch (self.nodes[from.idx()]) {
+ .leaf => acc + 1,
+ .split => |s| if (s.layout == layout)
+ self.weight(s.left, layout, acc) +
+ self.weight(s.right, layout, acc)
+ else
+ 1,
+ };
+ }
+
+ /// Resize the nearest split matching the layout by the given ratio.
+ /// Positive is right and down.
+ ///
+ /// The ratio is a signed delta representing the percentage to move
+ /// the divider. The percentage is of the entire grid size, not just
+ /// the specific split size.
+ /// We use the entire grid size because that's what Ghostty's
+ /// `resize_split` keybind does, because it maps to a general human
+ /// understanding of moving a split relative to the entire window
+ /// (generally).
+ ///
+ /// For example, a ratio of 0.1 and a layout of `vertical` will find
+ /// the nearest vertical split and move the divider down by 10% of
+ /// the total grid height.
+ ///
+ /// If no matching split is found, this does nothing, but will always
+ /// still return a cloned tree.
+ pub fn resize(
+ self: *const Self,
+ gpa: Allocator,
+ from: Node.Handle,
+ layout: Split.Layout,
+ ratio: f16,
+ ) Allocator.Error!Self {
+ assert(ratio >= -1 and ratio <= 1);
+ assert(!std.math.isNan(ratio));
+ assert(!std.math.isInf(ratio));
+
+ // Fast path empty trees.
+ if (self.isEmpty()) return .empty;
+
+ // From this point forward worst case we return a clone.
+ var result = try self.clone(gpa);
+ errdefer result.deinit();
+
+ // Find our nearest parent split node matching the layout.
+ const parent_handle = switch (self.findParentSplit(
+ layout,
+ from,
+ .root,
+ )) {
+ .deadend, .backtrack => return result,
+ .result => |v| v,
+ };
+
+ // Get our spatial layout, because we need the dimensions of this
+ // split with regards to the entire grid.
+ var sp = try result.spatial(gpa);
+ defer sp.deinit(gpa);
+
+ // Get the ratio of the split relative to the full grid.
+ const full_ratio = full_ratio: {
+ // Our scale is the amount we need to multiply our individual
+ // ratio by to get the full ratio. Its actually a ratio on its
+ // own but I'm trying to avoid that word: its the ratio of
+ // our spatial width/height to the total.
+ const scale = switch (layout) {
+ .horizontal => sp.slots[parent_handle.idx()].width / sp.slots[0].width,
+ .vertical => sp.slots[parent_handle.idx()].height / sp.slots[0].height,
+ };
+
+ const current = result.nodes[parent_handle.idx()].split.ratio;
+ break :full_ratio current * scale;
+ };
+
+ // Set the final new ratio, clamping it to [0, 1]
+ result.resizeInPlace(
+ parent_handle,
+ @min(@max(full_ratio + ratio, 0), 1),
+ );
+ return result;
+ }
+
+ fn findParentSplit(
+ self: *const Self,
+ layout: Split.Layout,
+ from: Node.Handle,
+ current: Node.Handle,
+ ) Backtrack {
+ if (from == current) return .backtrack;
+ return switch (self.nodes[current.idx()]) {
+ .leaf => .deadend,
+ .split => |s| switch (self.findParentSplit(
+ layout,
+ from,
+ s.left,
+ )) {
+ .result => |v| .{ .result = v },
+ .backtrack => if (s.layout == layout)
+ .{ .result = current }
+ else
+ .backtrack,
+ .deadend => switch (self.findParentSplit(
+ layout,
+ from,
+ s.right,
+ )) {
+ .deadend => .deadend,
+ .result => |v| .{ .result = v },
+ .backtrack => if (s.layout == layout)
+ .{ .result = current }
+ else
+ .backtrack,
+ },
+ },
+ };
+ }
+
+ /// Spatial representation of the split tree. See spatial.
+ pub const Spatial = struct {
+ /// The slots of the spatial representation in the same order
+ /// as the tree it was created from.
+ slots: []const Slot,
+
+ pub const empty: Spatial = .{ .slots = &.{} };
+
+ pub const Direction = enum { left, right, down, up };
+
+ const Slot = struct {
+ x: f16,
+ y: f16,
+ width: f16,
+ height: f16,
+
+ fn maxX(self: *const Slot) f16 {
+ return self.x + self.width;
+ }
+
+ fn maxY(self: *const Slot) f16 {
+ return self.y + self.height;
+ }
+ };
+
+ pub fn deinit(self: *Spatial, alloc: Allocator) void {
+ alloc.free(self.slots);
+ self.* = undefined;
+ }
+ };
+
+ /// Spatial representation of the split tree. This can be used to
+ /// better understand the layout of the tree in a 2D space.
+ ///
+ /// The bounds of the representation are always based on the total
+ /// 2D space being 1x1. The x/y coordinates and width/height dimensions
+ /// of each individual split and leaf are relative to this.
+ /// This means that the spatial representation is a normalized
+ /// representation of the actual space.
+ ///
+ /// The top-left corner of the tree is always (0, 0).
+ ///
+ /// We use a normalized form because we can calculate it without
+ /// accessing to the actual rendered view sizes. These actual sizes
+ /// may not be available at various times because GUI toolkits often
+ /// only make them available once they're part of a widget tree and
+ /// a SplitTree can represent views that aren't currently visible.
+ pub fn spatial(
+ self: *const Self,
+ alloc: Allocator,
+ ) Allocator.Error!Spatial {
+ // No nodes, empty spatial representation.
+ if (self.nodes.len == 0) return .empty;
+
+ // Get our total dimensions.
+ const dim = self.dimensions(.root);
+
+ // Create our slots which will match our nodes exactly.
+ const slots = try alloc.alloc(Spatial.Slot, self.nodes.len);
+ errdefer alloc.free(slots);
+ slots[0] = .{
+ .x = 0,
+ .y = 0,
+ .width = @floatFromInt(dim.width),
+ .height = @floatFromInt(dim.height),
+ };
+ self.fillSpatialSlots(slots, .root);
+
+ // Normalize the dimensions to 1x1 grid.
+ for (slots) |*slot| {
+ slot.x /= @floatFromInt(dim.width);
+ slot.y /= @floatFromInt(dim.height);
+ slot.width /= @floatFromInt(dim.width);
+ slot.height /= @floatFromInt(dim.height);
+ }
+
+ return .{ .slots = slots };
+ }
+
+ fn fillSpatialSlots(
+ self: *const Self,
+ slots: []Spatial.Slot,
+ current_: Node.Handle,
+ ) void {
+ const current = current_.idx();
+ assert(slots[current].width >= 0 and slots[current].height >= 0);
+ switch (self.nodes[current]) {
+ // Leaf node, current slot is already filled by caller.
+ .leaf => {},
+
+ .split => |s| {
+ switch (s.layout) {
+ .horizontal => {
+ slots[s.left.idx()] = .{
+ .x = slots[current].x,
+ .y = slots[current].y,
+ .width = slots[current].width * s.ratio,
+ .height = slots[current].height,
+ };
+ slots[s.right.idx()] = .{
+ .x = slots[current].x + slots[current].width * s.ratio,
+ .y = slots[current].y,
+ .width = slots[current].width * (1 - s.ratio),
+ .height = slots[current].height,
+ };
+ },
+
+ .vertical => {
+ slots[s.left.idx()] = .{
+ .x = slots[current].x,
+ .y = slots[current].y,
+ .width = slots[current].width,
+ .height = slots[current].height * s.ratio,
+ };
+ slots[s.right.idx()] = .{
+ .x = slots[current].x,
+ .y = slots[current].y + slots[current].height * s.ratio,
+ .width = slots[current].width,
+ .height = slots[current].height * (1 - s.ratio),
+ };
+ },
+ }
+
+ self.fillSpatialSlots(slots, s.left);
+ self.fillSpatialSlots(slots, s.right);
+ },
+ }
+ }
+
+ /// Get the dimensions of the tree starting from the given node.
+ ///
+ /// This creates relative dimensions (see Spatial) by assuming each
+ /// leaf is exactly 1x1 unit in size.
+ fn dimensions(self: *const Self, current: Node.Handle) struct {
+ width: u16,
+ height: u16,
+ } {
+ return switch (self.nodes[current.idx()]) {
+ .leaf => .{ .width = 1, .height = 1 },
+ .split => |s| split: {
+ const left = self.dimensions(s.left);
+ const right = self.dimensions(s.right);
+ break :split switch (s.layout) {
+ .horizontal => .{
+ .width = left.width + right.width,
+ .height = @max(left.height, right.height),
+ },
+
+ .vertical => .{
+ .width = @max(left.width, right.width),
+ .height = left.height + right.height,
+ },
+ };
+ },
+ };
+ }
+
+ /// Format the tree in a human-readable format. By default this will
+ /// output a diagram followed by a textual representation.
+ pub fn format(
+ self: *const Self,
+ writer: *std.Io.Writer,
+ ) !void {
+ if (self.nodes.len == 0) {
+ try writer.writeAll("empty");
+ return;
+ }
+ self.formatDiagram(writer) catch {};
+ try self.formatText(writer);
+ }
+
+ pub fn formatText(self: Self, writer: *std.Io.Writer) std.Io.Writer.Error!void {
+ if (self.nodes.len == 0) {
+ try writer.writeAll("empty");
+ return;
+ }
+ try self.formatTextInner(writer, .root, 0);
+ }
+
+ fn formatTextInner(
+ self: Self,
+ writer: *std.Io.Writer,
+ current: Node.Handle,
+ depth: usize,
+ ) std.Io.Writer.Error!void {
+ for (0..depth) |_| try writer.writeAll(" ");
+
+ if (self.zoomed) |zoomed| if (zoomed == current) {
+ try writer.writeAll("(zoomed) ");
+ };
+
+ switch (self.nodes[current.idx()]) {
+ .leaf => |v| if (@hasDecl(View, "splitTreeLabel"))
+ try writer.print("leaf: {s}\n", .{v.splitTreeLabel()})
+ else
+ try writer.print("leaf: {d}\n", .{current}),
+
+ .split => |s| {
+ try writer.print("split (layout: {t}, ratio: {d:.2})\n", .{
+ s.layout,
+ s.ratio,
+ });
+ try self.formatTextInner(writer, s.left, depth + 1);
+ try self.formatTextInner(writer, s.right, depth + 1);
+ },
+ }
+ }
+
+ pub fn formatDiagram(
+ self: Self,
+ writer: *std.Io.Writer,
+ ) std.Io.Writer.Error!void {
+ if (self.nodes.len == 0) {
+ try writer.writeAll("empty");
+ return;
+ }
+
+ // Use our arena's GPA to allocate some intermediate memory.
+ // Requiring allocation for formatting is nasty but this is really
+ // only used for debugging and testing and shouldn't hit OOM
+ // scenarios.
+ var arena: ArenaAllocator = .init(self.arena.child_allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ // Get our spatial representation.
+ const sp = spatial: {
+ const sp = self.spatial(alloc) catch return error.WriteFailed;
+
+ // Scale our spatial representation to have minimum width/height 1.
+ var min_w: f16 = 1;
+ var min_h: f16 = 1;
+ for (sp.slots) |slot| {
+ if (slot.width > 0) min_w = @min(min_w, slot.width);
+ if (slot.height > 0) min_h = @min(min_h, slot.height);
+ }
+
+ const ratio_w: f16 = 1 / min_w;
+ const ratio_h: f16 = 1 / min_h;
+ const slots = alloc.dupe(Spatial.Slot, sp.slots) catch return error.WriteFailed;
+ for (slots) |*slot| {
+ slot.x *= ratio_w;
+ slot.y *= ratio_h;
+ slot.width *= ratio_w;
+ slot.height *= ratio_h;
+ }
+
+ break :spatial .{ .slots = slots };
+ };
+
+ // The width we need for the largest label.
+ const max_label_width: usize = max_label_width: {
+ if (!@hasDecl(View, "splitTreeLabel")) {
+ break :max_label_width std.math.log10(sp.slots.len) + 1;
+ }
+
+ var max: usize = 0;
+ for (self.nodes) |node| switch (node) {
+ .split => {},
+ .leaf => |view| {
+ const label = view.splitTreeLabel();
+ max = @max(max, label.len);
+ },
+ };
+
+ break :max_label_width max;
+ };
+
+ // We need space for whitespace and ASCII art so add that.
+ // We need to accommodate the leaf handle, whitespace, and
+ // then the border.
+ const cell_width = cell_width: {
+ // Border + whitespace + label + whitespace + border.
+ break :cell_width 2 + max_label_width + 2;
+ };
+ const cell_height = cell_height: {
+ // Border + label + border. No whitespace needed on the
+ // vertical axis.
+ break :cell_height 1 + 1 + 1;
+ };
+
+ // Make a grid that can fit our entire ASCII diagram. We know
+ // the width/height based on node 0.
+ const grid = grid: {
+ // Get our initial width/height. Each leaf is 1x1 in this.
+ // We round up for this because partial widths/heights should
+ // take up an extra cell.
+ var width: usize = @intFromFloat(@ceil(sp.slots[0].width));
+ var height: usize = @intFromFloat(@ceil(sp.slots[0].height));
+
+ // We need space for whitespace and ASCII art so add that.
+ // We need to accommodate the leaf handle, whitespace, and
+ // then the border.
+ width *= cell_width;
+ height *= cell_height;
+
+ const rows = alloc.alloc([]u8, height) catch return error.WriteFailed;
+ for (0..rows.len) |y| {
+ rows[y] = alloc.alloc(u8, width + 1) catch return error.WriteFailed;
+ @memset(rows[y], ' ');
+ rows[y][width] = '\n';
+ }
+ break :grid rows;
+ };
+
+ // Draw each node
+ for (sp.slots, 0..) |slot, handle| {
+ // We only draw leaf nodes. Splits are only used for layout.
+ const node = self.nodes[handle];
+ switch (node) {
+ .leaf => {},
+ .split => continue,
+ }
+
+ // If our width/height is zero then we skip this.
+ if (slot.width == 0 or slot.height == 0) continue;
+
+ var x: usize = @intFromFloat(@floor(slot.x));
+ var y: usize = @intFromFloat(@floor(slot.y));
+ var width: usize = @intFromFloat(@max(@floor(slot.width), 1));
+ var height: usize = @intFromFloat(@max(@floor(slot.height), 1));
+ x *= cell_width;
+ y *= cell_height;
+ width *= cell_width;
+ height *= cell_height;
+
+ // Top border
+ {
+ const top = grid[y][x..][0..width];
+ top[0] = '+';
+ for (1..width - 1) |i| top[i] = '-';
+ top[width - 1] = '+';
+ }
+
+ // Bottom border
+ {
+ const bottom = grid[y + height - 1][x..][0..width];
+ bottom[0] = '+';
+ for (1..width - 1) |i| bottom[i] = '-';
+ bottom[width - 1] = '+';
+ }
+
+ // Left border
+ for (y + 1..y + height - 1) |y_cur| grid[y_cur][x] = '|';
+ for (y + 1..y + height - 1) |y_cur| grid[y_cur][x + width - 1] = '|';
+
+ // Get our label text
+ var buf: [10]u8 = undefined;
+ const label: []const u8 = if (@hasDecl(View, "splitTreeLabel"))
+ node.leaf.splitTreeLabel()
+ else
+ std.fmt.bufPrint(&buf, "{d}", .{handle}) catch return error.WriteFailed;
+
+ // Draw the handle in the center
+ const x_mid = width / 2 + x;
+ const y_mid = height / 2 + y;
+ const label_width = label.len;
+ const label_start = x_mid - label_width / 2;
+ const row = grid[y_mid][label_start..];
+ _ = std.fmt.bufPrint(row, "{s}", .{label}) catch return error.WriteFailed;
+ }
+
+ // Output every row
+ for (grid) |row| {
+ // We currently have a bug in our height calculation that
+ // results in trailing blank lines. Ignore those. We should
+ // really fix our height calculation instead. If someone wants
+ // to do that just remove this line and see the tests that fail
+ // and go from there.
+ if (row[0] == ' ') break;
+ try writer.writeAll(row);
+ }
+ }
+
+ fn viewRef(view: *View, gpa: Allocator) Allocator.Error!*View {
+ const func = @typeInfo(@TypeOf(View.ref)).@"fn";
+ return switch (func.params.len) {
+ 1 => view.ref(),
+ 2 => try view.ref(gpa),
+ else => @compileError("invalid view ref function"),
+ };
+ }
+
+ fn viewUnref(view: *View, gpa: Allocator) void {
+ const func = @typeInfo(@TypeOf(View.unref)).@"fn";
+ switch (func.params.len) {
+ 1 => view.unref(),
+ 2 => view.unref(gpa),
+ else => @compileError("invalid view unref function"),
+ }
+ }
+
+ /// Make this a valid gobject if we're in a GTK environment.
+ pub const getGObjectType = switch (build_config.app_runtime) {
+ .gtk => @import("gobject").ext.defineBoxed(
+ Self,
+ .{
+ // To get the type name we get the non-qualified type name
+ // of the view and append that to `GhosttySplitTree`.
+ .name = name: {
+ const type_name = @typeName(View);
+ const last = if (std.mem.lastIndexOfScalar(
+ u8,
+ type_name,
+ '.',
+ )) |idx|
+ type_name[idx + 1 ..]
+ else
+ type_name;
+ assert(last.len > 0);
+ break :name "GhosttySplitTree" ++ last;
+ },
+
+ .funcs = .{
+ .copy = &struct {
+ fn copy(self: *Self) callconv(.c) *Self {
+ const ptr = @import("glib").ext.create(Self);
+ ptr.* = if (self.nodes.len == 0)
+ .empty
+ else
+ self.clone(self.arena.child_allocator) catch @panic("oom");
+ return ptr;
+ }
+ }.copy,
+ .free = &struct {
+ fn free(self: *Self) callconv(.c) void {
+ self.deinit();
+ @import("glib").ext.destroy(self);
+ }
+ }.free,
+ },
+ },
+ ),
+
+ .none => void,
+ };
+ };
+}
+
+const TestTree = SplitTree(TestView);
+
+const TestView = struct {
+ const Self = @This();
+
+ label: []const u8,
+
+ pub fn ref(self: *Self, alloc: Allocator) Allocator.Error!*Self {
+ const ptr = try alloc.create(Self);
+ ptr.* = self.*;
+ return ptr;
+ }
+
+ pub fn unref(self: *Self, alloc: Allocator) void {
+ alloc.destroy(self);
+ }
+
+ pub fn splitTreeLabel(self: *const Self) []const u8 {
+ return self.label;
+ }
+};
+
+test "SplitTree: isSplit" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ // Empty tree should not be split
+ var empty: TestTree = .empty;
+ defer empty.deinit();
+ try testing.expect(!empty.isSplit());
+
+ // Single node tree should not be split
+ var v1: TestView = .{ .label = "A" };
+ var single: TestTree = try TestTree.init(alloc, &v1);
+ defer single.deinit();
+ try testing.expect(!single.isSplit());
+
+ // Split tree should be split
+ var v2: TestView = .{ .label = "B" };
+ var tree2: TestTree = try TestTree.init(alloc, &v2);
+ defer tree2.deinit();
+ var split = try single.split(
+ alloc,
+ .root,
+ .right,
+ 0.5,
+ &tree2,
+ );
+ defer split.deinit();
+ try testing.expect(split.isSplit());
+}
+
+test "SplitTree: empty tree" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ var t: TestTree = .empty;
+ defer t.deinit();
+
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{t});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\empty
+ );
+}
+
+test "SplitTree: single node" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ var v: TestTree.View = .{ .label = "A" };
+ var t: TestTree = try .init(alloc, &v);
+ defer t.deinit();
+
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---+
+ \\| A |
+ \\+---+
+ \\
+ );
+}
+
+test "SplitTree: split horizontal" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+ var t3 = try t1.split(
+ alloc,
+ .root, // at root
+ .right, // split right
+ 0.5,
+ &t2, // insert t2
+ );
+ defer t3.deinit();
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{t3});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---++---+
+ \\| A || B |
+ \\+---++---+
+ \\split (layout: horizontal, ratio: 0.50)
+ \\ leaf: A
+ \\ leaf: B
+ \\
+ );
+ }
+
+ // Split right at B
+ var vC: TestTree.View = .{ .label = "C" };
+ var tC: TestTree = try .init(alloc, &vC);
+ defer tC.deinit();
+ var it = t3.iterator();
+ var t4 = try t3.split(
+ alloc,
+ while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "B")) {
+ break entry.handle;
+ }
+ } else return error.NotFound,
+ .right,
+ 0.5,
+ &tC,
+ );
+ defer t4.deinit();
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{t4});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+--------++---++---+
+ \\| A || B || C |
+ \\+--------++---++---+
+ \\split (layout: horizontal, ratio: 0.50)
+ \\ leaf: A
+ \\ split (layout: horizontal, ratio: 0.50)
+ \\ leaf: B
+ \\ leaf: C
+ \\
+ );
+ }
+
+ // Split right at C
+ var vD: TestTree.View = .{ .label = "D" };
+ var tD: TestTree = try .init(alloc, &vD);
+ defer tD.deinit();
+ it = t4.iterator();
+ var t5 = try t4.split(
+ alloc,
+ while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "C")) {
+ break entry.handle;
+ }
+ } else return error.NotFound,
+ .right,
+ 0.5,
+ &tD,
+ );
+ defer t5.deinit();
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{t5});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(
+ \\+------------------++--------++---++---+
+ \\| A || B || C || D |
+ \\+------------------++--------++---++---+
+ \\split (layout: horizontal, ratio: 0.50)
+ \\ leaf: A
+ \\ split (layout: horizontal, ratio: 0.50)
+ \\ leaf: B
+ \\ split (layout: horizontal, ratio: 0.50)
+ \\ leaf: C
+ \\ leaf: D
+ \\
+ , str);
+ }
+
+ // Find "previous" from D back.
+ {
+ var current: u8 = 'D';
+ while (current != 'A') : (current -= 1) {
+ it = t5.iterator();
+ const handle = t5.previous(
+ while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, &.{current})) {
+ break entry.handle;
+ }
+ } else return error.NotFound,
+ ).?;
+
+ const entry = t5.nodes[handle.idx()].leaf;
+ try testing.expectEqualStrings(
+ entry.label,
+ &.{current - 1},
+ );
+ }
+
+ it = t5.iterator();
+ try testing.expect(t5.previous(
+ while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, &.{current})) {
+ break entry.handle;
+ }
+ } else return error.NotFound,
+ ) == null);
+ }
+
+ // Find "next" from A forward.
+ {
+ var current: u8 = 'A';
+ while (current != 'D') : (current += 1) {
+ it = t5.iterator();
+ const handle = t5.next(
+ while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, &.{current})) {
+ break entry.handle;
+ }
+ } else return error.NotFound,
+ ).?;
+
+ const entry = t5.nodes[handle.idx()].leaf;
+ try testing.expectEqualStrings(
+ entry.label,
+ &.{current + 1},
+ );
+ }
+
+ it = t5.iterator();
+ try testing.expect(t5.next(
+ while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, &.{current})) {
+ break entry.handle;
+ }
+ } else return error.NotFound,
+ ) == null);
+ }
+}
+
+test "SplitTree: split vertical" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+
+ var t3 = try t1.split(
+ alloc,
+ .root, // at root
+ .down, // split down
+ 0.5,
+ &t2, // insert t2
+ );
+ defer t3.deinit();
+
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t3, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---+
+ \\| A |
+ \\+---+
+ \\+---+
+ \\| B |
+ \\+---+
+ \\
+ );
+}
+
+test "SplitTree: split horizontal with zero ratio" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+
+ // A | B horizontal
+ var splitAB = try t1.split(
+ alloc,
+ .root, // at root
+ .right, // split right
+ 0,
+ &t2, // insert t2
+ );
+ defer splitAB.deinit();
+ const split = splitAB;
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---+
+ \\| B |
+ \\+---+
+ \\
+ );
+ }
+}
+
+test "SplitTree: split vertical with zero ratio" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+
+ // A | B horizontal
+ var splitAB = try t1.split(
+ alloc,
+ .root, // at root
+ .down, // split right
+ 0,
+ &t2, // insert t2
+ );
+ defer splitAB.deinit();
+ const split = splitAB;
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---+
+ \\| B |
+ \\+---+
+ \\
+ );
+ }
+}
+
+test "SplitTree: split horizontal with full width" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+
+ // A | B horizontal
+ var splitAB = try t1.split(
+ alloc,
+ .root, // at root
+ .right, // split right
+ 1,
+ &t2, // insert t2
+ );
+ defer splitAB.deinit();
+ const split = splitAB;
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---+
+ \\| A |
+ \\+---+
+ \\
+ );
+ }
+}
+
+test "SplitTree: split vertical with full width" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+
+ // A | B horizontal
+ var splitAB = try t1.split(
+ alloc,
+ .root, // at root
+ .down, // split right
+ 1,
+ &t2, // insert t2
+ );
+ defer splitAB.deinit();
+ const split = splitAB;
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---+
+ \\| A |
+ \\+---+
+ \\
+ );
+ }
+}
+
+test "SplitTree: remove leaf" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+ var t3 = try t1.split(
+ alloc,
+ .root, // at root
+ .right, // split right
+ 0.5,
+ &t2, // insert t2
+ );
+ defer t3.deinit();
+
+ // Remove "A"
+ var it = t3.iterator();
+ var t4 = try t3.remove(
+ alloc,
+ while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "A")) {
+ break entry.handle;
+ }
+ } else return error.NotFound,
+ );
+ defer t4.deinit();
+
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t4, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---+
+ \\| B |
+ \\+---+
+ \\
+ );
+}
+
+test "SplitTree: split twice, remove intermediary" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+ var v3: TestTree.View = .{ .label = "C" };
+ var t3: TestTree = try .init(alloc, &v3);
+ defer t3.deinit();
+
+ // A | B horizontal.
+ var split1 = try t1.split(
+ alloc,
+ .root, // at root
+ .right, // split right
+ 0.5,
+ &t2, // insert t2
+ );
+ defer split1.deinit();
+
+ // Insert C below that.
+ var split2 = try split1.split(
+ alloc,
+ .root, // at root
+ .down, // split down
+ 0.5,
+ &t3, // insert t3
+ );
+ defer split2.deinit();
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split2, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---++---+
+ \\| A || B |
+ \\+---++---+
+ \\+--------+
+ \\| C |
+ \\+--------+
+ \\
+ );
+ }
+
+ // Remove "B"
+ var it = split2.iterator();
+ var split3 = try split2.remove(
+ alloc,
+ while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "B")) {
+ break entry.handle;
+ }
+ } else return error.NotFound,
+ );
+ defer split3.deinit();
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split3, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---+
+ \\| A |
+ \\+---+
+ \\+---+
+ \\| C |
+ \\+---+
+ \\
+ );
+ }
+
+ // Remove every node from split2 (our most complex one), which should
+ // never crash. We don't test the result is correct, this just verifies
+ // we don't hit any assertion failures.
+ for (0..split2.nodes.len) |i| {
+ var t = try split2.remove(alloc, @enumFromInt(i));
+ t.deinit();
+ }
+}
+
+test "SplitTree: spatial goto" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+ var v3: TestTree.View = .{ .label = "C" };
+ var t3: TestTree = try .init(alloc, &v3);
+ defer t3.deinit();
+ var v4: TestTree.View = .{ .label = "D" };
+ var t4: TestTree = try .init(alloc, &v4);
+ defer t4.deinit();
+
+ // A | B horizontal
+ var splitAB = try t1.split(
+ alloc,
+ .root, // at root
+ .right, // split right
+ 0.5,
+ &t2, // insert t2
+ );
+ defer splitAB.deinit();
+
+ // A | C vertical
+ var splitAC = try splitAB.split(
+ alloc,
+ at: {
+ var it = splitAB.iterator();
+ break :at while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "A")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ },
+ .down, // split down
+ 0.8,
+ &t3, // insert t3
+ );
+ defer splitAC.deinit();
+
+ // B | D vertical
+ var splitBD = try splitAC.split(
+ alloc,
+ at: {
+ var it = splitAB.iterator();
+ break :at while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "B")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ },
+ .down, // split down
+ 0.3,
+ &t4, // insert t4
+ );
+ defer splitBD.deinit();
+ const split = splitBD;
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---++---+
+ \\| || B |
+ \\| |+---+
+ \\| |+---+
+ \\| A || |
+ \\| || |
+ \\| || |
+ \\| || D |
+ \\+---+| |
+ \\+---+| |
+ \\| C || |
+ \\+---++---+
+ \\
+ );
+ }
+
+ // Spatial C => right
+ {
+ const target = (try split.goto(
+ alloc,
+ from: {
+ var it = split.iterator();
+ break :from while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "C")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ },
+ .{ .spatial = .right },
+ )).?;
+ const view = split.nodes[target.idx()].leaf;
+ try testing.expectEqualStrings(view.label, "D");
+ }
+
+ // Spatial D => left
+ {
+ const target = (try split.goto(
+ alloc,
+ from: {
+ var it = split.iterator();
+ break :from while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "D")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ },
+ .{ .spatial = .left },
+ )).?;
+ const view = split.nodes[target.idx()].leaf;
+ try testing.expectEqualStrings("A", view.label);
+ }
+
+ // Spatial A => left (wrapped)
+ {
+ const target = (try split.goto(
+ alloc,
+ from: {
+ var it = split.iterator();
+ break :from while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "A")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ },
+ .{ .spatial = .left },
+ )).?;
+ const view = split.nodes[target.idx()].leaf;
+ try testing.expectEqualStrings("B", view.label);
+ }
+
+ // Spatial B => right (wrapped)
+ {
+ const target = (try split.goto(
+ alloc,
+ from: {
+ var it = split.iterator();
+ break :from while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "B")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ },
+ .{ .spatial = .right },
+ )).?;
+ const view = split.nodes[target.idx()].leaf;
+ try testing.expectEqualStrings("A", view.label);
+ }
+
+ // Spatial C => down (wrapped)
+ {
+ const target = (try split.goto(
+ alloc,
+ from: {
+ var it = split.iterator();
+ break :from while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "C")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ },
+ .{ .spatial = .down },
+ )).?;
+ const view = split.nodes[target.idx()].leaf;
+ try testing.expectEqualStrings("A", view.label);
+ }
+
+ // Equalize
+ var equal = try split.equalize(alloc);
+ defer equal.deinit();
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(equal, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---++---+
+ \\| A || B |
+ \\+---++---+
+ \\+---++---+
+ \\| C || D |
+ \\+---++---+
+ \\
+ );
+ }
+}
+
+test "SplitTree: resize" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+
+ // A | B horizontal
+ var split = try t1.split(
+ alloc,
+ .root, // at root
+ .right, // split right
+ 0.5,
+ &t2, // insert t2
+ );
+ defer split.deinit();
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---++---+
+ \\| A || B |
+ \\+---++---+
+ \\
+ );
+ }
+
+ // Resize
+ {
+ var resized = try split.resize(
+ alloc,
+ at: {
+ var it = split.iterator();
+ break :at while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "B")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ },
+ .horizontal, // resize right
+ 0.25,
+ );
+ defer resized.deinit();
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(resized, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+-------------++---+
+ \\| A || B |
+ \\+-------------++---+
+ \\
+ );
+ }
+
+ // Resize the other direction (negative ratio)
+ {
+ var resized = try split.resize(
+ alloc,
+ at: {
+ var it = split.iterator();
+ break :at while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "B")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ },
+ .horizontal, // resize left
+ -0.25,
+ );
+ defer resized.deinit();
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(resized, .formatDiagram)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\+---++-------------+
+ \\| A || B |
+ \\+---++-------------+
+ \\
+ );
+ }
+}
+
+test "SplitTree: clone empty tree" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ var t: TestTree = .empty;
+ defer t.deinit();
+
+ var t2 = try t.clone(alloc);
+ defer t2.deinit();
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{t2});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\empty
+ );
+ }
+}
+
+test "SplitTree: zoom" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+
+ // A | B horizontal
+ var split = try t1.split(
+ alloc,
+ .root, // at root
+ .right, // split right
+ 0.5,
+ &t2, // insert t2
+ );
+ defer split.deinit();
+ split.zoom(at: {
+ var it = split.iterator();
+ break :at while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "B")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ });
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatText)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\split (layout: horizontal, ratio: 0.50)
+ \\ leaf: A
+ \\ (zoomed) leaf: B
+ \\
+ );
+ }
+
+ // Clone preserves zoom
+ var clone = try split.clone(alloc);
+ defer clone.deinit();
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(clone, .formatText)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\split (layout: horizontal, ratio: 0.50)
+ \\ leaf: A
+ \\ (zoomed) leaf: B
+ \\
+ );
+ }
+}
+
+test "SplitTree: split resets zoom" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+
+ // Zoom A
+ t1.zoom(at: {
+ var it = t1.iterator();
+ break :at while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "A")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ });
+
+ // A | B horizontal
+ var split = try t1.split(
+ alloc,
+ .root, // at root
+ .right, // split right
+ 0.5,
+ &t2, // insert t2
+ );
+ defer split.deinit();
+
+ {
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatText)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\split (layout: horizontal, ratio: 0.50)
+ \\ leaf: A
+ \\ leaf: B
+ \\
+ );
+ }
+}
+
+test "SplitTree: remove and zoom" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var v1: TestTree.View = .{ .label = "A" };
+ var t1: TestTree = try .init(alloc, &v1);
+ defer t1.deinit();
+ var v2: TestTree.View = .{ .label = "B" };
+ var t2: TestTree = try .init(alloc, &v2);
+ defer t2.deinit();
+
+ // A | B horizontal
+ var split = try t1.split(
+ alloc,
+ .root, // at root
+ .right, // split right
+ 0.5,
+ &t2, // insert t2
+ );
+ defer split.deinit();
+ split.zoom(at: {
+ var it = split.iterator();
+ break :at while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "A")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ });
+
+ // Remove A, should unzoom
+ {
+ var removed = try split.remove(
+ alloc,
+ at: {
+ var it = split.iterator();
+ break :at while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "A")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ },
+ );
+ defer removed.deinit();
+ try testing.expect(removed.zoomed == null);
+
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(removed, .formatText)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\leaf: B
+ \\
+ );
+ }
+
+ // Remove B, should keep zoom
+ {
+ var removed = try split.remove(
+ alloc,
+ at: {
+ var it = split.iterator();
+ break :at while (it.next()) |entry| {
+ if (std.mem.eql(u8, entry.view.label, "B")) {
+ break entry.handle;
+ }
+ } else return error.NotFound;
+ },
+ );
+ defer removed.deinit();
+
+ const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(removed, .formatText)});
+ defer alloc.free(str);
+ try testing.expectEqualStrings(str,
+ \\(zoomed) leaf: A
+ \\
+ );
+ }
+}