From c32f7adb10481929b4785b0231064ac1cc0d0480 Mon Sep 17 00:00:00 2001 From: Bartosz Sokorski Date: Fri, 4 Jul 2025 00:26:43 +0200 Subject: [PATCH 01/36] Add linux kernel information to +version --- src/cli/version.zig | 41 ++++++++++++++++++++++------------------- src/os/kernel_info.zig | 27 +++++++++++++++++++++++++++ src/os/main.zig | 2 ++ 3 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 src/os/kernel_info.zig diff --git a/src/cli/version.zig b/src/cli/version.zig index a27d1050d..22608fa88 100644 --- a/src/cli/version.zig +++ b/src/cli/version.zig @@ -15,8 +15,6 @@ pub const Options = struct {}; /// The `version` command is used to display information about Ghostty. Recognized as /// either `+version` or `--version`. pub fn run(alloc: Allocator) !u8 { - _ = alloc; - const stdout = std.io.getStdOut().writer(); const tty = std.io.getStdOut().isTty(); @@ -34,32 +32,37 @@ pub fn run(alloc: Allocator) !u8 { try stdout.print(" - channel: {s}\n", .{@tagName(build_config.release_channel)}); try stdout.print("Build Config\n", .{}); - try stdout.print(" - Zig version: {s}\n", .{builtin.zig_version_string}); - try stdout.print(" - build mode : {}\n", .{builtin.mode}); - try stdout.print(" - app runtime: {}\n", .{build_config.app_runtime}); - try stdout.print(" - font engine: {}\n", .{build_config.font_backend}); - try stdout.print(" - renderer : {}\n", .{renderer.Renderer}); - try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)}); + try stdout.print(" - Zig version : {s}\n", .{builtin.zig_version_string}); + try stdout.print(" - build mode : {}\n", .{builtin.mode}); + try stdout.print(" - app runtime : {}\n", .{build_config.app_runtime}); + try stdout.print(" - font engine : {}\n", .{build_config.font_backend}); + try stdout.print(" - renderer : {}\n", .{renderer.Renderer}); + try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)}); if (comptime build_config.app_runtime == .gtk) { - try stdout.print(" - desktop env: {s}\n", .{@tagName(internal_os.desktopEnvironment())}); - try stdout.print(" - GTK version:\n", .{}); - try stdout.print(" build : {}\n", .{gtk_version.comptime_version}); - try stdout.print(" runtime : {}\n", .{gtk_version.getRuntimeVersion()}); - try stdout.print(" - libadwaita : enabled\n", .{}); - try stdout.print(" build : {}\n", .{adw_version.comptime_version}); - try stdout.print(" runtime : {}\n", .{adw_version.getRuntimeVersion()}); + if (comptime builtin.os.tag == .linux) { + const kernel_info = internal_os.getKernelInfo(alloc); + defer if (kernel_info) |k| alloc.free(k); + try stdout.print(" - kernel version: {s}\n", .{kernel_info orelse "Kernel information unavailable"}); + } + try stdout.print(" - desktop env : {s}\n", .{@tagName(internal_os.desktopEnvironment())}); + try stdout.print(" - GTK version :\n", .{}); + try stdout.print(" build : {}\n", .{gtk_version.comptime_version}); + try stdout.print(" runtime : {}\n", .{gtk_version.getRuntimeVersion()}); + try stdout.print(" - libadwaita : enabled\n", .{}); + try stdout.print(" build : {}\n", .{adw_version.comptime_version}); + try stdout.print(" runtime : {}\n", .{adw_version.getRuntimeVersion()}); if (comptime build_options.x11) { - try stdout.print(" - libX11 : enabled\n", .{}); + try stdout.print(" - libX11 : enabled\n", .{}); } else { - try stdout.print(" - libX11 : disabled\n", .{}); + try stdout.print(" - libX11 : disabled\n", .{}); } // We say `libwayland` since it is possible to build Ghostty without // Wayland integration but with Wayland-enabled GTK if (comptime build_options.wayland) { - try stdout.print(" - libwayland : enabled\n", .{}); + try stdout.print(" - libwayland : enabled\n", .{}); } else { - try stdout.print(" - libwayland : disabled\n", .{}); + try stdout.print(" - libwayland : disabled\n", .{}); } } return 0; diff --git a/src/os/kernel_info.zig b/src/os/kernel_info.zig new file mode 100644 index 000000000..9e3933dde --- /dev/null +++ b/src/os/kernel_info.zig @@ -0,0 +1,27 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +pub fn getKernelInfo(alloc: std.mem.Allocator) ?[]const u8 { + if (comptime builtin.os.tag != .linux) return null; + const path = "/proc/sys/kernel/osrelease"; + var file = std.fs.openFileAbsolute(path, .{}) catch return null; + defer file.close(); + + // 128 bytes should be enough to hold the kernel information + const kernel_info = file.readToEndAlloc(alloc, 128) catch return null; + defer alloc.free(kernel_info); + return alloc.dupe(u8, std.mem.trim(u8, kernel_info, &std.ascii.whitespace)) catch return null; +} + +test "read /proc/sys/kernel/osrelease" { + if (comptime builtin.os.tag != .linux) return null; + const allocator = std.testing.allocator; + + const kernel_info = try getKernelInfo(allocator); + defer allocator.free(kernel_info); + + // Since we can't hardcode the info in tests, just check + // if something was read from the file + try std.testing.expect(kernel_info.len > 0); + try std.testing.expect(!std.mem.eql(u8, kernel_info, "")); +} diff --git a/src/os/main.zig b/src/os/main.zig index 906e3d150..7398fc779 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -14,6 +14,7 @@ const openpkg = @import("open.zig"); const pipepkg = @import("pipe.zig"); const resourcesdir = @import("resourcesdir.zig"); const systemd = @import("systemd.zig"); +const kernelInfo = @import("kernel_info.zig"); // Namespaces pub const args = @import("args.zig"); @@ -58,6 +59,7 @@ pub const pipe = pipepkg.pipe; pub const resourcesDir = resourcesdir.resourcesDir; pub const ResourcesDir = resourcesdir.ResourcesDir; pub const ShellEscapeWriter = shell.ShellEscapeWriter; +pub const getKernelInfo = kernelInfo.getKernelInfo; test { _ = i18n; From 4b6c461d6fc47699e50a56f2612a8b0a98634bf2 Mon Sep 17 00:00:00 2001 From: trag1c Date: Sun, 6 Jul 2025 01:59:06 +0200 Subject: [PATCH 02/36] refactor nerd font codegen script --- src/font/nerd_font_attributes.zig | 2 +- src/font/nerd_font_codegen.py | 226 +++++++++++++++--------------- 2 files changed, 115 insertions(+), 113 deletions(-) diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 1465a8466..dfb11c5a5 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -1,4 +1,4 @@ -//! This is a generate file, produced by nerd_font_codegen.py +//! This is a generated file, produced by nerd_font_codegen.py //! DO NOT EDIT BY HAND! //! //! This file provides info extracted from the nerd fonts patcher script, diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index c2dd7314f..99915c9f2 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -1,102 +1,122 @@ """ -This file is mostly vibe coded because I don't like Python. It extracts the -patch sets from the nerd fonts font patcher file in order to extract scaling -rules and attributes for different codepoint ranges which it then codegens -in to a Zig file with a function that switches over codepoints and returns -the attributes and scaling rules. +This file extracts the patch sets from the nerd fonts font patcher file in order to +extract scaling rules and attributes for different codepoint ranges which it then +codegens in to a Zig file with a function that switches over codepoints and returns the +attributes and scaling rules. -This does include an `eval` call! This is spooky, but we trust -the nerd fonts code to be safe and not malicious or anything. +This does include an `eval` call! This is spooky, but we trust the nerd fonts code to +be safe and not malicious or anything. """ import ast import math -from pathlib import Path from collections import defaultdict +from contextlib import suppress +from pathlib import Path +from types import SimpleNamespace +from typing import Literal, TypedDict, cast + +type PatchSetAttributes = dict[Literal["default"] | int, PatchSetAttributeEntry] +type AttributeHash = tuple[str | None, str | None, str, float, float, float] +type ResolvedSymbol = PatchSetAttributes | PatchSetScaleRules | int | None + + +class PatchSetScaleRules(TypedDict): + ShiftMode: str + ScaleGroups: list[list[int] | range] + + +class PatchSetAttributeEntry(TypedDict): + align: str + valign: str + stretch: str + params: dict[str, float | bool] + + +class PatchSet(TypedDict): + SymStart: int + SymEnd: int + SrcStart: int | None + ScaleRules: PatchSetScaleRules | None + Attributes: PatchSetAttributes class PatchSetExtractor(ast.NodeVisitor): - def __init__(self): - self.symbol_table = {} - self.patch_set_values = [] + def __init__(self) -> None: + self.symbol_table: dict[str, ast.expr] = {} + self.patch_set_values: list[PatchSet] = [] - def visit_ClassDef(self, node): - if node.name == "font_patcher": - for item in node.body: - if isinstance(item, ast.FunctionDef) and item.name == "setup_patch_set": - self.visit_setup_patch_set(item) + def visit_ClassDef(self, node: ast.ClassDef) -> None: + if node.name != "font_patcher": + return + for item in node.body: + if isinstance(item, ast.FunctionDef) and item.name == "setup_patch_set": + self.visit_setup_patch_set(item) - def visit_setup_patch_set(self, node): + def visit_setup_patch_set(self, node: ast.FunctionDef) -> None: # First pass: gather variable assignments for stmt in node.body: - if isinstance(stmt, ast.Assign): - # Store simple variable assignments in the symbol table - if len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Name): - var_name = stmt.targets[0].id - self.symbol_table[var_name] = stmt.value + match stmt: + case ast.Assign(targets=[ast.Name(id=symbol)]): + # Store simple variable assignments in the symbol table + self.symbol_table[symbol] = stmt.value # Second pass: process self.patch_set for stmt in node.body: - if isinstance(stmt, ast.Assign): - for target in stmt.targets: - if isinstance(target, ast.Attribute) and target.attr == "patch_set": - if isinstance(stmt.value, ast.List): - for elt in stmt.value.elts: - if isinstance(elt, ast.Dict): - self.process_patch_entry(elt) + if not isinstance(stmt, ast.Assign): + continue + for target in stmt.targets: + if ( + isinstance(target, ast.Attribute) + and target.attr == "patch_set" + and isinstance(stmt.value, ast.List) + ): + for elt in stmt.value.elts: + if isinstance(elt, ast.Dict): + self.process_patch_entry(elt) - def resolve_symbol(self, node): + def resolve_symbol(self, node: ast.expr) -> ResolvedSymbol: """Resolve named variables to their actual values from the symbol table.""" if isinstance(node, ast.Name) and node.id in self.symbol_table: return self.safe_literal_eval(self.symbol_table[node.id]) return self.safe_literal_eval(node) - def safe_literal_eval(self, node): + def safe_literal_eval(self, node: ast.expr) -> ResolvedSymbol: """Try to evaluate or stringify an AST node.""" try: return ast.literal_eval(node) - except Exception: + except ValueError: # Spooky eval! But we trust nerd fonts to be safe... if hasattr(ast, "unparse"): return eval( - ast.unparse(node), {"box_keep": True}, {"self": SpoofSelf()} + ast.unparse(node), + {"box_keep": True}, + {"self": SimpleNamespace(args=SimpleNamespace(careful=True))}, ) - else: - return f"" + msg = f"" + raise ValueError(msg) from None - def process_patch_entry(self, dict_node): + def process_patch_entry(self, dict_node: ast.Dict) -> None: entry = {} + disallowed_key_nodes = frozenset({"Enabled", "Name", "Filename", "Exact"}) for key_node, value_node in zip(dict_node.keys, dict_node.values): - if isinstance(key_node, ast.Constant) and key_node.value in ( - "Enabled", - "Name", - "Filename", - "Exact", + if ( + isinstance(key_node, ast.Constant) + and key_node.value not in disallowed_key_nodes ): - continue - key = ast.literal_eval(key_node) - value = self.resolve_symbol(value_node) - entry[key] = value - self.patch_set_values.append(entry) + key = ast.literal_eval(cast("ast.Constant", key_node)) + entry[key] = self.resolve_symbol(value_node) + self.patch_set_values.append(cast("PatchSet", entry)) -def extract_patch_set_values(source_code): +def extract_patch_set_values(source_code: str) -> list[PatchSet]: tree = ast.parse(source_code) extractor = PatchSetExtractor() extractor.visit(tree) return extractor.patch_set_values -# We have to spoof `self` and `self.args` for the eval. -class SpoofArgs: - careful = True - - -class SpoofSelf: - args = SpoofArgs() - - -def parse_alignment(val): +def parse_alignment(val: str) -> str | None: return { "l": ".start", "r": ".end", @@ -105,28 +125,24 @@ def parse_alignment(val): }.get(val, ".none") -def get_param(d, key, default): - return float(d.get(key, default)) - - -def attr_key(attr): +def attr_key(attr: PatchSetAttributeEntry) -> AttributeHash: """Convert attributes to a hashable key for grouping.""" - stretch = attr.get("stretch", "") + params = attr.get("params", {}) return ( parse_alignment(attr.get("align", "")), parse_alignment(attr.get("valign", "")), - stretch, - float(attr.get("params", {}).get("overlap", 0.0)), - float(attr.get("params", {}).get("xy-ratio", -1.0)), - float(attr.get("params", {}).get("ypadding", 0.0)), + attr.get("stretch", ""), + float(params.get("overlap", 0.0)), + float(params.get("xy-ratio", -1.0)), + float(params.get("ypadding", 0.0)), ) -def coalesce_codepoints_to_ranges(codepoints): +def coalesce_codepoints_to_ranges(codepoints: list[int]) -> list[tuple[int, int]]: """Convert a sorted list of integers to a list of single values and ranges.""" - ranges = [] + ranges: list[tuple[int, int]] = [] cp_iter = iter(sorted(codepoints)) - try: + with suppress(StopIteration): start = prev = next(cp_iter) for cp in cp_iter: if cp == prev + 1: @@ -135,52 +151,49 @@ def coalesce_codepoints_to_ranges(codepoints): ranges.append((start, prev)) start = prev = cp ranges.append((start, prev)) - except StopIteration: - pass return ranges -def emit_zig_entry_multikey(codepoints, attr): +def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) -> str: align = parse_alignment(attr.get("align", "")) valign = parse_alignment(attr.get("valign", "")) stretch = attr.get("stretch", "") params = attr.get("params", {}) - overlap = get_param(params, "overlap", 0.0) - xy_ratio = get_param(params, "xy-ratio", -1.0) - y_padding = get_param(params, "ypadding", 0.0) + overlap = params.get("overlap", 0.0) + xy_ratio = params.get("xy-ratio", -1.0) + y_padding = params.get("ypadding", 0.0) ranges = coalesce_codepoints_to_ranges(codepoints) keys = "\n".join( - f" 0x{start:x}...0x{end:x}," if start != end else f" 0x{start:x}," + f" {start:#x}...{end:#x}," if start != end else f" {start:#x}," for start, end in ranges ) - s = f"""{keys} - => .{{\n""" + s = f"{keys}\n => .{{\n" # These translations don't quite capture the way # the actual patcher does scaling, but they're a # good enough compromise. - if ("xy" in stretch): + if "xy" in stretch: s += " .size_horizontal = .stretch,\n" s += " .size_vertical = .stretch,\n" - elif ("!" in stretch): + elif "!" in stretch: s += " .size_horizontal = .cover,\n" s += " .size_vertical = .fit,\n" - elif ("^" in stretch): + elif "^" in stretch: s += " .size_horizontal = .cover,\n" s += " .size_vertical = .cover,\n" else: s += " .size_horizontal = .fit,\n" s += " .size_vertical = .fit,\n" - if (align is not None): + if align is not None: s += f" .align_horizontal = {align},\n" - if (valign is not None): + if valign is not None: s += f" .align_vertical = {valign},\n" - if (overlap != 0.0): + if overlap: pad = -overlap s += f" .pad_left = {pad},\n" s += f" .pad_right = {pad},\n" @@ -188,35 +201,33 @@ def emit_zig_entry_multikey(codepoints, attr): s += f" .pad_top = {v_pad},\n" s += f" .pad_bottom = {v_pad},\n" - if (xy_ratio > 0): + if xy_ratio > 0: s += f" .max_xy_ratio = {xy_ratio},\n" s += " }," - return s -def generate_zig_switch_arms(patch_set): - entries = {} - for entry in patch_set: + +def generate_zig_switch_arms(patch_sets: list[PatchSet]) -> str: + entries: dict[int, PatchSetAttributeEntry] = {} + for entry in patch_sets: attributes = entry["Attributes"] for cp in range(entry["SymStart"], entry["SymEnd"] + 1): entries[cp] = attributes["default"] - for k, v in attributes.items(): - if isinstance(k, int): - entries[k] = v + entries |= {k: v for k, v in attributes.items() if isinstance(k, int)} del entries[0] # Group codepoints by attribute key - grouped = defaultdict(list) + grouped = defaultdict[AttributeHash, list[int]](list) for cp, attr in entries.items(): grouped[attr_key(attr)].append(cp) # Emit zig switch arms - result = [] - for _, codepoints in sorted(grouped.items(), key=lambda x: x[1]): + result: list[str] = [] + for codepoints in sorted(grouped.values()): # Use one of the attrs in the group to emit the value attr = entries[codepoints[0]] result.append(emit_zig_entry_multikey(codepoints, attr)) @@ -225,23 +236,16 @@ def generate_zig_switch_arms(patch_set): if __name__ == "__main__": - path = ( - Path(__file__).resolve().parent - / ".." - / ".." - / "vendor" - / "nerd-fonts" - / "font-patcher.py" - ) - with open(path, "r", encoding="utf-8") as f: - source = f.read() + project_root = Path(__file__).resolve().parents[2] + patcher_path = project_root / "vendor" / "nerd-fonts" / "font-patcher.py" + source = patcher_path.read_text(encoding="utf-8") patch_set = extract_patch_set_values(source) - out_path = Path(__file__).resolve().parent / "nerd_font_attributes.zig" + out_path = project_root / "src" / "font" / "nerd_font_attributes.zig" - with open(out_path, "w", encoding="utf-8") as f: - f.write("""//! This is a generate file, produced by nerd_font_codegen.py + with out_path.open("w", encoding="utf-8") as f: + f.write("""//! This is a generated file, produced by nerd_font_codegen.py //! DO NOT EDIT BY HAND! //! //! This file provides info extracted from the nerd fonts patcher script, @@ -254,6 +258,4 @@ pub fn getConstraint(cp: u21) Constraint { return switch (cp) { """) f.write(generate_zig_switch_arms(patch_set)) - f.write("\n") - - f.write(" else => .none,\n };\n}\n") + f.write("\n else => .none,\n };\n}\n") From 2fca0477bc7f3c955daf40a0d4663d63ef3d76a1 Mon Sep 17 00:00:00 2001 From: trag1c Date: Sun, 6 Jul 2025 02:28:28 +0200 Subject: [PATCH 03/36] rely on stdin/stdout instead of hardcoded paths --- src/font/nerd_font_codegen.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index 99915c9f2..52d70ac01 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -10,9 +10,9 @@ be safe and not malicious or anything. import ast import math +import sys from collections import defaultdict from contextlib import suppress -from pathlib import Path from types import SimpleNamespace from typing import Literal, TypedDict, cast @@ -236,16 +236,9 @@ def generate_zig_switch_arms(patch_sets: list[PatchSet]) -> str: if __name__ == "__main__": - project_root = Path(__file__).resolve().parents[2] - - patcher_path = project_root / "vendor" / "nerd-fonts" / "font-patcher.py" - source = patcher_path.read_text(encoding="utf-8") + source = sys.stdin.read() patch_set = extract_patch_set_values(source) - - out_path = project_root / "src" / "font" / "nerd_font_attributes.zig" - - with out_path.open("w", encoding="utf-8") as f: - f.write("""//! This is a generated file, produced by nerd_font_codegen.py + print("""//! This is a generated file, produced by nerd_font_codegen.py //! DO NOT EDIT BY HAND! //! //! This file provides info extracted from the nerd fonts patcher script, @@ -255,7 +248,6 @@ const Constraint = @import("face.zig").RenderOptions.Constraint; /// Get the a constraints for the provided codepoint. pub fn getConstraint(cp: u21) Constraint { - return switch (cp) { -""") - f.write(generate_zig_switch_arms(patch_set)) - f.write("\n else => .none,\n };\n}\n") + return switch (cp) {""") + print(generate_zig_switch_arms(patch_set)) + print(" else => .none,\n };\n}") From fff16bff6928e14a6e5f342f5578f73b0b998733 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sat, 5 Jul 2025 20:36:35 -0600 Subject: [PATCH 04/36] font/coretext: fix bitmap size calculation, prevent clipping Previously, many glyphs were having their top and right row/column of pixels clipped off due to not accounting for the slight bearing in the width and height calculation here. --- src/font/face/coretext.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 35f094848..89d771d95 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -356,8 +356,14 @@ pub const Face = struct { const x = glyph_size.x; const y = glyph_size.y; - const px_width: u32 = @intFromFloat(@ceil(width)); - const px_height: u32 = @intFromFloat(@ceil(height)); + // We have to include the fractional pixels that we won't be offsetting + // in our width and height calculations, that is, we offset by the floor + // of the bearings when we render the glyph, meaning there's still a bit + // of extra width to the area that's drawn in beyond just the width of + // the glyph itself, so we include that extra fraction of a pixel when + // calculating the width and height here. + const px_width: u32 = @intFromFloat(@ceil(width + rect.origin.x - @floor(rect.origin.x))); + const px_height: u32 = @intFromFloat(@ceil(height + rect.origin.y - @floor(rect.origin.y))); // Settings that are specific to if we are rendering text or emoji. const color: struct { From 02d82720d26b222ad0c768d95395d7f4bea864c6 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sat, 5 Jul 2025 20:40:12 -0600 Subject: [PATCH 05/36] font/freetype: fix negated force-autohint flag The behavior of this flag was the opposite of its description in the docs- luckily, at the same time, the default (true) was the opposite from what the default actually is in freetype, so users who haven't explicitly set this flag won't see a behavior difference from this. --- src/config/Config.zig | 4 ++-- src/font/face/freetype.zig | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 653ce4178..2910372f3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -435,7 +435,7 @@ pub const compatibility = std.StaticStringMap( /// * `hinting` - Enable or disable hinting. Enabled by default. /// /// * `force-autohint` - Always use the freetype auto-hinter instead of -/// the font's native hinter. Enabled by default. +/// the font's native hinter. Disabled by default. /// /// * `monochrome` - Instructs renderer to use 1-bit monochrome rendering. /// This will disable anti-aliasing, and probably not look very good unless @@ -7084,7 +7084,7 @@ pub const FreetypeLoadFlags = packed struct { // for Freetype itself. Ghostty hasn't made any opinionated changes // to these defaults. hinting: bool = true, - @"force-autohint": bool = true, + @"force-autohint": bool = false, monochrome: bool = false, autohint: bool = true, }; diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index c23ede04a..585d21c5b 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -348,7 +348,7 @@ pub const Face = struct { // use options from config .no_hinting = !do_hinting, - .force_autohint = !self.load_flags.@"force-autohint", + .force_autohint = self.load_flags.@"force-autohint", .no_autohint = !self.load_flags.autohint, // NO_SVG set to true because we don't currently support rendering From 8c3caee15cce43c8d671a8c1f63e99816a92cc92 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Jul 2025 19:54:01 -0700 Subject: [PATCH 06/36] macOS: zig build run disables window saved state --- macos/Sources/App/macOS/AppDelegate.swift | 5 ++--- macos/Sources/Ghostty/Package.swift | 20 ++++++++++++++++++++ src/build/GhosttyXcodebuild.zig | 20 ++++++++++++++++---- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index efc09ede9..53b6dce88 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -256,9 +256,8 @@ class AppDelegate: NSObject, // Setup signal handlers setupSignals() - // This is a hack used by our build scripts, specifically `zig build run`, - // to force our app to the foreground. - if ProcessInfo.processInfo.environment["GHOSTTY_MAC_ACTIVATE"] == "1" { + // If we launched via zig run then we need to force foreground. + if Ghostty.launchSource == .zig_run { // This never gets called until we click the dock icon. This forces it // activate immediately. applicationDidBecomeActive(.init(name: NSApplication.didBecomeActiveNotification)) diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 125a09825..e96f555d3 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -48,6 +48,26 @@ extension Ghostty { } } +// MARK: General Helpers + +extension Ghostty { + enum LaunchSource: String { + case cli + case app + case zig_run + } + + /// Returns the mechanism that launched the app. This is based on an env var so + /// its up to the env var being set in the correct circumstance. + static var launchSource: LaunchSource { + guard let envValue = ProcessInfo.processInfo.environment["GHOSTTY_MAC_LAUNCH_SOURCE"] else { + return .app + } + + return LaunchSource(rawValue: envValue) ?? .app + } +} + // MARK: Swift Types for C Types extension Ghostty { diff --git a/src/build/GhosttyXcodebuild.zig b/src/build/GhosttyXcodebuild.zig index 9b472eda8..052c9f3e4 100644 --- a/src/build/GhosttyXcodebuild.zig +++ b/src/build/GhosttyXcodebuild.zig @@ -88,6 +88,19 @@ pub fn init( // Our step to open the resulting Ghostty app. const open = open: { + const disable_save_state = RunStep.create(b, "disable save state"); + disable_save_state.has_side_effects = true; + disable_save_state.addArgs(&.{ + "/usr/libexec/PlistBuddy", + "-c", + // We'll have to change this to `Set` if we ever put this + // into our Info.plist. + "Add :NSQuitAlwaysKeepsWindows bool false", + b.fmt("{s}/Contents/Info.plist", .{app_path}), + }); + disable_save_state.expectExitCode(0); + disable_save_state.step.dependOn(&build.step); + const open = RunStep.create(b, "run Ghostty app"); open.has_side_effects = true; open.cwd = b.path(""); @@ -98,15 +111,14 @@ pub fn init( // Open depends on the app open.step.dependOn(&build.step); + open.step.dependOn(&disable_save_state.step); // This overrides our default behavior and forces logs to show // up on stderr (in addition to the centralized macOS log). open.setEnvironmentVariable("GHOSTTY_LOG", "1"); - // This is hack so that we can activate the app and bring it to - // the front forcibly even though we're executing directly - // via the binary and not launch services. - open.setEnvironmentVariable("GHOSTTY_MAC_ACTIVATE", "1"); + // Configure how we're launching + open.setEnvironmentVariable("GHOSTTY_MAC_LAUNCH_SOURCE", "zig_run"); if (b.args) |args| { open.addArgs(args); From 87f35bd1c12dd8b50221163f9fc10d17d09eb32a Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sat, 5 Jul 2025 20:58:15 -0600 Subject: [PATCH 07/36] renderer/opengl: explicit texture options This sets up for a couple improvments (see TODO comments) and also sets the glyph atlas textures to nearest neighbor sampling since we can do that now that we never scale glyphs. --- pkg/opengl/Texture.zig | 24 ++++++++++++++++++++++++ src/renderer/OpenGL.zig | 18 ++++++++++++++++++ src/renderer/opengl/Texture.zig | 12 ++++++++---- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig index 2c8e05eff..03e794855 100644 --- a/pkg/opengl/Texture.zig +++ b/pkg/opengl/Texture.zig @@ -92,6 +92,30 @@ pub const Format = enum(c_uint) { _, }; +/// Minification filter for textures. +pub const MinFilter = enum(c_int) { + nearest = c.GL_NEAREST, + linear = c.GL_LINEAR, + nearest_mipmap_nearest = c.GL_NEAREST_MIPMAP_NEAREST, + linear_mipmap_nearest = c.GL_LINEAR_MIPMAP_NEAREST, + nearest_mipmap_linear = c.GL_NEAREST_MIPMAP_LINEAR, + linear_mipmap_linear = c.GL_LINEAR_MIPMAP_LINEAR, +}; + +/// Magnification filter for textures. +pub const MagFilter = enum(c_int) { + nearest = c.GL_NEAREST, + linear = c.GL_LINEAR, +}; + +/// Texture coordinate wrapping mode. +pub const Wrap = enum(c_int) { + clamp_to_edge = c.GL_CLAMP_TO_EDGE, + clamp_to_border = c.GL_CLAMP_TO_BORDER, + mirrored_repeat = c.GL_MIRRORED_REPEAT, + repeat = c.GL_REPEAT, +}; + /// Data type for texture images. pub const DataType = enum(c_uint) { UnsignedByte = c.GL_UNSIGNED_BYTE, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 00df8e273..882d6fc03 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -356,6 +356,10 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options { .format = .rgba, .internal_format = .srgba, .target = .@"2D", + .min_filter = .linear, + .mag_filter = .linear, + .wrap_s = .clamp_to_edge, + .wrap_t = .clamp_to_edge, }; } @@ -388,6 +392,16 @@ pub inline fn imageTextureOptions( .format = format.toPixelFormat(), .internal_format = if (srgb) .srgba else .rgba, .target = .@"2D", + // TODO: Generate mipmaps for image textures and use + // linear_mipmap_linear filtering so that they + // look good even when scaled way down. + .min_filter = .linear, + .mag_filter = .linear, + // TODO: Separate out background image options, use + // repeating coordinate modes so we don't have + // to do the modulus in the shader. + .wrap_s = .clamp_to_edge, + .wrap_t = .clamp_to_edge, }; } @@ -409,6 +423,10 @@ pub fn initAtlasTexture( .format = format, .internal_format = internal_format, .target = .Rectangle, + .min_filter = .nearest, + .mag_filter = .nearest, + .wrap_s = .clamp_to_edge, + .wrap_t = .clamp_to_edge, }, atlas.size, atlas.size, diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig index 9be2b7078..2f3e7f46a 100644 --- a/src/renderer/opengl/Texture.zig +++ b/src/renderer/opengl/Texture.zig @@ -16,6 +16,10 @@ pub const Options = struct { format: gl.Texture.Format, internal_format: gl.Texture.InternalFormat, target: gl.Texture.Target, + min_filter: gl.Texture.MinFilter, + mag_filter: gl.Texture.MagFilter, + wrap_s: gl.Texture.Wrap, + wrap_t: gl.Texture.Wrap, }; texture: gl.Texture, @@ -48,10 +52,10 @@ pub fn init( { const texbind = tex.bind(opts.target) catch return error.OpenGLFailed; defer texbind.unbind(); - texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed; - texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed; - texbind.parameter(.MinFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed; - texbind.parameter(.MagFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed; + texbind.parameter(.WrapS, @intFromEnum(opts.wrap_s)) catch return error.OpenGLFailed; + texbind.parameter(.WrapT, @intFromEnum(opts.wrap_t)) catch return error.OpenGLFailed; + texbind.parameter(.MinFilter, @intFromEnum(opts.min_filter)) catch return error.OpenGLFailed; + texbind.parameter(.MagFilter, @intFromEnum(opts.mag_filter)) catch return error.OpenGLFailed; texbind.image2D( 0, opts.internal_format, From 8f50c7f2699bb09ec85a43776c8fb6f59e8abe1e Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sat, 5 Jul 2025 22:13:26 -0600 Subject: [PATCH 08/36] font/sprite: no more margin in atlas region We no longer need a margin in the atlas because we always sample with nearest neighbor and our glyphs are always pixel perfect, no worry about interpolation between adjacent glyphs anymore! --- src/font/sprite/canvas.zig | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig index b981449bc..a77b90a56 100644 --- a/src/font/sprite/canvas.zig +++ b/src/font/sprite/canvas.zig @@ -140,24 +140,7 @@ pub const Canvas = struct { const region_height = sfc_height -| self.clip_top -| self.clip_bottom; // Allocate our texture atlas region - const region = region: { - // Reserve a region with a 1px margin on the bottom and right edges - // so that we can avoid interpolation between adjacent glyphs during - // texture sampling. - var region = try atlas.reserve( - alloc, - region_width + 1, - region_height + 1, - ); - - // Modify the region to remove the margin so that we write to the - // non-zero location. The data in an Altlas is always initialized - // to zero (Atlas.clear) so we don't need to worry about zero-ing - // that. - region.width -= 1; - region.height -= 1; - break :region region; - }; + const region = try atlas.reserve(alloc, region_width, region_height); if (region.width > 0 and region.height > 0) { const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf); From 984d123fe48e03f8b828c036b2dcadb79b627624 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Jul 2025 21:04:59 -0700 Subject: [PATCH 09/36] macos: support configuration via CLI arguments This makes it so `zig build run` can take arguments such as `--config-default-files=false` or any other configuration. Previously, it only accepted commands such as `+version`. Incidentally, this also makes it so that the app in general can now take configuration arguments via the CLI if it is launched as a new instance via `open`. For example: open -n Ghostty.app --args --config-default-files=false This previously didn't work. This is kind of cool. To make this work, the libghostty C API was modified so that initialization requires the CLI args, and there is a new C API to try to execute an action if it was set. --- include/ghostty.h | 4 +-- macos/Ghostty-Info.plist | 4 +-- macos/Ghostty.xcodeproj/project.pbxproj | 4 +++ macos/Sources/App/macOS/main.swift | 33 +++++++++++++++---- macos/Sources/Ghostty/Ghostty.App.swift | 6 ---- macos/Sources/Ghostty/Package.swift | 5 ++- .../Extensions/FileHandle+Extension.swift | 9 +++++ src/apprt/embedded.zig | 2 +- src/build/GhosttyXcodebuild.zig | 4 --- src/main_c.zig | 30 ++++++++--------- src/os/desktop.zig | 11 +++++-- 11 files changed, 70 insertions(+), 42 deletions(-) create mode 100644 macos/Sources/Helpers/Extensions/FileHandle+Extension.swift diff --git a/include/ghostty.h b/include/ghostty.h index 181f7b7f8..73c708c6b 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -778,8 +778,8 @@ typedef struct { //------------------------------------------------------------------- // Published API -int ghostty_init(void); -void ghostty_cli_main(uintptr_t, char**); +int ghostty_init(uintptr_t, char**); +void ghostty_cli_try_action(void); ghostty_info_s ghostty_info(void); const char* ghostty_translate(const char*); diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist index dcce61373..ff391c0f8 100644 --- a/macos/Ghostty-Info.plist +++ b/macos/Ghostty-Info.plist @@ -48,8 +48,8 @@ LSEnvironment - GHOSTTY_MAC_APP - 1 + GHOSTTY_MAC_LAUNCH_SOURCE + app MDItemKeywords Terminal diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index cf806c7bd..08c3ef3b3 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; 9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; }; A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; }; + A505D21D2E1A2FA20018808F /* FileHandle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */; }; A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; }; A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.swift */; }; A51194132E05D006007258CC /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; }; @@ -158,6 +159,7 @@ 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; }; A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; + A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileHandle+Extension.swift"; sourceTree = ""; }; A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = ""; }; A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = ""; }; A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = ""; }; @@ -516,6 +518,7 @@ A586366A2DF0A98900E04A10 /* Array+Extension.swift */, A50297342DFA0F3300B4E924 /* Double+Extension.swift */, A586366E2DF25D8300E04A10 /* Duration+Extension.swift */, + A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */, A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */, A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */, A51194122E05D003007258CC /* Optional+Extension.swift */, @@ -799,6 +802,7 @@ A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */, A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */, A58636732DF4813400E04A10 /* UndoManager+Extension.swift in Sources */, + A505D21D2E1A2FA20018808F /* FileHandle+Extension.swift in Sources */, A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */, A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */, diff --git a/macos/Sources/App/macOS/main.swift b/macos/Sources/App/macOS/main.swift index 990ef8ef1..ad32f4e70 100644 --- a/macos/Sources/App/macOS/main.swift +++ b/macos/Sources/App/macOS/main.swift @@ -2,13 +2,32 @@ import AppKit import Cocoa import GhosttyKit -// We put the GHOSTTY_MAC_APP env var into the Info.plist to detect -// whether we launch from the app or not. A user can fake this if -// they want but they're doing so at their own detriment... -let process = ProcessInfo.processInfo -if ((process.environment["GHOSTTY_MAC_APP"] ?? "") == "") { - ghostty_cli_main(UInt(CommandLine.argc), CommandLine.unsafeArgv) - exit(1) +// Initialize Ghostty global state. We do this once right away because the +// CLI APIs require it and it lets us ensure it is done immediately for the +// rest of the app. +if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCESS { + Ghostty.logger.critical("ghostty_init failed") + + // We also write to stderr if this is executed from the CLI or zig run + switch Ghostty.launchSource { + case .cli, .zig_run: + let stderrHandle = FileHandle.standardError + stderrHandle.write( + "Ghostty failed to initialize! If you're executing Ghostty from the command line\n" + + "then this is usually because an invalid action or multiple actions were specified.\n" + + "Actions start with the `+` character.\n\n" + + "View all available actions by running `ghostty +help`.\n") + exit(1) + + case .app: + // For the app we exit immediately. We should handle this case more + // gracefully in the future. + exit(1) + } } +// This will run the CLI action and exit if one was specified. A CLI +// action is a command starting with a `+`, such as `ghostty +boo`. +ghostty_cli_try_action(); + _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index ba0b95212..17abe2b0e 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -45,12 +45,6 @@ extension Ghostty { } init() { - // Initialize ghostty global state. This happens once per process. - if ghostty_init() != GHOSTTY_SUCCESS { - logger.critical("ghostty_init failed, weird things may happen") - readiness = .error - } - // Initialize the global configuration. self.config = Config() if self.config.config == nil { diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index e96f555d3..f30f2f6f9 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -61,9 +61,12 @@ extension Ghostty { /// its up to the env var being set in the correct circumstance. static var launchSource: LaunchSource { guard let envValue = ProcessInfo.processInfo.environment["GHOSTTY_MAC_LAUNCH_SOURCE"] else { - return .app + // We default to the CLI because the app bundle always sets the + // source. If its unset we assume we're in a CLI environment. + return .cli } + // If the env var is set but its unknown then we default back to the app. return LaunchSource(rawValue: envValue) ?? .app } } diff --git a/macos/Sources/Helpers/Extensions/FileHandle+Extension.swift b/macos/Sources/Helpers/Extensions/FileHandle+Extension.swift new file mode 100644 index 000000000..b6df4a60f --- /dev/null +++ b/macos/Sources/Helpers/Extensions/FileHandle+Extension.swift @@ -0,0 +1,9 @@ +import Foundation + +extension FileHandle: @retroactive TextOutputStream { + /// Write a string to a filehandle. + public func write(_ string: String) { + let data = Data(string.utf8) + self.write(data) + } +} diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 0121494b7..30a2d9ff6 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -884,7 +884,7 @@ pub const Surface = struct { } // Remove this so that running `ghostty` within Ghostty works. - env.remove("GHOSTTY_MAC_APP"); + env.remove("GHOSTTY_MAC_LAUNCH_SOURCE"); // If we were launched from the desktop then we want to // remove the LANGUAGE env var so that we don't inherit diff --git a/src/build/GhosttyXcodebuild.zig b/src/build/GhosttyXcodebuild.zig index 052c9f3e4..7fa2d2f95 100644 --- a/src/build/GhosttyXcodebuild.zig +++ b/src/build/GhosttyXcodebuild.zig @@ -122,10 +122,6 @@ pub fn init( if (b.args) |args| { open.addArgs(args); - } else { - // This tricks the app into thinking it's running from the - // app bundle so we don't execute our CLI mode. - open.setEnvironmentVariable("GHOSTTY_MAC_APP", "1"); } break :open open; diff --git a/src/main_c.zig b/src/main_c.zig index 1b73d7327..0722900e7 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -46,17 +46,11 @@ const Info = extern struct { }; }; -/// Initialize ghostty global state. It is possible to have more than -/// one global state but it has zero practical benefit. -export fn ghostty_init() c_int { +/// Initialize ghostty global state. +export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int { assert(builtin.link_libc); - // Since in the lib we don't go through start.zig, we need - // to populate argv so that inspecting std.os.argv doesn't - // touch uninitialized memory. - var argv: [0][*:0]u8 = .{}; - std.os.argv = &argv; - + std.os.argv = argv[0..argc]; state.init() catch |err| { std.log.err("failed to initialize ghostty error={}", .{err}); return 1; @@ -65,15 +59,17 @@ export fn ghostty_init() c_int { return 0; } -/// This is the entrypoint for the CLI version of Ghostty. This -/// is mutually exclusive to ghostty_init. Do NOT run ghostty_init -/// if you are going to run this. This will not return. -export fn ghostty_cli_main(argc: usize, argv: [*][*:0]u8) noreturn { - std.os.argv = argv[0..argc]; - main.main() catch |err| { - std.log.err("failed to run ghostty error={}", .{err}); +/// Runs an action if it is specified. If there is no action this returns +/// false. If there is an action then this doesn't return. +export fn ghostty_cli_try_action() void { + const action = state.action orelse return; + std.log.info("executing CLI action={}", .{action}); + posix.exit(action.run(state.alloc) catch |err| { + std.log.err("CLI action failed error={}", .{err}); posix.exit(1); - }; + }); + + posix.exit(0); } /// Return metadata about Ghostty, such as version, build mode, etc. diff --git a/src/os/desktop.zig b/src/os/desktop.zig index 3bc843e5c..93bfb74bc 100644 --- a/src/os/desktop.zig +++ b/src/os/desktop.zig @@ -24,8 +24,15 @@ pub fn launchedFromDesktop() bool { // This special case is so that if we launch the app via the // app bundle (i.e. via open) then we still treat it as if it // was launched from the desktop. - if (build_config.artifact == .lib and - posix.getenv("GHOSTTY_MAC_APP") != null) break :macos true; + if (build_config.artifact == .lib) lib: { + const env = "GHOSTTY_MAC_LAUNCH_SOURCE"; + const source = posix.getenv(env) orelse break :lib; + + // Source can be "app", "cli", or "zig_run". We assume + // its the desktop only if its "app". We may want to do + // "zig_run" but at the moment there's no reason. + if (std.mem.eql(u8, source, "app")) break :macos true; + } break :macos c.getppid() == 1; }, From b752ade1128f7f4acd6a0a349ff0ddaa19d69e99 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 6 Jul 2025 01:05:17 -0500 Subject: [PATCH 10/36] gtk: don't allow focusing on tab overview or menu buttons This prevents GTK from focusing on those buttons, which would prevent focus from returning to the terminal after those buttons had been used. --- src/apprt/gtk/Window.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 555edb1e4..e6b502c80 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -214,6 +214,7 @@ pub fn init(self: *Window, app: *App) !void { { const btn = gtk.MenuButton.new(); btn.as(gtk.Widget).setTooltipText(i18n._("Main Menu")); + btn.as(gtk.Widget).setCanFocus(0); btn.setIconName("open-menu-symbolic"); btn.setPopover(self.titlebar_menu.asWidget()); _ = gobject.Object.signals.notify.connect( @@ -253,6 +254,7 @@ pub fn init(self: *Window, app: *App) !void { }, }; + btn.setCanFocus(0); btn.setFocusOnClick(0); self.headerbar.packEnd(btn); } From 64dd10b98f2d41a240bc4fbe56b7b174b4e6d1d6 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Sun, 6 Jul 2025 08:28:24 -0500 Subject: [PATCH 11/36] Add termio to terminal CODEOWNERS group The termio directory contains the implementation of many terminal features that those in the terminal reviewer group may want to be notified about. --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 56768d5ae..7995650b7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -155,6 +155,7 @@ /src/input/KeyEncoder.zig @ghostty-org/terminal /src/terminal/ @ghostty-org/terminal /src/terminfo/ @ghostty-org/terminal +/src/termio/ @ghostty-org/terminal /src/unicode/ @ghostty-org/terminal /src/Surface.zig @ghostty-org/terminal /src/surface_mouse.zig @ghostty-org/terminal From 9f3f9205d8d04a077e35ceaa37ef3aca3d5bb10c Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Sun, 29 Jun 2025 10:52:24 -0500 Subject: [PATCH 12/36] Add link-previews option --- src/Surface.zig | 36 +++++++++++++++++++++++++----------- src/config.zig | 1 + src/config/Config.zig | 14 ++++++++++++++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 6d0f1584b..769512606 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -270,6 +270,7 @@ const DerivedConfig = struct { title: ?[:0]const u8, title_report: bool, links: []Link, + link_previews: configpkg.LinkPreviews, const Link = struct { regex: oni.Regex, @@ -336,6 +337,7 @@ const DerivedConfig = struct { .title = config.title, .title_report = config.@"title-report", .links = links, + .link_previews = config.@"link-previews", // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. @@ -1242,7 +1244,7 @@ fn mouseRefreshLinks( // Get our link at the current position. This returns null if there // isn't a link OR if we shouldn't be showing links for some reason // (see further comments for cases). - const link_: ?apprt.action.MouseOverLink = link: { + const link_: ?apprt.action.MouseOverLink, const preview: bool = link: { // If we clicked and our mouse moved cells then we never // highlight links until the mouse is unclicked. This follows // standard macOS and Linux behavior where a click and drag cancels @@ -1257,18 +1259,21 @@ fn mouseRefreshLinks( if (!click_pt.coord().eql(pos_vp)) { log.debug("mouse moved while left click held, ignoring link hover", .{}); - break :link null; + break :link .{ null, false }; } } - const link = (try self.linkAtPos(pos)) orelse break :link null; + const link = (try self.linkAtPos(pos)) orelse break :link .{ null, false }; switch (link[0]) { .open => { const str = try self.io.terminal.screen.selectionString(alloc, .{ .sel = link[1], .trim = false, }); - break :link .{ .url = str }; + break :link .{ + .{ .url = str }, + self.config.link_previews == .true, + }; }, ._open_osc8 => { @@ -1276,9 +1281,14 @@ fn mouseRefreshLinks( const pin = link[1].start(); const uri = self.osc8URI(pin) orelse { log.warn("failed to get URI for OSC8 hyperlink", .{}); - break :link null; + break :link .{ null, false }; + }; + break :link .{ + .{ + .url = uri, + }, + self.config.link_previews != .false, }; - break :link .{ .url = uri }; }, } }; @@ -1294,11 +1304,15 @@ fn mouseRefreshLinks( .mouse_shape, .pointer, ); - _ = try self.rt_app.performAction( - .{ .surface = self }, - .mouse_over_link, - link, - ); + + if (preview) { + _ = try self.rt_app.performAction( + .{ .surface = self }, + .mouse_over_link, + link, + ); + } + try self.queueRender(); return; } diff --git a/src/config.zig b/src/config.zig index ac38eb89c..b6fecde4e 100644 --- a/src/config.zig +++ b/src/config.zig @@ -37,6 +37,7 @@ pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures; pub const WindowPaddingColor = Config.WindowPaddingColor; pub const BackgroundImagePosition = Config.BackgroundImagePosition; pub const BackgroundImageFit = Config.BackgroundImageFit; +pub const LinkPreviews = Config.LinkPreviews; // Alternate APIs pub const CAPI = @import("config/CAPI.zig"); diff --git a/src/config/Config.zig b/src/config/Config.zig index 2910372f3..31d749348 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1046,6 +1046,14 @@ link: RepeatableLink = .{}, /// `link`). If you want to customize URL matching, use `link` and disable this. @"link-url": bool = true, +/// Show link previews for a matched URL. +/// +/// When true, link previews are shown for all matched URLs. When false, link +/// previews are never shown. When set to "osc8", link previews are only shown +/// for hyperlinks created with the OSC 8 sequence (in this case, the link text +/// can differ from the link destination). +@"link-previews": LinkPreviews = .true, + /// Whether to start the window in a maximized state. This setting applies /// to new windows and does not apply to tabs, splits, etc. However, this setting /// will apply to all new windows, not just the first one. @@ -4326,6 +4334,12 @@ pub const WindowSubtitle = enum { @"working-directory", }; +pub const LinkPreviews = enum { + false, + true, + osc8, +}; + /// Color represents a color using RGB. /// /// This is a packed struct so that the C API to read color values just From eb5694794cac5d0f10f3d2663914c8b9278e409e Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 6 Jul 2025 00:21:01 -0500 Subject: [PATCH 13/36] keybind: add set_font_size Fixes #7795 This allows the font size to be set to an absolute value. --- src/Surface.zig | 8 ++++++++ src/config/Config.zig | 10 ++++++++- src/input/Binding.zig | 47 +++++++++++++++++++++++++++++++++++++++++++ src/input/command.zig | 2 ++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 6d0f1584b..d9d6a3012 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4484,6 +4484,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool try self.setFontSize(size); }, + .set_font_size => |points| { + log.debug("set font size={d}", .{points}); + + var size = self.font_size; + size.points = std.math.clamp(points, 1.0, 255.0); + try self.setFontSize(size); + }, + .prompt_surface_title => return try self.rt_app.performAction( .{ .surface = self }, .prompt_title, diff --git a/src/config/Config.zig b/src/config/Config.zig index 2910372f3..be76820d2 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6531,8 +6531,9 @@ pub const RepeatableCommand = struct { try list.parseCLI(alloc, "title:Foo,action:ignore"); try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle"); try list.parseCLI(alloc, "title:Quux,description:boo,action:increase_font_size:2.5"); + try list.parseCLI(alloc, "title:Baz,description:Raspberry Pie,action:set_font_size:3.14"); - try testing.expectEqual(@as(usize, 3), list.value.items.len); + try testing.expectEqual(@as(usize, 4), list.value.items.len); try testing.expectEqual(inputpkg.Binding.Action.ignore, list.value.items[0].action); try testing.expectEqualStrings("Foo", list.value.items[0].title); @@ -6549,6 +6550,13 @@ pub const RepeatableCommand = struct { try testing.expectEqualStrings("Quux", list.value.items[2].title); try testing.expectEqualStrings("boo", list.value.items[2].description); + try testing.expectEqual( + inputpkg.Binding.Action{ .set_font_size = 3.14 }, + list.value.items[3].action, + ); + try testing.expectEqualStrings("Baz", list.value.items[3].title); + try testing.expectEqualStrings("Raspberry Pie", list.value.items[3].description); + try list.parseCLI(alloc, ""); try testing.expectEqual(@as(usize, 0), list.value.items.len); } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 7cdb8047c..c342c9cc2 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -296,6 +296,12 @@ pub const Action = union(enum) { /// Reset the font size to the original configured size. reset_font_size, + /// Set the font size to the specified size in points (pt). + /// + /// For example, `set_font_size:14.5` will set the font size + /// to 14.5 points. + set_font_size: f32, + /// Clear the screen and all scrollback. clear_screen, @@ -1004,6 +1010,7 @@ pub const Action = union(enum) { .increase_font_size, .decrease_font_size, .reset_font_size, + .set_font_size, .prompt_surface_title, .clear_screen, .select_all, @@ -3065,6 +3072,7 @@ test "set: getEvent codepoint case folding" { try testing.expect(action == null); } } + test "Action: clone" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(testing.allocator); @@ -3083,3 +3091,42 @@ test "Action: clone" { try testing.expect(b == .text); } } + +test "parse: increase_font_size" { + const testing = std.testing; + + { + const binding = try parseSingle("a=increase_font_size:1.5"); + try testing.expect(binding.action == .increase_font_size); + try testing.expectEqual(1.5, binding.action.increase_font_size); + } +} + +test "parse: decrease_font_size" { + const testing = std.testing; + + { + const binding = try parseSingle("a=decrease_font_size:2.5"); + try testing.expect(binding.action == .decrease_font_size); + try testing.expectEqual(2.5, binding.action.decrease_font_size); + } +} + +test "parse: reset_font_size" { + const testing = std.testing; + + { + const binding = try parseSingle("a=reset_font_size"); + try testing.expect(binding.action == .reset_font_size); + } +} + +test "parse: set_font_size" { + const testing = std.testing; + + { + const binding = try parseSingle("a=set_font_size:13.5"); + try testing.expect(binding.action == .set_font_size); + try testing.expectEqual(13.5, binding.action.set_font_size); + } +} diff --git a/src/input/command.zig b/src/input/command.zig index 693d5c8d4..8938835d0 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Action = @import("Binding.zig").Action; @@ -460,6 +461,7 @@ fn actionCommands(action: Action.Key) []const Command { .esc, .text, .cursor_key, + .set_font_size, .scroll_page_fractional, .scroll_page_lines, .adjust_selection, From 0a1ade01580a28d66894e127543bab8ad63a4896 Mon Sep 17 00:00:00 2001 From: "Breno A." Date: Sat, 5 Jul 2025 18:10:06 -0300 Subject: [PATCH 14/36] pin GitHub Actions to specific SHAs --- .github/pinact.yml | 4 + .github/workflows/clean-artifacts.yml | 2 +- .github/workflows/milestone.yml | 4 +- .github/workflows/nix.yml | 8 +- .github/workflows/publish-tag.yml | 2 +- .github/workflows/release-pr.yml | 20 +-- .github/workflows/release-tag.yml | 36 ++-- .github/workflows/release-tip.yml | 50 +++--- .github/workflows/test.yml | 209 ++++++++++++---------- .github/workflows/update-colorschemes.yml | 10 +- nix/devShell.nix | 2 + 11 files changed, 191 insertions(+), 156 deletions(-) create mode 100644 .github/pinact.yml diff --git a/.github/pinact.yml b/.github/pinact.yml new file mode 100644 index 000000000..3a29d18ab --- /dev/null +++ b/.github/pinact.yml @@ -0,0 +1,4 @@ +version: 3 +ignore_actions: + - name: "DeterminateSystems/nix-installer-action" + ref: "main" diff --git a/.github/workflows/clean-artifacts.yml b/.github/workflows/clean-artifacts.yml index 5337c264a..69cb74ae5 100644 --- a/.github/workflows/clean-artifacts.yml +++ b/.github/workflows/clean-artifacts.yml @@ -10,7 +10,7 @@ jobs: timeout-minutes: 10 steps: - name: Remove old artifacts - uses: c-hive/gha-remove-artifacts@v1 + uses: c-hive/gha-remove-artifacts@44fc7acaf1b3d0987da0e8d4707a989d80e9554b # v1.4.0 with: age: "1 week" skip-tags: true diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml index 9ac5536ff..bc5c3d76c 100644 --- a/.github/workflows/milestone.yml +++ b/.github/workflows/milestone.yml @@ -15,7 +15,7 @@ jobs: name: Milestone Update steps: - name: Set Milestone for PR - uses: hustcer/milestone-action@v2 + uses: hustcer/milestone-action@09bdc6fda0f43a4df28cda5815cc47df74cfdba7 # v2.8 if: github.event.pull_request.merged == true with: action: bind-pr # `bind-pr` is the default action @@ -24,7 +24,7 @@ jobs: # Bind milestone to closed issue that has a merged PR fix - name: Set Milestone for Issue - uses: hustcer/milestone-action@v2 + uses: hustcer/milestone-action@09bdc6fda0f43a4df28cda5815cc47df74cfdba7 # v2.8 if: github.event.issue.state == 'closed' with: action: bind-issue diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index a905531c2..bf8fd7208 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -34,18 +34,18 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@v31 + uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" diff --git a/.github/workflows/publish-tag.yml b/.github/workflows/publish-tag.yml index 458982140..710d04647 100644 --- a/.github/workflows/publish-tag.yml +++ b/.github/workflows/publish-tag.yml @@ -64,7 +64,7 @@ jobs: mkdir blob mv appcast.xml blob/appcast.xml - name: Upload Appcast to R2 - uses: ryand56/r2-upload-action@latest + uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1 with: r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }} r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 37d5ba79b..cf96ffb21 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -8,7 +8,7 @@ jobs: runs-on: namespace-profile-ghostty-sm needs: [build-macos-debug] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install sentry-cli run: | @@ -29,7 +29,7 @@ jobs: runs-on: namespace-profile-ghostty-sm needs: [build-macos] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install sentry-cli run: | @@ -51,16 +51,16 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Important so that build number generation works fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -189,7 +189,7 @@ jobs: cp ghostty-macos-universal.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal.zip cp ghostty-macos-universal-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip - name: Upload to R2 - uses: ryand56/r2-upload-action@latest + uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 with: r2-account-id: ${{ secrets.CF_R2_PR_ACCOUNT_ID }} r2-access-key-id: ${{ secrets.CF_R2_PR_AWS_KEY }} @@ -203,16 +203,16 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Important so that build number generation works fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -341,7 +341,7 @@ jobs: cp ghostty-macos-universal-debug.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug.zip cp ghostty-macos-universal-debug-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-dsym.zip - name: Upload to R2 - uses: ryand56/r2-upload-action@latest + uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 with: r2-account-id: ${{ secrets.CF_R2_PR_ACCOUNT_ID }} r2-access-key-id: ${{ secrets.CF_R2_PR_AWS_KEY }} diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 33cf9f3a8..98ecf2fa3 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -56,7 +56,7 @@ jobs: fi - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Important so that build number generation works fetch-depth: 0 @@ -80,20 +80,20 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -111,7 +111,7 @@ jobs: nix develop -c minisign -S -m "ghostty-source.tar.gz" -s minisign.key < minisign.password - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: source-tarball path: |- @@ -128,12 +128,12 @@ jobs: GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -260,7 +260,7 @@ jobs: zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/ - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: macos path: |- @@ -277,7 +277,7 @@ jobs: curl -sL https://sentry.io/get-cli/ | bash - name: Download macOS Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: macos @@ -297,10 +297,10 @@ jobs: GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Download macOS Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: macos @@ -331,7 +331,7 @@ jobs: mv appcast_new.xml appcast.xml - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: sparkle path: |- @@ -348,17 +348,17 @@ jobs: GHOSTTY_VERSION: ${{ needs.setup.outputs.version }} steps: - name: Download macOS Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: macos - name: Download Sparkle Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: sparkle - name: Download Source Tarball Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: source-tarball @@ -378,7 +378,7 @@ jobs: mv Ghostty.dmg blob/${GHOSTTY_VERSION}/Ghostty.dmg mv appcast.xml blob/${GHOSTTY_VERSION}/appcast-staged.xml - name: Upload to R2 - uses: ryand56/r2-upload-action@latest + 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 }} diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 4d009ab7b..b0916e657 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -19,7 +19,7 @@ jobs: runs-on: namespace-profile-ghostty-sm needs: [build-macos] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Tip Tag run: | git config user.name "github-actions[bot]" @@ -31,7 +31,7 @@ jobs: runs-on: namespace-profile-ghostty-sm needs: [build-macos-debug-slow] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install sentry-cli run: | @@ -52,7 +52,7 @@ jobs: runs-on: namespace-profile-ghostty-sm needs: [build-macos-debug-fast] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install sentry-cli run: | @@ -73,7 +73,7 @@ jobs: runs-on: namespace-profile-ghostty-sm needs: [build-macos] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install sentry-cli run: | @@ -105,17 +105,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -132,7 +132,7 @@ jobs: nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password - name: Update Release - uses: softprops/action-gh-release@v2.3.2 + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -158,16 +158,16 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Important so that build number generation works fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -299,7 +299,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2.3.2 + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -331,7 +331,7 @@ jobs: cp Ghostty.dmg blob/${GHOSTTY_COMMIT_LONG}/Ghostty.dmg - name: Upload to R2 - uses: ryand56/r2-upload-action@latest + uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 with: r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }} r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }} @@ -349,7 +349,7 @@ jobs: cp appcast_new.xml blob/appcast.xml - name: Upload Appcast to R2 - uses: ryand56/r2-upload-action@latest + uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 with: r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }} r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }} @@ -373,16 +373,16 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Important so that build number generation works fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -507,7 +507,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2.3.2 + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -524,7 +524,7 @@ jobs: cp ghostty-macos-universal-debug-slow.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow.zip cp ghostty-macos-universal-debug-slow-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow-dsym.zip - name: Upload to R2 - uses: ryand56/r2-upload-action@latest + uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 with: r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }} r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }} @@ -548,16 +548,16 @@ jobs: timeout-minutes: 90 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Important so that build number generation works fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -682,7 +682,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2.3.2 + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -699,7 +699,7 @@ jobs: cp ghostty-macos-universal-debug-fast.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast.zip cp ghostty-macos-universal-debug-fast-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast-dsym.zip - name: Upload to R2 - uses: ryand56/r2-upload-action@latest + uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 with: r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }} r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b34327f7d..8af7140c6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,7 @@ jobs: - test-gtk - test-sentry-linux - test-macos + - pinact - prettier - alejandra - typos @@ -64,20 +65,20 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -95,20 +96,20 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -131,20 +132,20 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -160,20 +161,20 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -193,20 +194,20 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -237,20 +238,20 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -262,7 +263,7 @@ jobs: cp zig-out/dist/*.tar.gz ghostty-source.tar.gz - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: source-tarball path: |- @@ -273,13 +274,13 @@ jobs: needs: test steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -313,13 +314,13 @@ jobs: needs: test steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 - uses: DeterminateSystems/nix-installer-action@main with: determinate: true - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -353,13 +354,13 @@ jobs: needs: test steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -400,7 +401,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Download Source Tarball Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: source-tarball - name: Extract tarball @@ -408,7 +409,7 @@ jobs: mkdir dist tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix @@ -420,7 +421,7 @@ jobs: _LXD_SNAP_DEVCGROUP_CONFIG="/var/lib/snapd/cgroup/snap.lxd.device" sudo mkdir -p /var/lib/snapd/cgroup echo 'self-managed=true' | sudo tee "${_LXD_SNAP_DEVCGROUP_CONFIG}" - - uses: snapcore/action-build@v1 + - uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0 with: path: dist @@ -431,7 +432,7 @@ jobs: needs: test steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # This could be from a script if we wanted to but inlining here for now # in one place. @@ -500,20 +501,20 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -542,20 +543,20 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -581,20 +582,20 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -608,13 +609,13 @@ jobs: needs: test steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -637,17 +638,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@v4 # Check out repo so we can lint it + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -656,6 +657,34 @@ jobs: - name: zig fmt run: nix develop -c zig fmt --check . + pinact: + name: "GitHub Actions Pins" + if: github.repository == 'ghostty-org/ghostty' + runs-on: namespace-profile-ghostty-xsm + timeout-minutes: 60 + env: + ZIG_LOCAL_CACHE_DIR: /zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + with: + path: | + /nix + /zig + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + skipPush: true + useDaemon: false # sometimes fails on short jobs + - name: pinact check + run: nix develop -c pinact run --check + prettier: if: github.repository == 'ghostty-org/ghostty' runs-on: namespace-profile-ghostty-xsm @@ -664,17 +693,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@v4 # Check out repo so we can lint it + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -691,17 +720,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@v4 # Check out repo so we can lint it + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -718,17 +747,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@v4 # Check out repo so we can lint it + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -745,17 +774,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@v4 # Check out repo so we can lint it + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -772,17 +801,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@v4 # Check out repo so we can lint it + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -806,20 +835,20 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -834,13 +863,13 @@ jobs: needs: [test, build-dist] steps: - name: Install and configure Namespace CLI - uses: namespacelabs/nscloud-setup@v0 + uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10 - name: Configure Namespace powered Buildx - uses: namespacelabs/nscloud-setup-buildx-action@v0 + uses: namespacelabs/nscloud-setup-buildx-action@01628ae51ea5d6b0c90109c7dccbf511953aff29 # v0.0.18 - name: Download Source Tarball Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: source-tarball @@ -850,7 +879,7 @@ jobs: tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: dist file: dist/src/build/docker/debian/Dockerfile @@ -865,18 +894,18 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@v31 + uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -901,8 +930,8 @@ jobs: runs-on: ${{ matrix.variant.runner }} needs: [flatpak-check-zig-cache, test] steps: - - uses: actions/checkout@v4 - - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: flatpak/flatpak-github-actions/flatpak-builder@10a3c29f0162516f0f68006be14c92f34bd4fa6c # v6.5 with: bundle: com.mitchellh.ghostty manifest-path: flatpak/com.mitchellh.ghostty.yml diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 2533285e6..b9ded559e 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -17,22 +17,22 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.8 + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 with: path: | /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@v31 + uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -60,7 +60,7 @@ jobs: run: nix build .#ghostty - name: Create pull request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: title: Update iTerm2 colorschemes base: main diff --git a/nix/devShell.nix b/nix/devShell.nix index f4ea62235..8a8ab441f 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -58,6 +58,7 @@ jq, minisign, pandoc, + pinact, hyperfine, typos, uv, @@ -98,6 +99,7 @@ in # Linting nodePackages.prettier alejandra + pinact typos # Testing From 7de4d569b0c9483c5945f277f616ef40a821e82f Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 10:49:00 -0600 Subject: [PATCH 15/36] Revert "rely on stdin/stdout instead of hardcoded paths" This reverts commit 2fca0477bc7f3c955daf40a0d4663d63ef3d76a1. The idea of using stdin and stdout was the integrate it in to the build script, but since we don't want to do that because it contains an eval, it just makes it more annoying to use. --- src/font/nerd_font_codegen.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index 52d70ac01..99915c9f2 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -10,9 +10,9 @@ be safe and not malicious or anything. import ast import math -import sys from collections import defaultdict from contextlib import suppress +from pathlib import Path from types import SimpleNamespace from typing import Literal, TypedDict, cast @@ -236,9 +236,16 @@ def generate_zig_switch_arms(patch_sets: list[PatchSet]) -> str: if __name__ == "__main__": - source = sys.stdin.read() + project_root = Path(__file__).resolve().parents[2] + + patcher_path = project_root / "vendor" / "nerd-fonts" / "font-patcher.py" + source = patcher_path.read_text(encoding="utf-8") patch_set = extract_patch_set_values(source) - print("""//! This is a generated file, produced by nerd_font_codegen.py + + out_path = project_root / "src" / "font" / "nerd_font_attributes.zig" + + with out_path.open("w", encoding="utf-8") as f: + f.write("""//! This is a generated file, produced by nerd_font_codegen.py //! DO NOT EDIT BY HAND! //! //! This file provides info extracted from the nerd fonts patcher script, @@ -248,6 +255,7 @@ const Constraint = @import("face.zig").RenderOptions.Constraint; /// Get the a constraints for the provided codepoint. pub fn getConstraint(cp: u21) Constraint { - return switch (cp) {""") - print(generate_zig_switch_arms(patch_set)) - print(" else => .none,\n };\n}") + return switch (cp) { +""") + f.write(generate_zig_switch_arms(patch_set)) + f.write("\n else => .none,\n };\n}\n") From 8f989f6bfd44b60cfe492031b3ba247be89dd041 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 11:02:00 -0600 Subject: [PATCH 16/36] font: fix nerd font codegen to handle ypadding properly Previously `ypadding` was effectively ignored, since it's mutually exclusive with `overlap`. This had a noticeable effect on the heavy bracket characters U+276C...U+2771, which were much taller than they should have been. I also fixed the vertical overlap limit, since negative `overlap` values are used in the nerd font attributes to create padding on all sides of the cell, so we don't want to limit the magnitude of the overlap for vertical padding, we only want to limit it if the value is positive. That change fixed the vertical padding for a handful of ranges, which should give more consistent results. --- src/font/nerd_font_attributes.zig | 22 ++++++++++++---------- src/font/nerd_font_codegen.py | 11 ++++++++++- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index dfb11c5a5..a82699b0e 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -28,8 +28,8 @@ pub fn getConstraint(cp: u21) Constraint { .align_vertical = .center, .pad_left = 0.1, .pad_right = 0.1, - .pad_top = 0.01, - .pad_bottom = 0.01, + .pad_top = 0.1, + .pad_bottom = 0.1, }, 0x276c...0x2771, => .{ @@ -37,6 +37,8 @@ pub fn getConstraint(cp: u21) Constraint { .size_vertical = .fit, .align_horizontal = .center, .align_vertical = .center, + .pad_top = 0.3, + .pad_bottom = 0.3, }, 0xe0b0, => .{ @@ -204,8 +206,8 @@ pub fn getConstraint(cp: u21) Constraint { .align_vertical = .center, .pad_left = 0.03, .pad_right = 0.03, - .pad_top = 0.01, - .pad_bottom = 0.01, + .pad_top = 0.03, + .pad_bottom = 0.03, .max_xy_ratio = 0.86, }, 0xe0c5, @@ -216,8 +218,8 @@ pub fn getConstraint(cp: u21) Constraint { .align_vertical = .center, .pad_left = 0.03, .pad_right = 0.03, - .pad_top = 0.01, - .pad_bottom = 0.01, + .pad_top = 0.03, + .pad_bottom = 0.03, .max_xy_ratio = 0.86, }, 0xe0c6, @@ -228,8 +230,8 @@ pub fn getConstraint(cp: u21) Constraint { .align_vertical = .center, .pad_left = 0.03, .pad_right = 0.03, - .pad_top = 0.01, - .pad_bottom = 0.01, + .pad_top = 0.03, + .pad_bottom = 0.03, .max_xy_ratio = 0.78, }, 0xe0c7, @@ -240,8 +242,8 @@ pub fn getConstraint(cp: u21) Constraint { .align_vertical = .center, .pad_left = 0.03, .pad_right = 0.03, - .pad_top = 0.01, - .pad_bottom = 0.01, + .pad_top = 0.03, + .pad_bottom = 0.03, .max_xy_ratio = 0.78, }, 0xe0cc, diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index 99915c9f2..b62768cc5 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -6,6 +6,8 @@ attributes and scaling rules. This does include an `eval` call! This is spooky, but we trust the nerd fonts code to be safe and not malicious or anything. + +This script requires Python 3.12 or greater. """ import ast @@ -193,13 +195,20 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) if valign is not None: s += f" .align_vertical = {valign},\n" + # `overlap` and `ypadding` are mutually exclusive, + # this is asserted in the nerd fonts patcher itself. if overlap: pad = -overlap s += f" .pad_left = {pad},\n" s += f" .pad_right = {pad},\n" - v_pad = y_padding - math.copysign(min(0.01, abs(overlap)), overlap) + # In the nerd fonts patcher, overlap values + # are capped at 0.01 in the vertical direction. + v_pad = -min(0.01, overlap) s += f" .pad_top = {v_pad},\n" s += f" .pad_bottom = {v_pad},\n" + elif y_padding: + s += f" .pad_top = {y_padding},\n" + s += f" .pad_bottom = {y_padding},\n" if xy_ratio > 0: s += f" .max_xy_ratio = {xy_ratio},\n" From 3cf56b8af3d5766210bc25202d0f82e15ce0ce6d Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 6 Jul 2025 12:12:27 -0500 Subject: [PATCH 17/36] keybind: add copy_title action Fixes #7829 This will copy the terminal title to the clipboard. If the terminal title is not set it has no effect. --- src/Surface.zig | 11 +++++++++++ src/input/Binding.zig | 5 +++++ src/input/command.zig | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index d9d6a3012..faba60ced 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4443,6 +4443,17 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool return false; }, + .copy_title => { + const title = self.rt_surface.getTitle() orelse return false; + + self.rt_surface.setClipboardString(title, .standard, false) catch |err| { + log.err("error copying title to clipboard err={}", .{err}); + return true; + }; + + return true; + }, + .paste_from_clipboard => try self.startClipboardRequest( .standard, .{ .paste = {} }, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index c342c9cc2..6339674f8 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -281,6 +281,10 @@ pub const Action = union(enum) { /// If there is a URL under the cursor, copy it to the default clipboard. copy_url_to_clipboard, + /// Copy the terminal title to the clipboard. If the terminal title is not + /// set this has no effect. + copy_title, + /// Increase the font size by the specified amount in points (pt). /// /// For example, `increase_font_size:1.5` will increase the font size @@ -1005,6 +1009,7 @@ pub const Action = union(enum) { .reset, .copy_to_clipboard, .copy_url_to_clipboard, + .copy_title, .paste_from_clipboard, .paste_from_selection, .increase_font_size, diff --git a/src/input/command.zig b/src/input/command.zig index 8938835d0..434a4edf3 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -132,6 +132,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Copy the URL under the cursor to the clipboard.", }}, + .copy_title => comptime &.{.{ + .action = .copy_title, + .title = "Copy Terminal Title to Clipboard", + .description = "Copy the terminal title to the clipboard. If the terminal title is not set this has no effect.", + }}, + .paste_from_clipboard => comptime &.{.{ .action = .paste_from_clipboard, .title = "Paste from Clipboard", From c7e65b0c1c75c1512bab068a0c37a22f417bd0e4 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 11:23:49 -0600 Subject: [PATCH 18/36] font: respect cell width attributes in nerd font constraints This mostly applies to powerline glyphs, but is also relevant for heavy bracket characters, which need to always be 1 wide otherwise they look silly because they misalign depending on if there's a space after them or not. --- src/font/face.zig | 27 ++++++++++++++++++++------- src/font/face/coretext.zig | 3 ++- src/font/face/freetype.zig | 3 ++- src/font/nerd_font_attributes.zig | 19 +++++++++++++++++++ src/font/nerd_font_codegen.py | 6 ++++++ 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/font/face.zig b/src/font/face.zig index 363576ff0..245edcf4b 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -147,6 +147,9 @@ pub const RenderOptions = struct { /// Maximum ratio of width to height when resizing. max_xy_ratio: ?f64 = null, + /// Maximum number of cells horizontally to use. + max_constraint_width: u2 = 2, + pub const Size = enum { /// Don't change the size of this glyph. none, @@ -186,16 +189,26 @@ pub const RenderOptions = struct { pub fn constrain( self: Constraint, glyph: GlyphSize, - /// Available width + /// Width of one cell. cell_width: f64, - /// Available height + /// Height of one cell. cell_height: f64, + /// Number of cells horizontally available for this glyph. + constraint_width: u2, ) GlyphSize { var g = glyph; - const w = cell_width - - self.pad_left * cell_width - - self.pad_right * cell_width; + const available_width = + cell_width * @as(f64, @floatFromInt( + @min( + self.max_constraint_width, + constraint_width, + ), + )); + + const w = available_width - + self.pad_left * available_width - + self.pad_right * available_width; const h = cell_height - self.pad_top * cell_height - self.pad_bottom * cell_height; @@ -203,7 +216,7 @@ pub const RenderOptions = struct { // Subtract padding from the bearings so that our // alignment and sizing code works correctly. We // re-add before returning. - g.x -= self.pad_left * cell_width; + g.x -= self.pad_left * available_width; g.y -= self.pad_bottom * cell_height; switch (self.size_horizontal) { @@ -305,7 +318,7 @@ pub const RenderOptions = struct { } // Re-add our padding before returning. - g.x += self.pad_left * cell_width; + g.x += self.pad_left * available_width; g.y += self.pad_bottom * cell_height; return g; diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 89d771d95..5c9c259d2 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -337,7 +337,7 @@ pub const Face = struct { }; const metrics = opts.grid_metrics; - const cell_width: f64 = @floatFromInt(metrics.cell_width * opts.constraint_width); + const cell_width: f64 = @floatFromInt(metrics.cell_width); const cell_height: f64 = @floatFromInt(metrics.cell_height); const glyph_size = opts.constraint.constrain( @@ -349,6 +349,7 @@ pub const Face = struct { }, cell_width, cell_height, + opts.constraint_width, ); const width = glyph_size.width; diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 585d21c5b..b27b28ab8 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -390,7 +390,7 @@ pub const Face = struct { // Next we need to apply any constraints. const metrics = opts.grid_metrics; - const cell_width: f64 = @floatFromInt(metrics.cell_width * opts.constraint_width); + const cell_width: f64 = @floatFromInt(metrics.cell_width); const cell_height: f64 = @floatFromInt(metrics.cell_height); const glyph_x: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingX); @@ -405,6 +405,7 @@ pub const Face = struct { }, cell_width, cell_height, + opts.constraint_width, ); const width = glyph_size.width; diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index a82699b0e..817d838f8 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -13,6 +13,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .center, .align_vertical = .center, .pad_left = -0.02, @@ -24,6 +25,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .cover, .size_vertical = .fit, + .max_constraint_width = 1, .align_horizontal = .center, .align_vertical = .center, .pad_left = 0.1, @@ -35,6 +37,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .cover, .size_vertical = .fit, + .max_constraint_width = 1, .align_horizontal = .center, .align_vertical = .center, .pad_top = 0.3, @@ -44,6 +47,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center, .pad_left = -0.06, @@ -56,6 +60,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center, .max_xy_ratio = 0.7, @@ -64,6 +69,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center, .pad_left = -0.06, @@ -76,6 +82,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center, .max_xy_ratio = 0.7, @@ -84,6 +91,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center, .pad_left = -0.06, @@ -96,6 +104,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center, .max_xy_ratio = 0.5, @@ -104,6 +113,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center, .pad_left = -0.06, @@ -116,6 +126,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center, .max_xy_ratio = 0.5, @@ -125,6 +136,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center, .pad_left = -0.05, @@ -137,6 +149,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center, }, @@ -145,6 +158,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center, .pad_left = -0.05, @@ -157,6 +171,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center, }, @@ -287,6 +302,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center, .pad_left = -0.02, @@ -299,6 +315,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center, .pad_left = -0.02, @@ -311,6 +328,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center, .pad_left = -0.05, @@ -323,6 +341,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .stretch, .size_vertical = .stretch, + .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center, .pad_left = -0.05, diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index b62768cc5..f8ff7caa6 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -190,6 +190,12 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) s += " .size_horizontal = .fit,\n" s += " .size_vertical = .fit,\n" + # There are two cases where we want to limit the constraint width to 1: + # - If there's a `1` in the stretch mode string. + # - If the stretch mode is `xy` and there's not an explicit `2`. + if "1" in stretch or ("xy" in stretch and "2" not in stretch): + s += " .max_constraint_width = 1,\n" + if align is not None: s += f" .align_horizontal = {align},\n" if valign is not None: From 871f90e4485b3b123b8f9bd4e3327efa7d1ecd5a Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 6 Jul 2025 12:58:55 -0500 Subject: [PATCH 19/36] linux/kde: add KDE shortcut to desktop file Fixes #7673 This adds `Ctrl+Alt+T` as a KDE shortcut to the desktop file. If Konsole is installed (or any other prorgam that has the same shortcut) the user will need to go into the KDE system settings and manually reassign the `Ctrl+Alt+T` shortcut to Ghostty. If Ghostty is the only terminal installed that claims that shortcut KDE _should_ automatically enable the shortcut (but YMMV). Non-KDE systems will ignore this setting and if the user desires a global shortcut to open a Ghostty window it will need to be accomplished in other ways. --- dist/linux/app.desktop.in | 1 + 1 file changed, 1 insertion(+) diff --git a/dist/linux/app.desktop.in b/dist/linux/app.desktop.in index c39164158..32ba00cfd 100644 --- a/dist/linux/app.desktop.in +++ b/dist/linux/app.desktop.in @@ -19,6 +19,7 @@ X-TerminalArgAppId=--class= X-TerminalArgDir=--working-directory= X-TerminalArgHold=--wait-after-command DBusActivatable=true +X-KDE-Shortcuts=Ctrl+Alt+T [Desktop Action new-window] Name=New Window From a23b5328a5e230fd29cb53a0f468f7eca5149c3d Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 6 Jul 2025 13:12:00 -0500 Subject: [PATCH 20/36] keybind: rename copy_title to copy_title_to_clipboard Co-authored-by: Jon Parise --- src/Surface.zig | 2 +- src/input/Binding.zig | 4 ++-- src/input/command.zig | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index faba60ced..30235f26f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4443,7 +4443,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool return false; }, - .copy_title => { + .copy_title_to_clipboard => { const title = self.rt_surface.getTitle() orelse return false; self.rt_surface.setClipboardString(title, .standard, false) catch |err| { diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 6339674f8..21edf2636 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -283,7 +283,7 @@ pub const Action = union(enum) { /// Copy the terminal title to the clipboard. If the terminal title is not /// set this has no effect. - copy_title, + copy_title_to_clipboard, /// Increase the font size by the specified amount in points (pt). /// @@ -1009,7 +1009,7 @@ pub const Action = union(enum) { .reset, .copy_to_clipboard, .copy_url_to_clipboard, - .copy_title, + .copy_title_to_clipboard, .paste_from_clipboard, .paste_from_selection, .increase_font_size, diff --git a/src/input/command.zig b/src/input/command.zig index 434a4edf3..84e9afc79 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -132,8 +132,8 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Copy the URL under the cursor to the clipboard.", }}, - .copy_title => comptime &.{.{ - .action = .copy_title, + .copy_title_to_clipboard => comptime &.{.{ + .action = .copy_title_to_clipboard, .title = "Copy Terminal Title to Clipboard", .description = "Copy the terminal title to the clipboard. If the terminal title is not set this has no effect.", }}, From 7884872d4ec15b8a42f599443a337a1894d232c3 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 6 Jul 2025 14:15:11 -0500 Subject: [PATCH 21/36] keybind: don't clobber the clipboard if the title is empty --- src/Surface.zig | 1 + src/input/Binding.zig | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 30235f26f..372da325a 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4445,6 +4445,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .copy_title_to_clipboard => { const title = self.rt_surface.getTitle() orelse return false; + if (title.len == 0) return false; self.rt_surface.setClipboardString(title, .standard, false) catch |err| { log.err("error copying title to clipboard err={}", .{err}); diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 21edf2636..f76da360a 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -282,7 +282,7 @@ pub const Action = union(enum) { copy_url_to_clipboard, /// Copy the terminal title to the clipboard. If the terminal title is not - /// set this has no effect. + /// set or is empty this has no effect. copy_title_to_clipboard, /// Increase the font size by the specified amount in points (pt). From 52790fb92c0ba71bcd68c159e2f700c583097cc1 Mon Sep 17 00:00:00 2001 From: Robert Ian Hawdon Date: Wed, 23 Apr 2025 12:03:26 +0100 Subject: [PATCH 22/36] Added bold-color option --- src/config.zig | 1 + src/config/Config.zig | 113 ++++++++++++++++++++++++++++++++++++++- src/renderer/generic.zig | 29 ++++++---- src/terminal/style.zig | 69 +++++++++++++++++++----- 4 files changed, 187 insertions(+), 25 deletions(-) diff --git a/src/config.zig b/src/config.zig index b6fecde4e..efc9fd973 100644 --- a/src/config.zig +++ b/src/config.zig @@ -14,6 +14,7 @@ pub const entryFormatter = formatter.entryFormatter; pub const formatEntry = formatter.formatEntry; // Field types +pub const BoldColor = Config.BoldColor; pub const ClipboardAccess = Config.ClipboardAccess; pub const Command = Config.Command; pub const ConfirmCloseSurface = Config.ConfirmCloseSurface; diff --git a/src/config/Config.zig b/src/config/Config.zig index f9454bf98..a53986bc9 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -69,6 +69,10 @@ pub const compatibility = std.StaticStringMap( // this behavior. This applies to selection too. .{ "cursor-invert-fg-bg", compatCursorInvertFgBg }, .{ "selection-invert-fg-bg", compatSelectionInvertFgBg }, + + // Ghostty 1.2 merged `bold-is-bright` into the new `bold-color` + // by setting the value to "bright". + .{ "bold-is-bright", compatBoldIsBright }, }); /// The font families to use. @@ -2804,8 +2808,24 @@ else /// notifications using certain escape sequences such as OSC 9 or OSC 777. @"desktop-notifications": bool = true, -/// If `true`, the bold text will use the bright color palette. -@"bold-is-bright": bool = false, +/// Modifies the color used for bold text in the terminal. +/// +/// This can be set to a specific color, using the same format as +/// `background` or `foreground` (e.g. `#RRGGBB` but other formats +/// are also supported; see the aforementioned documentation). If a +/// specific color is set, this color will always be used for all +/// bold text regardless of the terminal's color scheme. +/// +/// This can also be set to `bright`, which uses the bright color palette +/// for bold text. For example, if the text is red, then the bold will +/// use the bright red color. The terminal palette is set with `palette` +/// but can also be overridden by the terminal application itself using +/// escape sequences such as OSC 4. (Since Ghostty 1.2.0, the previous +/// configuration `bold-is-bright` is deprecated and replaced by this +/// usage). +/// +/// Available since Ghostty 1.2.0. +@"bold-color": ?BoldColor = null, /// This will be used to set the `TERM` environment variable. /// HACK: We set this with an `xterm` prefix because vim uses that to enable key @@ -3910,6 +3930,23 @@ fn compatSelectionInvertFgBg( return true; } +fn compatBoldIsBright( + self: *Config, + alloc: Allocator, + key: []const u8, + value_: ?[]const u8, +) bool { + _ = alloc; + assert(std.mem.eql(u8, key, "bold-is-bright")); + + const set = cli.args.parseBool(value_ orelse "t") catch return false; + if (set) { + self.@"bold-color" = .bright; + } + + return true; +} + /// Create a shallow copy of this config. This will share all the memory /// allocated with the previous config but will have a new arena for /// any changes or new allocations. The config should have `deinit` @@ -4537,6 +4574,58 @@ pub const TerminalColor = union(enum) { } }; +/// Represents color values that can be used for bold. See `bold-color`. +pub const BoldColor = union(enum) { + color: Color, + bright, + + pub fn parseCLI(input_: ?[]const u8) !BoldColor { + const input = input_ orelse return error.ValueRequired; + if (std.mem.eql(u8, input, "bright")) return .bright; + return .{ .color = try Color.parseCLI(input) }; + } + + /// Used by Formatter + pub fn formatEntry(self: BoldColor, formatter: anytype) !void { + switch (self) { + .color => try self.color.formatEntry(formatter), + .bright => try formatter.formatEntry( + [:0]const u8, + @tagName(self), + ), + } + } + + test "parseCLI" { + const testing = std.testing; + + try testing.expectEqual( + BoldColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } }, + try BoldColor.parseCLI("#4e2a84"), + ); + try testing.expectEqual( + BoldColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } }, + try BoldColor.parseCLI("black"), + ); + try testing.expectEqual( + BoldColor.bright, + try BoldColor.parseCLI("bright"), + ); + + try testing.expectError(error.InvalidValue, BoldColor.parseCLI("a")); + } + + test "formatConfig" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var sc: BoldColor = .bright; + try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try testing.expectEqualSlices(u8, "a = bright\n", buf.items); + } +}; + pub const ColorList = struct { const Self = @This(); @@ -8236,3 +8325,23 @@ test "compatibility: removed selection-invert-fg-bg" { ); } } + +test "compatibility: removed bold-is-bright" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var cfg = try Config.default(alloc); + defer cfg.deinit(); + var it: TestIterator = .{ .data = &.{ + "--bold-is-bright", + } }; + try cfg.loadIter(alloc, &it); + try cfg.finalize(); + + try testing.expectEqual( + BoldColor.bright, + cfg.@"bold-color", + ); + } +} diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 829563075..3965d302a 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -519,7 +519,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { foreground: terminal.color.RGB, selection_background: ?configpkg.Config.TerminalColor, selection_foreground: ?configpkg.Config.TerminalColor, - bold_is_bright: bool, + bold_color: ?configpkg.BoldColor, min_contrast: f32, padding_color: configpkg.WindowPaddingColor, custom_shaders: configpkg.RepeatablePath, @@ -580,7 +580,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .background = config.background.toTerminalRGB(), .foreground = config.foreground.toTerminalRGB(), - .bold_is_bright = config.@"bold-is-bright", + .bold_color = config.@"bold-color", + .min_contrast = @floatCast(config.@"minimum-contrast"), .padding_color = config.@"window-padding-color", @@ -2540,10 +2541,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // the cell style (SGR), before applying any additional // configuration, inversions, selections, etc. const bg_style = style.bg(cell, color_palette); - const fg_style = style.fg( - color_palette, - self.config.bold_is_bright, - ) orelse self.foreground_color orelse self.default_foreground_color; + const fg_style = style.fg(.{ + .default = self.foreground_color orelse self.default_foreground_color, + .palette = color_palette, + .bold = self.config.bold_color, + }); // The final background color for the cell. const bg = bg: { @@ -2801,10 +2803,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .@"cell-background", => |_, tag| { const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - const fg_style = sty.fg( - color_palette, - self.config.bold_is_bright, - ) orelse self.foreground_color orelse self.default_foreground_color; + const fg_style = sty.fg(.{ + .default = self.foreground_color orelse self.default_foreground_color, + .palette = color_palette, + .bold = self.config.bold_color, + }); const bg_style = sty.bg( screen.cursor.page_cell, color_palette, @@ -2852,7 +2855,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - const fg_style = sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color; + const fg_style = sty.fg(.{ + .default = self.foreground_color orelse self.default_foreground_color, + .palette = color_palette, + .bold = self.config.bold_color, + }); const bg_style = sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color; break :blk switch (txt) { diff --git a/src/terminal/style.zig b/src/terminal/style.zig index 865e15f64..78afcdf39 100644 --- a/src/terminal/style.zig +++ b/src/terminal/style.zig @@ -1,5 +1,6 @@ const std = @import("std"); const assert = std.debug.assert; +const configpkg = @import("../config.zig"); const color = @import("color.zig"); const sgr = @import("sgr.zig"); const page = @import("page.zig"); @@ -115,24 +116,68 @@ pub const Style = struct { }; } - /// Returns the fg color for a cell with this style given the palette. + pub const Fg = struct { + /// The default color to use if the style doesn't specify a + /// foreground color and no configuration options override + /// it. + default: color.RGB, + + /// The current color palette. Required to map palette indices to + /// real color values. + palette: *const color.Palette, + + /// If specified, the color to use for bold text. + bold: ?configpkg.BoldColor = null, + }; + + /// Returns the fg color for a cell with this style given the palette + /// and various configuration options. pub fn fg( self: Style, - palette: *const color.Palette, - bold_is_bright: bool, - ) ?color.RGB { + opts: Fg, + ) color.RGB { + // Note we don't pull the bold check to the top-level here because + // we don't want to duplicate the conditional multiple times since + // certain colors require more checks (e.g. `bold_is_bright`). + return switch (self.fg_color) { - .none => null, - .palette => |idx| palette: { - if (bold_is_bright and self.flags.bold) { - const bright_offset = @intFromEnum(color.Name.bright_black); - if (idx < bright_offset) - break :palette palette[idx + bright_offset]; + .none => default: { + if (self.flags.bold) { + if (opts.bold) |bold| switch (bold) { + .bright => {}, + .color => |v| break :default v.toTerminalRGB(), + }; } - break :palette palette[idx]; + break :default opts.default; + }, + + .palette => |idx| palette: { + if (self.flags.bold) { + if (opts.bold) |bold| switch (bold) { + .color => |v| break :palette v.toTerminalRGB(), + .bright => { + const bright_offset = @intFromEnum(color.Name.bright_black); + if (idx < bright_offset) { + break :palette opts.palette[idx + bright_offset]; + } + }, + }; + } + + break :palette opts.palette[idx]; + }, + + .rgb => |rgb| rgb: { + if (self.flags.bold and rgb.eql(opts.default)) { + if (opts.bold) |bold| switch (bold) { + .color => |v| break :rgb v.toTerminalRGB(), + .bright => {}, + }; + } + + break :rgb rgb; }, - .rgb => |rgb| rgb, }; } From 3ff11cdd86cd8a5a0d26631a8935edf5f1dfeede Mon Sep 17 00:00:00 2001 From: "Sl (Shahaf Levi)" Date: Sun, 6 Jul 2025 23:38:22 +0300 Subject: [PATCH 23/36] Add Hebrew Translations --- CODEOWNERS | 1 + po/he_IL.UTF-8.po | 298 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 1 + 3 files changed, 300 insertions(+) create mode 100644 po/he_IL.UTF-8.po diff --git a/CODEOWNERS b/CODEOWNERS index 7995650b7..3bb6a4123 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -181,6 +181,7 @@ /po/zh_CN.UTF-8.po @ghostty-org/zh_CN /po/ga_IE.UTF-8.po @ghostty-org/ga_IE /po/ko_KR.UTF-8.po @ghostty-org/ko_KR +/po/he_IL.UTF-8.po @ghostty-org/he_IL # Packaging - Snap /snap/ @ghostty-org/snap diff --git a/po/he_IL.UTF-8.po b/po/he_IL.UTF-8.po new file mode 100644 index 000000000..636bf46e3 --- /dev/null +++ b/po/he_IL.UTF-8.po @@ -0,0 +1,298 @@ +# Hebrew translations for com.mitchellh.ghostty. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Sl (Shahaf Levi), Sl's Repository Ltd , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-06-28 17:01+0200\n" +"PO-Revision-Date: 2025-03-13 00:00+0000\n" +"Last-Translator: Sl (Shahaf Levi), Sl's Repository Ltd \n" +"Language-Team: Hebrew \n" +"Language: he\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "שינוי כותרת המסוף" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "השאר/י ריק כדי לשחזר את כותרת ברירת המחדל." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "ביטול" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "אישור" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "שגיאות בהגדרות" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "נמצאו אחת או יותר שגיאות בהגדרות. אנא בדוק/י את השגיאות המופיעות מטה ולאחר מכן טען/י את ההגדרות מחדש או התעלם/י מהשגיאות." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "התעלמות" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Reload Configuration" +msgstr "טעינה מחדש של ההגדרות" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "פיצול למעלה" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "פיצול למטה" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "פיצול שמאלה" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "פיצול ימינה" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "הרץ/י פקודה…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "העתקה" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 +msgid "Paste" +msgstr "הדבקה" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "ניקוי" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "איפוס" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "פיצול" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "שינוי כותרת…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "כרטיסייה" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:263 +msgid "New Tab" +msgstr "כרטיסייה חדשה" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "סגור/י כרטיסייה" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "חלון" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "חלון חדש" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "סגור/י חלון" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "הגדרות" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Open Configuration" +msgstr "פתיחת ההגדרות" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "לוח פקודות" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Terminal Inspector" +msgstr "בודק המסוף" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1036 +msgid "About Ghostty" +msgstr "אודות Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 +msgid "Quit" +msgstr "יציאה" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "אשר/י גישה ללוח ההעתקה" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "יש אפליקציה שמנסה לקרוא מלוח ההעתקה. התוכן הנוכחי של הלוח מופיע למטה." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "דחייה" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "אישור" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "זכור/י את הבחירה עבור פיצול זה" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "טען/י את ההגדרות מחדש כדי להציג את הבקשה הזו שוב" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "יש אפליקציה שמנסה לכתוב לתוך לוח ההעתקה. התוכן הנוכחי של הלוח מופיע למטה." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "אזהרה: ההדבקה עלולה להיות מסוכנת" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "הדבקת טקסט זה במסוף עלולה להיות מסוכנת, מכיוון שככל הנראה היא תוביל להרצה של פקודות מסוימות." + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "תפריט ראשי" + +#: src/apprt/gtk/Window.zig:238 +msgid "View Open Tabs" +msgstr "הצג/י כרטיסיות פתוחות" + +#: src/apprt/gtk/Window.zig:264 +msgid "New Split" +msgstr "פיצול חדש" + +#: src/apprt/gtk/Window.zig:327 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ את/ה מריץ/ה גרסת ניפוי שגיאות של Ghostty! הביצועים יהיו ירודים." + +#: src/apprt/gtk/Window.zig:773 +msgid "Reloaded the configuration" +msgstr "ההגדרות הוטענו מחדש" + +#: src/apprt/gtk/Window.zig:1017 +msgid "Ghostty Developers" +msgstr "המפתחים של Ghostty" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: בודק המסוף" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "סגירה" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "לצאת מGhostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "לסגור את החלון?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "לסגור את הכרטיסייה?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "לסגור את הפיצול?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "כל הפעלות המסוף יסתיימו." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "כל הפעלות המסוף בחלון זה יסתיימו." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "כל הפעלות המסוף בכרטיסייה זו יסתיימו." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "התהליך שרץ כרגע בפיצול זה יסתיים." + +#: src/apprt/gtk/Surface.zig:1257 +msgid "Copied to clipboard" +msgstr "הועתק ללוח ההעתקה" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index a4d6c1577..2ecae27ac 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -49,6 +49,7 @@ pub const locales = [_][:0]const u8{ "ca_ES.UTF-8", "bg_BG.UTF-8", "ga_IE.UTF-8", + "he_IL.UTF-8", }; /// Set for faster membership lookup of locales. From b10b0f06c329b865b72ee40c3122f009c457f2d4 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 15:53:59 -0600 Subject: [PATCH 24/36] font: remove unused fields from Glyph We can reintroduce `advance` if we ever want to do proportional string drawing, but we don't use it anywhere right now. And we also don't need `sprite` anymore since that was just there to disable constraints for sprites back when we did them on the GPU. --- src/font/Glyph.zig | 6 ------ src/font/face/coretext.zig | 6 ------ src/font/face/freetype.zig | 2 -- src/font/face/web_canvas.zig | 1 - src/font/sprite/Face.zig | 3 --- 5 files changed, 18 deletions(-) diff --git a/src/font/Glyph.zig b/src/font/Glyph.zig index fa29e44fa..f99370271 100644 --- a/src/font/Glyph.zig +++ b/src/font/Glyph.zig @@ -17,9 +17,3 @@ offset_y: i32, /// be normalized to be between 0 and 1 prior to use in shaders. atlas_x: u32, atlas_y: u32, - -/// horizontal position to increase drawing position for strings -advance_x: f32, - -/// Whether we drew this glyph ourselves with the sprite font. -sprite: bool = false, diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 5c9c259d2..7d750b0d6 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -333,7 +333,6 @@ pub const Face = struct { .offset_y = 0, .atlas_x = 0, .atlas_y = 0, - .advance_x = 0, }; const metrics = opts.grid_metrics; @@ -498,10 +497,6 @@ pub const Face = struct { break :offset_x result; }; - // Get our advance - var advances: [glyphs.len]macos.graphics.Size = undefined; - _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); - return .{ .width = px_width, .height = px_height, @@ -509,7 +504,6 @@ pub const Face = struct { .offset_y = offset_y, .atlas_x = region.x, .atlas_y = region.y, - .advance_x = @floatCast(advances[0].width), }; } diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index b27b28ab8..079cf5b2d 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -373,7 +373,6 @@ pub const Face = struct { .offset_y = 0, .atlas_x = 0, .atlas_y = 0, - .advance_x = 0, }; // For synthetic bold, we embolden the glyph. @@ -662,7 +661,6 @@ pub const Face = struct { .offset_y = offset_y, .atlas_x = region.x, .atlas_y = region.y, - .advance_x = f26dot6ToFloat(glyph.*.advance.x), }; } diff --git a/src/font/face/web_canvas.zig b/src/font/face/web_canvas.zig index 30540191d..7ea2f0426 100644 --- a/src/font/face/web_canvas.zig +++ b/src/font/face/web_canvas.zig @@ -235,7 +235,6 @@ pub const Face = struct { .offset_y = 0, .atlas_x = region.x, .atlas_y = region.y, - .advance_x = 0, }; } diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index 1463fb38b..dfff8fa75 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -195,7 +195,6 @@ pub fn renderGlyph( .offset_y = 0, .atlas_x = 0, .atlas_y = 0, - .advance_x = 0, }; const metrics = self.metrics; @@ -227,8 +226,6 @@ pub fn renderGlyph( .offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)), .atlas_x = region.x, .atlas_y = region.y, - .advance_x = @floatFromInt(width), - .sprite = true, }; } From 9583ea1b7ae2d2ae40fa03599901fb9378154ab7 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 25 Feb 2025 11:59:46 -0600 Subject: [PATCH 25/36] core/gtk: open urls using an apprt action instead of doing it directly Partial implementation of #5256 This implements the core changes necessary to open urls using an apprt action rather than doing it directly from the core. Implements the open_url action in the GTK and GLFW apprts. Note that this should not be merged until a macOS-savvy developer can add an implementation of the open_url action for the macOS apprt. --- include/ghostty.h | 17 ++++- src/Surface.zig | 20 +++++- src/apprt/action.zig | 49 ++++++++++++- src/apprt/gtk/App.zig | 11 +++ src/os/open.zig | 156 +++++++++++++++++++++++++++++++----------- 5 files changed, 209 insertions(+), 44 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 73c708c6b..16ca21d89 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -662,6 +662,19 @@ typedef struct { bool soft; } ghostty_action_reload_config_s; +// apprt.action.OpenUrlKind +typedef enum { + GHOSTTY_ACTION_OPEN_URL_KIND_TEXT, + GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN, +} ghostty_action_open_url_kind_e; + +// apprt.action.OpenUrl.C +typedef struct { + ghostty_action_open_url_kind_e kind; + const char* url; + uintptr_t len; +} ghostty_action_open_url_s; + // apprt.Action.Key typedef enum { GHOSTTY_ACTION_QUIT, @@ -711,7 +724,8 @@ typedef enum { GHOSTTY_ACTION_RING_BELL, GHOSTTY_ACTION_UNDO, GHOSTTY_ACTION_REDO, - GHOSTTY_ACTION_CHECK_FOR_UPDATES + GHOSTTY_ACTION_CHECK_FOR_UPDATES, + GHOSTTY_ACTION_OPEN_URL, } ghostty_action_tag_e; typedef union { @@ -739,6 +753,7 @@ typedef union { ghostty_action_color_change_s color_change; ghostty_action_reload_config_s reload_config; ghostty_action_config_change_s config_change; + ghostty_action_open_url_s open_url; } ghostty_action_u; typedef struct { diff --git a/src/Surface.zig b/src/Surface.zig index 33cf581af..db272dddc 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3724,7 +3724,11 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { .trim = false, }); defer self.alloc.free(str); - try internal_os.open(self.alloc, .unknown, str); + _ = try self.rt_app.performAction( + .{ .surface = self }, + .open_url, + .{ .kind = .unknown, .url = str }, + ); }, ._open_osc8 => { @@ -3732,7 +3736,11 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { log.warn("failed to get URI for OSC8 hyperlink", .{}); return false; }; - try internal_os.open(self.alloc, .unknown, uri); + _ = try self.rt_app.performAction( + .{ .surface = self }, + .open_url, + .{ .kind = .unknown, .url = uri }, + ); }, } @@ -4957,7 +4965,13 @@ fn writeScreenFile( defer self.alloc.free(pathZ); try self.rt_surface.setClipboardString(pathZ, .standard, false); }, - .open => try internal_os.open(self.alloc, .text, path), + .open => { + _ = try self.rt_app.performAction( + .{ .surface = self }, + .open_url, + .{ .kind = .text, .url = path }, + ); + }, .paste => self.io.queueMessage(try termio.Message.writeReq( self.alloc, path, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index b4c5164c2..6c33a296f 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const assert = std.debug.assert; const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); @@ -267,6 +268,11 @@ pub const Action = union(Key) { check_for_updates, + /// Open a URL using the native OS mechanisms. On macOS this might be `open` + /// or on Linux this might be `xdg-open`. The exact mechanism is up to the + /// apprt. + open_url: OpenUrl, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { quit, @@ -317,6 +323,7 @@ pub const Action = union(Key) { undo, redo, check_for_updates, + open_url, }; /// Sync with: ghostty_action_u @@ -357,7 +364,13 @@ pub const Action = union(Key) { // For ABI compatibility, we expect that this is our union size. // At the time of writing, we don't promise ABI compatibility // so we can change this but I want to be aware of it. - assert(@sizeOf(CValue) == 16); + assert(@sizeOf(CValue) == switch (builtin.target.os.tag) { + .windows => switch (builtin.target.cpu.arch) { + .x86 => 16, + else => 24, + }, + else => 24, + }); } /// Returns the value type for the given key. @@ -614,3 +627,37 @@ pub const ConfigChange = struct { }; } }; + +/// The type of the data at the URL to open. This is used as a hint to +/// potentially open the URL in a different way. +/// Sync with: ghostty_action_open_url_kind_s +pub const OpenUrlKind = enum(c_int) { + text, + unknown, +}; + +/// Open a URL +pub const OpenUrl = struct { + /// The type of data that the URL refers to. + kind: OpenUrlKind, + /// The URL. + url: []const u8, + + // Sync with: ghostty_action_open_url_s + pub const C = extern struct { + /// The type of data that the URL refers to. + kind: OpenUrlKind, + /// The URL (not zero terminated). + url: [*]const u8, + /// The number of bytes in the URL. + len: usize, + }; + + pub fn cval(self: OpenUrl) C { + return .{ + .kind = self.kind, + .url = self.url.ptr, + .len = self.url.len, + }; + } +}; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index c61254fbd..a046291ef 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -519,6 +519,7 @@ pub fn performAction( .secure_input => self.setSecureInput(target, value), .ring_bell => try self.ringBell(target), .toggle_command_palette => try self.toggleCommandPalette(target), + .open_url => self.openUrl(value), // Unimplemented .close_all_windows, @@ -1757,3 +1758,13 @@ fn initActions(self: *App) void { action_map.addAction(action.as(gio.Action)); } } + +// TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html +pub fn openUrl( + app: *App, + value: apprt.action.OpenUrl, +) void { + internal_os.open(app.core_app.alloc, value.kind, value.url) catch |err| { + log.warn("unable to open url: {}", .{err}); + }; +} diff --git a/src/os/open.zig b/src/os/open.zig index ce62a7e0b..6841c76ab 100644 --- a/src/os/open.zig +++ b/src/os/open.zig @@ -2,14 +2,10 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; -const log = std.log.scoped(.@"os-open"); +const apprt = @import("../apprt.zig"); +const CircBuf = @import("../datastruct/circ_buf.zig").CircBuf; -/// The type of the data at the URL to open. This is used as a hint -/// to potentially open the URL in a different way. -pub const Type = enum { - text, - unknown, -}; +const log = std.log.scoped(.@"os-open"); /// Open a URL in the default handling application. /// @@ -18,9 +14,39 @@ pub const Type = enum { /// log output and may allocate from another thread. pub fn open( alloc: Allocator, - typ: Type, + kind: apprt.action.OpenUrlKind, url: []const u8, ) !void { + // Make a copy of the URL so that we can use it in the thread without + // worrying about it getting freed by other threads. + const copy = try alloc.dupe(u8, url); + errdefer alloc.free(copy); + + // Run in a thread so that it never blocks the main thread, no matter how + // long it takes to execute. + const thread = try std.Thread.spawn(.{}, _openThread, .{ alloc, kind, copy }); + + // Don't worry about the thread any more. + thread.detach(); +} + +fn _openThread( + alloc: Allocator, + kind: apprt.action.OpenUrlKind, + url: []const u8, +) void { + _openThreadError(alloc, kind, url) catch |err| { + log.warn("error while opening url: {}", .{err}); + }; +} + +fn _openThreadError( + alloc: Allocator, + kind: apprt.action.OpenUrlKind, + url: []const u8, +) !void { + defer alloc.free(url); + var exe: std.process.Child = switch (builtin.os.tag) { .linux, .freebsd => .init( &.{ "xdg-open", url }, @@ -33,7 +59,7 @@ pub fn open( ), .macos => .init( - switch (typ) { + switch (kind) { .text => &.{ "open", "-t", url }, .unknown => &.{ "open", url }, }, @@ -44,43 +70,95 @@ pub fn open( else => @compileError("unsupported OS"), }; - // Pipe stdout/stderr so we can collect output from the command. + // Ignore stdin & stdout, collect the output from stderr. // This must be set before spawning the process. - exe.stdout_behavior = .Pipe; + exe.stdin_behavior = .Ignore; + exe.stdout_behavior = .Ignore; exe.stderr_behavior = .Pipe; - // Spawn the process on our same thread so we can detect failure - // quickly. - try exe.spawn(); + exe.spawn() catch |err| { + switch (err) { + error.FileNotFound => { + log.warn("Unable to find {s}. Please install {s} and ensure that it is available on the PATH.", .{ + exe.argv[0], + exe.argv[0], + }); + }, + else => |e| return e, + } + return; + }; - // Create a thread that handles collecting output and reaping - // the process. This is done in a separate thread because SOME - // open implementations block and some do not. It's easier to just - // spawn a thread to handle this so that we never block. - const thread = try std.Thread.spawn(.{}, openThread, .{ alloc, exe }); - thread.detach(); -} + const stderr = exe.stderr orelse { + log.warn("Unable to access the stderr of the spawned program!", .{}); + return; + }; -fn openThread(alloc: Allocator, exe_: std.process.Child) !void { - // 50 KiB is the default value used by std.process.Child.run and should - // be enough to get the output we care about. - const output_max_size = 50 * 1024; + var cb = try CircBuf(u8, 0).init(alloc, 50 * 1024); + defer cb.deinit(alloc); - var stdout: std.ArrayListUnmanaged(u8) = .{}; - var stderr: std.ArrayListUnmanaged(u8) = .{}; - defer { - stdout.deinit(alloc); - stderr.deinit(alloc); + // Read any error output and store it in a circular buffer so that we + // get that _last_ 50K of output. + while (true) { + var buf: [1024]u8 = undefined; + const len = try stderr.read(&buf); + if (len == 0) break; + try cb.appendSlice(buf[0..len]); } - // Copy the exe so it is non-const. This is necessary because wait() - // requires a mutable reference and we can't have one as a thread - // param. - var exe = exe_; - try exe.collectOutput(alloc, &stdout, &stderr, output_max_size); - _ = try exe.wait(); + // If we have any stderr output we log it. This makes it easier for users to + // debug why some open commands may not work as expected. + if (cb.len() > 0) log: { + { + var it = cb.iterator(.forward); + while (it.next()) |char| { + if (std.mem.indexOfScalar(u8, &std.ascii.whitespace, char.*)) |_| continue; + break; + } + // it's all whitespace, don't log + break :log; + } + var buf = std.ArrayList(u8).init(alloc); + defer buf.deinit(); + var it = cb.iterator(.forward); + while (it.next()) |char| { + if (char.* == '\n') { + log.err("{s} stderr: {s}", .{ exe.argv[0], buf.items }); + buf.clearRetainingCapacity(); + } + try buf.append(char.*); + } + if (buf.items.len > 0) + log.err("{s} stderr: {s}", .{buf.items}); + } - // If we have any stderr output we log it. This makes it easier for - // users to debug why some open commands may not work as expected. - if (stderr.items.len > 0) log.warn("wait stderr={s}", .{stderr.items}); + const rc = exe.wait() catch |err| { + switch (err) { + error.FileNotFound => { + log.warn("Unable to find {s}. Please install {s} and ensure that it is available on the PATH.", .{ + exe.argv[0], + exe.argv[0], + }); + }, + else => |e| return e, + } + return; + }; + + switch (rc) { + .Exited => |code| { + if (code != 0) { + log.warn("{s} exited with error code {d}", .{ exe.argv[0], code }); + } + }, + .Signal => |signal| { + log.warn("{s} was terminaled with signal {}", .{ exe.argv[0], signal }); + }, + .Stopped => |signal| { + log.warn("{s} was stopped with signal {}", .{ exe.argv[0], signal }); + }, + .Unknown => |code| { + log.warn("{s} had an unknown error {}", .{ exe.argv[0], code }); + }, + } } From 70a2a0afd5072dbe77d50f5616a44b2bcba72581 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 8 May 2025 19:28:28 -0500 Subject: [PATCH 26/36] better ABI check for apprt.Action.CValue --- src/apprt/action.zig | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 6c33a296f..79f8740d2 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const builtin = @import("builtin"); const assert = std.debug.assert; const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); @@ -364,12 +363,10 @@ pub const Action = union(Key) { // For ABI compatibility, we expect that this is our union size. // At the time of writing, we don't promise ABI compatibility // so we can change this but I want to be aware of it. - assert(@sizeOf(CValue) == switch (builtin.target.os.tag) { - .windows => switch (builtin.target.cpu.arch) { - .x86 => 16, - else => 24, - }, - else => 24, + assert(@sizeOf(CValue) == switch (@sizeOf(usize)) { + 4 => 16, + 8 => 24, + else => unreachable, }); } From cbcb0b795c43453d22d2138ae5edc8deb7fa3727 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Jul 2025 15:10:14 -0700 Subject: [PATCH 27/36] Fallback to cross-platform minimal open when apprt is not available --- include/ghostty.h | 2 +- src/Surface.zig | 42 ++++++----- src/apprt/action.zig | 33 +++++---- src/apprt/gtk/App.zig | 14 ++-- src/os/open.zig | 161 +++++++++++------------------------------- 5 files changed, 97 insertions(+), 155 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 16ca21d89..2a4a7fb6e 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -664,8 +664,8 @@ typedef struct { // apprt.action.OpenUrlKind typedef enum { - GHOSTTY_ACTION_OPEN_URL_KIND_TEXT, GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN, + GHOSTTY_ACTION_OPEN_URL_KIND_TEXT, } ghostty_action_open_url_kind_e; // apprt.action.OpenUrl.C diff --git a/src/Surface.zig b/src/Surface.zig index db272dddc..a4a8d46df 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3724,11 +3724,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { .trim = false, }); defer self.alloc.free(str); - _ = try self.rt_app.performAction( - .{ .surface = self }, - .open_url, - .{ .kind = .unknown, .url = str }, - ); + try self.openUrl(.{ .kind = .unknown, .url = str }); }, ._open_osc8 => { @@ -3736,17 +3732,35 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { log.warn("failed to get URI for OSC8 hyperlink", .{}); return false; }; - _ = try self.rt_app.performAction( - .{ .surface = self }, - .open_url, - .{ .kind = .unknown, .url = uri }, - ); + try self.openUrl(.{ .kind = .unknown, .url = uri }); }, } return true; } +fn openUrl( + self: *Surface, + action: apprt.action.OpenUrl, +) !void { + // If the apprt handles it then we're done. + if (try self.rt_app.performAction( + .{ .surface = self }, + .open_url, + action, + )) return; + + // apprt didn't handle it, fallback to our simple cross-platform + // URL opener. We log a warning because we want well-behaved + // apprts to handle this themselves. + log.warn("apprt did not handle open URL action, falling back to default opener", .{}); + try internal_os.open( + self.alloc, + action.kind, + action.url, + ); +} + /// Return the URI for an OSC8 hyperlink at the given position or null /// if there is no hyperlink. fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 { @@ -4965,13 +4979,7 @@ fn writeScreenFile( defer self.alloc.free(pathZ); try self.rt_surface.setClipboardString(pathZ, .standard, false); }, - .open => { - _ = try self.rt_app.performAction( - .{ .surface = self }, - .open_url, - .{ .kind = .text, .url = path }, - ); - }, + .open => try self.openUrl(.{ .kind = .text, .url = path }), .paste => self.io.queueMessage(try termio.Message.writeReq( self.alloc, path, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 79f8740d2..1c3c7c72c 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -625,28 +625,35 @@ pub const ConfigChange = struct { } }; -/// The type of the data at the URL to open. This is used as a hint to -/// potentially open the URL in a different way. -/// Sync with: ghostty_action_open_url_kind_s -pub const OpenUrlKind = enum(c_int) { - text, - unknown, -}; - /// Open a URL pub const OpenUrl = struct { /// The type of data that the URL refers to. - kind: OpenUrlKind, + kind: Kind, + /// The URL. url: []const u8, + /// The type of the data at the URL to open. This is used as a hint to + /// potentially open the URL in a different way. + /// + /// Sync with: ghostty_action_open_url_kind_e + pub const Kind = enum(c_int) { + /// The type is unknown. This is the default and apprts should + /// open the URL in the most generic way possible. For example, + /// on macOS this would be the equivalent of `open` or on Linux + /// this would be `xdg-open`. + unknown, + + /// The URL is known to be a text file. In this case, the apprt + /// should try to open the URL in a text editor or viewer or + /// some equivalent, if possible. + text, + }; + // Sync with: ghostty_action_open_url_s pub const C = extern struct { - /// The type of data that the URL refers to. - kind: OpenUrlKind, - /// The URL (not zero terminated). + kind: Kind, url: [*]const u8, - /// The number of bytes in the URL. len: usize, }; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index a046291ef..369090ee2 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -1759,12 +1759,18 @@ fn initActions(self: *App) void { } } -// TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html pub fn openUrl( app: *App, value: apprt.action.OpenUrl, ) void { - internal_os.open(app.core_app.alloc, value.kind, value.url) catch |err| { - log.warn("unable to open url: {}", .{err}); - }; + // TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html + + // Fallback to the minimal cross-platform way of opening a URL. + // This is always a safe fallback and enables for example Windows + // to open URLs (GTK on Windows via WSL is a thing). + internal_os.open( + app.core_app.alloc, + value.kind, + value.url, + ) catch |err| log.warn("unable to open url: {}", .{err}); } diff --git a/src/os/open.zig b/src/os/open.zig index 6841c76ab..9b069c80f 100644 --- a/src/os/open.zig +++ b/src/os/open.zig @@ -1,9 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; - const apprt = @import("../apprt.zig"); -const CircBuf = @import("../datastruct/circ_buf.zig").CircBuf; const log = std.log.scoped(.@"os-open"); @@ -12,41 +10,16 @@ const log = std.log.scoped(.@"os-open"); /// Any output on stderr is logged as a warning in the application logs. /// Output on stdout is ignored. The allocator is used to buffer the /// log output and may allocate from another thread. +/// +/// This function is purposely simple for the sake of providing +/// some portable way to open URLs. If you are implementing an +/// apprt for Ghostty, you should consider doing something special-cased +/// for your platform. pub fn open( alloc: Allocator, - kind: apprt.action.OpenUrlKind, + kind: apprt.action.OpenUrl.Kind, url: []const u8, ) !void { - // Make a copy of the URL so that we can use it in the thread without - // worrying about it getting freed by other threads. - const copy = try alloc.dupe(u8, url); - errdefer alloc.free(copy); - - // Run in a thread so that it never blocks the main thread, no matter how - // long it takes to execute. - const thread = try std.Thread.spawn(.{}, _openThread, .{ alloc, kind, copy }); - - // Don't worry about the thread any more. - thread.detach(); -} - -fn _openThread( - alloc: Allocator, - kind: apprt.action.OpenUrlKind, - url: []const u8, -) void { - _openThreadError(alloc, kind, url) catch |err| { - log.warn("error while opening url: {}", .{err}); - }; -} - -fn _openThreadError( - alloc: Allocator, - kind: apprt.action.OpenUrlKind, - url: []const u8, -) !void { - defer alloc.free(url); - var exe: std.process.Child = switch (builtin.os.tag) { .linux, .freebsd => .init( &.{ "xdg-open", url }, @@ -70,95 +43,43 @@ fn _openThreadError( else => @compileError("unsupported OS"), }; - // Ignore stdin & stdout, collect the output from stderr. + // Pipe stdout/stderr so we can collect output from the command. // This must be set before spawning the process. - exe.stdin_behavior = .Ignore; - exe.stdout_behavior = .Ignore; + exe.stdout_behavior = .Pipe; exe.stderr_behavior = .Pipe; - exe.spawn() catch |err| { - switch (err) { - error.FileNotFound => { - log.warn("Unable to find {s}. Please install {s} and ensure that it is available on the PATH.", .{ - exe.argv[0], - exe.argv[0], - }); - }, - else => |e| return e, - } - return; - }; + // Spawn the process on our same thread so we can detect failure + // quickly. + try exe.spawn(); - const stderr = exe.stderr orelse { - log.warn("Unable to access the stderr of the spawned program!", .{}); - return; - }; - - var cb = try CircBuf(u8, 0).init(alloc, 50 * 1024); - defer cb.deinit(alloc); - - // Read any error output and store it in a circular buffer so that we - // get that _last_ 50K of output. - while (true) { - var buf: [1024]u8 = undefined; - const len = try stderr.read(&buf); - if (len == 0) break; - try cb.appendSlice(buf[0..len]); - } - - // If we have any stderr output we log it. This makes it easier for users to - // debug why some open commands may not work as expected. - if (cb.len() > 0) log: { - { - var it = cb.iterator(.forward); - while (it.next()) |char| { - if (std.mem.indexOfScalar(u8, &std.ascii.whitespace, char.*)) |_| continue; - break; - } - // it's all whitespace, don't log - break :log; - } - var buf = std.ArrayList(u8).init(alloc); - defer buf.deinit(); - var it = cb.iterator(.forward); - while (it.next()) |char| { - if (char.* == '\n') { - log.err("{s} stderr: {s}", .{ exe.argv[0], buf.items }); - buf.clearRetainingCapacity(); - } - try buf.append(char.*); - } - if (buf.items.len > 0) - log.err("{s} stderr: {s}", .{buf.items}); - } - - const rc = exe.wait() catch |err| { - switch (err) { - error.FileNotFound => { - log.warn("Unable to find {s}. Please install {s} and ensure that it is available on the PATH.", .{ - exe.argv[0], - exe.argv[0], - }); - }, - else => |e| return e, - } - return; - }; - - switch (rc) { - .Exited => |code| { - if (code != 0) { - log.warn("{s} exited with error code {d}", .{ exe.argv[0], code }); - } - }, - .Signal => |signal| { - log.warn("{s} was terminaled with signal {}", .{ exe.argv[0], signal }); - }, - .Stopped => |signal| { - log.warn("{s} was stopped with signal {}", .{ exe.argv[0], signal }); - }, - .Unknown => |code| { - log.warn("{s} had an unknown error {}", .{ exe.argv[0], code }); - }, - } + // Create a thread that handles collecting output and reaping + // the process. This is done in a separate thread because SOME + // open implementations block and some do not. It's easier to just + // spawn a thread to handle this so that we never block. + const thread = try std.Thread.spawn(.{}, openThread, .{ alloc, exe }); + thread.detach(); +} + +fn openThread(alloc: Allocator, exe_: std.process.Child) !void { + // 50 KiB is the default value used by std.process.Child.run and should + // be enough to get the output we care about. + const output_max_size = 50 * 1024; + + var stdout: std.ArrayListUnmanaged(u8) = .{}; + var stderr: std.ArrayListUnmanaged(u8) = .{}; + defer { + stdout.deinit(alloc); + stderr.deinit(alloc); + } + + // Copy the exe so it is non-const. This is necessary because wait() + // requires a mutable reference and we can't have one as a thread + // param. + var exe = exe_; + try exe.collectOutput(alloc, &stdout, &stderr, output_max_size); + _ = try exe.wait(); + + // If we have any stderr output we log it. This makes it easier for + // users to debug why some open commands may not work as expected. + if (stderr.items.len > 0) log.warn("wait stderr={s}", .{stderr.items}); } From d3aece21d8eb5985681f0d57ed7f280c9a7065b5 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 16:32:22 -0600 Subject: [PATCH 28/36] font: more generic bearing adjustments This generally adjusts the bearings of any glyph whose original advance was narrower than the cell, which helps a lot with proportional fallback glyphs so they aren't just left-aligned. This only applies to situations where the glyph was originally narrower than the cell, so that we don't mess up ligatures, and this centers the old advance width in the new one rather than adjusting proportionally, because otherwise we can mess up glyphs that are meant to align with others when placed vertically. --- src/font/face/coretext.zig | 48 +++++++++++++++++++++++++++----------- src/font/face/freetype.zig | 46 +++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 7d750b0d6..6aedd7696 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -481,20 +481,42 @@ pub const Face = struct { // This should be the distance from the left of // the cell to the left of the glyph's bounding box. const offset_x: i32 = offset_x: { - var result: i32 = @intFromFloat(@round(x)); - - // If our cell was resized then we adjust our glyph's - // position relative to the new center. This keeps glyphs - // centered in the cell whether it was made wider or narrower. - if (metrics.original_cell_width) |original_width| { - const before: i32 = @intCast(original_width); - const after: i32 = @intCast(metrics.cell_width); - // Increase the offset by half of the difference - // between the widths to keep things centered. - result += @divTrunc(after - before, 2); + // If the glyph's advance is narrower than the cell width then we + // center the advance of the glyph within the cell width. At first + // I implemented this to proportionally scale the center position + // of the glyph but that messes up glyphs that are meant to align + // vertically with others, so this is a compromise. + // + // This makes it so that when the `adjust-cell-width` config is + // used, or when a fallback font with a different advance width + // is used, we don't get weirdly aligned glyphs. + // + // We don't do this if the constraint has a horizontal alignment, + // since in that case the position was already calculated with the + // new cell width in mind. + if (opts.constraint.align_horizontal == .none) { + var advances: [glyphs.len]macos.graphics.Size = undefined; + _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); + const advance = advances[0].width; + const new_advance = + cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1)); + // If the original advance is greater than the cell width then + // it's possible that this is a ligature or other glyph that is + // intended to overflow the cell to one side or the other, and + // adjusting the bearings could mess that up, so we just leave + // it alone if that's the case. + // + // We also don't want to do anything if the advance is zero or + // less, since this is used for stuff like combining characters. + if (advance > new_advance or advance <= 0.0) { + break :offset_x @intFromFloat(@round(x)); + } + break :offset_x @intFromFloat( + @round(x + (new_advance - advance) / 2), + ); + } else { + break :offset_x @intFromFloat(@round(x)); } - - break :offset_x result; }; return .{ diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 079cf5b2d..6aeb951af 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -638,20 +638,40 @@ pub const Face = struct { // This should be the distance from the left of // the cell to the left of the glyph's bounding box. const offset_x: i32 = offset_x: { - var result: i32 = @intFromFloat(@floor(x)); - - // If our cell was resized then we adjust our glyph's - // position relative to the new center. This keeps glyphs - // centered in the cell whether it was made wider or narrower. - if (metrics.original_cell_width) |original_width| { - const before: i32 = @intCast(original_width); - const after: i32 = @intCast(metrics.cell_width); - // Increase the offset by half of the difference - // between the widths to keep things centered. - result += @divTrunc(after - before, 2); + // If the glyph's advance is narrower than the cell width then we + // center the advance of the glyph within the cell width. At first + // I implemented this to proportionally scale the center position + // of the glyph but that messes up glyphs that are meant to align + // vertically with others, so this is a compromise. + // + // This makes it so that when the `adjust-cell-width` config is + // used, or when a fallback font with a different advance width + // is used, we don't get weirdly aligned glyphs. + // + // We don't do this if the constraint has a horizontal alignment, + // since in that case the position was already calculated with the + // new cell width in mind. + if (opts.constraint.align_horizontal == .none) { + const advance = f26dot6ToFloat(glyph.*.advance.x); + const new_advance = + cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1)); + // If the original advance is greater than the cell width then + // it's possible that this is a ligature or other glyph that is + // intended to overflow the cell to one side or the other, and + // adjusting the bearings could mess that up, so we just leave + // it alone if that's the case. + // + // We also don't want to do anything if the advance is zero or + // less, since this is used for stuff like combining characters. + if (advance > new_advance or advance <= 0.0) { + break :offset_x @intFromFloat(@floor(x)); + } + break :offset_x @intFromFloat( + @floor(x + (new_advance - advance) / 2), + ); + } else { + break :offset_x @intFromFloat(@floor(x)); } - - break :offset_x result; }; return Glyph{ From 65a7c81c94432a94eb5ccb8ca8d835c3f083290b Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sun, 6 Jul 2025 18:39:32 -0400 Subject: [PATCH 29/36] bash: conditionally add cursor shape sequences In #7808, we stopped using PS0 to reset the cursor shape because restoring PS0 in __ghostty_preexec was causing issues (#7802). The alternate approach of printing the cursor reset escape sequence directly from __ghostty_preexec caused a new issue: the input cursor would persist longer than intended, such as when a suspended vim process was restored to the foreground. This change takes a different approach. We now conditionally add the cursor shape escape sequences to PS0 (and PS1, for consistency) when they don't already appear. The fixes the cursor shape reset problem. The main downside to this approach is that PS0 will continue to contain this escape sequence; it won't be cleared/reset in __ghostty_preexec for the reasons described in #7808. This feels like an acceptable outcome because there's no harm in the modified PS0 existing for the life of the bash session (rather than it being modified and then restored for each command cycle), and it's consistent with how some other terminals' bash integration works (e.g. kitty). --- src/shell-integration/bash/ghostty.bash | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 21a6965ca..df4c7f9a7 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -122,8 +122,8 @@ function __ghostty_precmd() { # Cursor if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then - PS1=$PS1'\[\e[5 q\]' # blinking bar for input - builtin printf "\e[0 q" # reset to default cursor + [[ "$PS1" != *'\[\e[5 q\]'* ]] && PS1=$PS1'\[\e[5 q\]' # input + [[ "$PS0" != *'\[\e[0 q\]'* ]] && PS0=$PS0'\[\e[0 q\]' # reset fi # Title (working directory) From db08bf1655d10f5c459f41f2c01963bede98db55 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 16:37:15 -0600 Subject: [PATCH 30/36] font: adjust fallback font sizes to match primary metrics This better harmonizes fallback fonts with the primary font by matching the heights of lowercase letters. This should be a big improvement for users who use mixed scripts and so rely heavily on fallback fonts. --- src/font/Collection.zig | 169 ++++++++++++++++++++++++++++++++----- src/font/Metrics.zig | 12 +++ src/font/face/coretext.zig | 15 ++++ src/font/face/freetype.zig | 22 ++++- 4 files changed, 196 insertions(+), 22 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 8533331bc..cdbd3d84f 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -69,10 +69,14 @@ pub fn deinit(self: *Collection, alloc: Allocator) void { if (self.load_options) |*v| v.deinit(alloc); } -pub const AddError = Allocator.Error || error{ - CollectionFull, - DeferredLoadingUnavailable, -}; +pub const AddError = + Allocator.Error || + AdjustSizeError || + error{ + CollectionFull, + DeferredLoadingUnavailable, + SetSizeFailed, + }; /// Add a face to the collection for the given style. This face will be added /// next in priority if others exist already, i.e. it'll be the _last_ to be @@ -81,10 +85,9 @@ pub const AddError = Allocator.Error || error{ /// If no error is encountered then the collection takes ownership of the face, /// in which case face will be deallocated when the collection is deallocated. /// -/// If a loaded face is added to the collection, it should be the same -/// size as all the other faces in the collection. This function will not -/// verify or modify the size until the size of the entire collection is -/// changed. +/// If a loaded face is added to the collection, its size will be changed to +/// match the size specified in load_options, adjusted for harmonization with +/// the primary face. pub fn add( self: *Collection, alloc: Allocator, @@ -103,9 +106,106 @@ pub fn add( return error.DeferredLoadingUnavailable; try list.append(alloc, face); + + var owned: *Entry = list.at(idx); + + // If the face is already loaded, apply font size adjustment + // now, otherwise we'll apply it whenever we do load it. + if (owned.getLoaded()) |loaded| { + if (try self.adjustedSize(loaded)) |opts| { + loaded.setSize(opts.faceOptions()) catch return error.SetSizeFailed; + } + } + return .{ .style = style, .idx = @intCast(idx) }; } +pub const AdjustSizeError = font.Face.GetMetricsError; + +// Calculate a size for the provided face that will match it with the primary +// font, metrically, to improve consistency with fallback fonts. Right now we +// match the font based on the ex height, or the ideograph width if the font +// has ideographs in it. +// +// This returns null if load options is null or if self.load_options is null. +// +// +// This is very much like the `font-size-adjust` CSS property in how it works. +// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust +// +// TODO: In the future, provide config options that allow the user to select +// which metric should be matched for fallback fonts, instead of hard +// coding it as ex height. +pub fn adjustedSize( + self: *Collection, + face: *Face, +) AdjustSizeError!?LoadOptions { + const load_options = self.load_options orelse return null; + + // We silently do nothing if we can't get the primary + // face, because this might be the primary face itself. + const primary_face = self.getFace(.{ .idx = 0 }) catch return null; + + // We do nothing if the primary face and this face are the same. + if (@intFromPtr(primary_face) == @intFromPtr(face)) return null; + + const primary_metrics = try primary_face.getMetrics(); + const face_metrics = try face.getMetrics(); + + // We use the ex height to match our font sizes, so that the height of + // lower-case letters matches between all fonts in the fallback chain. + // + // We estimate ex height as 0.75 * cap height if it's not specifically + // provided, and we estimate cap height as 0.75 * ascent in the same case. + // + // If the fallback font has an ic_width we prefer that, for normalization + // of CJK font sizes when mixed with latin fonts. + // + // We estimate the ic_width as twice the cell width if it isn't provided. + var primary_cap = primary_metrics.cap_height orelse 0.0; + if (primary_cap <= 0) primary_cap = primary_metrics.ascent * 0.75; + + var primary_ex = primary_metrics.ex_height orelse 0.0; + if (primary_ex <= 0) primary_ex = primary_cap * 0.75; + + var primary_ic = primary_metrics.ic_width orelse 0.0; + if (primary_ic <= 0) primary_ic = primary_metrics.cell_width * 2; + + var face_cap = face_metrics.cap_height orelse 0.0; + if (face_cap <= 0) face_cap = face_metrics.ascent * 0.75; + + var face_ex = face_metrics.ex_height orelse 0.0; + if (face_ex <= 0) face_ex = face_cap * 0.75; + + var face_ic = face_metrics.ic_width orelse 0.0; + if (face_ic <= 0) face_ic = face_metrics.cell_width * 2; + + // If the line height of the scaled font would be larger than + // the line height of the primary font, we don't want that, so + // we take the minimum between matching the ic/ex and the line + // height. + // + // NOTE: We actually allow the line height to be up to 1.2 + // times the primary line height because empirically + // this is usually fine and is better for CJK. + // + // TODO: We should probably provide a config option that lets + // the user pick what metric to use for size adjustment. + const scale = @min( + 1.2 * primary_metrics.lineHeight() / face_metrics.lineHeight(), + if (face_metrics.ic_width != null) + primary_ic / face_ic + else + primary_ex / face_ex, + ); + + // Make a copy of our load options and multiply the size by our scale. + var opts = load_options; + opts.size.points *= @as(f32, @floatCast(scale)); + + return opts; +} + /// Return the Face represented by a given Index. The returned pointer /// is only valid as long as this collection is not modified. /// @@ -129,21 +229,38 @@ pub fn getFace(self: *Collection, index: Index) !*Face { break :item item; }; - return try self.getFaceFromEntry(item); + const face = try self.getFaceFromEntry( + item, + // We only want to adjust the size if this isn't the primary face. + index.style != .regular or index.idx > 0, + ); + + return face; } /// Get the face from an entry. /// /// This entry must not be an alias. -fn getFaceFromEntry(self: *Collection, entry: *Entry) !*Face { +fn getFaceFromEntry( + self: *Collection, + entry: *Entry, + /// Whether to adjust the font size to match the primary face after loading. + adjust: bool, +) !*Face { assert(entry.* != .alias); return switch (entry.*) { inline .deferred, .fallback_deferred => |*d, tag| deferred: { const opts = self.load_options orelse return error.DeferredLoadingUnavailable; - const face = try d.load(opts.library, opts.faceOptions()); + var face = try d.load(opts.library, opts.faceOptions()); d.deinit(); + + // If we need to adjust the size, do so. + if (adjust) if (try self.adjustedSize(&face)) |new_opts| { + try face.setSize(new_opts.faceOptions()); + }; + entry.* = switch (tag) { .deferred => .{ .loaded = face }, .fallback_deferred => .{ .fallback_loaded = face }, @@ -247,7 +364,7 @@ pub fn completeStyles( while (it.next()) |entry| { // Load our face. If we fail to load it, we just skip it and // continue on to try the next one. - const face = self.getFaceFromEntry(entry) catch |err| { + const face = self.getFaceFromEntry(entry, false) catch |err| { log.warn("error loading regular entry={d} err={}", .{ it.index - 1, err, @@ -371,7 +488,7 @@ fn syntheticBold(self: *Collection, entry: *Entry) !Face { const opts = self.load_options orelse return error.DeferredLoadingUnavailable; // Try to bold it. - const regular = try self.getFaceFromEntry(entry); + const regular = try self.getFaceFromEntry(entry, false); const face = try regular.syntheticBold(opts.faceOptions()); var buf: [256]u8 = undefined; @@ -391,7 +508,7 @@ fn syntheticItalic(self: *Collection, entry: *Entry) !Face { const opts = self.load_options orelse return error.DeferredLoadingUnavailable; // Try to italicize it. - const regular = try self.getFaceFromEntry(entry); + const regular = try self.getFaceFromEntry(entry, false); const face = try regular.syntheticItalic(opts.faceOptions()); var buf: [256]u8 = undefined; @@ -420,9 +537,12 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void { while (it.next()) |array| { var entry_it = array.value.iterator(0); while (entry_it.next()) |entry| switch (entry.*) { - .loaded, .fallback_loaded => |*f| try f.setSize( - opts.faceOptions(), - ), + .loaded, + .fallback_loaded, + => |*f| { + const new_opts = try self.adjustedSize(f) orelse opts.*; + try f.setSize(new_opts.faceOptions()); + }, // Deferred aren't loaded so we don't need to set their size. // The size for when they're loaded is set since `opts` changed. @@ -549,6 +669,16 @@ pub const Entry = union(enum) { } } + /// If this face is loaded, or is an alias to a loaded face, + /// then this returns the `Face`, otherwise returns null. + pub fn getLoaded(self: *Entry) ?*Face { + return switch (self.*) { + .deferred, .fallback_deferred => null, + .loaded, .fallback_loaded => |*face| face, + .alias => |v| v.getLoaded(), + }; + } + /// True if the entry is deferred. fn isDeferred(self: Entry) bool { return switch (self) { @@ -906,12 +1036,13 @@ test "metrics" { var c = init(); defer c.deinit(alloc); - c.load_options = .{ .library = lib }; + const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 }; + c.load_options = .{ .library = lib, .size = size }; _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, - .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + .{ .size = size }, ) }); try c.updateMetrics(); diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index bf527a021..069606c06 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -107,6 +107,18 @@ pub const FaceMetrics = struct { /// a provided ex height metric or measured from the height of the /// lowercase x glyph. ex_height: ?f64 = null, + + /// The width of the character "水" (CJK water ideograph, U+6C34), + /// if present. This is used for font size adjustment, to normalize + /// the width of CJK fonts mixed with latin fonts. + /// + /// NOTE: IC = Ideograph Character + ic_width: ?f64 = null, + + /// Convenience function for getting the line height (ascent - descent). + pub inline fn lineHeight(self: FaceMetrics) f64 { + return self.ascent - self.descent; + } }; /// Calculate our metrics based on values extracted from a font. diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 6aedd7696..c1f16e025 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -757,6 +757,20 @@ pub const Face = struct { break :cell_width max; }; + // Measure "水" (CJK water ideograph, U+6C34) for our ic width. + const ic_width: ?f64 = ic_width: { + const glyph = self.glyphIndex('水') orelse break :ic_width null; + + var advances: [1]macos.graphics.Size = undefined; + _ = ct_font.getAdvancesForGlyphs( + .horizontal, + &.{@intCast(glyph)}, + &advances, + ); + + break :ic_width advances[0].width; + }; + return .{ .cell_width = cell_width, .ascent = ascent, @@ -768,6 +782,7 @@ pub const Face = struct { .strikethrough_thickness = strikethrough_thickness, .cap_height = cap_height, .ex_height = ex_height, + .ic_width = ic_width, }; } diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 6aeb951af..db5a3622e 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -851,7 +851,7 @@ pub const Face = struct { while (c < 127) : (c += 1) { if (face.getCharIndex(c)) |glyph_index| { if (face.loadGlyph(glyph_index, .{ - .render = true, + .render = false, .no_svg = true, })) { max = @max( @@ -889,7 +889,7 @@ pub const Face = struct { defer self.ft_mutex.unlock(); if (face.getCharIndex('H')) |glyph_index| { if (face.loadGlyph(glyph_index, .{ - .render = true, + .render = false, .no_svg = true, })) { break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height); @@ -902,7 +902,7 @@ pub const Face = struct { defer self.ft_mutex.unlock(); if (face.getCharIndex('x')) |glyph_index| { if (face.loadGlyph(glyph_index, .{ - .render = true, + .render = false, .no_svg = true, })) { break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height); @@ -913,6 +913,21 @@ pub const Face = struct { }; }; + // Measure "水" (CJK water ideograph, U+6C34) for our ic width. + const ic_width: ?f64 = ic_width: { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + + const glyph = face.getCharIndex('水') orelse break :ic_width null; + + face.loadGlyph(glyph, .{ + .render = false, + .no_svg = true, + }) catch break :ic_width null; + + break :ic_width f26dot6ToF64(face.handle.*.glyph.*.advance.x); + }; + return .{ .cell_width = cell_width, @@ -928,6 +943,7 @@ pub const Face = struct { .cap_height = cap_height, .ex_height = ex_height, + .ic_width = ic_width, }; } From 327caf903c8e733480abb7da23c464e665ea3ee7 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 17:20:39 -0600 Subject: [PATCH 31/36] font: fix nerd font patcher ypadding twice what it should be The nerd font patcher uses `ypadding` as a single subtraction from the cell height, which means that half of it should go to the top padding and the other half to the bottom, this was making the heavy brackets way too small lol (0.4 of the cell height instead of 0.7) --- src/font/nerd_font_attributes.zig | 4 ++-- src/font/nerd_font_codegen.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 817d838f8..70920bb0a 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -40,8 +40,8 @@ pub fn getConstraint(cp: u21) Constraint { .max_constraint_width = 1, .align_horizontal = .center, .align_vertical = .center, - .pad_top = 0.3, - .pad_bottom = 0.3, + .pad_top = 0.15, + .pad_bottom = 0.15, }, 0xe0b0, => .{ diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index f8ff7caa6..e74b2ead1 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -213,8 +213,8 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) s += f" .pad_top = {v_pad},\n" s += f" .pad_bottom = {v_pad},\n" elif y_padding: - s += f" .pad_top = {y_padding},\n" - s += f" .pad_bottom = {y_padding},\n" + s += f" .pad_top = {y_padding / 2},\n" + s += f" .pad_bottom = {y_padding / 2},\n" if xy_ratio > 0: s += f" .max_xy_ratio = {xy_ratio},\n" From b7ffbf933f1b2274a14f6471eb932fee0987096a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Jul 2025 20:23:20 -0700 Subject: [PATCH 32/36] macos: open URLs with NSWorkspace APIs instead of `open` Fixes #5256 This updates the macOS apprt to implement the `OPEN_URL` apprt action to use the NSWorkspace APIs instead of the `open` command line utility. As part of this, we removed the `ghostty_config_open` libghostty API and instead introduced a new `ghostty_config_open_path` API that returns the path to open, and then we use the `NSWorkspace` APIs to open it (same function as the `OPEN_URL` action). --- include/ghostty.h | 8 ++- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++ macos/Sources/App/macOS/AppDelegate.swift | 2 +- macos/Sources/Ghostty/Ghostty.Action.swift | 30 +++++++++++ macos/Sources/Ghostty/Ghostty.App.swift | 51 +++++++++++++++++-- macos/Sources/Ghostty/Package.swift | 20 ++++++++ .../Extensions/NSWorkspace+Extension.swift | 29 +++++++++++ src/apprt/gtk/App.zig | 19 ++++++- src/config/CAPI.zig | 31 ++++++----- src/config/edit.zig | 13 ++--- src/main_c.zig | 34 ++++++++++++- 11 files changed, 212 insertions(+), 29 deletions(-) create mode 100644 macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift diff --git a/include/ghostty.h b/include/ghostty.h index 2a4a7fb6e..312e6595a 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -350,6 +350,11 @@ typedef struct { const char* message; } ghostty_diagnostic_s; +typedef struct { + const char* ptr; + uintptr_t len; +} ghostty_string_s; + typedef struct { double tl_px_x; double tl_px_y; @@ -797,6 +802,7 @@ int ghostty_init(uintptr_t, char**); void ghostty_cli_try_action(void); ghostty_info_s ghostty_info(void); const char* ghostty_translate(const char*); +void ghostty_string_free(ghostty_string_s); ghostty_config_t ghostty_config_new(); void ghostty_config_free(ghostty_config_t); @@ -811,7 +817,7 @@ ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t, uintptr_t); uint32_t ghostty_config_diagnostics_count(ghostty_config_t); ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t); -void ghostty_config_open(); +ghostty_string_s ghostty_config_open_path(void); ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*, ghostty_config_t); diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 08c3ef3b3..f6eedd864 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; }; A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; }; A505D21D2E1A2FA20018808F /* FileHandle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */; }; + A505D21F2E1B6DE00018808F /* NSWorkspace+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */; }; A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; }; A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.swift */; }; A51194132E05D006007258CC /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; }; @@ -160,6 +161,7 @@ 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; }; A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileHandle+Extension.swift"; sourceTree = ""; }; + A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWorkspace+Extension.swift"; sourceTree = ""; }; A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = ""; }; A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = ""; }; A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = ""; }; @@ -531,6 +533,7 @@ AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */, A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */, + A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */, A5985CD62C320C4500C57AD3 /* String+Extension.swift */, A58636722DF4813000E04A10 /* UndoManager+Extension.swift */, A5CC36142C9CDA03004D6760 /* View+Extension.swift */, @@ -819,6 +822,7 @@ A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */, + A505D21F2E1B6DE00018808F /* NSWorkspace+Extension.swift in Sources */, A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */, A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */, A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 53b6dce88..38500b7d3 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -932,7 +932,7 @@ class AppDelegate: NSObject, //MARK: - IB Actions @IBAction func openConfig(_ sender: Any?) { - ghostty.openConfig() + Ghostty.App.openConfig() } @IBAction func reloadConfig(_ sender: Any?) { diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index dfdb0bff5..a6559600d 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -40,4 +40,34 @@ extension Ghostty.Action { self.amount = c.amount } } + + struct OpenURL { + enum Kind { + case unknown + case text + + init(_ c: ghostty_action_open_url_kind_e) { + switch c { + case GHOSTTY_ACTION_OPEN_URL_KIND_TEXT: + self = .text + default: + self = .unknown + } + } + } + + let kind: Kind + let url: String + + init(c: ghostty_action_open_url_s) { + self.kind = Kind(c.kind) + + if let urlCString = c.url { + let data = Data(bytes: urlCString, count: Int(c.len)) + self.url = String(data: data, encoding: .utf8) ?? "" + } else { + self.url = "" + } + } + } } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 17abe2b0e..0fdea1760 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -114,9 +114,21 @@ extension Ghostty { ghostty_app_tick(app) } - func openConfig() { - guard let app = self.app else { return } - ghostty_app_open_config(app) + static func openConfig() { + let str = Ghostty.AllocatedString(ghostty_config_open_path()).string + guard !str.isEmpty else { return } + #if os(macOS) + let fileURL = URL(fileURLWithPath: str).absoluteString + var action = ghostty_action_open_url_s() + action.kind = GHOSTTY_ACTION_OPEN_URL_KIND_TEXT + fileURL.withCString { cStr in + action.url = cStr + action.len = UInt(fileURL.count) + _ = openURL(action) + } + #else + fatalError("Unsupported platform for opening config file") + #endif } /// Reload the configuration. @@ -488,7 +500,7 @@ extension Ghostty { pwdChanged(app, target: target, v: action.action.pwd) case GHOSTTY_ACTION_OPEN_CONFIG: - ghostty_config_open() + openConfig() case GHOSTTY_ACTION_FLOAT_WINDOW: toggleFloatWindow(app, target: target, mode: action.action.float_window) @@ -546,6 +558,9 @@ extension Ghostty { case GHOSTTY_ACTION_CHECK_FOR_UPDATES: checkForUpdates(app) + + case GHOSTTY_ACTION_OPEN_URL: + return openURL(action.action.open_url) case GHOSTTY_ACTION_UNDO: return undo(app, target: target) @@ -598,6 +613,34 @@ extension Ghostty { appDelegate.checkForUpdates(nil) } } + + private static func openURL( + _ v: ghostty_action_open_url_s + ) -> Bool { + let action = Ghostty.Action.OpenURL(c: v) + + // Convert the URL string to a URL object + guard let url = URL(string: action.url) else { + Ghostty.logger.warning("invalid URL for open URL action: \(action.url)") + return false + } + + switch action.kind { + case .text: + // Open with the default text editor + if let textEditor = NSWorkspace.shared.defaultTextEditor { + NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration()) + return true + } + + case .unknown: + break + } + + // Open with the default application for the URL + NSWorkspace.shared.open(url) + return true + } private static func undo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool { let undoManager: UndoManager? diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index f30f2f6f9..9b05934df 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -73,6 +73,26 @@ extension Ghostty { // MARK: Swift Types for C Types +extension Ghostty { + class AllocatedString { + private let cString: ghostty_string_s + + init(_ c: ghostty_string_s) { + self.cString = c + } + + var string: String { + guard let ptr = cString.ptr else { return "" } + let data = Data(bytes: ptr, count: Int(cString.len)) + return String(data: data, encoding: .utf8) ?? "" + } + + deinit { + ghostty_string_free(cString) + } + } +} + extension Ghostty { enum SetFloatWIndow { case on diff --git a/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift new file mode 100644 index 000000000..bc2d028b5 --- /dev/null +++ b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift @@ -0,0 +1,29 @@ +import AppKit +import UniformTypeIdentifiers + +extension NSWorkspace { + /// Returns the URL of the default text editor application. + /// - Returns: The URL of the default text editor, or nil if no default text editor is found. + var defaultTextEditor: URL? { + defaultApplicationURL(forContentType: UTType.plainText.identifier) + } + + /// Returns the URL of the default application for opening files with the specified content type. + /// - Parameter contentType: The content type identifier (UTI) to find the default application for. + /// - Returns: The URL of the default application, or nil if no default application is found. + func defaultApplicationURL(forContentType contentType: String) -> URL? { + return LSCopyDefaultApplicationURLForContentType( + contentType as CFString, + .all, + nil + )?.takeRetainedValue() as? URL + } + + /// Returns the URL of the default application for opening files with the specified file extension. + /// - Parameter ext: The file extension to find the default application for. + /// - Returns: The URL of the default application, or nil if no default application is found. + func defaultApplicationURL(forExtension ext: String) -> URL? { + guard let uti = UTType(filenameExtension: ext) else { return nil} + return defaultApplicationURL(forContentType: uti.identifier) + } +} diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 369090ee2..907f3a36d 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -496,7 +496,7 @@ pub fn performAction( .resize_split => self.resizeSplit(target, value), .equalize_splits => self.equalizeSplits(target), .goto_split => return self.gotoSplit(target, value), - .open_config => try configpkg.edit.open(self.core_app.alloc), + .open_config => return self.openConfig(), .config_change => self.configChange(target, value.config), .reload_config => try self.reloadConfig(target, value), .inspector => self.controlInspector(target, value), @@ -1759,7 +1759,22 @@ fn initActions(self: *App) void { } } -pub fn openUrl( +fn openConfig(self: *App) !bool { + // Get the config file path + const alloc = self.core_app.alloc; + const path = configpkg.edit.openPath(alloc) catch |err| { + log.warn("error getting config file path: {}", .{err}); + return false; + }; + defer alloc.free(path); + + // Open it using openURL. "path" isn't actually a URL but + // at the time of writing that works just fine for GTK. + self.openUrl(.{ .kind = .text, .url = path }); + return true; +} + +fn openUrl( app: *App, value: apprt.action.OpenUrl, ) void { diff --git a/src/config/CAPI.zig b/src/config/CAPI.zig index 0b7108a59..bdc59797a 100644 --- a/src/config/CAPI.zig +++ b/src/config/CAPI.zig @@ -1,7 +1,9 @@ const std = @import("std"); +const assert = std.debug.assert; const cli = @import("../cli.zig"); const inputpkg = @import("../input.zig"); -const global = &@import("../global.zig").state; +const state = &@import("../global.zig").state; +const c = @import("../main_c.zig"); const Config = @import("Config.zig"); const c_get = @import("c_get.zig"); @@ -12,14 +14,14 @@ const log = std.log.scoped(.config); /// Create a new configuration filled with the initial default values. export fn ghostty_config_new() ?*Config { - const result = global.alloc.create(Config) catch |err| { + const result = state.alloc.create(Config) catch |err| { log.err("error allocating config err={}", .{err}); return null; }; - result.* = Config.default(global.alloc) catch |err| { + result.* = Config.default(state.alloc) catch |err| { log.err("error creating config err={}", .{err}); - global.alloc.destroy(result); + state.alloc.destroy(result); return null; }; @@ -29,20 +31,20 @@ export fn ghostty_config_new() ?*Config { export fn ghostty_config_free(ptr: ?*Config) void { if (ptr) |v| { v.deinit(); - global.alloc.destroy(v); + state.alloc.destroy(v); } } /// Deep clone the configuration. export fn ghostty_config_clone(self: *Config) ?*Config { - const result = global.alloc.create(Config) catch |err| { + const result = state.alloc.create(Config) catch |err| { log.err("error allocating config err={}", .{err}); return null; }; - result.* = self.clone(global.alloc) catch |err| { + result.* = self.clone(state.alloc) catch |err| { log.err("error cloning config err={}", .{err}); - global.alloc.destroy(result); + state.alloc.destroy(result); return null; }; @@ -51,7 +53,7 @@ export fn ghostty_config_clone(self: *Config) ?*Config { /// Load the configuration from the CLI args. export fn ghostty_config_load_cli_args(self: *Config) void { - self.loadCliArgs(global.alloc) catch |err| { + self.loadCliArgs(state.alloc) catch |err| { log.err("error loading config err={}", .{err}); }; } @@ -60,7 +62,7 @@ export fn ghostty_config_load_cli_args(self: *Config) void { /// is usually done first. The default file locations are locations /// such as the home directory. export fn ghostty_config_load_default_files(self: *Config) void { - self.loadDefaultFiles(global.alloc) catch |err| { + self.loadDefaultFiles(state.alloc) catch |err| { log.err("error loading config err={}", .{err}); }; } @@ -69,7 +71,7 @@ export fn ghostty_config_load_default_files(self: *Config) void { /// file locations in the previously loaded configuration. This will /// recursively continue to load up to a built-in limit. export fn ghostty_config_load_recursive_files(self: *Config) void { - self.loadRecursiveFiles(global.alloc) catch |err| { + self.loadRecursiveFiles(state.alloc) catch |err| { log.err("error loading config err={}", .{err}); }; } @@ -122,10 +124,13 @@ export fn ghostty_config_get_diagnostic(self: *Config, idx: u32) Diagnostic { return .{ .message = message.ptr }; } -export fn ghostty_config_open() void { - edit.open(global.alloc) catch |err| { +export fn ghostty_config_open_path() c.String { + const path = edit.openPath(state.alloc) catch |err| { log.err("error opening config in editor err={}", .{err}); + return .empty; }; + + return .fromSlice(path); } /// Sync with ghostty_diagnostic_s diff --git a/src/config/edit.zig b/src/config/edit.zig index ae4394942..38dc98169 100644 --- a/src/config/edit.zig +++ b/src/config/edit.zig @@ -5,18 +5,19 @@ const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const internal_os = @import("../os/main.zig"); -/// Open the configuration in the OS default editor according to the default -/// paths the main config file could be in. +/// The path to the configuration that should be opened for editing. /// -/// On Linux, this will open the file at the XDG config path. This is the +/// On Linux, this will use the file at the XDG config path. This is the /// only valid path for Linux so we don't need to check for other paths. /// /// On macOS, both XDG and AppSupport paths are valid. Because Ghostty -/// prioritizes AppSupport over XDG, we will open AppSupport if it exists, +/// prioritizes AppSupport over XDG, we will use AppSupport if it exists, /// followed by XDG if it exists, and finally AppSupport if neither exist. /// For the existence check, we also prefer non-empty files over empty /// files. -pub fn open(alloc_gpa: Allocator) !void { +/// +/// The returned value is allocated using the provided allocator. +pub fn openPath(alloc_gpa: Allocator) ![:0]const u8 { // Use an arena to make memory management easier in here. var arena = ArenaAllocator.init(alloc_gpa); defer arena.deinit(); @@ -41,7 +42,7 @@ pub fn open(alloc_gpa: Allocator) !void { } }; - try internal_os.open(alloc_gpa, .text, config_path); + return try alloc_gpa.dupeZ(u8, config_path); } /// Returns the config path to use for open for the current OS. diff --git a/src/main_c.zig b/src/main_c.zig index 0722900e7..2c266cfb5 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -19,7 +19,12 @@ const internal_os = @import("os/main.zig"); // Some comptime assertions that our C API depends on. comptime { - assert(apprt.runtime == apprt.embedded); + // We allow tests to reference this file because we unit test + // some of the C API. At runtime though we should never get these + // functions unless we are building libghostty. + if (!builtin.is_test) { + assert(apprt.runtime == apprt.embedded); + } } /// Global options so we can log. This is identical to main. @@ -29,7 +34,9 @@ comptime { // These structs need to be referenced so the `export` functions // are truly exported by the C API lib. _ = @import("config.zig").CAPI; - _ = apprt.runtime.CAPI; + if (@hasDecl(apprt.runtime, "CAPI")) { + _ = apprt.runtime.CAPI; + } } /// ghostty_info_s @@ -46,6 +53,24 @@ const Info = extern struct { }; }; +/// ghostty_string_s +pub const String = extern struct { + ptr: ?[*]const u8, + len: usize, + + pub const empty: String = .{ + .ptr = null, + .len = 0, + }; + + pub fn fromSlice(slice: []const u8) String { + return .{ + .ptr = slice.ptr, + .len = slice.len, + }; + } +}; + /// Initialize ghostty global state. export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int { assert(builtin.link_libc); @@ -95,3 +120,8 @@ export fn ghostty_info() Info { export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { return internal_os.i18n._(msgid); } + +/// Free a string allocated by Ghostty. +export fn ghostty_string_free(str: String) void { + state.alloc.free(str.ptr.?[0..str.len]); +} From d33161ad66d46e5ca4d7f41a11abd85b9937a406 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 22:40:43 -0600 Subject: [PATCH 33/36] fix(font): include line gap in `lineHeight` helper --- src/font/Metrics.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index 069606c06..f96d753b3 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -115,9 +115,10 @@ pub const FaceMetrics = struct { /// NOTE: IC = Ideograph Character ic_width: ?f64 = null, - /// Convenience function for getting the line height (ascent - descent). + /// Convenience function for getting the line height + /// (ascent - descent + line_gap). pub inline fn lineHeight(self: FaceMetrics) f64 { - return self.ascent - self.descent; + return self.ascent - self.descent + self.line_gap; } }; From 08fd1688ff451c4cf2d1cc3e4864f05e71cefbdc Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 22:45:13 -0600 Subject: [PATCH 34/36] font: add test for size adjustment, fix small bug in resize Previously produced very wrong values when calling Collection.setSize, since it was assuming that the provided face had the same point size as the primary face, which isn't true during resize-- so instead we just have faces keep track of their set size, this is generally useful. --- src/font/Collection.zig | 64 ++++++++++++++++++++++++++++++++++++-- src/font/face/coretext.zig | 4 +++ src/font/face/freetype.zig | 5 +++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index cdbd3d84f..1d85d8a28 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -129,7 +129,6 @@ pub const AdjustSizeError = font.Face.GetMetricsError; // // This returns null if load options is null or if self.load_options is null. // -// // This is very much like the `font-size-adjust` CSS property in how it works. // ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust // @@ -199,8 +198,10 @@ pub fn adjustedSize( primary_ex / face_ex, ); - // Make a copy of our load options and multiply the size by our scale. + // Make a copy of our load options, set the size to the size of + // the provided face, and then multiply that by our scaling factor. var opts = load_options; + opts.size = face.size; opts.size.points *= @as(f32, @floatCast(scale)); return opts; @@ -1089,3 +1090,62 @@ test "metrics" { .cursor_height = 34, }, c.metrics); } + +// TODO: Also test CJK fallback sizing, we don't currently have a CJK test font. +test "adjusted sizes" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = font.embedded.inconsolata; + const fallback = font.embedded.monaspace_neon; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var c = init(); + defer c.deinit(alloc); + const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 }; + c.load_options = .{ .library = lib, .size = size }; + + // Add our primary face. + _ = try c.add(alloc, .regular, .{ .loaded = try .init( + lib, + testFont, + .{ .size = size }, + ) }); + + try c.updateMetrics(); + + // Add the fallback face. + const fallback_idx = try c.add(alloc, .regular, .{ .loaded = try .init( + lib, + fallback, + .{ .size = size }, + ) }); + + // The ex heights should match. + { + const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); + const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); + + try std.testing.expectApproxEqAbs( + primary_metrics.ex_height.?, + fallback_metrics.ex_height.?, + // We accept anything within half a pixel. + 0.5, + ); + } + + // Resize should keep that relationship. + try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 }); + { + const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); + const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); + + try std.testing.expectApproxEqAbs( + primary_metrics.ex_height.?, + fallback_metrics.ex_height.?, + // We accept anything within half a pixel. + 0.5, + ); + } +} diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index c1f16e025..00cc31b26 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -31,6 +31,9 @@ pub const Face = struct { /// tables). color: ?ColorState = null, + /// The current size this font is set to. + size: font.face.DesiredSize, + /// True if our build is using Harfbuzz. If we're not, we can avoid /// some Harfbuzz-specific code paths. const harfbuzz_shaper = font.options.backend.hasHarfbuzz(); @@ -106,6 +109,7 @@ pub const Face = struct { .font = ct_font, .hb_font = hb_font, .color = color, + .size = opts.size, }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index db5a3622e..ae3bd0968 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -59,6 +59,9 @@ pub const Face = struct { bold: bool = false, } = .{}, + /// The current size this font is set to. + size: font.face.DesiredSize, + /// Initialize a new font face with the given source in-memory. pub fn initFile( lib: Library, @@ -107,6 +110,7 @@ pub const Face = struct { .hb_font = hb_font, .ft_mutex = ft_mutex, .load_flags = opts.freetype_load_flags, + .size = opts.size, }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); @@ -203,6 +207,7 @@ pub const Face = struct { /// for clearing any glyph caches, font atlas data, etc. pub fn setSize(self: *Face, opts: font.face.Options) !void { try setSize_(self.face, opts.size); + self.size = opts.size; } fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void { From e1e2f823ba020c9b60b76aaabac2bee097f5566d Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 7 Jul 2025 08:57:20 -0600 Subject: [PATCH 35/36] font/coretext: fix horizontal bearing calculation This was subtly wrong in a way that was most obvious when text switched from regular to bold, where it would seem to wiggle since the bearings of each letter would shift by a pixel in either direction. This affected applications like fzf which uses bold to dynamically highlight the line you have selected. --- src/font/face/coretext.zig | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 00cc31b26..83f993715 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -366,8 +366,10 @@ pub const Face = struct { // of extra width to the area that's drawn in beyond just the width of // the glyph itself, so we include that extra fraction of a pixel when // calculating the width and height here. - const px_width: u32 = @intFromFloat(@ceil(width + rect.origin.x - @floor(rect.origin.x))); - const px_height: u32 = @intFromFloat(@ceil(height + rect.origin.y - @floor(rect.origin.y))); + const frac_x = rect.origin.x - @floor(rect.origin.x); + const frac_y = rect.origin.y - @floor(rect.origin.y); + const px_width: u32 = @intFromFloat(@ceil(width + frac_x)); + const px_height: u32 = @intFromFloat(@ceil(height + frac_y)); // Settings that are specific to if we are rendering text or emoji. const color: struct { @@ -513,13 +515,13 @@ pub const Face = struct { // We also don't want to do anything if the advance is zero or // less, since this is used for stuff like combining characters. if (advance > new_advance or advance <= 0.0) { - break :offset_x @intFromFloat(@round(x)); + break :offset_x @intFromFloat(@ceil(x - frac_x)); } break :offset_x @intFromFloat( - @round(x + (new_advance - advance) / 2), + @ceil(x - frac_x + (new_advance - advance) / 2), ); } else { - break :offset_x @intFromFloat(@round(x)); + break :offset_x @intFromFloat(@ceil(x - frac_x)); } }; From 5b1d3903796683112bbf4255a0825b839f4525e8 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 7 Jul 2025 10:29:54 -0500 Subject: [PATCH 36/36] gtk: rebuild gresources.c/h if CSS or icons change --- src/build/SharedDeps.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 0aab5ecf8..b6e9900e2 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -760,6 +760,9 @@ pub fn gtkDistResources( }); const resources_c = generate_c.addOutputFileArg("ghostty_resources.c"); generate_c.addFileArg(gresource_xml); + for (gresource.dependencies) |file| { + generate_c.addFileInput(b.path(file)); + } const generate_h = b.addSystemCommand(&.{ "glib-compile-resources", @@ -770,6 +773,9 @@ pub fn gtkDistResources( }); const resources_h = generate_h.addOutputFileArg("ghostty_resources.h"); generate_h.addFileArg(gresource_xml); + for (gresource.dependencies) |file| { + generate_h.addFileInput(b.path(file)); + } return .{ .resources_c = .{