aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/Tabstops.zig
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/terminal/Tabstops.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/Tabstops.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/Tabstops.zig271
1 files changed, 271 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/Tabstops.zig b/vendor/libghostty-vt/src/terminal/Tabstops.zig
new file mode 100644
index 00000000..68138cbf
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/Tabstops.zig
@@ -0,0 +1,271 @@
+//! Keep track of the location of tabstops.
+//!
+//! This is implemented as a bit set. There is a preallocation segment that
+//! is used for almost all screen sizes. Then there is a dynamically allocated
+//! segment if the screen is larger than the preallocation amount.
+//!
+//! In reality, tabstops don't need to be the most performant in any metric.
+//! This implementation tries to balance denser memory usage (by using a bitset)
+//! and minimizing unnecessary allocations.
+const Tabstops = @This();
+
+const std = @import("std");
+const tripwire = @import("../tripwire.zig");
+const Allocator = std.mem.Allocator;
+const testing = std.testing;
+const assert = @import("../quirks.zig").inlineAssert;
+const fastmem = @import("../fastmem.zig");
+
+/// Unit is the type we use per tabstop unit (see file docs).
+const Unit = u8;
+const unit_bits = @bitSizeOf(Unit);
+
+/// The number of columns we preallocate for. This is kind of high which
+/// costs us some memory, but this is more columns than my 6k monitor at
+/// 12-point font size, so this should prevent allocation in almost all
+/// real world scenarios for the price of wasting at most
+/// (columns / sizeOf(Unit)) bytes.
+const prealloc_columns = 512;
+
+/// The number of entries we need for our preallocation.
+const prealloc_count = prealloc_columns / unit_bits;
+
+/// We precompute all the possible masks since we never use a huge bit size.
+const masks = blk: {
+ var res: [unit_bits]Unit = undefined;
+ for (res, 0..) |_, i| {
+ res[i] = @shlExact(@as(Unit, 1), @as(u3, @intCast(i)));
+ }
+
+ break :blk res;
+};
+
+/// The number of columns this tabstop is set to manage. Use resize()
+/// to change this number.
+cols: usize = 0,
+
+/// Preallocated tab stops.
+prealloc_stops: [prealloc_count]Unit = @splat(0),
+
+/// Dynamically expanded stops above prealloc stops.
+dynamic_stops: []Unit = &[0]Unit{},
+
+/// Returns the entry in the stops array that would contain this column.
+inline fn entry(col: usize) usize {
+ return col / unit_bits;
+}
+
+inline fn index(col: usize) usize {
+ return @mod(col, unit_bits);
+}
+
+pub fn init(
+ alloc: Allocator,
+ cols: usize,
+ interval: usize,
+) Allocator.Error!Tabstops {
+ var res: Tabstops = .{};
+ try res.resize(alloc, cols);
+ res.reset(interval);
+ return res;
+}
+
+pub fn deinit(self: *Tabstops, alloc: Allocator) void {
+ if (self.dynamic_stops.len > 0) alloc.free(self.dynamic_stops);
+ self.* = undefined;
+}
+
+/// Set the tabstop at a certain column. The columns are 0-indexed.
+pub fn set(self: *Tabstops, col: usize) void {
+ const i = entry(col);
+ const idx = index(col);
+ if (i < prealloc_count) {
+ self.prealloc_stops[i] |= masks[idx];
+ return;
+ }
+
+ const dynamic_i = i - prealloc_count;
+ assert(dynamic_i < self.dynamic_stops.len);
+ self.dynamic_stops[dynamic_i] |= masks[idx];
+}
+
+/// Unset the tabstop at a certain column. The columns are 0-indexed.
+pub fn unset(self: *Tabstops, col: usize) void {
+ const i = entry(col);
+ const idx = index(col);
+ if (i < prealloc_count) {
+ self.prealloc_stops[i] ^= masks[idx];
+ return;
+ }
+
+ const dynamic_i = i - prealloc_count;
+ assert(dynamic_i < self.dynamic_stops.len);
+ self.dynamic_stops[dynamic_i] ^= masks[idx];
+}
+
+/// Get the value of a tabstop at a specific column. The columns are 0-indexed.
+pub fn get(self: Tabstops, col: usize) bool {
+ const i = entry(col);
+ const idx = index(col);
+ const mask = masks[idx];
+ const unit = if (i < prealloc_count)
+ self.prealloc_stops[i]
+ else unit: {
+ const dynamic_i = i - prealloc_count;
+ assert(dynamic_i < self.dynamic_stops.len);
+ break :unit self.dynamic_stops[dynamic_i];
+ };
+
+ return unit & mask == mask;
+}
+
+const resize_tw = tripwire.module(enum {
+ dynamic_alloc,
+}, resize);
+
+/// Resize this to support up to cols columns.
+// TODO: needs interval to set new tabstops
+pub fn resize(
+ self: *Tabstops,
+ alloc: Allocator,
+ cols: usize,
+) Allocator.Error!void {
+ const tw = resize_tw;
+
+ // Do nothing if it fits.
+ if (cols <= prealloc_columns) {
+ self.cols = cols;
+ return;
+ }
+
+ // What we need in the dynamic size
+ const size = cols - prealloc_columns;
+ if (size < self.dynamic_stops.len) {
+ self.cols = cols;
+ return;
+ }
+
+ // Note: we can probably try to realloc here but I'm not sure it matters.
+ try tw.check(.dynamic_alloc);
+ const new = try alloc.alloc(Unit, size);
+ errdefer comptime unreachable;
+ @memset(new, 0);
+ if (self.dynamic_stops.len > 0) {
+ fastmem.copy(Unit, new, self.dynamic_stops);
+ alloc.free(self.dynamic_stops);
+ }
+
+ self.dynamic_stops = new;
+ self.cols = cols;
+}
+
+/// Return the maximum number of columns this can support currently.
+pub fn capacity(self: Tabstops) usize {
+ return (prealloc_count + self.dynamic_stops.len) * unit_bits;
+}
+
+/// Unset all tabstops and then reset the initial tabstops to the given
+/// interval. An interval of 0 sets no tabstops.
+pub fn reset(self: *Tabstops, interval: usize) void {
+ @memset(&self.prealloc_stops, 0);
+ @memset(self.dynamic_stops, 0);
+
+ if (interval > 0) {
+ var i: usize = interval;
+ while (i < self.cols - 1) : (i += interval) {
+ self.set(i);
+ }
+ }
+}
+
+test "Tabstops: basic" {
+ var t: Tabstops = .{};
+ defer t.deinit(testing.allocator);
+ try testing.expectEqual(@as(usize, 0), entry(4));
+ try testing.expectEqual(@as(usize, 1), entry(8));
+ try testing.expectEqual(@as(usize, 0), index(0));
+ try testing.expectEqual(@as(usize, 1), index(1));
+ try testing.expectEqual(@as(usize, 1), index(9));
+
+ try testing.expectEqual(@as(Unit, 0b00001000), masks[3]);
+ try testing.expectEqual(@as(Unit, 0b00010000), masks[4]);
+
+ try testing.expect(!t.get(4));
+ t.set(4);
+ try testing.expect(t.get(4));
+ try testing.expect(!t.get(3));
+
+ t.reset(0);
+ try testing.expect(!t.get(4));
+
+ t.set(4);
+ try testing.expect(t.get(4));
+ t.unset(4);
+ try testing.expect(!t.get(4));
+}
+
+test "Tabstops: dynamic allocations" {
+ var t: Tabstops = .{};
+ defer t.deinit(testing.allocator);
+
+ // Grow the capacity by 2.
+ const cap = t.capacity();
+ try t.resize(testing.allocator, cap * 2);
+
+ // Set something that was out of range of the first
+ t.set(cap + 5);
+ try testing.expect(t.get(cap + 5));
+ try testing.expect(!t.get(cap + 4));
+
+ // Prealloc still works
+ try testing.expect(!t.get(5));
+}
+
+test "Tabstops: interval" {
+ var t: Tabstops = try init(testing.allocator, 80, 4);
+ defer t.deinit(testing.allocator);
+ try testing.expect(!t.get(0));
+ try testing.expect(t.get(4));
+ try testing.expect(!t.get(5));
+ try testing.expect(t.get(8));
+}
+
+test "Tabstops: count on 80" {
+ // https://superuser.com/questions/710019/why-there-are-11-tabstops-on-a-80-column-console
+
+ var t: Tabstops = try init(testing.allocator, 80, 8);
+ defer t.deinit(testing.allocator);
+
+ // Count the tabstops
+ const count: usize = count: {
+ var v: usize = 0;
+ var i: usize = 0;
+ while (i < 80) : (i += 1) {
+ if (t.get(i)) {
+ v += 1;
+ }
+ }
+
+ break :count v;
+ };
+
+ try testing.expectEqual(@as(usize, 9), count);
+}
+
+test "Tabstops: resize alloc failure preserves state" {
+ // This test verifies that if resize() fails during allocation,
+ // the original cols value is preserved (not corrupted).
+ var t: Tabstops = try init(testing.allocator, 80, 8);
+ defer t.deinit(testing.allocator);
+
+ const original_cols = t.cols;
+
+ // Trigger allocation failure when resizing beyond prealloc
+ resize_tw.errorAlways(.dynamic_alloc, error.OutOfMemory);
+ const result = t.resize(testing.allocator, prealloc_columns * 2);
+ try testing.expectError(error.OutOfMemory, result);
+ try resize_tw.end(.reset);
+
+ // cols should be unchanged after failed resize
+ try testing.expectEqual(original_cols, t.cols);
+}