From dd7bb1fab5f522fff4f6b6ba62d43c0bdd718c43 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Feb 2024 17:16:23 -0800 Subject: [PATCH] terminal/new: backspace, cursor left --- src/terminal/Terminal.zig | 1 + src/terminal/new/Screen.zig | 21 +++++- src/terminal/new/Terminal.zig | 121 ++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 8ceebc289..66d0d5ba7 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -3004,6 +3004,7 @@ test "Terminal: carriage return right of left margin moves to left margin" { try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); } +// X test "Terminal: backspace" { var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); diff --git a/src/terminal/new/Screen.zig b/src/terminal/new/Screen.zig index b2884555e..7857b252c 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal/new/Screen.zig @@ -152,7 +152,7 @@ pub fn cursorDown(self: *Screen) void { self.cursor.y += 1; } -/// Move the cursor to some absolute position. +/// Move the cursor to some absolute horizontal position. pub fn cursorHorizontalAbsolute(self: *Screen, x: size.CellCountInt) void { assert(x < self.pages.cols); @@ -161,6 +161,25 @@ pub fn cursorHorizontalAbsolute(self: *Screen, x: size.CellCountInt) void { self.cursor.x = x; } +/// Move the cursor to some absolute position. +pub fn cursorAbsolute(self: *Screen, x: size.CellCountInt, y: size.CellCountInt) void { + assert(x < self.pages.cols); + assert(y < self.pages.rows); + + const page_offset = if (y < self.cursor.y) + self.cursor.page_offset.backward(self.cursor.y - y).? + else if (y > self.cursor.y) + self.cursor.page_offset.forward(y - self.cursor.y).? + else + self.cursor.page_offset; + const page_rac = page_offset.rowAndCell(x); + self.cursor.page_offset = page_offset; + self.cursor.page_row = page_rac.row; + self.cursor.page_cell = page_rac.cell; + self.cursor.x = x; + self.cursor.y = y; +} + /// Scroll the active area and keep the cursor at the bottom of the screen. /// This is a very specialized function but it keeps it fast. pub fn cursorDownScroll(self: *Screen) !void { diff --git a/src/terminal/new/Terminal.zig b/src/terminal/new/Terminal.zig index 07f400978..be2d5ce81 100644 --- a/src/terminal/new/Terminal.zig +++ b/src/terminal/new/Terminal.zig @@ -430,6 +430,110 @@ pub fn linefeed(self: *Terminal) !void { if (self.modes.get(.linefeed)) self.carriageReturn(); } +/// Backspace moves the cursor back a column (but not less than 0). +pub fn backspace(self: *Terminal) void { + self.cursorLeft(1); +} + +/// Move the cursor to the left amount cells. If amount is 0, adjust it to 1. +pub fn cursorLeft(self: *Terminal, count_req: usize) void { + // Wrapping behavior depends on various terminal modes + const WrapMode = enum { none, reverse, reverse_extended }; + const wrap_mode: WrapMode = wrap_mode: { + if (!self.modes.get(.wraparound)) break :wrap_mode .none; + if (self.modes.get(.reverse_wrap_extended)) break :wrap_mode .reverse_extended; + if (self.modes.get(.reverse_wrap)) break :wrap_mode .reverse; + break :wrap_mode .none; + }; + + var count: size.CellCountInt = @intCast(@max(count_req, 1)); + + // If we are in no wrap mode, then we move the cursor left and exit + // since this is the fastest and most typical path. + if (wrap_mode == .none) { + self.screen.cursorLeft(count); + self.screen.cursor.pending_wrap = false; + return; + } + + // If we have a pending wrap state and we are in either reverse wrap + // modes then we decrement the amount we move by one to match xterm. + if (self.screen.cursor.pending_wrap) { + count -= 1; + self.screen.cursor.pending_wrap = false; + } + + // The margins we can move to. + const top = self.scrolling_region.top; + const bottom = self.scrolling_region.bottom; + const right_margin = self.scrolling_region.right; + const left_margin = if (self.screen.cursor.x < self.scrolling_region.left) + 0 + else + self.scrolling_region.left; + + // Handle some edge cases when our cursor is already on the left margin. + if (self.screen.cursor.x == left_margin) { + switch (wrap_mode) { + // In reverse mode, if we're already before the top margin + // then we just set our cursor to the top-left and we're done. + .reverse => if (self.screen.cursor.y <= top) { + self.screen.cursorAbsolute(left_margin, top); + return; + }, + + // Handled in while loop + .reverse_extended => {}, + + // Handled above + .none => unreachable, + } + } + + while (true) { + // We can move at most to the left margin. + const max = self.screen.cursor.x - left_margin; + + // We want to move at most the number of columns we have left + // or our remaining count. Do the move. + const amount = @min(max, count); + count -= amount; + self.screen.cursorLeft(amount); + + // If we have no more to move, then we're done. + if (count == 0) break; + + // If we are at the top, then we are done. + if (self.screen.cursor.y == top) { + if (wrap_mode != .reverse_extended) break; + + self.screen.cursorAbsolute(right_margin, bottom); + count -= 1; + continue; + } + + // UNDEFINED TERMINAL BEHAVIOR. This situation is not handled in xterm + // and currently results in a crash in xterm. Given no other known + // terminal [to me] implements XTREVWRAP2, I decided to just mimick + // the behavior of xterm up and not including the crash by wrapping + // up to the (0, 0) and stopping there. My reasoning is that for an + // appropriately sized value of "count" this is the behavior that xterm + // would have. This is unit tested. + if (self.screen.cursor.y == 0) { + assert(self.screen.cursor.x == left_margin); + break; + } + + // If our previous line is not wrapped then we are done. + if (wrap_mode != .reverse_extended) { + if (!self.screen.cursor.page_row.flags.wrap) break; + } + + self.screen.cursorAbsolute(right_margin, self.screen.cursor.y - 1); + count -= 1; + } +} + /// Move the cursor to the next line in the scrolling region, possibly scrolling. /// /// If the cursor is outside of the scrolling region: move the cursor one line @@ -933,3 +1037,20 @@ test "Terminal: carriage return right of left margin moves to left margin" { t.carriageReturn(); try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); } + +test "Terminal: backspace" { + var t = try init(testing.allocator, 80, 80); + defer t.deinit(testing.allocator); + + // BS + for ("hello") |c| try t.print(c); + t.backspace(); + try t.print('y'); + try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); + try testing.expectEqual(@as(usize, 5), t.screen.cursor.x); + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("helly", str); + } +}