mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-14 18:01:58 +00:00
terminal: cut the fixed heap cost of a new terminal nearly in half (#14138)
This focuses explicitly on the non-mmap allocations for a `ghostty_terminal_new` result. The result is that we lower this portion of the memory by almost half. The total benefit is smaller since 70% of a terminal is mmap'd allocations, but this still yields an absolute ~5KB savings on macOS on every new terminal (not just empty, but also with a normal prompt and so on). Four changes to make it happy, broken down into individual commits. Nothing crazy: - **Page list nodes are pooled individually.** The node pool was a `std.heap.MemoryPool`, which sits on an arena that preheats and grows 1.5x, so we paid for wasted space. Nodes now come from `UntouchedPool` (the same pool as page buffers) with a preheat of one, so the cost is exactlyone node (well, exactly one bucket element size in whatever allocator). - **Pin pool and tracked pin set are sized for two pins.** Every screen tracks exactly a viewport pin and a cursor pin at creation, but we preheated eight pins and let the tracked pin map grow to 17 slots on the first insert via doubling. - **The kitty temp dir path is allocated only when set.** The C wrapper embedded a 1 KiB `max_path_bytes` buffer that only embedders that set `kitty_image_medium_temp_file` ever wrote to. - **The default palette is shared instead of copied.** `DynamicPalette` carried two full 1 KiB palettes, `current` and `original`, and `original` was almost always the built-in default. It is now a pointer to the shared built-in default, or to an allocator-owned copy when a custom default is set. This introduces a new OOM path but we gracefully handle it by either ignoring or resetting. Memory measurements: | Per terminal | Before | After | |---------------------------------------------|----------|----------| | phys_footprint delta, fresh | 30,066 B | 25,069 B | | phys_footprint delta, styled prompt written | 47,023 B | 41,944 B | | malloc zone bytes dirtied, fresh | 12,698 B | 7,782 B | | malloc blocks live after `terminal_new` | 11,904 B | 6,688 B | | malloc blocks live after the prompt | 12,320 B | 7,104 B | I ran `ghostty-bench +terminal-stream` on a 500 MB ascii corpus, main vs this branch interleaved, and there is no noticeable change. **AI usage:** Fable did validation of the work, I did the implementations, commit messages, and PR notes.
This commit is contained in:
@@ -32,14 +32,22 @@ const log = std.log.scoped(.page_list);
|
||||
const native_freestanding = builtin.os.tag == .freestanding and
|
||||
!builtin.target.cpu.arch.isWasm();
|
||||
|
||||
/// The number of PageList.Nodes we preheat the pool with. A node is
|
||||
/// a very small struct so we can afford to preheat many, but the exact
|
||||
/// number is uncertain. Any number too large is wasting memory, any number
|
||||
/// too small will cause the pool to have to allocate more memory later.
|
||||
/// This should be set to some reasonable minimum that we expect a terminal
|
||||
/// window to scroll into quickly.
|
||||
/// The number of pages we preheat the page pool with. For operating systems
|
||||
/// that support it, pages are demand-paged (see PagePool) so this only
|
||||
/// costs us address space. For other operating systems, we don't preheat.
|
||||
const page_preheat = 4;
|
||||
|
||||
/// The number of nodes we preheat the node pool with. Unlike pages, nodes
|
||||
/// are ordinary heap memory so every idle preheated node costs real
|
||||
/// memory. A new PageList needs exactly one node for its first page, so
|
||||
/// we preheat that one and let the pool grow on demand.
|
||||
const node_preheat = 1;
|
||||
|
||||
/// The number of pins we preheat the pin pool with: the viewport pin that
|
||||
/// every PageList tracks and the cursor pin that every Screen tracks.
|
||||
/// Selections, searches, and so on grow the pool on demand.
|
||||
const pin_preheat = 2;
|
||||
|
||||
/// The list of pages in the screen. These are expected to be in order
|
||||
/// where the first page is the topmost page (scrollback) and the last is
|
||||
/// the bottommost page (the current active page).
|
||||
@@ -299,7 +307,14 @@ const Node = struct {
|
||||
};
|
||||
|
||||
/// The memory pool we get page nodes from.
|
||||
const NodePool = std.heap.memory_pool.Managed(List.Node);
|
||||
///
|
||||
/// We don't use a std memory pool here because it is backed by an arena
|
||||
/// and we end up paying a lot of wasted memory for the growth factor when
|
||||
/// in practice we don't usually use many nodes.
|
||||
///
|
||||
/// We don't need the "untouched" property of our UntouchedPool but
|
||||
/// this gives us a GPA-allocated pool so we reuse it here.
|
||||
const NodePool = datastruct.UntouchedPool(List.Node, .of(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
|
||||
@@ -356,11 +371,11 @@ pub const MemoryPool = struct {
|
||||
page_alloc: Allocator,
|
||||
preheat: usize,
|
||||
) Allocator.Error!MemoryPool {
|
||||
var node_pool = try NodePool.initCapacity(gen_alloc, preheat);
|
||||
var node_pool = try NodePool.initCapacity(gen_alloc, gen_alloc, node_preheat);
|
||||
errdefer node_pool.deinit();
|
||||
var page_pool = try PagePool.initCapacity(gen_alloc, page_alloc, preheat);
|
||||
errdefer page_pool.deinit();
|
||||
var pin_pool = try PinPool.initCapacity(gen_alloc, 8);
|
||||
var pin_pool = try PinPool.initCapacity(gen_alloc, pin_preheat);
|
||||
errdefer pin_pool.deinit();
|
||||
return .{
|
||||
.alloc = gen_alloc,
|
||||
@@ -654,11 +669,10 @@ pub fn init(
|
||||
try tw.check(.viewport_pin);
|
||||
const viewport_pin = try pool.pins.create();
|
||||
viewport_pin.* = .{ .node = page_list.first.? };
|
||||
var tracked_pins: PinSet = .{};
|
||||
errdefer tracked_pins.deinit(pool.alloc);
|
||||
|
||||
try tw.check(.viewport_pin_track);
|
||||
try tracked_pins.putNoClobber(pool.alloc, viewport_pin, {});
|
||||
var tracked_pins = try initTrackedPins(pool.alloc, viewport_pin);
|
||||
errdefer tracked_pins.deinit(pool.alloc);
|
||||
|
||||
errdefer comptime unreachable;
|
||||
const result: PageList = .{
|
||||
@@ -680,6 +694,17 @@ pub fn init(
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Create the tracked pin set for a new PageList with the viewport pin
|
||||
/// already tracked. The set is sized for exactly the viewport pin and the
|
||||
/// cursor pin that every Screen tracks.
|
||||
fn initTrackedPins(alloc: Allocator, viewport_pin: *Pin) Allocator.Error!PinSet {
|
||||
var set: PinSet = .{};
|
||||
errdefer set.deinit(alloc);
|
||||
try set.entries.setCapacity(alloc, pin_preheat);
|
||||
set.putAssumeCapacityNoClobber(viewport_pin, {});
|
||||
return set;
|
||||
}
|
||||
|
||||
const initPages_tw = tripwire.module(enum {
|
||||
page_node,
|
||||
page_buf_std,
|
||||
@@ -899,18 +924,19 @@ fn verifyIntegrity(self: *const PageList) IntegrityError!void {
|
||||
|
||||
/// Release every page in the list during a teardown walk (deinit,
|
||||
/// reset, or an errdefer unwinding a partially built list): heap-owned
|
||||
/// pages go back to the page allocator and pool-owned pages back to the
|
||||
/// page pool. The nodes themselves are not touched; they live in the
|
||||
/// node pool.
|
||||
/// pages go back to the page allocator, pool-owned pages back to the
|
||||
/// page pool, and the nodes back to the node pool's free list.
|
||||
fn releasePages(pool: *MemoryPool, list: List) void {
|
||||
const page_alloc = pool.pages.allocator;
|
||||
var it = list.first;
|
||||
while (it) |node| : (it = node.next) {
|
||||
while (it) |node| {
|
||||
it = node.next;
|
||||
const page = node.restore(.discard);
|
||||
switch (node.owned) {
|
||||
.pool => releasePoolPage(pool, page),
|
||||
.heap => page_alloc.free(page.memory),
|
||||
}
|
||||
pool.nodes.destroy(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -943,8 +969,7 @@ pub fn deinit(self: *PageList) void {
|
||||
// Always deallocate our hashmap.
|
||||
self.tracked_pins.deinit(self.pool.alloc);
|
||||
|
||||
// Release every page. We don't need to deallocate the list or
|
||||
// nodes because they all reside in the pool.
|
||||
// Release every page and node back to the pools, then free the pools.
|
||||
releasePages(&self.pool, self.pages);
|
||||
self.pool.deinit();
|
||||
}
|
||||
@@ -1088,9 +1113,8 @@ pub fn clone(
|
||||
// Create our viewport. In a clone, the viewport always goes
|
||||
// to the top.
|
||||
const viewport_pin = try pool.pins.create();
|
||||
var tracked_pins: PinSet = .{};
|
||||
var tracked_pins = try initTrackedPins(pool.alloc, viewport_pin);
|
||||
errdefer tracked_pins.deinit(pool.alloc);
|
||||
try tracked_pins.putNoClobber(pool.alloc, viewport_pin, {});
|
||||
|
||||
// Our list of pages
|
||||
var page_list: List = .{};
|
||||
@@ -7550,9 +7574,8 @@ pub const Builder = struct {
|
||||
viewport_pin.* = active_top;
|
||||
|
||||
// Setup our one viewport tracked pin
|
||||
var tracked_pins: PinSet = .{};
|
||||
var tracked_pins = try initTrackedPins(self.pool.alloc, viewport_pin);
|
||||
errdefer tracked_pins.deinit(self.pool.alloc);
|
||||
try tracked_pins.putNoClobber(self.pool.alloc, viewport_pin, {});
|
||||
|
||||
// Initialize limits
|
||||
var limits: Limits = .init(self.options.cols, self.options.rows);
|
||||
|
||||
@@ -356,6 +356,7 @@ pub fn init(
|
||||
pub fn deinit(self: *Terminal, alloc: Allocator) void {
|
||||
self.tabstops.deinit(alloc);
|
||||
self.screens.deinit(alloc);
|
||||
self.colors.palette.deinit(alloc);
|
||||
self.pwd.deinit(alloc);
|
||||
self.title.deinit(alloc);
|
||||
self.glyph_glossary.deinit(alloc);
|
||||
|
||||
@@ -105,9 +105,10 @@ const TerminalWrapper = struct {
|
||||
/// created by `new` or transferred from snapshot decoding until `free`.
|
||||
/// Freestanding owners contain no native allocation and expose failing I/O.
|
||||
io: Io,
|
||||
/// We also need to store a temp dir path for some operations (e.g., kitty
|
||||
/// graphics). This provides stable storage for the API calls.
|
||||
tmp_dir_path: [max_path_bytes]u8,
|
||||
/// Allocator-owned copy of the temporary directory path for some
|
||||
/// operations (e.g. kitty graphics). This is only allocated once the
|
||||
/// embedder sets the option.
|
||||
tmp_dir_path: ?[]u8 = null,
|
||||
/// The terminfo name reported for XTGETTCAP "TN". The stream handler holds
|
||||
/// a slice into this.
|
||||
terminfo_name_buf: [Handler.max_terminfo_name_bytes]u8,
|
||||
@@ -728,7 +729,6 @@ fn wrap(
|
||||
wrapper.* = .{
|
||||
.terminal = t,
|
||||
.io = io,
|
||||
.tmp_dir_path = undefined,
|
||||
.terminfo_name_buf = undefined,
|
||||
.stream = Stream.init(.{
|
||||
.allocator = alloc,
|
||||
@@ -1363,8 +1363,9 @@ fn setTyped(
|
||||
},
|
||||
.color_palette => {
|
||||
wrapper.terminal.colors.palette.changeDefault(
|
||||
wrapper.terminal.gpa(),
|
||||
if (value) |v| color.paletteZval(v) else color.default,
|
||||
);
|
||||
) catch return .out_of_memory;
|
||||
wrapper.terminal.flags.dirty.palette = true;
|
||||
},
|
||||
.kitty_image_storage_limit => {
|
||||
@@ -1393,22 +1394,31 @@ fn setTyped(
|
||||
},
|
||||
.kitty_image_medium_temp_file => {
|
||||
if (comptime !build_options.kitty_graphics) return .success;
|
||||
const alloc = wrapper.terminal.gpa();
|
||||
if (value) |v| {
|
||||
if (v.len > wrapper.tmp_dir_path.len) return .out_of_memory;
|
||||
@memcpy(wrapper.tmp_dir_path[0..v.len], v.ptr[0..v.len]);
|
||||
if (v.len > max_path_bytes) return .out_of_memory;
|
||||
const path = alloc.dupe(u8, v.ptr[0..v.len]) catch
|
||||
return .out_of_memory;
|
||||
var it = wrapper.terminal.screens.all.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const screen = entry.value.*;
|
||||
screen.kitty_images.image_limits.temporary_file = .{
|
||||
.enabled = .{ .directory = wrapper.tmp_dir_path[0..v.len] },
|
||||
.enabled = .{ .directory = path },
|
||||
};
|
||||
}
|
||||
|
||||
// Every screen points at the new copy now so the previous
|
||||
// one can be released.
|
||||
if (wrapper.tmp_dir_path) |old| alloc.free(old);
|
||||
wrapper.tmp_dir_path = path;
|
||||
} else {
|
||||
var it = wrapper.terminal.screens.all.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const screen = entry.value.*;
|
||||
screen.kitty_images.image_limits.temporary_file = .disabled;
|
||||
}
|
||||
if (wrapper.tmp_dir_path) |old| alloc.free(old);
|
||||
wrapper.tmp_dir_path = null;
|
||||
}
|
||||
},
|
||||
.apc_max_bytes => {
|
||||
@@ -1727,7 +1737,7 @@ fn getTyped(
|
||||
.color_background_default => out.* = (t.colors.background.default orelse return .no_value).cval(),
|
||||
.color_cursor_default => out.* = (t.colors.cursor.default orelse return .no_value).cval(),
|
||||
.color_palette => out.* = color.paletteCval(&t.colors.palette.current),
|
||||
.color_palette_default => out.* = color.paletteCval(&t.colors.palette.original),
|
||||
.color_palette_default => out.* = color.paletteCval(t.colors.palette.original),
|
||||
.kitty_image_storage_limit => {
|
||||
if (comptime !build_options.kitty_graphics) return .no_value;
|
||||
out.* = @intCast(t.screens.active.kitty_images.total_limit);
|
||||
@@ -1861,6 +1871,7 @@ pub fn free(terminal_: Terminal) callconv(lib.calling_conv) void {
|
||||
wrapper.searches.deinit(alloc);
|
||||
wrapper.stream.deinit();
|
||||
t.deinit(alloc);
|
||||
if (wrapper.tmp_dir_path) |path| alloc.free(path);
|
||||
wrapper.io.deinit(alloc);
|
||||
alloc.destroy(t);
|
||||
alloc.destroy(wrapper);
|
||||
|
||||
@@ -2,6 +2,7 @@ const colorpkg = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const fraction = @import("fraction.zig");
|
||||
const x11_color = @import("x11_color.zig");
|
||||
@@ -322,22 +323,37 @@ pub const DynamicPalette = struct {
|
||||
/// The current palette including any user modifications.
|
||||
current: Palette,
|
||||
|
||||
/// The original/default palette values.
|
||||
original: Palette,
|
||||
/// The original/default palette values. This points at the shared
|
||||
/// built-in default palette unless a different default was given
|
||||
/// (see `changeDefault`), in which case it points at an
|
||||
/// allocator-owned copy that `deinit` frees. Sharing the built-in
|
||||
/// default saves a full palette copy for every terminal.
|
||||
original: *const Palette,
|
||||
|
||||
/// A bitset where each bit represents whether the corresponding
|
||||
/// palette index has been modified from its default value.
|
||||
mask: PaletteMask,
|
||||
|
||||
pub const default: DynamicPalette = .init(colorpkg.default);
|
||||
/// A dynamic palette using the built-in default palette. This owns
|
||||
/// no memory, but `deinit` is still safe to call on it.
|
||||
pub const default: DynamicPalette = .{
|
||||
.current = colorpkg.default,
|
||||
.original = &colorpkg.default,
|
||||
.mask = .initEmpty(),
|
||||
};
|
||||
|
||||
/// Initialize a dynamic palette with a default palette.
|
||||
pub fn init(def: Palette) DynamicPalette {
|
||||
return .{
|
||||
.current = def,
|
||||
.original = def,
|
||||
.mask = .initEmpty(),
|
||||
};
|
||||
/// Initialize a dynamic palette with a default palette. The result
|
||||
/// must be released with `deinit`. No memory is allocated if `def`
|
||||
/// is the built-in default palette.
|
||||
pub fn init(alloc: Allocator, def: Palette) Allocator.Error!DynamicPalette {
|
||||
var result: DynamicPalette = .default;
|
||||
try result.changeDefault(alloc, def);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *DynamicPalette, alloc: Allocator) void {
|
||||
if (self.ownedOriginal()) |owned| alloc.destroy(owned);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Set a custom color at the given palette index.
|
||||
@@ -354,16 +370,46 @@ pub const DynamicPalette = struct {
|
||||
|
||||
/// Reset all colors to their original values.
|
||||
pub fn resetAll(self: *DynamicPalette) void {
|
||||
self.* = .init(self.original);
|
||||
self.current = self.original.*;
|
||||
self.mask = .initEmpty();
|
||||
}
|
||||
|
||||
/// Change the default palette, but preserve the changed values.
|
||||
pub fn changeDefault(self: *DynamicPalette, def: Palette) void {
|
||||
self.original = def;
|
||||
///
|
||||
/// The built-in default palette is shared rather than copied, so
|
||||
/// changing back to it releases any owned copy (see `resetDefault`).
|
||||
/// Any other default is copied into memory owned by this palette,
|
||||
/// allocated on the first change and reused after that. On allocation
|
||||
/// failure nothing changes.
|
||||
pub fn changeDefault(
|
||||
self: *DynamicPalette,
|
||||
alloc: Allocator,
|
||||
def: Palette,
|
||||
) Allocator.Error!void {
|
||||
if (std.meta.eql(def, colorpkg.default)) return self.resetDefault(alloc);
|
||||
|
||||
const owned = self.ownedOriginal() orelse try alloc.create(Palette);
|
||||
owned.* = def;
|
||||
self.original = owned;
|
||||
self.applyDefault(def);
|
||||
}
|
||||
|
||||
/// Change the default palette back to the built-in default palette,
|
||||
/// preserving the changed values. This releases any owned copy and
|
||||
/// never allocates, so it is safe to use as the fallback when
|
||||
/// `changeDefault` fails.
|
||||
pub fn resetDefault(self: *DynamicPalette, alloc: Allocator) void {
|
||||
if (self.ownedOriginal()) |owned| alloc.destroy(owned);
|
||||
self.original = &colorpkg.default;
|
||||
self.applyDefault(colorpkg.default);
|
||||
}
|
||||
|
||||
/// Rebuild the current palette from a new default, preserving the
|
||||
/// changed values.
|
||||
fn applyDefault(self: *DynamicPalette, def: Palette) void {
|
||||
// Fast path, the palette is usually not changed.
|
||||
if (self.mask.count() == 0) {
|
||||
self.current = self.original;
|
||||
self.current = def;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -374,6 +420,16 @@ pub const DynamicPalette = struct {
|
||||
while (it.next()) |idx| current[idx] = self.current[idx];
|
||||
self.current = current;
|
||||
}
|
||||
|
||||
/// The allocator-owned copy of the original palette, or null if the
|
||||
/// original is the shared built-in default.
|
||||
fn ownedOriginal(self: *const DynamicPalette) ?*Palette {
|
||||
if (self.original == &colorpkg.default) return null;
|
||||
|
||||
// Only the shared built-in default is ever const; anything else
|
||||
// is a copy that we allocated ourselves.
|
||||
return @constCast(self.original);
|
||||
}
|
||||
};
|
||||
|
||||
/// RGB value that can be changed and reset. This can also be totally unset
|
||||
@@ -991,16 +1047,18 @@ test "RGB: encode" {
|
||||
test "DynamicPalette: init" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
var p: DynamicPalette = .default;
|
||||
defer p.deinit(testing.allocator);
|
||||
try testing.expectEqual(default, p.current);
|
||||
try testing.expectEqual(default, p.original);
|
||||
try testing.expectEqual(default, p.original.*);
|
||||
try testing.expectEqual(&default, p.original);
|
||||
try testing.expectEqual(@as(usize, 0), p.mask.count());
|
||||
}
|
||||
|
||||
test "DynamicPalette: set" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
var p: DynamicPalette = .default;
|
||||
const new_color = RGB{ .r = 255, .g = 0, .b = 0 };
|
||||
|
||||
p.set(0, new_color);
|
||||
@@ -1014,7 +1072,7 @@ test "DynamicPalette: set" {
|
||||
test "DynamicPalette: reset" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
var p: DynamicPalette = .default;
|
||||
const new_color = RGB{ .r = 255, .g = 0, .b = 0 };
|
||||
|
||||
p.set(0, new_color);
|
||||
@@ -1029,7 +1087,7 @@ test "DynamicPalette: reset" {
|
||||
test "DynamicPalette: resetAll" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
var p: DynamicPalette = .default;
|
||||
const new_color = RGB{ .r = 255, .g = 0, .b = 0 };
|
||||
|
||||
p.set(0, new_color);
|
||||
@@ -1039,27 +1097,79 @@ test "DynamicPalette: resetAll" {
|
||||
|
||||
p.resetAll();
|
||||
try testing.expectEqual(default, p.current);
|
||||
try testing.expectEqual(default, p.original);
|
||||
try testing.expectEqual(default, p.original.*);
|
||||
try testing.expectEqual(@as(usize, 0), p.mask.count());
|
||||
}
|
||||
|
||||
test "DynamicPalette: changeDefault with no changes" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
var p: DynamicPalette = .default;
|
||||
defer p.deinit(testing.allocator);
|
||||
var new_palette = default;
|
||||
new_palette[0] = RGB{ .r = 100, .g = 100, .b = 100 };
|
||||
|
||||
p.changeDefault(new_palette);
|
||||
try testing.expectEqual(new_palette, p.original);
|
||||
try p.changeDefault(testing.allocator, new_palette);
|
||||
try testing.expectEqual(new_palette, p.original.*);
|
||||
try testing.expectEqual(new_palette, p.current);
|
||||
try testing.expectEqual(@as(usize, 0), p.mask.count());
|
||||
}
|
||||
|
||||
test "DynamicPalette: changeDefault shares the built-in default" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var new_palette = default;
|
||||
new_palette[0] = RGB{ .r = 100, .g = 100, .b = 100 };
|
||||
|
||||
// A custom default is an owned copy.
|
||||
var p: DynamicPalette = try .init(alloc, new_palette);
|
||||
defer p.deinit(alloc);
|
||||
try testing.expect(p.original != &default);
|
||||
try testing.expectEqual(new_palette, p.original.*);
|
||||
|
||||
// Changing the custom default reuses the copy.
|
||||
const owned = p.original;
|
||||
new_palette[1] = RGB{ .r = 101, .g = 101, .b = 101 };
|
||||
try p.changeDefault(alloc, new_palette);
|
||||
try testing.expectEqual(owned, p.original);
|
||||
try testing.expectEqual(new_palette, p.original.*);
|
||||
|
||||
// Changing back to the built-in default releases the copy (the
|
||||
// testing allocator would report the leak) and shares it again.
|
||||
try p.changeDefault(alloc, default);
|
||||
try testing.expectEqual(&default, p.original);
|
||||
try testing.expectEqual(default, p.current);
|
||||
|
||||
// resetDefault does the same directly, preserving changed values,
|
||||
// and never allocates.
|
||||
try p.changeDefault(alloc, new_palette);
|
||||
p.set(2, RGB{ .r = 1, .g = 2, .b = 3 });
|
||||
var reset_failing: std.testing.FailingAllocator = .init(alloc, .{
|
||||
.fail_index = 0,
|
||||
});
|
||||
p.resetDefault(reset_failing.allocator());
|
||||
try testing.expect(!reset_failing.has_induced_failure);
|
||||
try testing.expectEqual(&default, p.original);
|
||||
try testing.expectEqual(default[0], p.current[0]);
|
||||
try testing.expectEqual(RGB{ .r = 1, .g = 2, .b = 3 }, p.current[2]);
|
||||
try testing.expect(p.mask.isSet(2));
|
||||
|
||||
// The built-in default never allocates.
|
||||
var failing: std.testing.FailingAllocator = .init(alloc, .{
|
||||
.fail_index = 0,
|
||||
});
|
||||
var q: DynamicPalette = try .init(failing.allocator(), default);
|
||||
defer q.deinit(failing.allocator());
|
||||
try testing.expectEqual(&default, q.original);
|
||||
try testing.expect(!failing.has_induced_failure);
|
||||
}
|
||||
|
||||
test "DynamicPalette: changeDefault preserves changes" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
var p: DynamicPalette = .default;
|
||||
defer p.deinit(testing.allocator);
|
||||
const custom_color = RGB{ .r = 255, .g = 0, .b = 0 };
|
||||
|
||||
p.set(5, custom_color);
|
||||
@@ -1069,9 +1179,9 @@ test "DynamicPalette: changeDefault preserves changes" {
|
||||
new_palette[0] = RGB{ .r = 100, .g = 100, .b = 100 };
|
||||
new_palette[5] = RGB{ .r = 50, .g = 50, .b = 50 };
|
||||
|
||||
p.changeDefault(new_palette);
|
||||
try p.changeDefault(testing.allocator, new_palette);
|
||||
|
||||
try testing.expectEqual(new_palette, p.original);
|
||||
try testing.expectEqual(new_palette, p.original.*);
|
||||
try testing.expectEqual(new_palette[0], p.current[0]);
|
||||
try testing.expectEqual(custom_color, p.current[5]);
|
||||
try testing.expect(p.mask.isSet(5));
|
||||
@@ -1081,7 +1191,8 @@ test "DynamicPalette: changeDefault preserves changes" {
|
||||
test "DynamicPalette: changeDefault with multiple changes" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
var p: DynamicPalette = .default;
|
||||
defer p.deinit(testing.allocator);
|
||||
const red = RGB{ .r = 255, .g = 0, .b = 0 };
|
||||
const green = RGB{ .r = 0, .g = 255, .b = 0 };
|
||||
const blue = RGB{ .r = 0, .g = 0, .b = 255 };
|
||||
@@ -1094,7 +1205,7 @@ test "DynamicPalette: changeDefault with multiple changes" {
|
||||
new_palette[0] = RGB{ .r = 50, .g = 50, .b = 50 };
|
||||
new_palette[1] = RGB{ .r = 60, .g = 60, .b = 60 };
|
||||
|
||||
p.changeDefault(new_palette);
|
||||
try p.changeDefault(testing.allocator, new_palette);
|
||||
|
||||
try testing.expectEqual(new_palette[0], p.current[0]);
|
||||
try testing.expectEqual(red, p.current[1]);
|
||||
|
||||
@@ -761,7 +761,7 @@ pub const Header = struct {
|
||||
|
||||
/// Allocator-owned semantic state decoded from one TERMINAL payload.
|
||||
///
|
||||
/// The palette and fixed header are values. `tabstops`, `pwd`, and `title`
|
||||
/// The fixed header is a value. `tabstops`, `palette`, `pwd`, and `title`
|
||||
/// own allocations and are released by `deinit`.
|
||||
const DecodedPayload = struct {
|
||||
header: Header,
|
||||
@@ -772,6 +772,7 @@ const DecodedPayload = struct {
|
||||
|
||||
fn deinit(self: *DecodedPayload, alloc: Allocator) void {
|
||||
self.tabstops.deinit(alloc);
|
||||
self.palette.deinit(alloc);
|
||||
alloc.free(self.pwd);
|
||||
alloc.free(self.title);
|
||||
self.* = undefined;
|
||||
@@ -874,7 +875,8 @@ fn decodePayload(
|
||||
var override_mask: [32]u8 = undefined;
|
||||
try reader.readSliceAll(&override_mask);
|
||||
|
||||
var palette: terminal_color.DynamicPalette = .init(original);
|
||||
var palette: terminal_color.DynamicPalette = try .init(alloc, original);
|
||||
errdefer palette.deinit(alloc);
|
||||
for (0..256) |index| {
|
||||
const mask = @as(u8, 1) << @intCast(index % 8);
|
||||
if (override_mask[index / 8] & mask != 0) {
|
||||
@@ -992,6 +994,10 @@ pub fn decode(
|
||||
});
|
||||
errdefer result.deinit(alloc);
|
||||
|
||||
// The terminal owns the decoded palette now, so the payload must not
|
||||
// release it.
|
||||
payload.palette = .default;
|
||||
|
||||
// Recreate the optional screen now so ScreenSet routing is complete before
|
||||
// its empty screens are replaced by the following SCREEN records.
|
||||
if (header.screen_count == 2) {
|
||||
@@ -1445,9 +1451,7 @@ test "TERMINAL payload round trip" {
|
||||
tabstops.set(test_header.columns - 1);
|
||||
|
||||
// Use overrides at both ends of the palette to make ordering explicit.
|
||||
var palette: terminal_color.DynamicPalette = .init(
|
||||
terminal_color.default,
|
||||
);
|
||||
var palette: terminal_color.DynamicPalette = .default;
|
||||
palette.set(0, .{ .r = 1, .g = 2, .b = 3 });
|
||||
palette.set(255, .{ .r = 4, .g = 5, .b = 6 });
|
||||
|
||||
@@ -1512,9 +1516,7 @@ test "TERMINAL payload rejects noncanonical state" {
|
||||
0,
|
||||
);
|
||||
defer tabstops.deinit(testing.allocator);
|
||||
var palette: terminal_color.DynamicPalette = .init(
|
||||
terminal_color.default,
|
||||
);
|
||||
var palette: terminal_color.DynamicPalette = .default;
|
||||
var encoded: [2048]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&encoded);
|
||||
try testing.expectError(
|
||||
@@ -1557,9 +1559,7 @@ test "TERMINAL payload ignores tab-stop padding" {
|
||||
0,
|
||||
);
|
||||
defer tabstops.deinit(testing.allocator);
|
||||
var palette: terminal_color.DynamicPalette = .init(
|
||||
terminal_color.default,
|
||||
);
|
||||
var palette: terminal_color.DynamicPalette = .default;
|
||||
palette.set(1, .{ .r = 1, .g = 2, .b = 3 });
|
||||
|
||||
var destination: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
|
||||
@@ -270,7 +270,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
|
||||
const rgb = color.toTerminalRGB() orelse break :cursor .unset;
|
||||
break :cursor .init(rgb);
|
||||
},
|
||||
.palette = .init(opts.config.palette),
|
||||
.palette = .default,
|
||||
},
|
||||
.kitty_image_storage_limit = opts.config.image_storage_limit,
|
||||
.kitty_image_loading_limits = .allWithTempDir(global.tmpDirPath()),
|
||||
@@ -278,6 +278,10 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
|
||||
});
|
||||
errdefer term.deinit(alloc);
|
||||
|
||||
// The default palette may be an allocator-owned copy, so it is set
|
||||
// once the terminal owns its memory and can release it on deinit.
|
||||
try term.colors.palette.changeDefault(alloc, opts.config.palette);
|
||||
|
||||
// Setup our terminal size in pixels for certain requests.
|
||||
term.width_px = term.cols * opts.size.cell.width;
|
||||
term.height_px = term.rows * opts.size.cell.height;
|
||||
@@ -463,8 +467,16 @@ pub fn changeConfig(self: *Termio, td: *ThreadData, config: *DerivedConfig) !voi
|
||||
// - command, working-directory: we never restart the underlying
|
||||
// process so we don't care or need to know about these.
|
||||
|
||||
// Update the default palette.
|
||||
self.terminal.colors.palette.changeDefault(config.palette);
|
||||
// Update the default palette. A config change must not fail here, so
|
||||
// if we can't allocate the copy of the configured palette we fall back
|
||||
// to the built-in default, which never allocates.
|
||||
self.terminal.colors.palette.changeDefault(
|
||||
self.alloc,
|
||||
config.palette,
|
||||
) catch |err| {
|
||||
log.warn("error changing default palette, using built-in default err={}", .{err});
|
||||
self.terminal.colors.palette.resetDefault(self.alloc);
|
||||
};
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
|
||||
// Update all our other colors
|
||||
|
||||
Reference in New Issue
Block a user