diff --git a/TODO.md b/TODO.md index b2d0c4569..ff3872dbe 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,6 @@ Bugs: * Underline should use freetype underline thickness hint -* Any printing action forces scroll to jump to bottom, this makes it impossible - to scroll up while logs are coming in or something Performance: diff --git a/src/Grid.zig b/src/Grid.zig index d6981de40..ea4884313 100644 --- a/src/Grid.zig +++ b/src/Grid.zig @@ -282,7 +282,7 @@ pub fn updateCells(self: *Grid, term: Terminal) !void { ); // Build each cell - var rowIter = term.screen.rowIterator(); + var rowIter = term.screen.rowIterator(.viewport); var y: usize = 0; while (rowIter.next()) |line| { defer y += 1; diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 132504ae1..856d36a3f 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -82,16 +82,66 @@ pub const Cell = struct { pub const RowIterator = struct { screen: *const Screen, - index: usize, + tag: RowIndexTag, + value: usize = 0, pub fn next(self: *RowIterator) ?Row { - if (self.index >= self.screen.rows) return null; - const res = self.screen.getRow(self.index); - self.index += 1; + if (self.value > self.tag.max(self.screen)) return null; + const idx = self.tag.index(self.value); + const res = self.screen.getRow(idx); + self.value += 1; return res; } }; +/// RowIndex represents a row within the screen. There are various meanings +/// of a row index and this union represents the available types. For example, +/// when talking about row "0" you may want the first row in the viewport, +/// the first row in the scrollback, or the first row in the active area. +/// +/// All row indexes are 0-indexed. +pub const RowIndex = union(RowIndexTag) { + /// The index is from the top of the screen. The screen includes all + /// the history. + screen: usize, + + /// The index is from the top of the viewport. Therefore, depending + /// on where the user has scrolled the viewport, "0" is different. + viewport: usize, + + /// The index is from the top of the active area. The active area is + /// always "rows" tall, and 0 is the top row. The active area is the + /// "edit-able" area where the terminal cursor is. + active: usize, + + // TODO: others +}; + +/// The tags of RowIndex +pub const RowIndexTag = enum { + screen, + viewport, + active, + + /// The max value for the given tag. + pub fn max(self: RowIndexTag, screen: *const Screen) usize { + return switch (self) { + .screen => screen.totalRows(), + .viewport => screen.rows, + .active => screen.rows, + } - 1; + } + + /// Construct a RowIndex from a tag. + pub fn index(self: RowIndexTag, value: usize) RowIndex { + return switch (self) { + .screen => .{ .screen = value }, + .viewport => .{ .viewport = value }, + .active => .{ .active = value }, + }; + } +}; + /// Each screen maintains its own cursor state. cursor: Cursor = .{}, @@ -160,20 +210,43 @@ fn bottomOffset(self: Screen) usize { } /// Returns an iterator that can be used to iterate over all of the rows -/// from index zero. -pub fn rowIterator(self: *const Screen) RowIterator { - return .{ .screen = self, .index = 0 }; +/// from index zero of the given row index type. This can therefore iterate +/// from row 0 of the active area, history, viewport, etc. +pub fn rowIterator(self: *const Screen, tag: RowIndexTag) RowIterator { + return .{ .screen = self, .tag = tag }; } -/// Get the visible portion of the screen. -pub fn getVisible(self: Screen) []Cell { - return self.storage; +/// Region gets the contiguous portions of memory that constitute an +/// entire region. This is an efficient way to clear regions, for example +/// since you can memcpy directly into it. +/// +/// This has two elements because internally we use a ring buffer and +/// so any region can be split into two if it crosses the ring buffer +/// boundary. +pub fn region(self: *const Screen, tag: RowIndexTag) [2][]Cell { + const top = self.rowIndex(tag.index(0)); + const bot = self.rowIndex(tag.index(tag.max(self))); + + // The bottom and top are available in one contiguous slice. + if (bot >= top) { + return .{ + self.storage[top .. bot + self.cols], + self.storage[0..0], // just so its a valid slice, but zero length + }; + } + + // The bottom and top are split into two slices, so we slice to the + // bottom of the storage, then from the top. + return .{ + self.storage[top..self.storage.len], + self.storage[0 .. bot + self.cols], + }; } /// Get a single row in the active area by index (0-indexed). -pub fn getRow(self: Screen, idx: usize) Row { +pub fn getRow(self: Screen, idx: RowIndex) Row { // Get the index of the first byte of the the row at index. - const real_idx = self.viewportRowIndex(idx); + const real_idx = self.rowIndex(idx); // The storage is sliced to return exactly the number of columns. return self.storage[real_idx .. real_idx + self.cols]; @@ -183,21 +256,30 @@ pub fn getRow(self: Screen, idx: usize) Row { pub fn getCell(self: Screen, row: usize, col: usize) *Cell { assert(row < self.rows); assert(col < self.cols); - const row_idx = self.viewportRowIndex(row); + const row_idx = self.rowIndex(.{ .active = row }); return &self.storage[row_idx + col]; } -/// Returns the index for the given row (0-indexed) into the underlying -/// storage array. The row is 0-indexed from the top of the viewport. -fn viewportRowIndex(self: Screen, idx: usize) usize { - assert(idx < self.rows); - return self.rowIndex(self.visible_offset + idx); -} - /// Returns the index for the given row (0-indexed) into the underlying /// storage array. The row is 0-indexed from the top of the screen. -fn rowIndex(self: Screen, y: usize) usize { - assert(y < self.totalRows()); +fn rowIndex(self: Screen, idx: RowIndex) usize { + const y = switch (idx) { + .screen => |y| y: { + assert(y < self.totalRows()); + break :y y; + }, + + .viewport => |y| y: { + assert(y < self.rows); + break :y y + self.visible_offset; + }, + + .active => |y| y: { + assert(y < self.rows); + break :y self.bottomOffset() + y; + }, + }; + const val = (self.top + y) * self.cols; if (val < self.storage.len) return val; return val - self.storage.len; @@ -344,18 +426,28 @@ fn scrollDelta(self: *Screen, delta: isize, grow: bool) void { /// Copy row at src to dst. pub fn copyRow(self: *Screen, dst: usize, src: usize) void { - const src_row = self.getRow(src); - const dst_row = self.getRow(dst); + const src_row = self.getRow(.{ .active = src }); + const dst_row = self.getRow(.{ .active = dst }); std.mem.copy(Cell, dst_row, src_row); } -/// Resize the screen. The rows or cols can be bigger or smaller. Due to -/// the internal representation of a screen, this usually involves a significant -/// amount of copying compared to any other operations. +/// Resize the screen. The rows or cols can be bigger or smaller. This +/// function can only be used to resize the viewport. The scrollback size +/// (in lines) can't be changed. But due to the resize, more or less scrollback +/// "space" becomes available due to the width of lines. /// -/// This will trim data if the size is getting smaller. It is expected that -/// callers will reflow the text prior to calling this. +/// Due to the internal representation of a screen, this usually involves a +/// significant amount of copying compared to any other operations. +/// +/// This will trim data if the size is getting smaller. This will reflow the +/// soft wrapped text. pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { + // We do this in a pretty inefficient way because this implementation + // is easier and resizing is relatively rare. I welcome anyone to improve + // on this! Our naive approach is to just iterate over the entire screen + // (including scrollback) and reflow the entire thing by rewriting it. + // TODO: above not implemented yet + // Make a copy so we can access the old indexes. const old = self.*; @@ -382,8 +474,8 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { while (y < old.rows) : (y += 1) { // Copy the old row into the new row, just losing the columsn // if we got thinner. - const old_row = old.getRow(y); - const new_row = self.getRow(y - start); + const old_row = old.getRow(.{ .viewport = y }); + const new_row = self.getRow(.{ .viewport = y - start }); std.mem.copy(Cell, new_row, old_row[0..col_end]); // If our new row is wider, then we copy zeroes into the rest. @@ -394,7 +486,7 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // If we grew rows, then set the remaining data to zero. if (rows > old.rows) { - std.mem.set(Cell, self.storage[self.viewportRowIndex(old.rows)..], .{ .char = 0 }); + std.mem.set(Cell, self.storage[self.rowIndex(.{ .viewport = old.rows })..], .{ .char = 0 }); } // Free the old data @@ -495,8 +587,8 @@ fn selectionSlices(self: Screen, sel: Selection) struct { // Get the true "top" and "bottom" const sel_top = sel.topLeft(); const sel_bot = sel.bottomRight(); - const top = self.rowIndex(sel_top.y); - const bot = self.rowIndex(sel_bot.y); + const top = self.rowIndex(.{ .screen = sel_top.y }); + const bot = self.rowIndex(.{ .screen = sel_bot.y }); // The bottom and top are available in one contiguous slice. if (bot >= top) { @@ -522,7 +614,7 @@ pub fn testString(self: Screen, alloc: Allocator) ![]const u8 { var i: usize = 0; var y: usize = 0; - var rows = self.rowIterator(); + var rows = self.rowIterator(.viewport); while (rows.next()) |row| { defer y += 1; @@ -550,13 +642,13 @@ pub fn testString(self: Screen, alloc: Allocator) ![]const u8 { fn testWriteString(self: *Screen, text: []const u8) void { var y: usize = 0; var x: usize = 0; - var row = self.getRow(y); + var row = self.getRow(.{ .active = y }); for (text) |c| { // Explicit newline forces a new row if (c == '\n') { y += 1; x = 0; - row = self.getRow(y); + row = self.getRow(.{ .active = y }); continue; } @@ -565,7 +657,7 @@ fn testWriteString(self: *Screen, text: []const u8) void { row[x - 1].attrs.wrap = 1; y += 1; x = 0; - row = self.getRow(y); + row = self.getRow(.{ .active = y }); } row[x].char = @intCast(u32, c); @@ -583,22 +675,34 @@ test "Screen" { // Sanity check that our test helpers work const str = "1ABCD\n2EFGH\n3IJKL"; s.testWriteString(str); - var contents = try s.testString(alloc); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); + { + var contents = try s.testString(alloc); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } // Test the row iterator var count: usize = 0; - var iter = s.rowIterator(); + var iter = s.rowIterator(.viewport); while (iter.next()) |row| { // Rows should be pointer equivalent to getRow - const row_other = s.getRow(count); + const row_other = s.getRow(.{ .viewport = count }); try testing.expectEqual(row.ptr, row_other.ptr); count += 1; } // Should go through all rows try testing.expectEqual(@as(usize, 3), count); + + // Should be able to easily clear screen + const reg = s.region(.viewport); + std.mem.set(Cell, reg[0], .{ .char = 'A' }); + std.mem.set(Cell, reg[1], .{ .char = 'A' }); + { + var contents = try s.testString(alloc); + defer alloc.free(contents); + try testing.expectEqualStrings("AAAAA\nAAAAA\nAAAAA", contents); + } } test "Screen: scrolling" { @@ -616,9 +720,9 @@ test "Screen: scrolling" { try testing.expect(s.viewportIsBottom()); // Test our row index - try testing.expectEqual(@as(usize, 5), s.viewportRowIndex(0)); - try testing.expectEqual(@as(usize, 10), s.viewportRowIndex(1)); - try testing.expectEqual(@as(usize, 0), s.viewportRowIndex(2)); + try testing.expectEqual(@as(usize, 5), s.rowIndex(.{ .active = 0 })); + try testing.expectEqual(@as(usize, 10), s.rowIndex(.{ .active = 1 })); + try testing.expectEqual(@as(usize, 0), s.rowIndex(.{ .active = 2 })); { // Test our contents rotated @@ -654,9 +758,9 @@ test "Screen: scrolling" { // try testing.expect(s.viewportIsBottom()); // // // Test our row index -// try testing.expectEqual(@as(usize, 5), s.viewportRowIndex(0)); -// try testing.expectEqual(@as(usize, 10), s.viewportRowIndex(1)); -// try testing.expectEqual(@as(usize, 15), s.viewportRowIndex(2)); +// try testing.expectEqual(@as(usize, 5), s.rowIndex(0)); +// try testing.expectEqual(@as(usize, 10), s.rowIndex(1)); +// try testing.expectEqual(@as(usize, 15), s.rowIndex(2)); // } test "Screen: scroll down from 0" { @@ -687,9 +791,9 @@ test "Screen: scrollback" { s.scroll(.{ .delta = 1 }); // Test our row index - try testing.expectEqual(@as(usize, 5), s.viewportRowIndex(0)); - try testing.expectEqual(@as(usize, 10), s.viewportRowIndex(1)); - try testing.expectEqual(@as(usize, 15), s.viewportRowIndex(2)); + try testing.expectEqual(@as(usize, 5), s.rowIndex(.{ .active = 0 })); + try testing.expectEqual(@as(usize, 10), s.rowIndex(.{ .active = 1 })); + try testing.expectEqual(@as(usize, 15), s.rowIndex(.{ .active = 2 })); { // Test our contents rotated @@ -759,6 +863,26 @@ test "Screen: scrollback" { defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); } + + // Should be able to easily clear active area only + const reg = s.region(.active); + std.mem.set(Cell, reg[0], .{ .char = 0 }); + std.mem.set(Cell, reg[1], .{ .char = 0 }); + { + var contents = try s.testString(alloc); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD", contents); + } + + // Scrolling to the bottom + s.scroll(.{ .bottom = {} }); + + { + // Test our contents rotated + var contents = try s.testString(alloc); + defer alloc.free(contents); + try testing.expectEqualStrings("", contents); + } } test "Screen: scrollback empty" { @@ -918,7 +1042,7 @@ test "Screen: selectionString wrap around" { // we're out of space. s.scroll(.{ .delta = 1 }); try testing.expect(s.viewportIsBottom()); - try testing.expectEqual(@as(usize, 0), s.viewportRowIndex(2)); + try testing.expectEqual(@as(usize, 0), s.rowIndex(.{ .active = 2 })); s.testWriteString("1ABCD\n2EFGH\n3IJKL"); { diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index a36c55381..775ca0452 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -339,9 +339,6 @@ pub fn print(self: *Terminal, c: u21) !void { // If we're not on the main display, do nothing for now if (self.status_display != .main) return; - // If we're not at the bottom, then we need to move there - if (!self.screen.viewportIsBottom()) self.screen.scroll(.{ .bottom = {} }); - // If we're soft-wrapping, then handle that first. if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1) { // Mark that the cell is wrapped, which guarantees that there is @@ -383,7 +380,7 @@ pub fn decaln(self: *Terminal) void { // Fill with Es, does not move cursor. We reset fg/bg so we can just // optimize here by doing row copies. - const filled = self.screen.getRow(0); + const filled = self.screen.getRow(.{ .active = 0 }); var col: usize = 0; while (col < self.cols) : (col += 1) { filled[col] = .{ .char = 'E' }; @@ -391,7 +388,7 @@ pub fn decaln(self: *Terminal) void { var row: usize = 1; while (row < self.rows) : (row += 1) { - std.mem.copy(Screen.Cell, self.screen.getRow(row), filled); + std.mem.copy(Screen.Cell, self.screen.getRow(.{ .active = row }), filled); } } @@ -542,8 +539,9 @@ pub fn eraseDisplay( switch (mode) { .complete => { - const all = self.screen.getVisible(); - std.mem.set(Screen.Cell, all, self.screen.cursor.pen); + const region = self.screen.region(.active); + std.mem.set(Screen.Cell, region[0], self.screen.cursor.pen); + std.mem.set(Screen.Cell, region[1], self.screen.cursor.pen); // Unsets pending wrap state self.screen.cursor.pending_wrap = false; @@ -615,12 +613,12 @@ pub fn eraseLine( switch (mode) { .right => { - const row = self.screen.getRow(self.screen.cursor.y); + const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); std.mem.set(Screen.Cell, row[self.screen.cursor.x..], self.screen.cursor.pen); }, .left => { - const row = self.screen.getRow(self.screen.cursor.y); + const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); std.mem.set(Screen.Cell, row[0 .. self.screen.cursor.x + 1], self.screen.cursor.pen); // Unsets pending wrap state @@ -628,7 +626,7 @@ pub fn eraseLine( }, .complete => { - const row = self.screen.getRow(self.screen.cursor.y); + const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); std.mem.set(Screen.Cell, row, self.screen.cursor.pen); }, @@ -653,7 +651,7 @@ pub fn deleteChars(self: *Terminal, count: usize) !void { const tracy = trace(@src()); defer tracy.end(); - const line = self.screen.getRow(self.screen.cursor.y); + const line = self.screen.getRow(.{ .active = self.screen.cursor.y }); // Our last index is at most the end of the number of chars we have // in the current line. @@ -821,7 +819,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { } // Get the current row - const row = self.screen.getRow(self.screen.cursor.y); + const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); // Determine our indexes. const start = self.screen.cursor.x; @@ -943,7 +941,7 @@ pub fn deleteLines(self: *Terminal, count: usize) void { } while (y <= self.scrolling_region.bottom) : (y += 1) { - const row = self.screen.getRow(y); + const row = self.screen.getRow(.{ .active = y }); std.mem.set(Screen.Cell, row, self.screen.cursor.pen); } } @@ -985,6 +983,10 @@ pub fn scrollUp(self: *Terminal, count: usize) void { pub const ScrollViewport = union(enum) { /// Scroll to the top of the scrollback top: void, + + /// Scroll to the bottom, i.e. the top of the active area + bottom: void, + delta: isize, }; @@ -995,6 +997,7 @@ pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) void { self.screen.scroll(switch (behavior) { .top => .{ .top = {} }, + .bottom => .{ .bottom = {} }, .delta => |delta| .{ .delta_no_grow = delta }, }); } @@ -1067,7 +1070,7 @@ test "Terminal: soft wrap" { } } -test "Terminal: print scrolls back to bottom" { +test "Terminal: print writes to bottom if scrolled" { var t = try init(testing.allocator, 5, 2); defer t.deinit(testing.allocator); @@ -1095,6 +1098,7 @@ test "Terminal: print scrolls back to bottom" { // Type try t.print('A'); + t.scrollViewport(.{ .bottom = {} }); { var str = try t.plainString(testing.allocator); defer testing.allocator.free(str);