diff --git a/src/Window.zig b/src/Window.zig index f5d9758c8..32ad18587 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -161,7 +161,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window { try stream.readStart(ttyReadAlloc, ttyRead); // Create our terminal - var term = Terminal.init(grid.size.columns, grid.size.rows); + var term = try Terminal.init(alloc, grid.size.columns, grid.size.rows); errdefer term.deinit(alloc); // Setup a timer for blinking the cursor diff --git a/src/terminal/Tabstops.zig b/src/terminal/Tabstops.zig new file mode 100644 index 000000000..d819a7d71 --- /dev/null +++ b/src/terminal/Tabstops.zig @@ -0,0 +1,179 @@ +//! 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 Allocator = std.mem.Allocator; +const testing = std.testing; +const assert = std.debug.assert; + +/// Unit is the type we use per tabstop unit (see file docs). +const Unit = u8; +const unit_bits = @bitSizeOf(Unit); + +/// The number of columsn 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) |_, i| { + res[i] = @shlExact(@as(Unit, 1), @intCast(u3, i)); + } + + break :blk res; +}; + +/// Preallocated tab stops. +prealloc_stops: [prealloc_count]Unit = [1]Unit{0} ** prealloc_count, + +/// 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) !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. +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]; +} + +/// Get the value of a tabstop at a specific column. +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; +} + +/// Resize this to support up to cols columns. +pub fn resize(self: *Tabstops, alloc: Allocator, cols: usize) !void { + // Do nothing if it fits. + if (cols <= prealloc_columns) return; + + // What we need in the dynamic size + const size = cols - prealloc_columns; + if (size < self.dynamic_stops.len) return; + + // Note: we can probably try to realloc here but I'm not sure it matters. + const new = try alloc.alloc(Unit, size); + if (self.dynamic_stops.len > 0) { + std.mem.copy(Unit, new, self.dynamic_stops); + alloc.free(self.dynamic_stops); + } + + self.dynamic_stops = new; +} + +/// Return the total number of columns this can support currently. +pub fn capacity(self: Tabstops) usize { + return prealloc_count + self.dynamic_stops.len; +} + +/// 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 { + std.mem.set(Unit, &self.prealloc_stops, 0); + std.mem.set(Unit, self.dynamic_stops, 0); + + if (interval > 0) { + const cap = self.capacity(); + var i: usize = interval - 1; + while (i < cap) : (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(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)); +} + +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(3)); + try testing.expect(!t.get(4)); + try testing.expect(t.get(7)); +} diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 7f0e94657..35fd7f0af 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -9,6 +9,7 @@ const testing = std.testing; const Allocator = std.mem.Allocator; const ansi = @import("ansi.zig"); const Parser = @import("Parser.zig"); +const Tabstops = @import("Tabstops.zig"); const log = std.log.scoped(.terminal); @@ -18,6 +19,9 @@ screen: Screen, /// Cursor position. cursor: Cursor, +/// Where the tabstops are. +tabstops: Tabstops, + /// The size of the terminal. rows: usize, cols: usize, @@ -49,17 +53,19 @@ const Cursor = struct { }; /// Initialize a new terminal. -pub fn init(cols: usize, rows: usize) Terminal { - return .{ +pub fn init(alloc: Allocator, cols: usize, rows: usize) !Terminal { + return Terminal{ .cols = cols, .rows = rows, .screen = .{}, .cursor = .{ .x = 0, .y = 0 }, + .tabstops = try Tabstops.init(alloc, cols, 8), .parser = Parser.init(), }; } pub fn deinit(self: *Terminal, alloc: Allocator) void { + self.tabstops.deinit(alloc); for (self.screen.items) |*line| line.deinit(alloc); self.screen.deinit(alloc); self.* = undefined; @@ -113,7 +119,7 @@ pub fn appendChar(self: *Terminal, alloc: Allocator, c: u8) !void { for (actions) |action_opt| { switch (action_opt orelse continue) { .print => |p| try self.print(alloc, p), - .execute => |code| try self.execute(code), + .execute => |code| try self.execute(alloc, code), } } } @@ -129,11 +135,11 @@ fn print(self: *Terminal, alloc: Allocator, c: u8) !void { self.cursor.x += 1; } -fn execute(self: *Terminal, c: u8) !void { +fn execute(self: *Terminal, alloc: Allocator, c: u8) !void { switch (@intToEnum(ansi.C0, c)) { .BEL => self.bell(), .BS => self.backspace(), - .HT => self.horizontal_tab(), + .HT => try self.horizontal_tab(alloc), .LF => self.linefeed(), .CR => self.carriage_return(), } @@ -150,9 +156,16 @@ pub fn backspace(self: *Terminal) void { self.cursor.x -|= 1; } -/// TODO -pub fn horizontal_tab(self: *Terminal) void { - _ = self; +/// Horizontal tab moves the cursor to the next tabstop, clearing +/// the screen to the left the tabstop. +pub fn horizontal_tab(self: *Terminal, alloc: Allocator) !void { + while (self.cursor.x < self.cols) { + // Clear + try self.print(alloc, ' '); + + // If this is the tabstop, then we're done. + if (self.tabstops.get(self.cursor.x)) return; + } } /// Carriage return moves the cursor to the first column. @@ -184,10 +197,11 @@ fn getOrPutCell(self: *Terminal, alloc: Allocator, x: usize, y: usize) !*Cell { test { _ = Parser; + _ = Tabstops; } test "Terminal: input with no control characters" { - var t = init(80, 80); + var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); // Basic grid writing @@ -202,7 +216,7 @@ test "Terminal: input with no control characters" { } test "Terminal: C0 control LF and CR" { - var t = init(80, 80); + var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); // Basic grid writing @@ -217,7 +231,7 @@ test "Terminal: C0 control LF and CR" { } test "Terminal: C0 control BS" { - var t = init(80, 80); + var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); // BS