From 1d30577506da009e699afda849afa2cddad24a24 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 27 Feb 2024 19:22:32 -0800 Subject: [PATCH] terminal/new: scroll clear --- src/terminal/new/PageList.zig | 63 +++++++++++++++++++++++++++++++++++ src/terminal/new/Screen.zig | 7 ++++ src/terminal/new/Terminal.zig | 59 ++++++++++++++++---------------- src/terminal/new/page.zig | 13 ++++++++ 4 files changed, 113 insertions(+), 29 deletions(-) diff --git a/src/terminal/new/PageList.zig b/src/terminal/new/PageList.zig index 5fbd7c480..06e5ede30 100644 --- a/src/terminal/new/PageList.zig +++ b/src/terminal/new/PageList.zig @@ -254,6 +254,36 @@ pub fn scroll(self: *PageList, behavior: Scroll) void { } } +/// Clear the screen by scrolling written contents up into the scrollback. +/// This will not update the viewport. +pub fn scrollClear(self: *PageList) !void { + // Go through the active area backwards to find the first non-empty + // row. We use this to determine how many rows to scroll up. + const non_empty: usize = non_empty: { + var page = self.pages.last.?; + var n: usize = 0; + while (true) { + const rows: [*]Row = page.data.rows.ptr(page.data.memory); + for (0..page.data.size.rows) |i| { + const rev_i = page.data.size.rows - i - 1; + const row = rows[rev_i]; + const cells = row.cells.ptr(page.data.memory)[0..self.cols]; + for (cells) |cell| { + if (!cell.isEmpty()) break :non_empty self.rows - n; + } + + n += 1; + if (n > self.rows) break :non_empty 0; + } + + page = page.prev orelse break :non_empty 0; + } + }; + + // Scroll + for (0..non_empty) |_| _ = try self.grow(); +} + /// Grow the active area by exactly one row. /// /// This may allocate, but also may not if our current page has more @@ -958,6 +988,39 @@ test "PageList scroll delta row forward into active" { } } +test "PageList scroll clear" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + { + const cell = s.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; + cell.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'A' }, + }; + } + { + const cell = s.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?; + cell.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'A' }, + }; + } + + try s.scrollClear(); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 2, + } }, pt); + } +} + test "PageList grow fit in capacity" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/new/Screen.zig b/src/terminal/new/Screen.zig index 1b506fecd..7979908ba 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal/new/Screen.zig @@ -277,6 +277,13 @@ pub fn scroll(self: *Screen, behavior: Scroll) void { } } +/// See PageList.scrollClear. In addition to that, we reset the cursor +/// to be on top. +pub fn scrollClear(self: *Screen) !void { + try self.pages.scrollClear(); + self.cursorAbsolute(0, 0); +} + // Erase the region specified by tl and bl, inclusive. Erased cells are // colored with the current style background color. This will erase all // cells in the rows. diff --git a/src/terminal/new/Terminal.zig b/src/terminal/new/Terminal.zig index b13b2d786..627a3bcb3 100644 --- a/src/terminal/new/Terminal.zig +++ b/src/terminal/new/Terminal.zig @@ -1614,19 +1614,20 @@ pub fn eraseDisplay( const protected = self.screen.protected_mode == .iso or protected_req; switch (mode) { - // .scroll_complete => { - // self.screen.scroll(.{ .clear = {} }) catch |err| { - // log.warn("scroll clear failed, doing a normal clear err={}", .{err}); - // self.eraseDisplay(alloc, .complete, protected_req); - // return; - // }; - // - // // Unsets pending wrap state - // self.screen.cursor.pending_wrap = false; - // - // // Clear all Kitty graphics state for this screen - // self.screen.kitty_images.delete(alloc, self, .{ .all = true }); - // }, + .scroll_complete => { + self.screen.scrollClear() catch |err| { + log.warn("scroll clear failed, doing a normal clear err={}", .{err}); + self.eraseDisplay(.complete, protected_req); + return; + }; + + // Unsets pending wrap state + self.screen.cursor.pending_wrap = false; + + // Clear all Kitty graphics state for this screen + // TODO + // self.screen.kitty_images.delete(alloc, self, .{ .all = true }); + }, .complete => { // If we're on the primary screen and our last non-empty row is @@ -7001,22 +7002,22 @@ test "Terminal: eraseDisplay protected below" { } } -// test "Terminal: eraseDisplay scroll complete" { -// const alloc = testing.allocator; -// var t = try init(alloc, 10, 5); -// defer t.deinit(alloc); -// -// try t.print('A'); -// t.carriageReturn(); -// try t.linefeed(); -// t.eraseDisplay(.scroll_complete, false); -// -// { -// const str = try t.plainString(testing.allocator); -// defer testing.allocator.free(str); -// try testing.expectEqualStrings("", str); -// } -// } +test "Terminal: eraseDisplay scroll complete" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 5); + defer t.deinit(alloc); + + try t.print('A'); + t.carriageReturn(); + try t.linefeed(); + t.eraseDisplay(.scroll_complete, false); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("", str); + } +} test "Terminal: eraseDisplay protected above" { const alloc = testing.allocator; diff --git a/src/terminal/new/page.zig b/src/terminal/new/page.zig index a3cb6305e..ca89fed62 100644 --- a/src/terminal/new/page.zig +++ b/src/terminal/new/page.zig @@ -599,6 +599,19 @@ pub const Cell = packed struct(u64) { }; } + /// Returns true if the cell has no text or styling. + pub fn isEmpty(self: Cell) bool { + return switch (self.content_tag) { + .codepoint, + .codepoint_grapheme, + => !self.hasText(), + + .bg_color_palette, + .bg_color_rgb, + => false, + }; + } + pub fn hasGrapheme(self: Cell) bool { return self.content_tag == .codepoint_grapheme; }