ghostty/src/terminal/Terminal.zig
Mitchell Hashimoto 2767f19ced bell doesn't crash
2022-04-27 09:12:23 -07:00

229 lines
6.4 KiB
Zig

//! The primary terminal emulation structure. This represents a single
//! "terminal" containing a grid of characters and exposes various operations
//! on that grid. This also maintains the scrollback buffer.
const Terminal = @This();
const std = @import("std");
const builtin = @import("builtin");
const testing = std.testing;
const Allocator = std.mem.Allocator;
const ansi = @import("ansi.zig");
const Parser = @import("Parser.zig");
const log = std.log.scoped(.terminal);
/// Screen is the current screen state.
screen: Screen,
/// Cursor position.
cursor: Cursor,
/// The size of the terminal.
rows: usize,
cols: usize,
/// VT stream parser
parser: Parser,
/// Screen represents a presentable terminal screen made up of lines and cells.
const Screen = std.ArrayListUnmanaged(Line);
const Line = std.ArrayListUnmanaged(Cell);
/// Cell is a single cell within the terminal.
const Cell = struct {
/// Each cell contains exactly one character. The character is UTF-8 encoded.
char: u32,
// TODO(mitchellh): this is where we'll track fg/bg and other attrs.
/// True if the cell should be skipped for drawing
pub fn empty(self: Cell) bool {
return self.char == 0;
}
};
/// Cursor represents the cursor state.
const Cursor = struct {
x: usize,
y: usize,
};
/// Initialize a new terminal.
pub fn init(cols: usize, rows: usize) Terminal {
return .{
.cols = cols,
.rows = rows,
.screen = .{},
.cursor = .{ .x = 0, .y = 0 },
.parser = Parser.init(),
};
}
pub fn deinit(self: *Terminal, alloc: Allocator) void {
for (self.screen.items) |*line| line.deinit(alloc);
self.screen.deinit(alloc);
self.* = undefined;
}
/// Resize the underlying terminal.
pub fn resize(self: *Terminal, cols: usize, rows: usize) void {
// TODO: actually doing anything for this
self.cols = cols;
self.rows = rows;
}
/// Return the current string value of the terminal. Newlines are
/// encoded as "\n". This omits any formatting such as fg/bg.
///
/// The caller must free the string.
pub fn plainString(self: Terminal, alloc: Allocator) ![]const u8 {
// Create a buffer that has the number of lines we have times the maximum
// width it could possibly be. In all likelihood we aren't using the full
// width (of at least the last line) but the error margine here won't be
// much.
const buffer = try alloc.alloc(u8, self.screen.items.len * self.cols * 4);
var i: usize = 0;
for (self.screen.items) |line, y| {
if (y > 0) {
buffer[i] = '\n';
i += 1;
}
for (line.items) |cell| {
i += try std.unicode.utf8Encode(@intCast(u21, cell.char), buffer[i..]);
}
}
return buffer[0..i];
}
/// Append a string of characters. See appendChar.
pub fn append(self: *Terminal, alloc: Allocator, str: []const u8) !void {
for (str) |c| {
try self.appendChar(alloc, c);
}
}
/// Append a single character to the terminal.
///
/// This may allocate if necessary to store the character in the grid.
pub fn appendChar(self: *Terminal, alloc: Allocator, c: u8) !void {
log.debug("char: {}", .{c});
const actions = self.parser.next(c);
for (actions) |action_opt| {
switch (action_opt orelse continue) {
.print => |p| try self.print(alloc, p),
.execute => |code| try self.execute(code),
}
}
}
fn print(self: *Terminal, alloc: Allocator, c: u8) !void {
// Build our cell
const cell = try self.getOrPutCell(alloc, self.cursor.x, self.cursor.y);
cell.* = .{
.char = @intCast(u32, c),
};
// Move the cursor
self.cursor.x += 1;
}
fn execute(self: *Terminal, c: u8) !void {
switch (@intToEnum(ansi.C0, c)) {
.BEL => self.bell(),
.BS => self.backspace(),
.LF => self.linefeed(),
.CR => self.carriage_return(),
}
}
pub fn bell(self: *Terminal) void {
// TODO: bell
_ = self;
log.info("bell", .{});
}
/// Backspace moves the cursor back a column (but not less than 0).
pub fn backspace(self: *Terminal) void {
self.cursor.x -|= 1;
}
/// Carriage return moves the cursor to the first column.
pub fn carriage_return(self: *Terminal) void {
self.cursor.x = 0;
}
/// Linefeed moves the cursor to the next line.
pub fn linefeed(self: *Terminal) void {
// TODO: end of screen
self.cursor.y += 1;
}
fn getOrPutCell(self: *Terminal, alloc: Allocator, x: usize, y: usize) !*Cell {
// If we don't have enough lines to get to y, then add it.
if (self.screen.items.len < y + 1) {
try self.screen.ensureTotalCapacity(alloc, y + 1);
self.screen.appendNTimesAssumeCapacity(.{}, y + 1 - self.screen.items.len);
}
const line = &self.screen.items[y];
if (line.items.len < x + 1) {
try line.ensureTotalCapacity(alloc, x + 1);
line.appendNTimesAssumeCapacity(undefined, x + 1 - line.items.len);
}
return &line.items[x];
}
test {
_ = Parser;
}
test "Terminal: input with no control characters" {
var t = init(80, 80);
defer t.deinit(testing.allocator);
// Basic grid writing
try t.append(testing.allocator, "hello");
try testing.expectEqual(@as(usize, 0), t.cursor.y);
try testing.expectEqual(@as(usize, 5), t.cursor.x);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hello", str);
}
}
test "Terminal: C0 control LF and CR" {
var t = init(80, 80);
defer t.deinit(testing.allocator);
// Basic grid writing
try t.append(testing.allocator, "hello\r\nworld");
try testing.expectEqual(@as(usize, 1), t.cursor.y);
try testing.expectEqual(@as(usize, 5), t.cursor.x);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hello\nworld", str);
}
}
test "Terminal: C0 control BS" {
var t = init(80, 80);
defer t.deinit(testing.allocator);
// BS
try t.append(testing.allocator, "hello");
try t.appendChar(testing.allocator, @enumToInt(ansi.C0.BS));
try t.append(testing.allocator, "y");
try testing.expectEqual(@as(usize, 0), t.cursor.y);
try testing.expectEqual(@as(usize, 5), t.cursor.x);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("helly", str);
}
}