diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 5b48c913a..4dd03c02e 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -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); diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 5de446b9f..068fd7df2 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -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; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 834f07006..5f50b4925 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -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),