mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
screen operations operate on the active area
This fixes tons of bugs around escape sequences while scrolling that used to work on the offset from the viewport (which is wrong). This now calculates the actual active area and does that. This also fixes the TODO in the diff.
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:
|
||||||
|
|
||||||
|
@ -107,6 +107,11 @@ pub const RowIndex = union(enum) {
|
|||||||
/// on where the user has scrolled the viewport, "0" is different.
|
/// on where the user has scrolled the viewport, "0" is different.
|
||||||
viewport: usize,
|
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
|
// TODO: others
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -204,7 +209,7 @@ pub fn getRow(self: Screen, idx: RowIndex) 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.rowIndex(.{ .viewport = row });
|
const row_idx = self.rowIndex(.{ .active = row });
|
||||||
return &self.storage[row_idx + col];
|
return &self.storage[row_idx + col];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,6 +226,11 @@ fn rowIndex(self: Screen, idx: RowIndex) usize {
|
|||||||
assert(y < self.rows);
|
assert(y < self.rows);
|
||||||
break :y y + self.visible_offset;
|
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;
|
||||||
@ -369,8 +379,8 @@ 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(.{ .viewport = src });
|
const src_row = self.getRow(.{ .active = src });
|
||||||
const dst_row = self.getRow(.{ .viewport = dst });
|
const dst_row = self.getRow(.{ .active = dst });
|
||||||
std.mem.copy(Cell, dst_row, src_row);
|
std.mem.copy(Cell, dst_row, src_row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -581,13 +591,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(.{ .viewport = 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(.{ .viewport = y });
|
row = self.getRow(.{ .active = y });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -596,7 +606,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(.{ .viewport = y });
|
row = self.getRow(.{ .active = y });
|
||||||
}
|
}
|
||||||
|
|
||||||
row[x].char = @intCast(u32, c);
|
row[x].char = @intCast(u32, c);
|
||||||
@ -647,9 +657,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.rowIndex(.{ .viewport = 0 }));
|
try testing.expectEqual(@as(usize, 5), s.rowIndex(.{ .active = 0 }));
|
||||||
try testing.expectEqual(@as(usize, 10), s.rowIndex(.{ .viewport = 1 }));
|
try testing.expectEqual(@as(usize, 10), s.rowIndex(.{ .active = 1 }));
|
||||||
try testing.expectEqual(@as(usize, 0), s.rowIndex(.{ .viewport = 2 }));
|
try testing.expectEqual(@as(usize, 0), s.rowIndex(.{ .active = 2 }));
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test our contents rotated
|
// Test our contents rotated
|
||||||
@ -718,9 +728,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.rowIndex(.{ .viewport = 0 }));
|
try testing.expectEqual(@as(usize, 5), s.rowIndex(.{ .active = 0 }));
|
||||||
try testing.expectEqual(@as(usize, 10), s.rowIndex(.{ .viewport = 1 }));
|
try testing.expectEqual(@as(usize, 10), s.rowIndex(.{ .active = 1 }));
|
||||||
try testing.expectEqual(@as(usize, 15), s.rowIndex(.{ .viewport = 2 }));
|
try testing.expectEqual(@as(usize, 15), s.rowIndex(.{ .active = 2 }));
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test our contents rotated
|
// Test our contents rotated
|
||||||
@ -949,7 +959,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.rowIndex(.{ .viewport = 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(.{ .viewport = 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(.{ .viewport = row }), filled);
|
std.mem.copy(Screen.Cell, self.screen.getRow(.{ .active = row }), filled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -615,12 +612,12 @@ pub fn eraseLine(
|
|||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
.right => {
|
.right => {
|
||||||
const row = self.screen.getRow(.{ .viewport = 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(.{ .viewport = 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 +625,7 @@ pub fn eraseLine(
|
|||||||
},
|
},
|
||||||
|
|
||||||
.complete => {
|
.complete => {
|
||||||
const row = self.screen.getRow(.{ .viewport = 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 +650,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 +818,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the current row
|
// Get the current row
|
||||||
const row = self.screen.getRow(.{ .viewport = 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 +940,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(.{ .viewport = 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 +982,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 +996,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 +1069,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 +1097,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