mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
horizontal tab, implement tabstops using a default value
This commit is contained in:
@ -161,7 +161,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window {
|
|||||||
try stream.readStart(ttyReadAlloc, ttyRead);
|
try stream.readStart(ttyReadAlloc, ttyRead);
|
||||||
|
|
||||||
// Create our terminal
|
// 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);
|
errdefer term.deinit(alloc);
|
||||||
|
|
||||||
// Setup a timer for blinking the cursor
|
// Setup a timer for blinking the cursor
|
||||||
|
179
src/terminal/Tabstops.zig
Normal file
179
src/terminal/Tabstops.zig
Normal file
@ -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));
|
||||||
|
}
|
@ -9,6 +9,7 @@ const testing = std.testing;
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ansi = @import("ansi.zig");
|
const ansi = @import("ansi.zig");
|
||||||
const Parser = @import("Parser.zig");
|
const Parser = @import("Parser.zig");
|
||||||
|
const Tabstops = @import("Tabstops.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.terminal);
|
const log = std.log.scoped(.terminal);
|
||||||
|
|
||||||
@ -18,6 +19,9 @@ screen: Screen,
|
|||||||
/// Cursor position.
|
/// Cursor position.
|
||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
|
|
||||||
|
/// Where the tabstops are.
|
||||||
|
tabstops: Tabstops,
|
||||||
|
|
||||||
/// The size of the terminal.
|
/// The size of the terminal.
|
||||||
rows: usize,
|
rows: usize,
|
||||||
cols: usize,
|
cols: usize,
|
||||||
@ -49,17 +53,19 @@ const Cursor = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Initialize a new terminal.
|
/// Initialize a new terminal.
|
||||||
pub fn init(cols: usize, rows: usize) Terminal {
|
pub fn init(alloc: Allocator, cols: usize, rows: usize) !Terminal {
|
||||||
return .{
|
return Terminal{
|
||||||
.cols = cols,
|
.cols = cols,
|
||||||
.rows = rows,
|
.rows = rows,
|
||||||
.screen = .{},
|
.screen = .{},
|
||||||
.cursor = .{ .x = 0, .y = 0 },
|
.cursor = .{ .x = 0, .y = 0 },
|
||||||
|
.tabstops = try Tabstops.init(alloc, cols, 8),
|
||||||
.parser = Parser.init(),
|
.parser = Parser.init(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Terminal, alloc: Allocator) void {
|
pub fn deinit(self: *Terminal, alloc: Allocator) void {
|
||||||
|
self.tabstops.deinit(alloc);
|
||||||
for (self.screen.items) |*line| line.deinit(alloc);
|
for (self.screen.items) |*line| line.deinit(alloc);
|
||||||
self.screen.deinit(alloc);
|
self.screen.deinit(alloc);
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
@ -113,7 +119,7 @@ pub fn appendChar(self: *Terminal, alloc: Allocator, c: u8) !void {
|
|||||||
for (actions) |action_opt| {
|
for (actions) |action_opt| {
|
||||||
switch (action_opt orelse continue) {
|
switch (action_opt orelse continue) {
|
||||||
.print => |p| try self.print(alloc, p),
|
.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;
|
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)) {
|
switch (@intToEnum(ansi.C0, c)) {
|
||||||
.BEL => self.bell(),
|
.BEL => self.bell(),
|
||||||
.BS => self.backspace(),
|
.BS => self.backspace(),
|
||||||
.HT => self.horizontal_tab(),
|
.HT => try self.horizontal_tab(alloc),
|
||||||
.LF => self.linefeed(),
|
.LF => self.linefeed(),
|
||||||
.CR => self.carriage_return(),
|
.CR => self.carriage_return(),
|
||||||
}
|
}
|
||||||
@ -150,9 +156,16 @@ pub fn backspace(self: *Terminal) void {
|
|||||||
self.cursor.x -|= 1;
|
self.cursor.x -|= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TODO
|
/// Horizontal tab moves the cursor to the next tabstop, clearing
|
||||||
pub fn horizontal_tab(self: *Terminal) void {
|
/// the screen to the left the tabstop.
|
||||||
_ = self;
|
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.
|
/// 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 {
|
test {
|
||||||
_ = Parser;
|
_ = Parser;
|
||||||
|
_ = Tabstops;
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Terminal: input with no control characters" {
|
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);
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
// Basic grid writing
|
// Basic grid writing
|
||||||
@ -202,7 +216,7 @@ test "Terminal: input with no control characters" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test "Terminal: C0 control LF and CR" {
|
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);
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
// Basic grid writing
|
// Basic grid writing
|
||||||
@ -217,7 +231,7 @@ test "Terminal: C0 control LF and CR" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test "Terminal: C0 control BS" {
|
test "Terminal: C0 control BS" {
|
||||||
var t = init(80, 80);
|
var t = try init(testing.allocator, 80, 80);
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
// BS
|
// BS
|
||||||
|
Reference in New Issue
Block a user