mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'reflow'
This actually doesn't implement reflow but I wanted to merge this because it has an important refactor and clarity around screen operations. It is now clearly defined in the API what portion of the screen you're accessing (active, viewport, etc.) and the terminal only operates on the active area (fixing TODO item in diff). This is all groundwork I found was necessary to even start thinking about reflow.
This commit is contained in:
2
TODO.md
2
TODO.md
@ -1,8 +1,6 @@
|
|||||||
Bugs:
|
Bugs:
|
||||||
|
|
||||||
* Underline should use freetype underline thickness hint
|
* 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:
|
Performance:
|
||||||
|
|
||||||
|
@ -282,7 +282,7 @@ pub fn updateCells(self: *Grid, term: Terminal) !void {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Build each cell
|
// Build each cell
|
||||||
var rowIter = term.screen.rowIterator();
|
var rowIter = term.screen.rowIterator(.viewport);
|
||||||
var y: usize = 0;
|
var y: usize = 0;
|
||||||
while (rowIter.next()) |line| {
|
while (rowIter.next()) |line| {
|
||||||
defer y += 1;
|
defer y += 1;
|
||||||
|
@ -82,16 +82,66 @@ pub const Cell = struct {
|
|||||||
|
|
||||||
pub const RowIterator = struct {
|
pub const RowIterator = struct {
|
||||||
screen: *const Screen,
|
screen: *const Screen,
|
||||||
index: usize,
|
tag: RowIndexTag,
|
||||||
|
value: usize = 0,
|
||||||
|
|
||||||
pub fn next(self: *RowIterator) ?Row {
|
pub fn next(self: *RowIterator) ?Row {
|
||||||
if (self.index >= self.screen.rows) return null;
|
if (self.value > self.tag.max(self.screen)) return null;
|
||||||
const res = self.screen.getRow(self.index);
|
const idx = self.tag.index(self.value);
|
||||||
self.index += 1;
|
const res = self.screen.getRow(idx);
|
||||||
|
self.value += 1;
|
||||||
return res;
|
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.
|
/// Each screen maintains its own cursor state.
|
||||||
cursor: Cursor = .{},
|
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
|
/// Returns an iterator that can be used to iterate over all of the rows
|
||||||
/// from index zero.
|
/// from index zero of the given row index type. This can therefore iterate
|
||||||
pub fn rowIterator(self: *const Screen) RowIterator {
|
/// from row 0 of the active area, history, viewport, etc.
|
||||||
return .{ .screen = self, .index = 0 };
|
pub fn rowIterator(self: *const Screen, tag: RowIndexTag) RowIterator {
|
||||||
|
return .{ .screen = self, .tag = tag };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the visible portion of the screen.
|
/// Region gets the contiguous portions of memory that constitute an
|
||||||
pub fn getVisible(self: Screen) []Cell {
|
/// entire region. This is an efficient way to clear regions, for example
|
||||||
return self.storage;
|
/// 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).
|
/// 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.
|
// 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.
|
// The storage is sliced to return exactly the number of columns.
|
||||||
return self.storage[real_idx .. real_idx + self.cols];
|
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 {
|
pub fn getCell(self: Screen, row: usize, col: usize) *Cell {
|
||||||
assert(row < self.rows);
|
assert(row < self.rows);
|
||||||
assert(col < self.cols);
|
assert(col < self.cols);
|
||||||
const row_idx = self.viewportRowIndex(row);
|
const row_idx = self.rowIndex(.{ .active = row });
|
||||||
return &self.storage[row_idx + col];
|
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
|
/// 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.
|
/// storage array. The row is 0-indexed from the top of the screen.
|
||||||
fn rowIndex(self: Screen, y: usize) usize {
|
fn rowIndex(self: Screen, idx: RowIndex) usize {
|
||||||
|
const y = switch (idx) {
|
||||||
|
.screen => |y| y: {
|
||||||
assert(y < self.totalRows());
|
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;
|
const val = (self.top + y) * self.cols;
|
||||||
if (val < self.storage.len) return val;
|
if (val < self.storage.len) return val;
|
||||||
return val - self.storage.len;
|
return val - self.storage.len;
|
||||||
@ -344,18 +426,28 @@ fn scrollDelta(self: *Screen, delta: isize, grow: bool) void {
|
|||||||
|
|
||||||
/// Copy row at src to dst.
|
/// Copy row at src to dst.
|
||||||
pub fn copyRow(self: *Screen, dst: usize, src: usize) void {
|
pub fn copyRow(self: *Screen, dst: usize, src: usize) void {
|
||||||
const src_row = self.getRow(src);
|
const src_row = self.getRow(.{ .active = src });
|
||||||
const dst_row = self.getRow(dst);
|
const dst_row = self.getRow(.{ .active = dst });
|
||||||
std.mem.copy(Cell, dst_row, src_row);
|
std.mem.copy(Cell, dst_row, src_row);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resize the screen. The rows or cols can be bigger or smaller. Due to
|
/// Resize the screen. The rows or cols can be bigger or smaller. This
|
||||||
/// the internal representation of a screen, this usually involves a significant
|
/// function can only be used to resize the viewport. The scrollback size
|
||||||
/// amount of copying compared to any other operations.
|
/// (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
|
/// Due to the internal representation of a screen, this usually involves a
|
||||||
/// callers will reflow the text prior to calling this.
|
/// 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 {
|
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.
|
// Make a copy so we can access the old indexes.
|
||||||
const old = self.*;
|
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) {
|
while (y < old.rows) : (y += 1) {
|
||||||
// Copy the old row into the new row, just losing the columsn
|
// Copy the old row into the new row, just losing the columsn
|
||||||
// if we got thinner.
|
// if we got thinner.
|
||||||
const old_row = old.getRow(y);
|
const old_row = old.getRow(.{ .viewport = y });
|
||||||
const new_row = self.getRow(y - start);
|
const new_row = self.getRow(.{ .viewport = y - start });
|
||||||
std.mem.copy(Cell, new_row, old_row[0..col_end]);
|
std.mem.copy(Cell, new_row, old_row[0..col_end]);
|
||||||
|
|
||||||
// If our new row is wider, then we copy zeroes into the rest.
|
// 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 we grew rows, then set the remaining data to zero.
|
||||||
if (rows > old.rows) {
|
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
|
// Free the old data
|
||||||
@ -495,8 +587,8 @@ fn selectionSlices(self: Screen, sel: Selection) struct {
|
|||||||
// Get the true "top" and "bottom"
|
// Get the true "top" and "bottom"
|
||||||
const sel_top = sel.topLeft();
|
const sel_top = sel.topLeft();
|
||||||
const sel_bot = sel.bottomRight();
|
const sel_bot = sel.bottomRight();
|
||||||
const top = self.rowIndex(sel_top.y);
|
const top = self.rowIndex(.{ .screen = sel_top.y });
|
||||||
const bot = self.rowIndex(sel_bot.y);
|
const bot = self.rowIndex(.{ .screen = sel_bot.y });
|
||||||
|
|
||||||
// The bottom and top are available in one contiguous slice.
|
// The bottom and top are available in one contiguous slice.
|
||||||
if (bot >= top) {
|
if (bot >= top) {
|
||||||
@ -522,7 +614,7 @@ pub fn testString(self: Screen, alloc: Allocator) ![]const u8 {
|
|||||||
|
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
var y: usize = 0;
|
var y: usize = 0;
|
||||||
var rows = self.rowIterator();
|
var rows = self.rowIterator(.viewport);
|
||||||
while (rows.next()) |row| {
|
while (rows.next()) |row| {
|
||||||
defer y += 1;
|
defer y += 1;
|
||||||
|
|
||||||
@ -550,13 +642,13 @@ pub fn testString(self: Screen, alloc: Allocator) ![]const u8 {
|
|||||||
fn testWriteString(self: *Screen, text: []const u8) void {
|
fn testWriteString(self: *Screen, text: []const u8) void {
|
||||||
var y: usize = 0;
|
var y: usize = 0;
|
||||||
var x: usize = 0;
|
var x: usize = 0;
|
||||||
var row = self.getRow(y);
|
var row = self.getRow(.{ .active = y });
|
||||||
for (text) |c| {
|
for (text) |c| {
|
||||||
// Explicit newline forces a new row
|
// Explicit newline forces a new row
|
||||||
if (c == '\n') {
|
if (c == '\n') {
|
||||||
y += 1;
|
y += 1;
|
||||||
x = 0;
|
x = 0;
|
||||||
row = self.getRow(y);
|
row = self.getRow(.{ .active = y });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -565,7 +657,7 @@ fn testWriteString(self: *Screen, text: []const u8) void {
|
|||||||
row[x - 1].attrs.wrap = 1;
|
row[x - 1].attrs.wrap = 1;
|
||||||
y += 1;
|
y += 1;
|
||||||
x = 0;
|
x = 0;
|
||||||
row = self.getRow(y);
|
row = self.getRow(.{ .active = y });
|
||||||
}
|
}
|
||||||
|
|
||||||
row[x].char = @intCast(u32, c);
|
row[x].char = @intCast(u32, c);
|
||||||
@ -583,22 +675,34 @@ test "Screen" {
|
|||||||
// Sanity check that our test helpers work
|
// Sanity check that our test helpers work
|
||||||
const str = "1ABCD\n2EFGH\n3IJKL";
|
const str = "1ABCD\n2EFGH\n3IJKL";
|
||||||
s.testWriteString(str);
|
s.testWriteString(str);
|
||||||
|
{
|
||||||
var contents = try s.testString(alloc);
|
var contents = try s.testString(alloc);
|
||||||
defer alloc.free(contents);
|
defer alloc.free(contents);
|
||||||
try testing.expectEqualStrings(str, contents);
|
try testing.expectEqualStrings(str, contents);
|
||||||
|
}
|
||||||
|
|
||||||
// Test the row iterator
|
// Test the row iterator
|
||||||
var count: usize = 0;
|
var count: usize = 0;
|
||||||
var iter = s.rowIterator();
|
var iter = s.rowIterator(.viewport);
|
||||||
while (iter.next()) |row| {
|
while (iter.next()) |row| {
|
||||||
// Rows should be pointer equivalent to getRow
|
// 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);
|
try testing.expectEqual(row.ptr, row_other.ptr);
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should go through all rows
|
// Should go through all rows
|
||||||
try testing.expectEqual(@as(usize, 3), count);
|
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" {
|
test "Screen: scrolling" {
|
||||||
@ -616,9 +720,9 @@ test "Screen: scrolling" {
|
|||||||
try testing.expect(s.viewportIsBottom());
|
try testing.expect(s.viewportIsBottom());
|
||||||
|
|
||||||
// Test our row index
|
// Test our row index
|
||||||
try testing.expectEqual(@as(usize, 5), s.viewportRowIndex(0));
|
try testing.expectEqual(@as(usize, 5), s.rowIndex(.{ .active = 0 }));
|
||||||
try testing.expectEqual(@as(usize, 10), s.viewportRowIndex(1));
|
try testing.expectEqual(@as(usize, 10), s.rowIndex(.{ .active = 1 }));
|
||||||
try testing.expectEqual(@as(usize, 0), s.viewportRowIndex(2));
|
try testing.expectEqual(@as(usize, 0), s.rowIndex(.{ .active = 2 }));
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test our contents rotated
|
// Test our contents rotated
|
||||||
@ -654,9 +758,9 @@ test "Screen: scrolling" {
|
|||||||
// try testing.expect(s.viewportIsBottom());
|
// try testing.expect(s.viewportIsBottom());
|
||||||
//
|
//
|
||||||
// // Test our row index
|
// // Test our row index
|
||||||
// try testing.expectEqual(@as(usize, 5), s.viewportRowIndex(0));
|
// try testing.expectEqual(@as(usize, 5), s.rowIndex(0));
|
||||||
// try testing.expectEqual(@as(usize, 10), s.viewportRowIndex(1));
|
// try testing.expectEqual(@as(usize, 10), s.rowIndex(1));
|
||||||
// try testing.expectEqual(@as(usize, 15), s.viewportRowIndex(2));
|
// try testing.expectEqual(@as(usize, 15), s.rowIndex(2));
|
||||||
// }
|
// }
|
||||||
|
|
||||||
test "Screen: scroll down from 0" {
|
test "Screen: scroll down from 0" {
|
||||||
@ -687,9 +791,9 @@ test "Screen: scrollback" {
|
|||||||
s.scroll(.{ .delta = 1 });
|
s.scroll(.{ .delta = 1 });
|
||||||
|
|
||||||
// Test our row index
|
// Test our row index
|
||||||
try testing.expectEqual(@as(usize, 5), s.viewportRowIndex(0));
|
try testing.expectEqual(@as(usize, 5), s.rowIndex(.{ .active = 0 }));
|
||||||
try testing.expectEqual(@as(usize, 10), s.viewportRowIndex(1));
|
try testing.expectEqual(@as(usize, 10), s.rowIndex(.{ .active = 1 }));
|
||||||
try testing.expectEqual(@as(usize, 15), s.viewportRowIndex(2));
|
try testing.expectEqual(@as(usize, 15), s.rowIndex(.{ .active = 2 }));
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test our contents rotated
|
// Test our contents rotated
|
||||||
@ -759,6 +863,26 @@ test "Screen: scrollback" {
|
|||||||
defer alloc.free(contents);
|
defer alloc.free(contents);
|
||||||
try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", 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" {
|
test "Screen: scrollback empty" {
|
||||||
@ -918,7 +1042,7 @@ test "Screen: selectionString wrap around" {
|
|||||||
// we're out of space.
|
// we're out of space.
|
||||||
s.scroll(.{ .delta = 1 });
|
s.scroll(.{ .delta = 1 });
|
||||||
try testing.expect(s.viewportIsBottom());
|
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");
|
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -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 we're not on the main display, do nothing for now
|
||||||
if (self.status_display != .main) return;
|
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 we're soft-wrapping, then handle that first.
|
||||||
if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1) {
|
if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1) {
|
||||||
// Mark that the cell is wrapped, which guarantees that there is
|
// 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
|
// Fill with Es, does not move cursor. We reset fg/bg so we can just
|
||||||
// optimize here by doing row copies.
|
// optimize here by doing row copies.
|
||||||
const filled = self.screen.getRow(0);
|
const filled = self.screen.getRow(.{ .active = 0 });
|
||||||
var col: usize = 0;
|
var col: usize = 0;
|
||||||
while (col < self.cols) : (col += 1) {
|
while (col < self.cols) : (col += 1) {
|
||||||
filled[col] = .{ .char = 'E' };
|
filled[col] = .{ .char = 'E' };
|
||||||
@ -391,7 +388,7 @@ pub fn decaln(self: *Terminal) void {
|
|||||||
|
|
||||||
var row: usize = 1;
|
var row: usize = 1;
|
||||||
while (row < self.rows) : (row += 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) {
|
switch (mode) {
|
||||||
.complete => {
|
.complete => {
|
||||||
const all = self.screen.getVisible();
|
const region = self.screen.region(.active);
|
||||||
std.mem.set(Screen.Cell, all, self.screen.cursor.pen);
|
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
|
// Unsets pending wrap state
|
||||||
self.screen.cursor.pending_wrap = false;
|
self.screen.cursor.pending_wrap = false;
|
||||||
@ -615,12 +613,12 @@ pub fn eraseLine(
|
|||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
.right => {
|
.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);
|
std.mem.set(Screen.Cell, row[self.screen.cursor.x..], self.screen.cursor.pen);
|
||||||
},
|
},
|
||||||
|
|
||||||
.left => {
|
.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);
|
std.mem.set(Screen.Cell, row[0 .. self.screen.cursor.x + 1], self.screen.cursor.pen);
|
||||||
|
|
||||||
// Unsets pending wrap state
|
// Unsets pending wrap state
|
||||||
@ -628,7 +626,7 @@ pub fn eraseLine(
|
|||||||
},
|
},
|
||||||
|
|
||||||
.complete => {
|
.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);
|
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());
|
const tracy = trace(@src());
|
||||||
defer tracy.end();
|
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
|
// Our last index is at most the end of the number of chars we have
|
||||||
// in the current line.
|
// in the current line.
|
||||||
@ -821,7 +819,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the current row
|
// 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.
|
// Determine our indexes.
|
||||||
const start = self.screen.cursor.x;
|
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) {
|
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);
|
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) {
|
pub const ScrollViewport = union(enum) {
|
||||||
/// Scroll to the top of the scrollback
|
/// Scroll to the top of the scrollback
|
||||||
top: void,
|
top: void,
|
||||||
|
|
||||||
|
/// Scroll to the bottom, i.e. the top of the active area
|
||||||
|
bottom: void,
|
||||||
|
|
||||||
delta: isize,
|
delta: isize,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -995,6 +997,7 @@ pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) void {
|
|||||||
|
|
||||||
self.screen.scroll(switch (behavior) {
|
self.screen.scroll(switch (behavior) {
|
||||||
.top => .{ .top = {} },
|
.top => .{ .top = {} },
|
||||||
|
.bottom => .{ .bottom = {} },
|
||||||
.delta => |delta| .{ .delta_no_grow = delta },
|
.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);
|
var t = try init(testing.allocator, 5, 2);
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
@ -1095,6 +1098,7 @@ test "Terminal: print scrolls back to bottom" {
|
|||||||
|
|
||||||
// Type
|
// Type
|
||||||
try t.print('A');
|
try t.print('A');
|
||||||
|
t.scrollViewport(.{ .bottom = {} });
|
||||||
{
|
{
|
||||||
var str = try t.plainString(testing.allocator);
|
var str = try t.plainString(testing.allocator);
|
||||||
defer testing.allocator.free(str);
|
defer testing.allocator.free(str);
|
||||||
|
Reference in New Issue
Block a user