mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
terminal/new: resize without reflow updates cursor
This commit is contained in:
@ -357,6 +357,16 @@ pub const Resize = struct {
|
|||||||
/// Whether to reflow the text. If this is false then the text will
|
/// Whether to reflow the text. If this is false then the text will
|
||||||
/// be truncated if the new size is smaller than the old size.
|
/// be truncated if the new size is smaller than the old size.
|
||||||
reflow: bool = true,
|
reflow: bool = true,
|
||||||
|
|
||||||
|
/// Set this to a cursor position and the resize will retain the
|
||||||
|
/// cursor position and update this so that the cursor remains over
|
||||||
|
/// the same original cell in the reflowed environment.
|
||||||
|
cursor: ?*Cursor = null,
|
||||||
|
|
||||||
|
pub const Cursor = struct {
|
||||||
|
x: size.CellCountInt,
|
||||||
|
y: size.CellCountInt,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Resize
|
/// Resize
|
||||||
@ -614,7 +624,15 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void {
|
|||||||
// behavior because it seemed fine in an ocean of differing behavior
|
// behavior because it seemed fine in an ocean of differing behavior
|
||||||
// between terminal apps. I'm completely open to changing it as long
|
// between terminal apps. I'm completely open to changing it as long
|
||||||
// as resize behavior isn't regressed in a user-hostile way.
|
// as resize behavior isn't regressed in a user-hostile way.
|
||||||
_ = self.trimTrailingBlankRows(self.rows - rows);
|
const trimmed = self.trimTrailingBlankRows(self.rows - rows);
|
||||||
|
|
||||||
|
// If we have a cursor, we want to preserve the y value as
|
||||||
|
// best we can. We need to subtract the number of rows that
|
||||||
|
// moved into the scrollback.
|
||||||
|
if (opts.cursor) |cursor| {
|
||||||
|
const scrollback = self.rows - rows - trimmed;
|
||||||
|
cursor.y -|= scrollback;
|
||||||
|
}
|
||||||
|
|
||||||
// If we didn't trim enough, just modify our row count and this
|
// If we didn't trim enough, just modify our row count and this
|
||||||
// will create additional history.
|
// will create additional history.
|
||||||
@ -624,20 +642,45 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void {
|
|||||||
// Making rows larger we adjust our row count, and then grow
|
// Making rows larger we adjust our row count, and then grow
|
||||||
// to the row count.
|
// to the row count.
|
||||||
.gt => gt: {
|
.gt => gt: {
|
||||||
self.rows = rows;
|
// If our rows increased and our cursor is NOT at the bottom,
|
||||||
|
// we want to try to preserve the y value of the old cursor.
|
||||||
|
// In other words, we don't want to "pull down" scrollback.
|
||||||
|
// This is purely a UX feature.
|
||||||
|
if (opts.cursor) |cursor| cursor: {
|
||||||
|
if (cursor.y >= self.rows - 1) break :cursor;
|
||||||
|
|
||||||
// Perform a quick count to make sure we have at least
|
// Cursor is not at the bottom, so we just grow our
|
||||||
// the number of rows we need. This should be fast because
|
// rows and we're done. Cursor does NOT change for this
|
||||||
// we only need to count up to "rows"
|
// since we're not pulling down scrollback.
|
||||||
|
for (0..rows - self.rows) |_| _ = try self.grow();
|
||||||
|
self.rows = rows;
|
||||||
|
break :gt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor is at the bottom or we don't care about cursors.
|
||||||
|
// In this case, if we have enough rows in our pages, we
|
||||||
|
// just update our rows and we're done. This effectively
|
||||||
|
// "pulls down" scrollback.
|
||||||
|
//
|
||||||
|
// If we don't have enough scrollback, we add the difference,
|
||||||
|
// to the active area.
|
||||||
var count: usize = 0;
|
var count: usize = 0;
|
||||||
var page = self.pages.first;
|
var page = self.pages.first;
|
||||||
while (page) |p| : (page = p.next) {
|
while (page) |p| : (page = p.next) {
|
||||||
count += p.data.size.rows;
|
count += p.data.size.rows;
|
||||||
if (count >= rows) break :gt;
|
if (count >= rows) break;
|
||||||
|
} else {
|
||||||
|
assert(count < rows);
|
||||||
|
for (count..rows) |_| _ = try self.grow();
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(count < rows);
|
// Update our cursor. W
|
||||||
for (count..rows) |_| _ = try self.grow();
|
if (opts.cursor) |cursor| {
|
||||||
|
const grow_len: size.CellCountInt = @intCast(rows -| count);
|
||||||
|
cursor.y += rows - self.rows - grow_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rows = rows;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -663,6 +706,11 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void {
|
|||||||
page.size.cols = cols;
|
page.size.cols = cols;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.cursor) |cursor| {
|
||||||
|
// If our cursor is off the edge we trimmed, update to edge
|
||||||
|
if (cursor.x >= cols) cursor.x = cols - 1;
|
||||||
|
}
|
||||||
|
|
||||||
self.cols = cols;
|
self.cols = cols;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -2129,11 +2177,19 @@ test "PageList resize (no reflow) more rows" {
|
|||||||
defer s.deinit();
|
defer s.deinit();
|
||||||
try testing.expectEqual(@as(usize, 3), s.totalRows());
|
try testing.expectEqual(@as(usize, 3), s.totalRows());
|
||||||
|
|
||||||
|
// Cursor is at the bottom
|
||||||
|
var cursor: Resize.Cursor = .{ .x = 0, .y = 2 };
|
||||||
|
|
||||||
// Resize
|
// Resize
|
||||||
try s.resize(.{ .rows = 10, .reflow = false });
|
try s.resize(.{ .rows = 10, .reflow = false, .cursor = &cursor });
|
||||||
try testing.expectEqual(@as(usize, 10), s.rows);
|
try testing.expectEqual(@as(usize, 10), s.rows);
|
||||||
try testing.expectEqual(@as(usize, 10), s.totalRows());
|
try testing.expectEqual(@as(usize, 10), s.totalRows());
|
||||||
|
|
||||||
|
// Our cursor should not move because we have no scrollback so
|
||||||
|
// we just grew.
|
||||||
|
try testing.expectEqual(@as(size.CellCountInt, 0), cursor.x);
|
||||||
|
try testing.expectEqual(@as(size.CellCountInt, 2), cursor.y);
|
||||||
|
|
||||||
{
|
{
|
||||||
const pt = s.getCell(.{ .active = .{} }).?.screenPoint();
|
const pt = s.getCell(.{ .active = .{} }).?.screenPoint();
|
||||||
try testing.expectEqual(point.Point{ .screen = .{
|
try testing.expectEqual(point.Point{ .screen = .{
|
||||||
@ -2158,10 +2214,18 @@ test "PageList resize (no reflow) more rows with history" {
|
|||||||
} }, pt);
|
} }, pt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cursor is at the bottom
|
||||||
|
var cursor: Resize.Cursor = .{ .x = 0, .y = 2 };
|
||||||
|
|
||||||
// Resize
|
// Resize
|
||||||
try s.resize(.{ .rows = 5, .reflow = false });
|
try s.resize(.{ .rows = 5, .reflow = false, .cursor = &cursor });
|
||||||
try testing.expectEqual(@as(usize, 5), s.rows);
|
try testing.expectEqual(@as(usize, 5), s.rows);
|
||||||
try testing.expectEqual(@as(usize, 53), s.totalRows());
|
try testing.expectEqual(@as(usize, 53), s.totalRows());
|
||||||
|
|
||||||
|
// Our cursor should move since it's in the scrollback
|
||||||
|
try testing.expectEqual(@as(size.CellCountInt, 0), cursor.x);
|
||||||
|
try testing.expectEqual(@as(size.CellCountInt, 4), cursor.y);
|
||||||
|
|
||||||
{
|
{
|
||||||
const pt = s.getCell(.{ .active = .{} }).?.screenPoint();
|
const pt = s.getCell(.{ .active = .{} }).?.screenPoint();
|
||||||
try testing.expectEqual(point.Point{ .screen = .{
|
try testing.expectEqual(point.Point{ .screen = .{
|
||||||
@ -2205,6 +2269,55 @@ test "PageList resize (no reflow) less rows" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "PageList resize (no reflow) less rows cursor in scrollback" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 10, 10, 0);
|
||||||
|
defer s.deinit();
|
||||||
|
try testing.expectEqual(@as(usize, 10), s.totalRows());
|
||||||
|
|
||||||
|
// This is required for our writing below to work
|
||||||
|
try testing.expect(s.pages.first == s.pages.last);
|
||||||
|
const page = &s.pages.first.?.data;
|
||||||
|
|
||||||
|
// Write into all rows so we don't get trim behavior
|
||||||
|
for (0..s.rows) |y| {
|
||||||
|
const rac = page.getRowAndCell(0, y);
|
||||||
|
rac.cell.* = .{
|
||||||
|
.content_tag = .codepoint,
|
||||||
|
.content = .{ .codepoint = @intCast(y) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's say our cursor is in the scrollback
|
||||||
|
var cursor: Resize.Cursor = .{ .x = 0, .y = 2 };
|
||||||
|
{
|
||||||
|
const get = s.getCell(.{ .active = .{
|
||||||
|
.x = cursor.x,
|
||||||
|
.y = cursor.y,
|
||||||
|
} }).?;
|
||||||
|
try testing.expectEqual(@as(u21, 2), get.cell.content.codepoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
try s.resize(.{ .rows = 5, .reflow = false, .cursor = &cursor });
|
||||||
|
try testing.expectEqual(@as(usize, 5), s.rows);
|
||||||
|
try testing.expectEqual(@as(usize, 10), s.totalRows());
|
||||||
|
|
||||||
|
// Our cursor should move since it's in the scrollback
|
||||||
|
try testing.expectEqual(@as(size.CellCountInt, 0), cursor.x);
|
||||||
|
try testing.expectEqual(@as(size.CellCountInt, 0), cursor.y);
|
||||||
|
|
||||||
|
{
|
||||||
|
const pt = s.getCell(.{ .active = .{} }).?.screenPoint();
|
||||||
|
try testing.expectEqual(point.Point{ .screen = .{
|
||||||
|
.x = 0,
|
||||||
|
.y = 5,
|
||||||
|
} }, pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "PageList resize (no reflow) less rows trims blank lines" {
|
test "PageList resize (no reflow) less rows trims blank lines" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
@ -2232,10 +2345,25 @@ test "PageList resize (no reflow) less rows trims blank lines" {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Let's say our cursor is at the top
|
||||||
|
var cursor: Resize.Cursor = .{ .x = 0, .y = 0 };
|
||||||
|
{
|
||||||
|
const get = s.getCell(.{ .active = .{
|
||||||
|
.x = cursor.x,
|
||||||
|
.y = cursor.y,
|
||||||
|
} }).?;
|
||||||
|
try testing.expectEqual(@as(u21, 'A'), get.cell.content.codepoint);
|
||||||
|
}
|
||||||
|
|
||||||
// Resize
|
// Resize
|
||||||
try s.resize(.{ .rows = 2, .reflow = false });
|
try s.resize(.{ .rows = 2, .reflow = false, .cursor = &cursor });
|
||||||
try testing.expectEqual(@as(usize, 2), s.rows);
|
try testing.expectEqual(@as(usize, 2), s.rows);
|
||||||
try testing.expectEqual(@as(usize, 2), s.totalRows());
|
try testing.expectEqual(@as(usize, 2), s.totalRows());
|
||||||
|
|
||||||
|
// Our cursor should not move since we trimmed
|
||||||
|
try testing.expectEqual(@as(size.CellCountInt, 0), cursor.x);
|
||||||
|
try testing.expectEqual(@as(size.CellCountInt, 0), cursor.y);
|
||||||
|
|
||||||
{
|
{
|
||||||
const pt = s.getCell(.{ .active = .{} }).?.screenPoint();
|
const pt = s.getCell(.{ .active = .{} }).?.screenPoint();
|
||||||
try testing.expectEqual(point.Point{ .screen = .{
|
try testing.expectEqual(point.Point{ .screen = .{
|
||||||
@ -2481,6 +2609,74 @@ test "PageList resize (no reflow) more cols forces smaller cap" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "PageList resize (no reflow) more rows adds blank rows if cursor at bottom" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 5, 3, null);
|
||||||
|
defer s.deinit();
|
||||||
|
|
||||||
|
// Grow to 5 total rows, simulating 3 active + 2 scrollback
|
||||||
|
try s.growRows(2);
|
||||||
|
try testing.expect(s.pages.first == s.pages.last);
|
||||||
|
const page = &s.pages.first.?.data;
|
||||||
|
for (0..s.totalRows()) |y| {
|
||||||
|
const rac = page.getRowAndCell(0, y);
|
||||||
|
rac.cell.* = .{
|
||||||
|
.content_tag = .codepoint,
|
||||||
|
.content = .{ .codepoint = @intCast(y) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active should be on row 3
|
||||||
|
{
|
||||||
|
const pt = s.getCell(.{ .active = .{} }).?.screenPoint();
|
||||||
|
try testing.expectEqual(point.Point{ .screen = .{
|
||||||
|
.x = 0,
|
||||||
|
.y = 2,
|
||||||
|
} }, pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's say our cursor is at the bottom
|
||||||
|
var cursor: Resize.Cursor = .{ .x = 0, .y = s.rows - 2 };
|
||||||
|
{
|
||||||
|
const get = s.getCell(.{ .active = .{
|
||||||
|
.x = cursor.x,
|
||||||
|
.y = cursor.y,
|
||||||
|
} }).?;
|
||||||
|
try testing.expectEqual(@as(u21, 3), get.cell.content.codepoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
const original_cursor = cursor;
|
||||||
|
try s.resizeWithoutReflow(.{ .rows = 10, .reflow = false, .cursor = &cursor });
|
||||||
|
try testing.expectEqual(@as(usize, 5), s.cols);
|
||||||
|
try testing.expectEqual(@as(usize, 10), s.rows);
|
||||||
|
|
||||||
|
// Our cursor should not change
|
||||||
|
try testing.expectEqual(original_cursor, cursor);
|
||||||
|
|
||||||
|
// 12 because we have our 10 rows in the active + 2 in the scrollback
|
||||||
|
// because we're preserving the cursor.
|
||||||
|
try testing.expectEqual(@as(usize, 12), s.totalRows());
|
||||||
|
|
||||||
|
// Active should be at the same place it was.
|
||||||
|
{
|
||||||
|
const pt = s.getCell(.{ .active = .{} }).?.screenPoint();
|
||||||
|
try testing.expectEqual(point.Point{ .screen = .{
|
||||||
|
.x = 0,
|
||||||
|
.y = 2,
|
||||||
|
} }, pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go through our active, we should get only 3,4,5
|
||||||
|
for (0..3) |y| {
|
||||||
|
const get = s.getCell(.{ .active = .{ .y = y } }).?;
|
||||||
|
const expected: u21 = @intCast(y + 2);
|
||||||
|
try testing.expectEqual(expected, get.cell.content.codepoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "PageList resize reflow more cols no wrapped rows" {
|
test "PageList resize reflow more cols no wrapped rows" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
@ -617,7 +617,21 @@ pub fn resizeWithoutReflow(
|
|||||||
cols: size.CellCountInt,
|
cols: size.CellCountInt,
|
||||||
rows: size.CellCountInt,
|
rows: size.CellCountInt,
|
||||||
) !void {
|
) !void {
|
||||||
try self.pages.resize(.{ .rows = rows, .cols = cols, .reflow = false });
|
var cursor: PageList.Resize.Cursor = .{
|
||||||
|
.x = self.cursor.x,
|
||||||
|
.y = self.cursor.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
try self.pages.resize(.{
|
||||||
|
.rows = rows,
|
||||||
|
.cols = cols,
|
||||||
|
.reflow = false,
|
||||||
|
.cursor = &cursor,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.cursor.x = cursor.x;
|
||||||
|
self.cursor.y = cursor.y;
|
||||||
|
self.cursorReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a style attribute for the current cursor.
|
/// Set a style attribute for the current cursor.
|
||||||
@ -1853,8 +1867,14 @@ test "Screen: resize (no reflow) less rows" {
|
|||||||
defer s.deinit();
|
defer s.deinit();
|
||||||
const str = "1ABCD\n2EFGH\n3IJKL";
|
const str = "1ABCD\n2EFGH\n3IJKL";
|
||||||
try s.testWriteString(str);
|
try s.testWriteString(str);
|
||||||
|
try testing.expectEqual(5, s.cursor.x);
|
||||||
|
try testing.expectEqual(2, s.cursor.y);
|
||||||
try s.resizeWithoutReflow(10, 2);
|
try s.resizeWithoutReflow(10, 2);
|
||||||
|
|
||||||
|
// Since we shrunk, we should adjust our cursor
|
||||||
|
try testing.expectEqual(5, s.cursor.x);
|
||||||
|
try testing.expectEqual(1, s.cursor.y);
|
||||||
|
|
||||||
{
|
{
|
||||||
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
defer alloc.free(contents);
|
defer alloc.free(contents);
|
||||||
@ -2139,21 +2159,20 @@ test "Screen: resize more rows with populated scrollback" {
|
|||||||
try s.resize(5, 10);
|
try s.resize(5, 10);
|
||||||
|
|
||||||
// Cursor should still be on the "4"
|
// Cursor should still be on the "4"
|
||||||
// TODO
|
{
|
||||||
// {
|
const list_cell = s.pages.getCell(.{ .active = .{
|
||||||
// const list_cell = s.pages.getCell(.{ .active = .{
|
.x = s.cursor.x,
|
||||||
// .x = s.cursor.x,
|
.y = s.cursor.y,
|
||||||
// .y = s.cursor.y,
|
} }).?;
|
||||||
// } }).?;
|
try testing.expectEqual(@as(u21, '4'), list_cell.cell.content.codepoint);
|
||||||
// try testing.expectEqual(@as(u21, '4'), list_cell.cell.content.codepoint);
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// {
|
{
|
||||||
// const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
// defer alloc.free(contents);
|
defer alloc.free(contents);
|
||||||
// const expected = "3IJKL\n4ABCD\n5EFGH";
|
const expected = "3IJKL\n4ABCD\n5EFGH";
|
||||||
// try testing.expectEqualStrings(expected, contents);
|
try testing.expectEqualStrings(expected, contents);
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// test "Screen: resize more cols no reflow" {
|
// test "Screen: resize more cols no reflow" {
|
||||||
|
Reference in New Issue
Block a user