implement auto-wrap, always enabled for now (ignores mode 7)

This commit is contained in:
Mitchell Hashimoto
2022-07-08 17:37:22 -07:00
parent 9e82d682c5
commit 4cc38e7281
3 changed files with 83 additions and 8 deletions

View File

@ -26,6 +26,11 @@ pub const Cell = struct {
bold: u1 = 0,
underline: u1 = 0,
inverse: u1 = 0,
/// If 1, this line is soft-wrapped. Only the last cell in a row
/// should have this set. The first cell of the next row is actually
/// part of this row in raw input.
wrap: u1 = 0,
} = .{},
/// True if the cell should be skipped for drawing
@ -152,9 +157,11 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void {
self.rows = rows;
self.cols = cols;
// TODO: reflow due to soft wrap
// If we're increasing height, then copy all rows (start at 0).
// Otherwise start at the latest row that includes the bottom row,
// aka trip the top.
// aka strip the top.
var y: usize = if (rows >= old.rows) 0 else old.rows - rows;
const start = y;
const col_end = @minimum(old.cols, cols);

View File

@ -62,6 +62,9 @@ const Cursor = struct {
// pen is the current cell styling to apply to new cells.
pen: Screen.Cell = .{ .char = 0 },
// The last column flag (LCF) used to do soft wrapping.
pending_wrap: bool = false,
};
/// Initialize a new terminal.
@ -198,6 +201,18 @@ pub fn print(self: *Terminal, c: u21) !void {
const tracy = trace(@src());
defer tracy.end();
// If we're soft-wrapping, then handle that first.
if (self.cursor.pending_wrap) {
// Mark that the cell is wrapped, which guarantees that there is
// at least one cell after it in the next row.
const cell = self.screen.getCell(self.cursor.y, self.cursor.x);
cell.attrs.wrap = 1;
// Move to the next line
self.index();
self.cursor.x = 0;
}
// Build our cell
const cell = self.screen.getCell(self.cursor.y, self.cursor.x);
cell.* = self.cursor.pen;
@ -206,9 +221,12 @@ pub fn print(self: *Terminal, c: u21) !void {
// Move the cursor
self.cursor.x += 1;
// TODO: wrap
// If we're at the column limit, then we need to wrap the next time.
// This is unlikely so we do the increment above and decrement here
// if we need to rather than check once.
if (self.cursor.x == self.cols) {
self.cursor.x -= 1;
self.cursor.pending_wrap = true;
}
}
@ -246,6 +264,9 @@ pub fn decaln(self: *Terminal) void {
///
/// This unsets the pending wrap state without wrapping.
pub fn index(self: *Terminal) void {
// Unset pending wrap state
self.cursor.pending_wrap = false;
// If we're at the end of the screen, scroll up. This is surprisingly
// common because most terminals live with a full screen so we do this
// check first.
@ -315,6 +336,9 @@ pub fn setCursorPos(self: *Terminal, row: usize, col: usize) void {
self.cursor.x = @minimum(params.x_max, col) -| 1;
self.cursor.y = @minimum(params.y_max, row + params.y_offset) -| 1;
// Unset pending wrap state
self.cursor.pending_wrap = false;
}
/// Erase the display.
@ -548,9 +572,9 @@ pub fn carriageReturn(self: *Terminal) void {
// TODO: left/right margin mode
// TODO: origin mode
// TODO: wrap state
self.cursor.x = 0;
self.cursor.pending_wrap = false;
}
/// Linefeed moves the cursor to the next line.
@ -721,6 +745,21 @@ test "Terminal: input with no control characters" {
}
}
test "Terminal: soft wrap" {
var t = try init(testing.allocator, 3, 80);
defer t.deinit(testing.allocator);
// Basic grid writing
for ("hello") |c| try t.print(c);
try testing.expectEqual(@as(usize, 1), t.cursor.y);
try testing.expectEqual(@as(usize, 2), t.cursor.x);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hel\nlo", str);
}
}
test "Terminal: linefeed and carriage return" {
var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator);
@ -739,6 +778,28 @@ test "Terminal: linefeed and carriage return" {
}
}
test "Terminal: linefeed unsets pending wrap" {
var t = try init(testing.allocator, 5, 80);
defer t.deinit(testing.allocator);
// Basic grid writing
for ("hello") |c| try t.print(c);
try testing.expect(t.cursor.pending_wrap == true);
t.linefeed();
try testing.expect(t.cursor.pending_wrap == false);
}
test "Terminal: carriage return unsets pending wrap" {
var t = try init(testing.allocator, 5, 80);
defer t.deinit(testing.allocator);
// Basic grid writing
for ("hello") |c| try t.print(c);
try testing.expect(t.cursor.pending_wrap == true);
t.carriageReturn();
try testing.expect(t.cursor.pending_wrap == false);
}
test "Terminal: backspace" {
var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator);
@ -788,6 +849,13 @@ test "Terminal: setCursorPosition" {
try testing.expectEqual(@as(usize, 79), t.cursor.x);
try testing.expectEqual(@as(usize, 79), t.cursor.y);
// Should reset pending wrap
t.setCursorPos(0, 80);
try t.print('c');
try testing.expect(t.cursor.pending_wrap);
t.setCursorPos(0, 80);
try testing.expect(!t.cursor.pending_wrap);
// Origin mode
t.mode_origin = true;

View File

@ -48,11 +48,11 @@ pub fn Stream(comptime Handler: type) type {
//log.debug("char: {x}", .{c});
const actions = self.parser.next(c);
for (actions) |action_opt| {
// if (action_opt) |action| {
// if (action != .print) {
// log.info("action: {}", .{action});
// }
// }
if (action_opt) |action| {
if (action != .print) {
log.info("action: {}", .{action});
}
}
switch (action_opt orelse continue) {
.print => |p| if (@hasDecl(T, "print")) try self.handler.print(p),
.execute => |code| try self.execute(code),