mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-27 19:16:27 +00:00
Merge remote-tracking branch 'origin/main' into shaping-positions
This commit is contained in:
@@ -5183,7 +5183,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
.prompt_surface_title => return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.prompt_title,
|
||||
{},
|
||||
.surface,
|
||||
),
|
||||
|
||||
.prompt_tab_title => return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.prompt_title,
|
||||
.tab,
|
||||
),
|
||||
|
||||
.clear_screen => {
|
||||
|
||||
@@ -189,8 +189,9 @@ pub const Action = union(Key) {
|
||||
set_title: SetTitle,
|
||||
|
||||
/// Set the title of the target to a prompted value. It is up to
|
||||
/// the apprt to prompt.
|
||||
prompt_title,
|
||||
/// the apprt to prompt. The value specifies whether to prompt for the
|
||||
/// surface title or the tab title.
|
||||
prompt_title: PromptTitle,
|
||||
|
||||
/// The current working directory has changed for the target terminal.
|
||||
pwd: Pwd,
|
||||
@@ -536,6 +537,12 @@ pub const MouseVisibility = enum(c_int) {
|
||||
hidden,
|
||||
};
|
||||
|
||||
/// Whether to prompt for the surface title or tab title.
|
||||
pub const PromptTitle = enum(c_int) {
|
||||
surface,
|
||||
tab,
|
||||
};
|
||||
|
||||
pub const MouseOverLink = struct {
|
||||
url: [:0]const u8,
|
||||
|
||||
|
||||
@@ -693,7 +693,7 @@ pub const Application = extern struct {
|
||||
|
||||
.progress_report => return Action.progressReport(target, value),
|
||||
|
||||
.prompt_title => return Action.promptTitle(target),
|
||||
.prompt_title => return Action.promptTitle(target, value),
|
||||
|
||||
.quit => self.quit(),
|
||||
|
||||
@@ -2250,12 +2250,18 @@ const Action = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn promptTitle(target: apprt.Target) bool {
|
||||
switch (target) {
|
||||
.app => return false,
|
||||
.surface => |v| {
|
||||
v.rt_surface.surface.promptTitle();
|
||||
return true;
|
||||
pub fn promptTitle(target: apprt.Target, value: apprt.action.PromptTitle) bool {
|
||||
switch (value) {
|
||||
.surface => switch (target) {
|
||||
.app => return false,
|
||||
.surface => |v| {
|
||||
v.rt_surface.surface.promptTitle();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
.tab => {
|
||||
// GTK does not yet support tab title prompting
|
||||
return false;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,6 +519,11 @@ pub const Action = union(enum) {
|
||||
/// version can be found by running `ghostty +version`.
|
||||
prompt_surface_title,
|
||||
|
||||
/// Change the title of the current tab/window via a pop-up prompt. The
|
||||
/// title set via this prompt overrides any title set by the terminal
|
||||
/// and persists across focus changes within the tab.
|
||||
prompt_tab_title,
|
||||
|
||||
/// Create a new split in the specified direction.
|
||||
///
|
||||
/// Valid arguments:
|
||||
@@ -1191,6 +1196,7 @@ pub const Action = union(enum) {
|
||||
.reset_font_size,
|
||||
.set_font_size,
|
||||
.prompt_surface_title,
|
||||
.prompt_tab_title,
|
||||
.clear_screen,
|
||||
.select_all,
|
||||
.scroll_to_top,
|
||||
|
||||
@@ -413,10 +413,16 @@ fn actionCommands(action: Action.Key) []const Command {
|
||||
|
||||
.prompt_surface_title => comptime &.{.{
|
||||
.action = .prompt_surface_title,
|
||||
.title = "Change Title...",
|
||||
.title = "Change Terminal Title...",
|
||||
.description = "Prompt for a new title for the current terminal.",
|
||||
}},
|
||||
|
||||
.prompt_tab_title => comptime &.{.{
|
||||
.action = .prompt_tab_title,
|
||||
.title = "Change Tab Title...",
|
||||
.description = "Prompt for a new title for the current tab.",
|
||||
}},
|
||||
|
||||
.new_split => comptime &.{
|
||||
.{
|
||||
.action = .{ .new_split = .left },
|
||||
|
||||
@@ -1220,7 +1220,7 @@ const ReflowCursor = struct {
|
||||
// with graphemes then we increase capacity.
|
||||
if (self.page.graphemeCount() >= self.page.graphemeCapacity()) {
|
||||
try self.adjustCapacity(list, .{
|
||||
.hyperlink_bytes = cap.grapheme_bytes * 2,
|
||||
.grapheme_bytes = cap.grapheme_bytes * 2,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10758,3 +10758,86 @@ test "PageList clears history" {
|
||||
.x = 0,
|
||||
}, s.getTopLeft(.active));
|
||||
}
|
||||
|
||||
test "PageList resize reflow grapheme map capacity exceeded" {
|
||||
// This test verifies that when reflowing content with many graphemes,
|
||||
// the grapheme map capacity is correctly increased when needed.
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 4, 10, 0);
|
||||
defer s.deinit();
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
|
||||
// Get the grapheme capacity from the page. We need more than this many
|
||||
// graphemes in a single destination page to trigger capacity increase
|
||||
// during reflow. Since each source page can only hold this many graphemes,
|
||||
// we create two source pages with graphemes that will merge into one
|
||||
// destination page.
|
||||
const grapheme_capacity = s.pages.first.?.data.graphemeCapacity();
|
||||
// Use slightly more than half the capacity per page, so combined they
|
||||
// exceed the capacity of a single destination page.
|
||||
const graphemes_per_page = grapheme_capacity / 2 + grapheme_capacity / 4;
|
||||
|
||||
// Grow to the capacity of the first page and add more rows
|
||||
// so that we have two pages total.
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
page.pauseIntegrityChecks(true);
|
||||
for (page.size.rows..page.capacity.rows) |_| {
|
||||
_ = try s.grow();
|
||||
}
|
||||
page.pauseIntegrityChecks(false);
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
try s.growRows(graphemes_per_page);
|
||||
try testing.expectEqual(@as(usize, 2), s.totalPages());
|
||||
|
||||
// We now have two pages.
|
||||
try testing.expect(s.pages.first.? != s.pages.last.?);
|
||||
try testing.expectEqual(s.pages.last.?, s.pages.first.?.next);
|
||||
}
|
||||
|
||||
// Add graphemes to both pages. We add graphemes to rows at the END of the
|
||||
// first page, and graphemes to rows at the START of the second page.
|
||||
// When reflowing to 2 columns, these rows will wrap and stay together
|
||||
// on the same destination page, requiring capacity increase.
|
||||
|
||||
// Add graphemes to the end of the first page (last rows)
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
const start_row = page.size.rows - graphemes_per_page;
|
||||
for (0..graphemes_per_page) |i| {
|
||||
const y = start_row + i;
|
||||
const rac = page.getRowAndCell(0, y);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 'A' },
|
||||
};
|
||||
try page.appendGrapheme(rac.row, rac.cell, @as(u21, @intCast(0x0301)));
|
||||
}
|
||||
}
|
||||
|
||||
// Add graphemes to the beginning of the second page
|
||||
{
|
||||
const page = &s.pages.last.?.data;
|
||||
const count = @min(graphemes_per_page, page.size.rows);
|
||||
for (0..count) |y| {
|
||||
const rac = page.getRowAndCell(0, y);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 'B' },
|
||||
};
|
||||
try page.appendGrapheme(rac.row, rac.cell, @as(u21, @intCast(0x0302)));
|
||||
}
|
||||
}
|
||||
|
||||
// Resize to fewer columns to trigger reflow.
|
||||
// The graphemes from both pages will be copied to destination pages.
|
||||
// They will all end up in a contiguous region of the destination.
|
||||
// If the bug exists (hyperlink_bytes increased instead of grapheme_bytes),
|
||||
// this will fail with GraphemeMapOutOfMemory when we exceed capacity.
|
||||
try s.resize(.{ .cols = 2, .reflow = true });
|
||||
|
||||
// Verify the resize succeeded
|
||||
try testing.expectEqual(@as(usize, 2), s.cols);
|
||||
}
|
||||
|
||||
@@ -855,13 +855,17 @@ fn HashMapUnmanaged(
|
||||
pub fn layoutForCapacity(new_capacity: Size) Layout {
|
||||
assert(new_capacity == 0 or std.math.isPowerOfTwo(new_capacity));
|
||||
|
||||
// Cast to usize to prevent overflow in size calculations.
|
||||
// See: https://github.com/ziglang/zig/pull/19048
|
||||
const cap: usize = new_capacity;
|
||||
|
||||
// Pack our metadata, keys, and values.
|
||||
const meta_start = @sizeOf(Header);
|
||||
const meta_end = @sizeOf(Header) + new_capacity * @sizeOf(Metadata);
|
||||
const meta_end = @sizeOf(Header) + cap * @sizeOf(Metadata);
|
||||
const keys_start = std.mem.alignForward(usize, meta_end, key_align);
|
||||
const keys_end = keys_start + new_capacity * @sizeOf(K);
|
||||
const keys_end = keys_start + cap * @sizeOf(K);
|
||||
const vals_start = std.mem.alignForward(usize, keys_end, val_align);
|
||||
const vals_end = vals_start + new_capacity * @sizeOf(V);
|
||||
const vals_end = vals_start + cap * @sizeOf(V);
|
||||
|
||||
// Our total memory size required is the end of our values
|
||||
// aligned to the base required alignment.
|
||||
@@ -1511,3 +1515,26 @@ test "OffsetHashMap remake map" {
|
||||
try expectEqual(5, map.get(5).?);
|
||||
}
|
||||
}
|
||||
|
||||
test "layoutForCapacity no overflow for large capacity" {
|
||||
// Test that layoutForCapacity correctly handles large capacities without overflow.
|
||||
// Prior to the fix, new_capacity (u32) was multiplied before widening to usize,
|
||||
// causing overflow when new_capacity * @sizeOf(K) exceeded 2^32.
|
||||
// See: https://github.com/ghostty-org/ghostty/issues/9862
|
||||
const Map = AutoHashMapUnmanaged(u64, u64);
|
||||
|
||||
// Use 2^30 capacity - this would overflow in u32 when multiplied by @sizeOf(u64)=8
|
||||
// 0x40000000 * 8 = 0x2_0000_0000 which wraps to 0 in u32
|
||||
const large_cap: Map.Size = 1 << 30;
|
||||
const layout = Map.layoutForCapacity(large_cap);
|
||||
|
||||
// With the fix, total_size should be at least cap * (sizeof(K) + sizeof(V))
|
||||
// = 2^30 * 16 = 2^34 bytes = 16 GiB
|
||||
// Without the fix, this would wrap and produce a much smaller value.
|
||||
const min_expected: usize = @as(usize, large_cap) * (@sizeOf(u64) + @sizeOf(u64));
|
||||
try expect(layout.total_size >= min_expected);
|
||||
|
||||
// Also verify the individual offsets don't wrap
|
||||
try expect(layout.keys_start > 0);
|
||||
try expect(layout.vals_start > layout.keys_start);
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ fn setupBash(
|
||||
resource_dir: []const u8,
|
||||
env: *EnvMap,
|
||||
) !?config.Command {
|
||||
var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 3);
|
||||
var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 2);
|
||||
defer args.deinit(alloc);
|
||||
|
||||
// Iterator that yields each argument in the original command line.
|
||||
@@ -273,11 +273,6 @@ fn setupBash(
|
||||
} else return null;
|
||||
try args.append(alloc, "--posix");
|
||||
|
||||
// On macOS, we request a login shell to match that platform's norms.
|
||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
||||
try args.append(alloc, "--login");
|
||||
}
|
||||
|
||||
// Stores the list of intercepted command line flags that will be passed
|
||||
// to our shell integration script: --norc --noprofile
|
||||
// We always include at least "1" so the script can differentiate between
|
||||
@@ -357,9 +352,8 @@ fn setupBash(
|
||||
);
|
||||
try env.put("ENV", integ_dir);
|
||||
|
||||
// Since we built up a command line, we don't need to wrap it in
|
||||
// ANOTHER shell anymore and can do a direct command.
|
||||
return .{ .direct = try args.toOwnedSlice(alloc) };
|
||||
// Join the accumulated arguments to form the final command string.
|
||||
return .{ .shell = try std.mem.joinZ(alloc, " ", args.items) };
|
||||
}
|
||||
|
||||
test "bash" {
|
||||
@@ -373,12 +367,7 @@ test "bash" {
|
||||
|
||||
const command = try setupBash(alloc, .{ .shell = "bash" }, ".", &env);
|
||||
|
||||
try testing.expect(command.?.direct.len >= 2);
|
||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
||||
}
|
||||
try testing.expectEqualStrings("bash --posix", command.?.shell);
|
||||
try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?);
|
||||
try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_INJECT").?);
|
||||
}
|
||||
@@ -421,12 +410,7 @@ test "bash: inject flags" {
|
||||
|
||||
const command = try setupBash(alloc, .{ .shell = "bash --norc" }, ".", &env);
|
||||
|
||||
try testing.expect(command.?.direct.len >= 2);
|
||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
||||
}
|
||||
try testing.expectEqualStrings("bash --posix", command.?.shell);
|
||||
try testing.expectEqualStrings("1 --norc", env.get("GHOSTTY_BASH_INJECT").?);
|
||||
}
|
||||
|
||||
@@ -437,12 +421,7 @@ test "bash: inject flags" {
|
||||
|
||||
const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, ".", &env);
|
||||
|
||||
try testing.expect(command.?.direct.len >= 2);
|
||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
||||
}
|
||||
try testing.expectEqualStrings("bash --posix", command.?.shell);
|
||||
try testing.expectEqualStrings("1 --noprofile", env.get("GHOSTTY_BASH_INJECT").?);
|
||||
}
|
||||
}
|
||||
@@ -459,24 +438,14 @@ test "bash: rcfile" {
|
||||
// bash --rcfile
|
||||
{
|
||||
const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, ".", &env);
|
||||
try testing.expect(command.?.direct.len >= 2);
|
||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
||||
}
|
||||
try testing.expectEqualStrings("bash --posix", command.?.shell);
|
||||
try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?);
|
||||
}
|
||||
|
||||
// bash --init-file
|
||||
{
|
||||
const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, ".", &env);
|
||||
try testing.expect(command.?.direct.len >= 2);
|
||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
||||
}
|
||||
try testing.expectEqualStrings("bash --posix", command.?.shell);
|
||||
try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?);
|
||||
}
|
||||
}
|
||||
@@ -538,35 +507,13 @@ test "bash: additional arguments" {
|
||||
// "-" argument separator
|
||||
{
|
||||
const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, ".", &env);
|
||||
try testing.expect(command.?.direct.len >= 6);
|
||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
||||
}
|
||||
|
||||
const offset = if (comptime builtin.target.os.tag.isDarwin()) 3 else 2;
|
||||
try testing.expectEqualStrings("-", command.?.direct[offset + 0]);
|
||||
try testing.expectEqualStrings("--arg", command.?.direct[offset + 1]);
|
||||
try testing.expectEqualStrings("file1", command.?.direct[offset + 2]);
|
||||
try testing.expectEqualStrings("file2", command.?.direct[offset + 3]);
|
||||
try testing.expectEqualStrings("bash --posix - --arg file1 file2", command.?.shell);
|
||||
}
|
||||
|
||||
// "--" argument separator
|
||||
{
|
||||
const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, ".", &env);
|
||||
try testing.expect(command.?.direct.len >= 6);
|
||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
||||
}
|
||||
|
||||
const offset = if (comptime builtin.target.os.tag.isDarwin()) 3 else 2;
|
||||
try testing.expectEqualStrings("--", command.?.direct[offset + 0]);
|
||||
try testing.expectEqualStrings("--arg", command.?.direct[offset + 1]);
|
||||
try testing.expectEqualStrings("file1", command.?.direct[offset + 2]);
|
||||
try testing.expectEqualStrings("file2", command.?.direct[offset + 3]);
|
||||
try testing.expectEqualStrings("bash --posix -- --arg file1 file2", command.?.shell);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user