aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/font/nerd_font_codegen.py
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/font/nerd_font_codegen.py
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/font/nerd_font_codegen.py')
-rw-r--r--vendor/libghostty-vt/src/font/nerd_font_codegen.py576
1 files changed, 576 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/font/nerd_font_codegen.py b/vendor/libghostty-vt/src/font/nerd_font_codegen.py
new file mode 100644
index 00000000..664555a3
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/nerd_font_codegen.py
@@ -0,0 +1,576 @@
+"""
+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 script requires Python 3.12 or greater, requires that the `fontTools`
+python module is installed, and requires that the path to a copy of the
+SymbolsNerdFont (not Mono!) font is passed as the first argument to it.
+"""
+
+import ast
+import sys
+import math
+from fontTools.ttLib import TTFont, TTLibError
+from fontTools.pens.boundsPen import BoundsPen
+from collections import defaultdict
+from contextlib import suppress
+from pathlib import Path
+from types import SimpleNamespace
+from typing import Literal, TypedDict, cast
+from urllib.request import urlretrieve
+
+type PatchSetAttributes = dict[Literal["default"] | int, PatchSetAttributeEntry]
+type AttributeHash = tuple[
+ str | None,
+ str | None,
+ str,
+ float,
+ float,
+ float,
+ float,
+ 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]
+
+ relative_x: float
+ relative_y: float
+ relative_width: float
+ relative_height: float
+
+
+class PatchSet(TypedDict):
+ Name: str
+ Filename: str
+ Exact: bool
+ SymStart: int
+ SymEnd: int
+ SrcStart: int | None
+ ScaleRules: PatchSetScaleRules | None
+ Attributes: PatchSetAttributes
+
+
+class PatchSetExtractor(ast.NodeVisitor):
+ def __init__(self) -> None:
+ self.symbol_table: dict[str, ast.expr] = {}
+ self.patch_set_values: list[PatchSet] = []
+ self.nf_version: str = ""
+
+ def visit_Assign(self, node):
+ if (
+ node.col_offset == 0 # top-level assignment
+ and len(node.targets) == 1 # no funny destructuring business
+ and isinstance(node.targets[0], ast.Name) # no setitem et cetera
+ and node.targets[0].id == "version" # it's the version string!
+ ):
+ self.nf_version = ast.literal_eval(node.value)
+ else:
+ return self.generic_visit(node)
+
+ 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: ast.FunctionDef) -> None:
+ # First pass: gather variable assignments
+ for stmt in node.body:
+ 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 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: 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: ast.expr) -> ResolvedSymbol:
+ """Try to evaluate or stringify an AST node."""
+ try:
+ return ast.literal_eval(node)
+ except ValueError:
+ # Spooky eval! But we trust nerd fonts to be safe...
+ if hasattr(ast, "unparse"):
+ return eval(
+ ast.unparse(node),
+ {"box_enabled": False, "box_keep": False},
+ {
+ "self": SimpleNamespace(
+ args=SimpleNamespace(
+ careful=False,
+ custom=False,
+ fontawesome=True,
+ fontawesomeextension=True,
+ fontlogos=True,
+ octicons=True,
+ codicons=True,
+ powersymbols=True,
+ pomicons=True,
+ powerline=True,
+ powerlineextra=True,
+ material=True,
+ weather=True,
+ )
+ ),
+ },
+ )
+ msg = f"<cannot eval: {type(node).__name__}>"
+ raise ValueError(msg) from None
+
+ def process_patch_entry(self, dict_node: ast.Dict) -> None:
+ entry = {}
+ for key_node, value_node in zip(dict_node.keys, dict_node.values):
+ if isinstance(key_node, ast.Constant):
+ if key_node.value == "Enabled":
+ if self.safe_literal_eval(value_node):
+ continue # This patch set is enabled, continue to next key
+ else:
+ return # This patch set is disabled, skip
+ 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: str) -> tuple[list[PatchSet], str]:
+ tree = ast.parse(source_code)
+ extractor = PatchSetExtractor()
+ extractor.visit(tree)
+ return extractor.patch_set_values, extractor.nf_version
+
+
+def parse_alignment(val: str) -> str | None:
+ return {
+ "l": ".start",
+ "r": ".end",
+ "c": ".center1", # font-patcher specific centering rule, see face.zig
+ "": None,
+ }.get(val, ".none")
+
+
+def attr_key(attr: PatchSetAttributeEntry) -> AttributeHash:
+ """Convert attributes to a hashable key for grouping."""
+ params = attr.get("params", {})
+ return (
+ parse_alignment(attr.get("align", "")),
+ parse_alignment(attr.get("valign", "")),
+ attr.get("stretch", ""),
+ float(params.get("overlap", 0.0)),
+ float(params.get("xy-ratio", -1.0)),
+ float(params.get("ypadding", 0.0)),
+ float(attr.get("relative_x", 0.0)),
+ float(attr.get("relative_y", 0.0)),
+ float(attr.get("relative_width", 1.0)),
+ float(attr.get("relative_height", 1.0)),
+ )
+
+
+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: list[tuple[int, int]] = []
+ cp_iter = iter(sorted(codepoints))
+ with suppress(StopIteration):
+ start = prev = next(cp_iter)
+ for cp in cp_iter:
+ if cp == prev + 1:
+ prev = cp
+ else:
+ ranges.append((start, prev))
+ start = prev = cp
+ ranges.append((start, prev))
+ return ranges
+
+
+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", {})
+
+ relative_x = attr.get("relative_x", 0.0)
+ relative_y = attr.get("relative_y", 0.0)
+ relative_width = attr.get("relative_width", 1.0)
+ relative_height = attr.get("relative_height", 1.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" {start:#x}...{end:#x}," if start != end else f" {start:#x},"
+ for start, end in ranges
+ )
+
+ s = f"{keys}\n => .{{\n"
+
+ # This maps the font_patcher stretch rules to a Constrain instance
+ # NOTE: some comments in font_patcher indicate that only x or y
+ # would also be a valid spec, but no icons use it, so we won't
+ # support it until we have to.
+ if "pa" in stretch:
+ if "!" in stretch or overlap:
+ s += " .size = .cover,\n"
+ else:
+ s += " .size = .fit_cover1,\n"
+ elif "xy" in stretch:
+ s += " .size = .stretch,\n"
+ else:
+ print(f"Warning: Unknown stretch rule {stretch}")
+
+ # `^` indicates that scaling should use the
+ # full cell height, not just the icon height,
+ # even when the constraint width is 1
+ if "^" not in stretch:
+ s += " .height = .icon,\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 not `pa` and there's not an explicit `2`.
+ if "1" in stretch or ("pa" not 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:
+ s += f" .align_vertical = {valign},\n"
+
+ if relative_width != 1.0:
+ s += f" .relative_width = {relative_width:.16f},\n"
+ if relative_height != 1.0:
+ s += f" .relative_height = {relative_height:.16f},\n"
+ if relative_x != 0.0:
+ s += f" .relative_x = {relative_x:.16f},\n"
+ if relative_y != 0.0:
+ s += f" .relative_y = {relative_y:.16f},\n"
+
+ # `overlap` and `ypadding` are mutually exclusive,
+ # this is asserted in the nerd fonts patcher itself.
+ if overlap:
+ pad = -overlap / 2
+ s += f" .pad_left = {pad},\n"
+ s += f" .pad_right = {pad},\n"
+ # In the nerd fonts patcher, overlap values
+ # are capped at 0.01 in the vertical direction.
+ v_pad = -min(0.01, overlap) / 2
+ s += f" .pad_top = {v_pad},\n"
+ s += f" .pad_bottom = {v_pad},\n"
+ elif y_padding:
+ 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"
+
+ s += " },"
+ return s
+
+
+def generate_codepoint_tables(
+ patch_sets: list[PatchSet],
+ nerd_font: TTFont,
+ nf_version: str,
+) -> dict[str, dict[int, int]]:
+ # We may already have the table saved from a previous run.
+ if Path("nerd_font_codepoint_tables.py").exists():
+ import nerd_font_codepoint_tables
+
+ if nerd_font_codepoint_tables.version == nf_version:
+ return nerd_font_codepoint_tables.cp_tables
+
+ cp_tables: dict[str, dict[int, int]] = {}
+ cp_nerdfont_used: set[int] = set()
+ cmap = nerd_font.getBestCmap()
+ for entry in patch_sets:
+ patch_set_name = entry["Name"]
+ print(f"Info: Extracting codepoint table from patch set '{patch_set_name}'")
+
+ # Extract codepoint map from original font file; download if needed
+ source_filename = entry["Filename"]
+ target_folder = Path("nerd_font_symbol_fonts")
+ target_folder.mkdir(exist_ok=True)
+ target_file = target_folder / Path(source_filename).name
+ if not target_file.exists():
+ print(f"Info: Downloading '{source_filename}'")
+ urlretrieve(
+ f"https://github.com/ryanoasis/nerd-fonts/raw/refs/tags/v{nf_version}/src/glyphs/{source_filename}",
+ target_file,
+ )
+ try:
+ with TTFont(target_file) as patchfont:
+ patch_cmap = patchfont.getBestCmap()
+ except TTLibError:
+ # Not a TTF/OTF font. This is OK if this patch set is exact, so we
+ # let if pass. If there's a problem, later checks will catch it.
+ patch_cmap = None
+
+ # A glyph's scale rules are specified using its codepoint in
+ # the original font, which is sometimes different from its
+ # Nerd Font codepoint. If entry["Exact"] is False, the codepoints are
+ # mapped according to the following rules:
+ # * entry["SymStart"] and entry["SymEnd"] denote the patch set's codepoint
+ # range in the original font.
+ # * entry["SrcStart"] is the starting point of the patch set's mapped
+ # codepoint range. It must not be None if entry["Exact"] is False.
+ # * The destination codepoint range is packed; that is, while there may be
+ # gaps without glyphs in the original font's codepoint range, there are
+ # none in the Nerd Font range. Hence there is no constant codepoint
+ # offset; instead we must iterate through the range and increment the
+ # destination codepoint every time we encounter a glyph in the original
+ # font.
+ # If entry["Exact"] is True, the origin and Nerd Font codepoints are the
+ # same, gaps included, and entry["SrcStart"] must be None.
+ if entry["Exact"]:
+ assert entry["SrcStart"] is None
+ cp_nerdfont = 0
+ else:
+ assert entry["SrcStart"]
+ assert patch_cmap is not None
+ cp_nerdfont = entry["SrcStart"] - 1
+
+ if patch_set_name not in cp_tables:
+ # There are several patch sets with the same name, representing
+ # different codepoint ranges within the same original font. Merging
+ # these into a single table is OK. However, we need to keep separate
+ # tables for the different fonts to correctly deal with cases where
+ # they fill in each other's gaps.
+ cp_tables[patch_set_name] = {}
+ for cp_original in range(entry["SymStart"], entry["SymEnd"] + 1):
+ if patch_cmap and cp_original not in patch_cmap:
+ continue
+ if not entry["Exact"]:
+ cp_nerdfont += 1
+ else:
+ cp_nerdfont = cp_original
+ if cp_nerdfont not in cmap:
+ raise ValueError(
+ f"Missing codepoint in Symbols Only Font: {hex(cp_nerdfont)} in patch set '{patch_set_name}'"
+ )
+ elif cp_nerdfont in cp_nerdfont_used:
+ raise ValueError(
+ f"Overlap for codepoint {hex(cp_nerdfont)} in patch set '{patch_set_name}'"
+ )
+ cp_tables[patch_set_name][cp_original] = cp_nerdfont
+ cp_nerdfont_used.add(cp_nerdfont)
+
+ # Store the table and corresponding Nerd Fonts version together in a module.
+ with open("nerd_font_codepoint_tables.py", "w") as f:
+ print(
+ """#! This is a generated file, produced by nerd_font_codegen.py
+#! DO NOT EDIT BY HAND!
+#!
+#! This file specifies the mapping of codepoints in the original symbol
+#! fonts to codepoints in a patched Nerd Font. This is extracted from
+#! the nerd fonts patcher script and the symbol font files.""",
+ file=f,
+ )
+ print(f'version = "{nf_version}"', file=f)
+ print("cp_tables = {", file=f)
+ for name, table in cp_tables.items():
+ print(f' "{name}": {{', file=f)
+ for key, value in table.items():
+ print(f" {hex(key)}: {hex(value)},", file=f)
+ print(" },", file=f)
+ print("}", file=f)
+
+ return cp_tables
+
+
+def generate_zig_switch_arms(
+ patch_sets: list[PatchSet],
+ nerd_font: TTFont,
+ nf_version: str,
+) -> str:
+ cmap = nerd_font.getBestCmap()
+ glyphs = nerd_font.getGlyphSet()
+ cp_tables = generate_codepoint_tables(patch_sets, nerd_font, nf_version)
+
+ entries: dict[int, PatchSetAttributeEntry] = {}
+ for entry in patch_sets:
+ patch_set_name = entry["Name"]
+ print(f"Info: Extracting rules from patch set '{patch_set_name}'")
+
+ attributes = entry["Attributes"]
+ patch_set_entries: dict[int, PatchSetAttributeEntry] = {}
+
+ cp_table = cp_tables[patch_set_name]
+ for cp_original in range(entry["SymStart"], entry["SymEnd"] + 1):
+ if cp_original not in cp_table:
+ continue
+ cp_nerdfont = cp_table[cp_original]
+ if cp_nerdfont in entries:
+ raise ValueError(
+ f"Overlap for codepoint {hex(cp_nerdfont)} in patch set '{patch_set_name}'"
+ )
+ if cp_original in attributes:
+ patch_set_entries[cp_nerdfont] = attributes[cp_original].copy()
+ else:
+ patch_set_entries[cp_nerdfont] = attributes["default"].copy()
+
+ if entry["ScaleRules"] is not None:
+ for group in entry["ScaleRules"]["ScaleGroups"]:
+ xMin = math.inf
+ yMin = math.inf
+ xMax = -math.inf
+ yMax = -math.inf
+ individual_bounds: dict[int, tuple[int, int, int, int]] = {}
+ individual_advances: set[float] = set()
+ for cp_original in group:
+ if cp_original not in cp_table:
+ # There is one special case where a scale group includes
+ # a glyph from the original font that's not in any patch
+ # set, and hence not in the Symbols Only font. The point
+ # of this glyph is to add extra vertical padding to a
+ # stretched (^xy) scale group, which means that its
+ # scaled and aligned position would span the line height
+ # plus overlap. Thus, we can use any other stretched
+ # glyph with overlap as stand-in to get the vertical
+ # bounds, such as 0xE0B0 (powerline left hard divider).
+ # We don't worry about the horizontal bounds, as they by
+ # design should not affect the group's bounding box.
+ if (
+ patch_set_name == "Progress Indicators"
+ and cp_original == 0xEDFF
+ ):
+ glyph = glyphs[cmap[0xE0B0]]
+ bounds = BoundsPen(glyphSet=glyphs)
+ glyph.draw(bounds)
+ yMin = min(bounds.bounds[1], yMin)
+ yMax = max(bounds.bounds[3], yMax)
+ else:
+ # Other cases are due to lazily specified scale
+ # groups with gaps in the codepoint range.
+ print(
+ f"Info: Skipping scale group codepoint {hex(cp_original)}, which does not exist in patch set '{patch_set_name}'"
+ )
+ continue
+
+ cp_nerdfont = cp_table[cp_original]
+ glyph = glyphs[cmap[cp_nerdfont]]
+ individual_advances.add(glyph.width)
+ bounds = BoundsPen(glyphSet=glyphs)
+ glyph.draw(bounds)
+ individual_bounds[cp_nerdfont] = bounds.bounds
+ xMin = min(bounds.bounds[0], xMin)
+ yMin = min(bounds.bounds[1], yMin)
+ xMax = max(bounds.bounds[2], xMax)
+ yMax = max(bounds.bounds[3], yMax)
+ group_width = xMax - xMin
+ group_height = yMax - yMin
+ group_is_monospace = (len(individual_bounds) > 1) and (
+ len(individual_advances) == 1
+ )
+ for cp_original in group:
+ if cp_original not in cp_table:
+ continue
+ cp_nerdfont = cp_table[cp_original]
+ if (
+ # Scale groups may cut across patch sets, but we're only
+ # updating a single patch set at a time, so we skip
+ # codepoints not in it.
+ cp_nerdfont not in patch_set_entries
+ # Codepoints may contribute to the bounding box of multiple groups,
+ # but should be scaled according to the first group they are found
+ # in. Hence, to avoid overwriting, we need to skip codepoints that
+ # have already been assigned a scale group.
+ or "relative_height" in patch_set_entries[cp_nerdfont]
+ ):
+ continue
+ this_bounds = individual_bounds[cp_nerdfont]
+ this_height = this_bounds[3] - this_bounds[1]
+ patch_set_entries[cp_nerdfont]["relative_height"] = (
+ this_height / group_height
+ )
+ patch_set_entries[cp_nerdfont]["relative_y"] = (
+ this_bounds[1] - yMin
+ ) / group_height
+ # Horizontal alignment should only be grouped if the group is monospace,
+ # that is, if all glyphs in the group have the same advance width.
+ if group_is_monospace:
+ this_width = this_bounds[2] - this_bounds[0]
+ patch_set_entries[cp_nerdfont]["relative_width"] = (
+ this_width / group_width
+ )
+ patch_set_entries[cp_nerdfont]["relative_x"] = (
+ this_bounds[0] - xMin
+ ) / group_width
+ entries |= patch_set_entries
+
+ # Group codepoints by attribute key
+ grouped = defaultdict[AttributeHash, list[int]](list)
+ for cp, attr in entries.items():
+ grouped[attr_key(attr)].append(cp)
+
+ # Emit zig switch arms
+ 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))
+
+ return "\n".join(result)
+
+
+if __name__ == "__main__":
+ project_root = Path(__file__).resolve().parents[2]
+
+ nf_path = sys.argv[1]
+
+ nerd_font = TTFont(nf_path)
+
+ patcher_path = project_root / "vendor" / "nerd-fonts" / "font-patcher.py"
+ source = patcher_path.read_text(encoding="utf-8")
+ patch_set, nf_version = 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
+//! DO NOT EDIT BY HAND!
+//!
+//! This file provides info extracted from the nerd fonts patcher script,
+//! specifying the scaling/positioning attributes of various glyphs.
+
+const Constraint = @import("face.zig").RenderOptions.Constraint;
+
+/// Get the constraints for the provided codepoint.
+pub fn getConstraint(cp: u21) ?Constraint {
+ return switch (cp) {
+""")
+ f.write(generate_zig_switch_arms(patch_set, nerd_font, nf_version))
+ f.write("\n else => null,\n };\n}\n")