From 542f605d54dcbb37262a00d58074610180c32ae0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Nov 2023 20:39:33 -0800 Subject: [PATCH 1/6] terminal: add explicit errorset to scroll screen --- src/circ_buf.zig | 4 ++-- src/terminal/Screen.zig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/circ_buf.zig b/src/circ_buf.zig index 2cb004c70..4157fd0a4 100644 --- a/src/circ_buf.zig +++ b/src/circ_buf.zig @@ -93,7 +93,7 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { /// Resize the buffer to the given size (larger or smaller). /// If larger, new values will be set to the default value. - pub fn resize(self: *Self, alloc: Allocator, size: usize) !void { + pub fn resize(self: *Self, alloc: Allocator, size: usize) Allocator.Error!void { // Rotate to zero so it is aligned. try self.rotateToZero(alloc); @@ -116,7 +116,7 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { } /// Rotate the data so that it is zero-aligned. - fn rotateToZero(self: *Self, alloc: Allocator) !void { + fn rotateToZero(self: *Self, alloc: Allocator) Allocator.Error!void { // TODO: this does this in the worst possible way by allocating. // rewrite to not allocate, its possible, I'm just lazy right now. diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 52b00baea..b764ed4f5 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1794,7 +1794,7 @@ pub const Scroll = union(enum) { /// "move" the screen. It is up to the caller to determine if they actually /// want to do that yet (i.e. are they writing to the end of the screen /// or not). -pub fn scroll(self: *Screen, behavior: Scroll) !void { +pub fn scroll(self: *Screen, behavior: Scroll) Allocator.Error!void { // No matter what, scrolling marks our image state as dirty since // it could move placements. If there are no placements or no images // this is still a very cheap operation. @@ -1830,7 +1830,7 @@ fn scrollRow(self: *Screen, idx: RowIndex) void { assert(screen_pt.inViewport(self)); } -fn scrollDelta(self: *Screen, delta: isize, viewport_only: bool) !void { +fn scrollDelta(self: *Screen, delta: isize, viewport_only: bool) Allocator.Error!void { const tracy = trace(@src()); defer tracy.end(); From b220179c3a173485fbfc3c20feedd7be03da139d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Nov 2023 20:39:57 -0800 Subject: [PATCH 2/6] terminal: add "clear" screen scroll mode --- src/terminal/Screen.zig | 130 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index b764ed4f5..55a4f16f0 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1788,6 +1788,15 @@ pub const Scroll = union(enum) { /// this will change nothing. If the row is outside the viewport, the /// viewport will change so that this row is at the top of the viewport. row: RowIndex, + + /// Scroll down and move all viewport contents into the scrollback + /// so that the screen is clear. This isn't eqiuivalent to "screen" with + /// the value set to the viewport size because this will handle the case + /// that the viewport is not full. + /// + /// This will ignore empty trailing rows. An empty row is a row that + /// has never been written to at all. A row with spaces is not empty. + clear: void, }; /// Scroll the screen by the given behavior. Note that this will always @@ -1815,9 +1824,27 @@ pub fn scroll(self: *Screen, behavior: Scroll) Allocator.Error!void { // Scroll to a specific row .row => |idx| self.scrollRow(idx), + + // Scroll until the viewport is clear by moving the viewport contents + // into the scrollback. + .clear => try self.scrollClear(), } } +fn scrollClear(self: *Screen) Allocator.Error!void { + // The full amount of rows in the viewport + const full_amount = self.rowsWritten() - self.viewport; + + // Find the number of non-empty rows + const non_empty = for (0..full_amount) |i| { + const rev_i = full_amount - i - 1; + const row = self.getRow(.{ .viewport = rev_i }); + if (!row.isEmpty()) break rev_i + 1; + } else full_amount; + + try self.scroll(.{ .screen = @intCast(non_empty) }); +} + fn scrollRow(self: *Screen, idx: RowIndex) void { // Convert the given row to a screen point. const screen_idx = idx.toScreen(self); @@ -3691,6 +3718,109 @@ test "Screen: scrolling with scrollback available doesn't move selection" { } } +test "Screen: scroll and clear full screen" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 3, 5, 5); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); + + { + const contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); + } + + try s.scroll(.{ .clear = {} }); + { + const contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + try testing.expectEqualStrings("", contents); + } + { + const contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); + } +} + +test "Screen: scroll and clear partial screen" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 3, 5, 5); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH"); + + { + const contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n2EFGH", contents); + } + + try s.scroll(.{ .clear = {} }); + { + const contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + try testing.expectEqualStrings("", contents); + } + { + const contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n2EFGH", contents); + } +} + +test "Screen: scroll and clear empty screen" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 3, 5, 5); + defer s.deinit(); + try s.scroll(.{ .clear = {} }); + try testing.expectEqual(@as(usize, 0), s.viewport); +} + +test "Screen: scroll and clear ignore blank lines" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 3, 5, 10); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH"); + try s.scroll(.{ .clear = {} }); + { + const contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + try testing.expectEqualStrings("", contents); + } + + // Move back to top-left + s.cursor.x = 0; + s.cursor.y = 0; + + // Write and clear + try s.testWriteString("3ABCD\n"); + try s.scroll(.{ .clear = {} }); + { + const contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + try testing.expectEqualStrings("", contents); + } + + // Move back to top-left + s.cursor.x = 0; + s.cursor.y = 0; + try s.testWriteString("X"); + + { + const contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n2EFGH\n3ABCD\nX", contents); + } +} + test "Screen: history region with no scrollback" { const testing = std.testing; const alloc = testing.allocator; From 39c2549b1af4e4f2bbba24f809e14e1bcd4295f7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Nov 2023 20:45:57 -0800 Subject: [PATCH 3/6] terminal: add ESC [ 22 J (scroll and clear) --- src/terminal/Screen.zig | 28 ++++++++++++++++++++++++++++ src/terminal/Terminal.zig | 31 +++++++++++++++++++++++++++++++ src/terminal/csi.zig | 4 ++++ 3 files changed, 63 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 55a4f16f0..8ebc5da34 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -6421,6 +6421,34 @@ test "Screen: resize less cols with reflow previously wrapped and scrollback" { try testing.expectEqual(@as(usize, 2), s.cursor.y); } +// test "Screen: resize less cols with scrollback keeps cursor row" { +// const testing = std.testing; +// const alloc = testing.allocator; +// +// var s = try init(alloc, 3, 5, 5); +// defer s.deinit(); +// const str = "1A\n2B\n3C\n4D\n5E"; +// try s.testWriteString(str); +// +// // Put our cursor on the end +// s.cursor.x = 1; +// s.cursor.y = s.rows - 1; +// try testing.expectEqual(@as(u32, 'E'), s.getCell(.active, s.cursor.y, s.cursor.x).char); +// +// try s.resize(3, 3); +// +// { +// const contents = try s.testString(alloc, .viewport); +// defer alloc.free(contents); +// const expected = "3C\n4D\n5E"; +// try testing.expectEqualStrings(expected, contents); +// } +// +// // Cursor should be on the last line +// try testing.expectEqual(@as(usize, 1), s.cursor.x); +// try testing.expectEqual(@as(usize, 2), s.cursor.y); +// } +// test "Screen: resize more rows, less cols with reflow with scrollback" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index b6544d36d..6544a6d67 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1144,6 +1144,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 }); + }, + .complete => { var it = self.screen.rowIterator(.active); while (it.next()) |row| { @@ -6017,6 +6031,23 @@ test "Terminal: eraseDisplay protected above" { var t = try init(alloc, 10, 5); defer t.deinit(alloc); + try t.print('A'); + t.carriageReturn(); + try t.linefeed(); + t.eraseDisplay(alloc, .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, 3); + defer t.deinit(alloc); + try t.print('A'); t.carriageReturn(); try t.linefeed(); diff --git a/src/terminal/csi.zig b/src/terminal/csi.zig index 757d932f7..877f5986e 100644 --- a/src/terminal/csi.zig +++ b/src/terminal/csi.zig @@ -4,6 +4,10 @@ pub const EraseDisplay = enum(u8) { above = 1, complete = 2, scrollback = 3, + + /// This is an extension added by Kitty to move the viewport into the + /// scrollback and then erase the display. + scroll_complete = 22, }; // Modes for the EL CSI command. From b5cad7184d54f4a3602907fd9bb2ff402cc6277f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Nov 2023 20:47:04 -0800 Subject: [PATCH 4/6] terminal: ED handles invalid values --- src/terminal/stream.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index be0381f23..a9f5d7c24 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -294,7 +294,10 @@ pub fn Stream(comptime Handler: type) type { const mode_: ?csi.EraseDisplay = switch (action.params.len) { 0 => .below, - 1 => if (action.params[0] <= 3) @enumFromInt(action.params[0]) else null, + 1 => if (action.params[0] <= 3) + std.meta.intToEnum(csi.EraseDisplay, action.params[0]) catch null + else + null, else => null, }; From 7066fb7bbbd3e69fccc7bdd25b2151ecbc26b598 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Nov 2023 21:07:16 -0800 Subject: [PATCH 5/6] terminal: ESC [ 2 J does a scroll and clear if viewport is at a prompt --- src/terminal/Terminal.zig | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 6544a6d67..22a57a546 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1159,6 +1159,42 @@ pub fn eraseDisplay( }, .complete => { + // If we're on the primary screen and our last non-empty row is + // a prompt, then we do a scroll_complete instead. This is a + // heuristic to get the generally desirable behavior that ^L + // at a prompt scrolls the screen contents prior to clearing. + // Most shells send `ESC [ H ESC [ 2 J` so we can't just check + // our current cursor position. See #905 + if (self.active_screen == .primary) at_prompt: { + // Go from the bottom of the viewport up and see if we're + // at a prompt. + const viewport_max = Screen.RowIndexTag.viewport.maxLen(&self.screen); + for (0..viewport_max) |y| { + const bottom_y = viewport_max - y - 1; + const row = self.screen.getRow(.{ .viewport = bottom_y }); + if (row.isEmpty()) continue; + switch (row.getSemanticPrompt()) { + // If we're at a prompt or input area, then we are at a prompt. + .prompt, + .prompt_continuation, + .input, + => break, + + // If we have command output, then we're most certainly not + // at a prompt. + .command => break :at_prompt, + + // If we don't know, we keep searching. + .unknown => {}, + } + } else break :at_prompt; + + self.screen.scroll(.{ .clear = {} }) catch { + // If we fail, we just fall back to doing a normal clear + // so we don't worry about the error. + }; + } + var it = self.screen.rowIterator(.active); while (it.next()) |row| { row.setWrapped(false); From a325ab57123733f119b0f344c4ff9bac8b8d7e15 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Nov 2023 21:13:22 -0800 Subject: [PATCH 6/6] terminal: remove invalid test --- src/terminal/Screen.zig | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 8ebc5da34..55a4f16f0 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -6421,34 +6421,6 @@ test "Screen: resize less cols with reflow previously wrapped and scrollback" { try testing.expectEqual(@as(usize, 2), s.cursor.y); } -// test "Screen: resize less cols with scrollback keeps cursor row" { -// const testing = std.testing; -// const alloc = testing.allocator; -// -// var s = try init(alloc, 3, 5, 5); -// defer s.deinit(); -// const str = "1A\n2B\n3C\n4D\n5E"; -// try s.testWriteString(str); -// -// // Put our cursor on the end -// s.cursor.x = 1; -// s.cursor.y = s.rows - 1; -// try testing.expectEqual(@as(u32, 'E'), s.getCell(.active, s.cursor.y, s.cursor.x).char); -// -// try s.resize(3, 3); -// -// { -// const contents = try s.testString(alloc, .viewport); -// defer alloc.free(contents); -// const expected = "3C\n4D\n5E"; -// try testing.expectEqualStrings(expected, contents); -// } -// -// // Cursor should be on the last line -// try testing.expectEqual(@as(usize, 1), s.cursor.x); -// try testing.expectEqual(@as(usize, 2), s.cursor.y); -// } -// test "Screen: resize more rows, less cols with reflow with scrollback" { const testing = std.testing; const alloc = testing.allocator;