mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-18 05:20:29 +00:00
Merge remote-tracking branch 'upstream/main' into ligature-detect
This commit is contained in:
2
.github/workflows/publish-tag.yml
vendored
2
.github/workflows/publish-tag.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
mkdir blob
|
||||
mv appcast.xml blob/appcast.xml
|
||||
- name: Upload Appcast to R2
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
|
||||
|
||||
23
flake.lock
generated
23
flake.lock
generated
@@ -41,27 +41,26 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755776884,
|
||||
"narHash": "sha256-CPM7zm6csUx7vSfKvzMDIjepEJv1u/usmaT7zydzbuI=",
|
||||
"lastModified": 1768068402,
|
||||
"narHash": "sha256-bAXnnJZKJiF7Xr6eNW6+PhBf1lg2P1aFUO9+xgWkXfA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "4fb695d10890e9fc6a19deadf85ff79ffb78da86",
|
||||
"rev": "8bc5473b6bc2b6e1529a9c4040411e1199c43b4c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "release-25.05",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1763191728,
|
||||
"narHash": "sha256-gI9PpaoX4/f28HkjcTbFVpFhtOxSDtOEdFaHZrdETe0=",
|
||||
"rev": "1d4c88323ac36805d09657d13a5273aea1b34f0c",
|
||||
"lastModified": 1768032153,
|
||||
"narHash": "sha256-zvxtwlM8ZlulmZKyYCQAPpkm5dngSEnnHjmjV7Teloc=",
|
||||
"rev": "3146c6aa9995e7351a398e17470e15305e6e18ff",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre896415.1d4c88323ac3/nixexprs.tar.xz"
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre925418.3146c6aa9995/nixexprs.tar.xz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
@@ -126,17 +125,17 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1758405547,
|
||||
"narHash": "sha256-WgaDgvIZMPvlZcZrpPMjkaalTBnGF2lTG+62znXctWM=",
|
||||
"lastModified": 1768231828,
|
||||
"narHash": "sha256-wL/8Iij4T2OLkhHcc4NieOjf7YeJffaUYbCiCqKv/+0=",
|
||||
"owner": "jcollie",
|
||||
"repo": "zon2nix",
|
||||
"rev": "bf983aa90ff169372b9fa8c02e57ea75e0b42245",
|
||||
"rev": "c28e93f3ba133d4c1b1d65224e2eebede61fd071",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "jcollie",
|
||||
"repo": "zon2nix",
|
||||
"rev": "bf983aa90ff169372b9fa8c02e57ea75e0b42245",
|
||||
"rev": "c28e93f3ba133d4c1b1d65224e2eebede61fd071",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
};
|
||||
|
||||
zon2nix = {
|
||||
url = "github:jcollie/zon2nix?rev=bf983aa90ff169372b9fa8c02e57ea75e0b42245";
|
||||
url = "github:jcollie/zon2nix?rev=c28e93f3ba133d4c1b1d65224e2eebede61fd071";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager?ref=release-25.05";
|
||||
url = "github:nix-community/home-manager";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
@@ -117,7 +117,6 @@
|
||||
wayland-gnome = runVM ./nix/vm/wayland-gnome.nix;
|
||||
wayland-plasma6 = runVM ./nix/vm/wayland-plasma6.nix;
|
||||
x11-cinnamon = runVM ./nix/vm/x11-cinnamon.nix;
|
||||
x11-gnome = runVM ./nix/vm/x11-gnome.nix;
|
||||
x11-plasma6 = runVM ./nix/vm/x11-plasma6.nix;
|
||||
x11-xfce = runVM ./nix/vm/x11-xfce.nix;
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
wasmtime,
|
||||
wraptest,
|
||||
zig,
|
||||
zig_0_15,
|
||||
zip,
|
||||
llvmPackages_latest,
|
||||
bzip2,
|
||||
|
||||
@@ -20,16 +20,6 @@
|
||||
wayland-scanner,
|
||||
pkgs,
|
||||
}: let
|
||||
# The Zig hook has no way to select the release type without actual
|
||||
# overriding of the default flags.
|
||||
#
|
||||
# TODO: Once
|
||||
# https://github.com/ziglang/zig/issues/14281#issuecomment-1624220653 is
|
||||
# ultimately acted on and has made its way to a nixpkgs implementation, this
|
||||
# can probably be removed in favor of that.
|
||||
zig_hook = zig_0_15.hook.overrideAttrs {
|
||||
zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off";
|
||||
};
|
||||
gi_typelib_path = import ./build-support/gi-typelib-path.nix {
|
||||
inherit pkgs lib stdenv;
|
||||
};
|
||||
@@ -73,7 +63,7 @@ in
|
||||
ncurses
|
||||
pandoc
|
||||
pkg-config
|
||||
zig_hook
|
||||
zig_0_15
|
||||
gobject-introspection
|
||||
wrapGAppsHook4
|
||||
blueprint-compiler
|
||||
@@ -92,12 +82,16 @@ in
|
||||
|
||||
GI_TYPELIB_PATH = gi_typelib_path;
|
||||
|
||||
dontSetZigDefaultFlags = true;
|
||||
|
||||
zigBuildFlags = [
|
||||
"--system"
|
||||
"${finalAttrs.deps}"
|
||||
"-Dversion-string=${finalAttrs.version}-${revision}-nix"
|
||||
"-Dgtk-x11=${lib.boolToString enableX11}"
|
||||
"-Dgtk-wayland=${lib.boolToString enableWayland}"
|
||||
"-Dcpu=baseline"
|
||||
"-Doptimize=${optimize}"
|
||||
"-Dstrip=${lib.boolToString strip}"
|
||||
];
|
||||
|
||||
|
||||
@@ -274,7 +274,7 @@ in {
|
||||
client.succeed("${su "${ghostty} +new-window"}")
|
||||
client.wait_until_succeeds("${wm_class} | grep -q 'com.mitchellh.ghostty-debug'")
|
||||
|
||||
with subtest("SSH from client to server and verify that the Ghostty terminfo is copied.")
|
||||
with subtest("SSH from client to server and verify that the Ghostty terminfo is copied."):
|
||||
client.sleep(2)
|
||||
client.send_chars("ssh ghostty@server\n")
|
||||
server.wait_for_file("${user.home}/.terminfo/x/xterm-ghostty", timeout=30)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services.xserver = {
|
||||
services = {
|
||||
displayManager = {
|
||||
gdm = {
|
||||
enable = true;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-gnome.nix
|
||||
];
|
||||
|
||||
services.displayManager = {
|
||||
defaultSession = "gnome-xorg";
|
||||
};
|
||||
}
|
||||
@@ -287,7 +287,7 @@ fn writeBashCompletions(writer: *std.Io.Writer) !void {
|
||||
\\ else prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
\\ fi
|
||||
\\
|
||||
\\ # current completion is double quoted add a space so the curor progresses
|
||||
\\ # current completion is double quoted add a space so the cursor progresses
|
||||
\\ if [[ "$2" == \"*\" ]]; then
|
||||
\\ COMPREPLY=( "$cur " );
|
||||
\\ return;
|
||||
|
||||
@@ -49,7 +49,12 @@ const Node = struct {
|
||||
/// The memory pool we get page nodes from.
|
||||
const NodePool = std.heap.MemoryPool(List.Node);
|
||||
|
||||
/// The standard page capacity that we use as a starting point for
|
||||
/// all pages. This is chosen as a sane default that fits most terminal
|
||||
/// usage to support using our pool.
|
||||
const std_capacity = pagepkg.std_capacity;
|
||||
|
||||
/// The byte size required for a standard page.
|
||||
const std_size = Page.layout(std_capacity).total_size;
|
||||
|
||||
/// The memory pool we use for page memory buffers. We use a separate pool
|
||||
@@ -223,19 +228,30 @@ pub const Viewport = union(enum) {
|
||||
/// But this gives us a nice fast heuristic for determining min/max size.
|
||||
/// Therefore, if the page size is violated you should always also verify
|
||||
/// that we have enough space for the active area.
|
||||
fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) !usize {
|
||||
fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) usize {
|
||||
// Invariant required to ensure our divCeil below cannot overflow.
|
||||
comptime {
|
||||
const max_rows = std.math.maxInt(size.CellCountInt);
|
||||
_ = std.math.divCeil(usize, max_rows, 1) catch unreachable;
|
||||
}
|
||||
|
||||
// Get our capacity to fit our rows. If the cols are too big, it may
|
||||
// force less rows than we want meaning we need more than one page to
|
||||
// represent a viewport.
|
||||
const cap = try std_capacity.adjust(.{ .cols = cols });
|
||||
const cap = initialCapacity(cols);
|
||||
|
||||
// Calculate the number of standard sized pages we need to represent
|
||||
// an active area.
|
||||
const pages_exact = if (cap.rows >= rows) 1 else try std.math.divCeil(
|
||||
const pages_exact = if (cap.rows >= rows) 1 else std.math.divCeil(
|
||||
usize,
|
||||
rows,
|
||||
cap.rows,
|
||||
);
|
||||
) catch {
|
||||
// Not possible:
|
||||
// - initialCapacity guarantees at least 1 row
|
||||
// - numerator/denominator can't overflow because of comptime check above
|
||||
unreachable;
|
||||
};
|
||||
|
||||
// We always need at least one page extra so that we
|
||||
// can fit partial pages to spread our active area across two pages.
|
||||
@@ -255,6 +271,49 @@ fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) !usize {
|
||||
return PagePool.item_size * pages;
|
||||
}
|
||||
|
||||
/// Calculates the initial capacity for a new page for a given column
|
||||
/// count. This will attempt to fit within std_size at all times so we
|
||||
/// can use our memory pool, but if cols is too big, this will return a
|
||||
/// larger capacity.
|
||||
///
|
||||
/// The returned capacity is always guaranteed to layout properly (not
|
||||
/// overflow). We are able to support capacities up to the maximum int
|
||||
/// value of cols, so this will never overflow.
|
||||
fn initialCapacity(cols: size.CellCountInt) Capacity {
|
||||
// This is an important invariant that ensures that this function
|
||||
// can never return an error. We verify here that our standard capacity
|
||||
// when increased to maximum possible columns can always support at
|
||||
// least one row in memory.
|
||||
//
|
||||
// IF THIS EVER FAILS: We probably need to modify our logic below
|
||||
// to reduce other elements of the capacity (styles, graphemes, etc.).
|
||||
// But, instead, I recommend taking a step back and re-evaluating
|
||||
// life choices.
|
||||
comptime {
|
||||
var cap = std_capacity;
|
||||
cap.cols = std.math.maxInt(size.CellCountInt);
|
||||
_ = Page.layout(cap);
|
||||
}
|
||||
|
||||
if (std_capacity.adjust(
|
||||
.{ .cols = cols },
|
||||
)) |cap| {
|
||||
// If we can adjust our standard capacity, we fit within the
|
||||
// standard size and we're good!
|
||||
return cap;
|
||||
} else |err| {
|
||||
// Ensure our error set doesn't change.
|
||||
comptime assert(@TypeOf(err) == error{OutOfMemory});
|
||||
}
|
||||
|
||||
// This code path means that our standard capacity can't even
|
||||
// accommodate our column count! The only solution is to increase
|
||||
// our capacity and go non-standard.
|
||||
var cap: Capacity = std_capacity;
|
||||
cap.cols = cols;
|
||||
return cap;
|
||||
}
|
||||
|
||||
/// This is the page allocator we'll use for all our underlying
|
||||
/// VM page allocations.
|
||||
inline fn pageAllocator() Allocator {
|
||||
@@ -310,7 +369,7 @@ pub fn init(
|
||||
);
|
||||
|
||||
// Get our minimum max size, see doc comments for more details.
|
||||
const min_max_size = try minMaxSize(cols, rows);
|
||||
const min_max_size = minMaxSize(cols, rows);
|
||||
|
||||
// We always track our viewport pin to ensure this is never an allocation
|
||||
const viewport_pin = try pool.pins.create();
|
||||
@@ -344,17 +403,31 @@ fn initPages(
|
||||
serial: *u64,
|
||||
cols: size.CellCountInt,
|
||||
rows: size.CellCountInt,
|
||||
) !struct { List, usize } {
|
||||
) Allocator.Error!struct { List, usize } {
|
||||
var page_list: List = .{};
|
||||
var page_size: usize = 0;
|
||||
|
||||
// Add pages as needed to create our initial viewport.
|
||||
const cap = try std_capacity.adjust(.{ .cols = cols });
|
||||
const cap = initialCapacity(cols);
|
||||
const layout = Page.layout(cap);
|
||||
const pooled = layout.total_size <= std_size;
|
||||
const page_alloc = pool.pages.arena.child_allocator;
|
||||
|
||||
var rem = rows;
|
||||
while (rem > 0) {
|
||||
const node = try pool.nodes.create();
|
||||
const page_buf = try pool.pages.create();
|
||||
// no errdefer because the pool deinit will clean these up
|
||||
const page_buf = if (pooled)
|
||||
try pool.pages.create()
|
||||
else
|
||||
try page_alloc.alignedAlloc(
|
||||
u8,
|
||||
.fromByteUnits(std.heap.page_size_min),
|
||||
layout.total_size,
|
||||
);
|
||||
errdefer if (pooled)
|
||||
pool.pages.destroy(page_buf)
|
||||
else
|
||||
page_alloc.free(page_buf);
|
||||
|
||||
// In runtime safety modes we have to memset because the Zig allocator
|
||||
// interface will always memset to 0xAA for undefined. In non-safe modes
|
||||
@@ -364,10 +437,7 @@ fn initPages(
|
||||
// Initialize the first set of pages to contain our viewport so that
|
||||
// the top of the first page is always the active area.
|
||||
node.* = .{
|
||||
.data = .initBuf(
|
||||
.init(page_buf),
|
||||
Page.layout(cap),
|
||||
),
|
||||
.data = .initBuf(.init(page_buf), layout),
|
||||
.serial = serial.*,
|
||||
};
|
||||
node.data.size.rows = @min(rem, node.data.capacity.rows);
|
||||
@@ -533,10 +603,8 @@ pub fn reset(self: *PageList) void {
|
||||
// We need enough pages/nodes to keep our active area. This should
|
||||
// never fail since we by definition have allocated a page already
|
||||
// that fits our size but I'm not confident to make that assertion.
|
||||
const cap = std_capacity.adjust(
|
||||
.{ .cols = self.cols },
|
||||
) catch @panic("reset: std_capacity.adjust failed");
|
||||
assert(cap.rows > 0); // adjust should never return 0 rows
|
||||
const cap = initialCapacity(self.cols);
|
||||
assert(cap.rows > 0);
|
||||
|
||||
// The number of pages we need is the number of rows in the active
|
||||
// area divided by the row capacity of a page.
|
||||
@@ -828,7 +896,7 @@ pub fn resize(self: *PageList, opts: Resize) !void {
|
||||
// when increasing beyond our initial minimum max size or explicit max
|
||||
// size to fit the active area.
|
||||
const old_min_max_size = self.min_max_size;
|
||||
self.min_max_size = try minMaxSize(
|
||||
self.min_max_size = minMaxSize(
|
||||
opts.cols orelse self.cols,
|
||||
opts.rows orelse self.rows,
|
||||
);
|
||||
@@ -1592,7 +1660,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void {
|
||||
// We only set the new min_max_size if we're not reflowing. If we are
|
||||
// reflowing, then resize handles this for us.
|
||||
const old_min_max_size = self.min_max_size;
|
||||
self.min_max_size = if (!opts.reflow) try minMaxSize(
|
||||
self.min_max_size = if (!opts.reflow) minMaxSize(
|
||||
opts.cols orelse self.cols,
|
||||
opts.rows orelse self.rows,
|
||||
) else old_min_max_size;
|
||||
@@ -4551,6 +4619,38 @@ test "PageList init rows across two pages" {
|
||||
}, s.scrollbar());
|
||||
}
|
||||
|
||||
test "PageList init more than max cols" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Initialize with more columns than we can fit in our standard
|
||||
// capacity. This is going to force us to go to a non-standard page
|
||||
// immediately.
|
||||
var s = try init(
|
||||
alloc,
|
||||
std_capacity.maxCols().? + 1,
|
||||
80,
|
||||
null,
|
||||
);
|
||||
defer s.deinit();
|
||||
try testing.expect(s.viewport == .active);
|
||||
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
|
||||
|
||||
// We expect a single, non-standard page
|
||||
try testing.expect(s.pages.first != null);
|
||||
try testing.expect(s.pages.first.?.data.memory.len > std_size);
|
||||
|
||||
// Initial total rows should be our row count
|
||||
try testing.expectEqual(s.rows, s.total_rows);
|
||||
|
||||
// Scrollbar should be where we expect it
|
||||
try testing.expectEqual(Scrollbar{
|
||||
.total = s.rows,
|
||||
.offset = 0,
|
||||
.len = s.rows,
|
||||
}, s.scrollbar());
|
||||
}
|
||||
|
||||
test "PageList pointFromPin active no history" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -1602,9 +1602,6 @@ pub fn insertLines(self: *Terminal, count: usize) void {
|
||||
const cur_rac = cur_p.rowAndCell();
|
||||
const cur_row: *Row = cur_rac.row;
|
||||
|
||||
// Mark the row as dirty
|
||||
cur_p.markDirty();
|
||||
|
||||
// If this is one of the lines we need to shift, do so
|
||||
if (y > adjusted_count) {
|
||||
const off_p = cur_p.up(adjusted_count).?;
|
||||
@@ -1699,9 +1696,6 @@ pub fn insertLines(self: *Terminal, count: usize) void {
|
||||
dst_row.* = src_row.*;
|
||||
src_row.* = dst;
|
||||
|
||||
// Make sure the row is marked as dirty though.
|
||||
dst_row.dirty = true;
|
||||
|
||||
// Ensure what we did didn't corrupt the page
|
||||
cur_p.node.data.assertIntegrity();
|
||||
} else {
|
||||
@@ -1728,6 +1722,9 @@ pub fn insertLines(self: *Terminal, count: usize) void {
|
||||
);
|
||||
}
|
||||
|
||||
// Mark the row as dirty
|
||||
cur_p.markDirty();
|
||||
|
||||
// We have successfully processed a line.
|
||||
y -= 1;
|
||||
// Move our pin up to the next row.
|
||||
@@ -1805,9 +1802,6 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
|
||||
const cur_rac = cur_p.rowAndCell();
|
||||
const cur_row: *Row = cur_rac.row;
|
||||
|
||||
// Mark the row as dirty
|
||||
cur_p.markDirty();
|
||||
|
||||
// If this is one of the lines we need to shift, do so
|
||||
if (y < rem - adjusted_count) {
|
||||
const off_p = cur_p.down(adjusted_count).?;
|
||||
@@ -1897,9 +1891,6 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
|
||||
dst_row.* = src_row.*;
|
||||
src_row.* = dst;
|
||||
|
||||
// Make sure the row is marked as dirty though.
|
||||
dst_row.dirty = true;
|
||||
|
||||
// Ensure what we did didn't corrupt the page
|
||||
cur_p.node.data.assertIntegrity();
|
||||
} else {
|
||||
@@ -1926,6 +1917,9 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
|
||||
);
|
||||
}
|
||||
|
||||
// Mark the row as dirty
|
||||
cur_p.markDirty();
|
||||
|
||||
// We have successfully processed a line.
|
||||
y += 1;
|
||||
// Move our pin down to the next row.
|
||||
@@ -5419,6 +5413,52 @@ test "Terminal: insertLines top/bottom scroll region" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: insertLines across page boundary marks all shifted rows dirty" {
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
const first_page = t.screens.active.pages.pages.first.?;
|
||||
const first_page_nrows = first_page.data.capacity.rows;
|
||||
|
||||
// Fill up the first page minus 3 rows
|
||||
for (0..first_page_nrows - 3) |_| try t.linefeed();
|
||||
|
||||
// Add content that will cross a page boundary
|
||||
try t.printString("1AAAA");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("2BBBB");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("3CCCC");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("4DDDD");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("5EEEE");
|
||||
|
||||
// Verify we now have a second page
|
||||
try testing.expect(first_page.next != null);
|
||||
|
||||
t.setCursorPos(1, 1);
|
||||
t.clearDirty();
|
||||
t.insertLines(1);
|
||||
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
|
||||
|
||||
{
|
||||
const str = try t.plainString(testing.allocator);
|
||||
defer testing.allocator.free(str);
|
||||
try testing.expectEqualStrings("\n1AAAA\n2BBBB\n3CCCC\n4DDDD", str);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: insertLines (legacy test)" {
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
|
||||
@@ -8042,6 +8082,52 @@ test "Terminal: deleteLines colors with bg color" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: deleteLines across page boundary marks all shifted rows dirty" {
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
const first_page = t.screens.active.pages.pages.first.?;
|
||||
const first_page_nrows = first_page.data.capacity.rows;
|
||||
|
||||
// Fill up the first page minus 3 rows
|
||||
for (0..first_page_nrows - 3) |_| try t.linefeed();
|
||||
|
||||
// Add content that will cross a page boundary
|
||||
try t.printString("1AAAA");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("2BBBB");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("3CCCC");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("4DDDD");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("5EEEE");
|
||||
|
||||
// Verify we now have a second page
|
||||
try testing.expect(first_page.next != null);
|
||||
|
||||
t.setCursorPos(1, 1);
|
||||
t.clearDirty();
|
||||
t.deleteLines(1);
|
||||
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
|
||||
|
||||
{
|
||||
const str = try t.plainString(testing.allocator);
|
||||
defer testing.allocator.free(str);
|
||||
try testing.expectEqualStrings("2BBBB\n3CCCC\n4DDDD\n5EEEE", str);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: deleteLines (legacy)" {
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 80, .rows = 80 });
|
||||
|
||||
@@ -196,7 +196,8 @@ pub const Page = struct {
|
||||
// We need to go through and initialize all the rows so that
|
||||
// they point to a valid offset into the cells, since the rows
|
||||
// zero-initialized aren't valid.
|
||||
const cells_ptr = cells.ptr(buf)[0 .. cap.cols * cap.rows];
|
||||
const cells_len = @as(usize, cap.cols) * @as(usize, cap.rows);
|
||||
const cells_ptr = cells.ptr(buf)[0..cells_len];
|
||||
for (rows.ptr(buf)[0..cap.rows], 0..) |*row, y| {
|
||||
const start = y * cap.cols;
|
||||
row.* = .{
|
||||
@@ -1556,7 +1557,7 @@ pub const Page = struct {
|
||||
const rows_start = 0;
|
||||
const rows_end: usize = rows_start + (rows_count * @sizeOf(Row));
|
||||
|
||||
const cells_count: usize = @intCast(cap.cols * cap.rows);
|
||||
const cells_count: usize = @as(usize, cap.cols) * @as(usize, cap.rows);
|
||||
const cells_start = alignForward(usize, rows_end, @alignOf(Cell));
|
||||
const cells_end = cells_start + (cells_count * @sizeOf(Cell));
|
||||
|
||||
@@ -1662,43 +1663,42 @@ pub const Capacity = struct {
|
||||
cols: ?size.CellCountInt = null,
|
||||
};
|
||||
|
||||
/// Returns the maximum number of columns that can be used with this
|
||||
/// capacity while still fitting at least one row. Returns null if even
|
||||
/// a single column cannot fit (which would indicate an unusable capacity).
|
||||
///
|
||||
/// Note that this is the maximum number of columns that never increases
|
||||
/// the amount of memory the original capacity will take. If you modify
|
||||
/// the original capacity to add rows, then you can fit more columns.
|
||||
pub fn maxCols(self: Capacity) ?size.CellCountInt {
|
||||
const available_bits = self.availableBitsForGrid();
|
||||
|
||||
// If we can't even fit the row metadata, return null
|
||||
if (available_bits <= @bitSizeOf(Row)) return null;
|
||||
|
||||
// We do the math of how many columns we can fit in the remaining
|
||||
// bits ignoring the metadata of a row.
|
||||
const remaining_bits = available_bits - @bitSizeOf(Row);
|
||||
const max_cols = remaining_bits / @bitSizeOf(Cell);
|
||||
|
||||
// Clamp to CellCountInt max
|
||||
return @min(std.math.maxInt(size.CellCountInt), max_cols);
|
||||
}
|
||||
|
||||
/// Adjust the capacity parameters while retaining the same total size.
|
||||
///
|
||||
/// Adjustments always happen by limiting the rows in the page. Everything
|
||||
/// else can grow. If it is impossible to achieve the desired adjustment,
|
||||
/// OutOfMemory is returned.
|
||||
pub fn adjust(self: Capacity, req: Adjustment) Allocator.Error!Capacity {
|
||||
var adjusted = self;
|
||||
if (req.cols) |cols| {
|
||||
// The math below only works if there is no alignment gap between
|
||||
// the end of the rows array and the start of the cells array.
|
||||
//
|
||||
// To guarantee this, we assert that Row's size is a multiple of
|
||||
// Cell's alignment, so that any length array of Rows will end on
|
||||
// a valid alignment for the start of the Cell array.
|
||||
assert(@sizeOf(Row) % @alignOf(Cell) == 0);
|
||||
|
||||
const layout = Page.layout(self);
|
||||
|
||||
// In order to determine the amount of space in the page available
|
||||
// for rows & cells (which will allow us to calculate the number of
|
||||
// rows we can fit at a certain column width) we need to layout the
|
||||
// "meta" members of the page (i.e. everything else) from the end.
|
||||
const hyperlink_map_start = alignBackward(usize, layout.total_size - layout.hyperlink_map_layout.total_size, hyperlink.Map.base_align.toByteUnits());
|
||||
const hyperlink_set_start = alignBackward(usize, hyperlink_map_start - layout.hyperlink_set_layout.total_size, hyperlink.Set.base_align.toByteUnits());
|
||||
const string_alloc_start = alignBackward(usize, hyperlink_set_start - layout.string_alloc_layout.total_size, StringAlloc.base_align.toByteUnits());
|
||||
const grapheme_map_start = alignBackward(usize, string_alloc_start - layout.grapheme_map_layout.total_size, GraphemeMap.base_align.toByteUnits());
|
||||
const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - layout.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align.toByteUnits());
|
||||
const styles_start = alignBackward(usize, grapheme_alloc_start - layout.styles_layout.total_size, StyleSet.base_align.toByteUnits());
|
||||
const available_bits = self.availableBitsForGrid();
|
||||
|
||||
// The size per row is:
|
||||
// - The row metadata itself
|
||||
// - The cells per row (n=cols)
|
||||
const bits_per_row: usize = size: {
|
||||
var bits: usize = @bitSizeOf(Row); // Row metadata
|
||||
bits += @bitSizeOf(Cell) * @as(usize, @intCast(cols)); // Cells (n=cols)
|
||||
break :size bits;
|
||||
};
|
||||
const available_bits: usize = styles_start * 8;
|
||||
const bits_per_row: usize = @bitSizeOf(Row) + @bitSizeOf(Cell) * @as(usize, @intCast(cols));
|
||||
const new_rows: usize = @divFloor(available_bits, bits_per_row);
|
||||
|
||||
// If our rows go to zero then we can't fit any row metadata
|
||||
@@ -1711,6 +1711,34 @@ pub const Capacity = struct {
|
||||
|
||||
return adjusted;
|
||||
}
|
||||
|
||||
/// Computes the number of bits available for rows and cells in the page.
|
||||
///
|
||||
/// This is done by laying out the "meta" members (styles, graphemes,
|
||||
/// hyperlinks, strings) from the end of the page and finding where they
|
||||
/// start, which gives us the space available for rows and cells.
|
||||
fn availableBitsForGrid(self: Capacity) usize {
|
||||
// The math below only works if there is no alignment gap between
|
||||
// the end of the rows array and the start of the cells array.
|
||||
//
|
||||
// To guarantee this, we assert that Row's size is a multiple of
|
||||
// Cell's alignment, so that any length array of Rows will end on
|
||||
// a valid alignment for the start of the Cell array.
|
||||
assert(@sizeOf(Row) % @alignOf(Cell) == 0);
|
||||
|
||||
const l = Page.layout(self);
|
||||
|
||||
// Layout meta members from the end to find styles_start
|
||||
const hyperlink_map_start = alignBackward(usize, l.total_size - l.hyperlink_map_layout.total_size, hyperlink.Map.base_align.toByteUnits());
|
||||
const hyperlink_set_start = alignBackward(usize, hyperlink_map_start - l.hyperlink_set_layout.total_size, hyperlink.Set.base_align.toByteUnits());
|
||||
const string_alloc_start = alignBackward(usize, hyperlink_set_start - l.string_alloc_layout.total_size, StringAlloc.base_align.toByteUnits());
|
||||
const grapheme_map_start = alignBackward(usize, string_alloc_start - l.grapheme_map_layout.total_size, GraphemeMap.base_align.toByteUnits());
|
||||
const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - l.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align.toByteUnits());
|
||||
const styles_start = alignBackward(usize, grapheme_alloc_start - l.styles_layout.total_size, StyleSet.base_align.toByteUnits());
|
||||
|
||||
// Multiply by 8 to convert bytes to bits
|
||||
return styles_start * 8;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Row = packed struct(u64) {
|
||||
@@ -2070,6 +2098,40 @@ test "Page capacity adjust cols too high" {
|
||||
);
|
||||
}
|
||||
|
||||
test "Capacity maxCols basic" {
|
||||
const cap = std_capacity;
|
||||
const max = cap.maxCols().?;
|
||||
|
||||
// maxCols should be >= current cols (since current capacity is valid)
|
||||
try testing.expect(max >= cap.cols);
|
||||
|
||||
// Adjusting to maxCols should succeed with at least 1 row
|
||||
const adjusted = try cap.adjust(.{ .cols = max });
|
||||
try testing.expect(adjusted.rows >= 1);
|
||||
|
||||
// Adjusting to maxCols + 1 should fail
|
||||
try testing.expectError(
|
||||
error.OutOfMemory,
|
||||
cap.adjust(.{ .cols = max + 1 }),
|
||||
);
|
||||
}
|
||||
|
||||
test "Capacity maxCols preserves total size" {
|
||||
const cap = std_capacity;
|
||||
const original_size = Page.layout(cap).total_size;
|
||||
const max = cap.maxCols().?;
|
||||
const adjusted = try cap.adjust(.{ .cols = max });
|
||||
const adjusted_size = Page.layout(adjusted).total_size;
|
||||
try testing.expectEqual(original_size, adjusted_size);
|
||||
}
|
||||
|
||||
test "Capacity maxCols with 1 row exactly" {
|
||||
const cap = std_capacity;
|
||||
const max = cap.maxCols().?;
|
||||
const adjusted = try cap.adjust(.{ .cols = max });
|
||||
try testing.expectEqual(@as(size.CellCountInt, 1), adjusted.rows);
|
||||
}
|
||||
|
||||
test "Page init" {
|
||||
var page = try Page.init(.{
|
||||
.cols = 120,
|
||||
|
||||
@@ -1641,7 +1641,7 @@ pub fn Stream(comptime Handler: type) type {
|
||||
},
|
||||
},
|
||||
else => {
|
||||
log.warn("invalid set curor style command: {f}", .{input});
|
||||
log.warn("invalid set cursor style command: {f}", .{input});
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user