diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/Tabstops.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (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.zig | 271 |
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); +} |
