Previously `ghostty_app_key_is_binding` (unlike Surface) is just using
`config.keybind` to check whether a KeyEvent is in the set or not.
After this, I can add unit tests for keybinding more easily with dummy
configs.
I didn't find any usages of this in GTK, so it shouldn't affect
anything. ci will see if this is the case:)
This fixes a hardcoded build issue on macOS where Zig unconditionally
forces xcodebuild -create-xcframework to run during compilation, even
when the caller explicitly specifies that they only want the raw
standard C objects/headers (-Demit-lib-vt).
This fixes a hardcoded build issue on macOS where Zig unconditionally forces xcodebuild -create-xcframework to run during compilation, even when the caller explicitly specifies that they only want the raw standard C objects/headers (-Demit-lib-vt).
The Bug:
Around line 155 in build.zig, the libghostty-vt xcframework was being packaged unconditionally for Darwin builds. This caused developers (and wrappers like go-libghostty) attempting to natively build the vt library locally using only the minimal macOS Command Line Tools to experience an immediate crash, as xcodebuild -create-xcframework strictly demands a full Xcode application installation.
The Fix:
Guarded the GhosttyLibVt xcframework creation step with config.emit_xcframework. Because src/build/Config.zig intuitively forces emit_xcframework to default to false whenever emit_lib_vt is invoked, this structurally allows lightweight macOS builds to safely skip the xcodebuild invocation while still correctly compiling the standard .a object library files.
This allows libghostty-vt to be cross-compiled for macOS from non-macOS
platforms. I've updated pkg/apple-sdk to fallback to Zig's embedded
macOS headers if the macOS SDK is not found. Additionally,
CombineArchivesStep has been updated to use Linux tooling on Linux. CI
updated to test this.
This allows libghostty-vt to be cross-compiled for macOS from non-macOS
platforms. I've updated pkg/apple-sdk to fallback to Zig's embedded
macOS headers if the macOS SDK is not found.
Additionally, CombineArchivesStep has been updated to use Linux
tooling on Linux.
Previously `ghostty_app_key_is_binding` (unlike Surface) is just using `config.keybind` to check whether a KeyEvent is in the set or not.
After this, I can add unit tests for keybinding more easily, with dummy configs.
Extract the tight per-byte parsing loop from TerminalParser.step into
a separate noinline function (parseAll). This eliminates a ~20%
benchmark regression that appeared after the highway vendor changes
despite zero changes to the parser source code.
The root cause: the parser benchmark processes 50 MB of input through
a byte-at-a-time DFA loop that is highly sensitive to instruction
cache-line placement on Apple Silicon. The M-series cores fetch
aligned 16-byte blocks; when the loop head lands near the end of a
64-byte cache line (offset 60), only one instruction fits in the
first fetch versus four when aligned to offset 48. This causes ~29%
more cycles for identical instruction counts.
Previously the loop was inlined into the large step() function, so
any code change anywhere in the binary (like the highway vendor
restructuring) could shift the loop across a cache-line boundary.
By making parseAll noinline, the loop gets its own function placement
that is stable regardless of surrounding code changes.
The previous runtime_detect.zig called std.zig.system.resolveTargetQuery
which pulled in the entire Zig target/CPU model table infrastructure for
every architecture (~4,000 symbols, ~175 KB of data tables, ~130 KB of
code). This bloated the binary by ~500 KB and shifted code layout enough
to cause a measurable icache/branch-predictor regression in unrelated
hot paths like the terminal parser (~20% more cycles for identical
instruction counts).
Replace with minimal, direct CPU feature detection per architecture:
CPUID + XGETBV inline assembly on x86, sysctlbyname on Darwin AArch64,
and getauxval/prctl via std.os.linux (direct syscalls, no libc) on
Linux for AArch64, PPC, S390x, RISC-V, and LoongArch.
Split into per-architecture files under src/detect/ for
maintainability.
This uses a custom fork of `hwy/targtes.cpp` that uses an extern
function written in Zig to use Zig's standard CPU detection to avoid
a dependency on Apple SDK headers.
This is on the path to removing Apple SDK requirements to build
libghostty-vt, but will require a lot more work outside of this. The goal
is to get this out of our external dependencies first and then we can
work on removing the internal side.
Adds a FreeType-based `Discover` implementation for Windows. It walks
the system font directory (`%SYSTEMROOT%\Fonts`) and the per-user
directory (`%LOCALAPPDATA%\Microsoft\Windows\Fonts`), matches
descriptors by FreeType `family_name` (falling back to the SFNT name
table), and, when a codepoint is in the descriptor, filters on CMap
coverage.
Wired up as a new `.freetype_windows` backend which `Backend.default()`
now returns on Windows. Existing freetype-only paths are untouched and
no other platform is affected; cross-platform switches were extended to
handle the new enum value the same way they handle `.freetype`.
With this in place, the standard code paths (`+list-fonts`,
`SharedGridSet` font-family lookup, `CodepointResolver` fallback) work
on Windows without any `os.tag == .windows` branches in the caller.
Verified by the `build-libghostty-windows-gnu` CI job. No runtime binary
ships yet on Windows (no apprt), but this is a drop-in for the discovery
API that the Win32 apprt (and the revisited `+list-fonts` PR #12384)
will use. Once this lands, #12384 can be closed and `+list-fonts` will
work on Windows through the ordinary discovery code path, which
addresses the review feedback that `+list-fonts` should only show fonts
the internal discovery can find.
---
AI usage disclosure: developed with Claude Code (Claude Opus 4.7).
Claude drafted the implementation based on my design direction -- I
picked the "add a Discover backend" shape over the ad-hoc approach in
the earlier `+list-fonts` PR. I reviewed each diff and validated it with
a Windows GNU-ABI smoke build before pushing.
Part of the Win32 apprt upstreaming series (see discussion #2563 /
mattn/ghostty#1).
This code was motivated by the need for the glyph protocol handler
(#12352) to be able to validate the provided `glyf` payload, without
having to link freetype or anything (because libghostty-vt needs to be
static). As such it's written specifically to meet those needs, but in
such a way that it can be expanded if we find a need for more in-depth
inspection of `glyf`s in the future.
Two more holdouts in DeferredFace.zig test helpers calling
Fontconfig.init / CoreText.init with no args; Nix test CI surfaced
them for the fontconfig path.
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
> [!IMPORTANT]
> Stacked on #12214. Review that first. (i am targeting `main` so here
you will see the full changeset, including 12214
Two changes that make the static libghostty archive consumable by
external linkers (MSVC link.exe, .NET NativeAOT, Rust, Go, etc.):
**Fat static archive on all platforms**
The static archive previously only bundled vendored deps on macOS (via
libtool). On Windows and Linux the archive contained only the
Zig-compiled code, requiring consumers to find and link freetype,
harfbuzz, glslang, spirv-cross, simdutf, oniguruma, etc. separately.
Now all platforms produce a single fat archive:
- macOS: libtool (unchanged)
- Windows: zig ar qcL --format=coff (MSVC's lib.exe can't read
Zig-produced GNU-format archives, so we use the bundled LLVM archiver)
- Linux: ar -M with MRI scripts (same approach as libghostty-vt)
**MSVC ubsan suppression for C deps**
Zig's ubsan runtime can't be bundled on Windows (LNK4229), leaving
__ubsan_handle_* symbols unresolved. freetype, glslang, spirv-cross, and
highway already suppress ubsan. This adds MSVC-conditional suppression
to seven more: harfbuzz, libpng, dcimgui, wuffs, oniguruma, zlib, and
stb.
Gated on abi == .msvc so ubsan coverage is preserved on Linux/macOS.
## Test plan
- [x] zig build produces a fat ghostty-static.lib (~230MB) with ~200
object files
- [x] MSVC's lib /LIST can read the archive
- [x] .NET NativeAOT consumer resolves all symbols (0 unresolved)
- [x] Linux/macOS builds unaffected (ubsan remains enabled)
CI on Windows (MSVC) surfaced three remaining callers of the old
zero-arg `Discover.init()` in shaper test helpers that the earlier
commit missed. Pass `lib` to match the new signature.
Co-authored-by: Claude <noreply@anthropic.com>
* Unset the Nix compiler and linker environment in the fuzz dev shell so
AFL++ uses the system or Homebrew Apple toolchain directly.
* Force afl-cc to link with lld because the newer Apple linker asserts
on the custom sections emitted by AFL's LLVM instrumentation.
* Pin fuzz-libghostty to the host target so the build does not inherit
stray SDK targets from the environment.
On macOS 26.4, AFL builds were picking up Nix compiler-wrapper
variables and Apple SDK target settings from the shell environment.
That caused afl-cc to drive the wrong linker and target configuration,
which broke even simple fuzz harness builds. Unset the Nix compiler and
linker environment in the fuzz dev shell so AFL++ uses the system or
Homebrew Apple toolchain directly.
Also force afl-cc to link with lld because the newer Apple linker
asserts on the custom sections emitted by AFL's LLVM
instrumentation. Finally, pin fuzz-libghostty to the host target so the
build does not inherit stray SDK targets from the environment.
On Windows the configured shell was always executed as `cmd.exe /C
<shell>`. That inserts a cmd.exe even for simple values like `command =
wsl ~` or `command = pwsh -NoLogo`, producing two processes where one
would do.
Two concrete side effects:
An extra cmd.exe appears in every Windows terminal's process tree
(visible in Task Manager / process listings), two processes per surface
where only one is the user's shell.
cmd.exe state set by AutoRun (`HKCU\Software\Microsoft\Command
Processor\AutoRun`, used commonly for DOSKEY aliases or `cd` in
`init.cmd`) lives in the wrapping cmd process, not in the user's shell.
Since AutoRun state like DOSKEY aliases is per-process, the user's
aliases don't reach the shell they actually interact with.
Run the shell value directly instead. If it contains whitespace, split
on whitespace into argv. A bare `cmd.exe` is resolved via `%COMSPEC%`
(the documented path to the current command processor). Other bare
values are left to PATH resolution in `Command.startWindows` (#12387).
The simple whitespace split does not honor Windows CLI quoting rules;
users who need quoted arguments should use the direct command form,
which takes an argv array as-is. For the common case (`wsl ~`, `pwsh
-NoLogo`, `cmd.exe /k init.cmd`, etc.) this covers the shapes users
actually write today.
Also skips the termios focus timer on Windows in `focusGained`, since
Windows has no termios -- the callback was arming a timer whose tick did
nothing and just added noise.
---
AI usage disclosure: developed with Claude Code (Claude Opus 4.7).
Claude drafted the implementation based on my design direction -- I
picked which pieces belong in this PR (drop the cmd wrapper, use
`%COMSPEC%`, skip the termios focus timer) and which belong in sibling
PRs. I reviewed each diff and validated it with a Windows GNU-ABI smoke
build before pushing.
Part of the Win32 apprt upstreaming series (see discussion #2563 /
mattn/ghostty#1).
Because we generally read this value from an environment variable, we
the resulting value can include a trailing slash (as on macOS). This
results in less-friendly path operations for callers who are building
paths based on this value.
`std.fs.path.join()` handles trailing slashes just fine, but it's an
allocating API. For callers who just want to format a path, they have to
assume they need to include their own path separator.
We can make this friendlier by always trimming trailing path separators
from the environment variable values before returning the slice.
This behavior matches "higher-level" languages' standard libraries (I
checked Python, Node, Ruby, and Perl). Other "systems" languages (Go,
Rust) just return the system value as-is, like we were doing before.
Windows users often set bare command names in the Ghostty config
(`command = bash`) or pass them via `-e`, matching how they would on
Linux/macOS. Today that fails because `CreateProcessW` does not do
program search for `lpApplicationName` on its own.
Thanks to @qwerasd205 for pointing out that passing `NULL` for
`lpApplicationName` is exactly how Windows docs say to get program
search for free. This PR does that: drop the explicit utf16 conversion
for `lpApplicationName`, pass `null`, and make sure the program name is
the first token of `lpCommandLine`. Windows then walks parent-app dir,
CWD, system dirs, and PATH (and appends `.exe` for extensionless names).
The child also sees its `argv[0]` exactly as we wrote it rather than a
resolved absolute path, which is less surprising.
Net change is +15 / -7 in `src/Command.zig`; no new helpers, no changes
outside that file. The earlier version of this PR (which added
PATH/PATHEXT handling in `internal_os.path.expand`) is obsoleted by this
approach and has been force-pushed away.
---
AI usage disclosure: developed with Claude Code (Claude Opus 4.7).
Claude drafted the implementation based on my direction after
@qwerasd205's review suggested the NULL-lpApplicationName approach. I
reviewed the diff, built and verified it on the Windows GNU-ABI target,
and am responsible for the code landing here.
Part of the Win32 apprt upstreaming series (see discussion #2563 /
mattn/ghostty#1).
Because we generally read this value from an environment variable, we
the resulting value can include a trailing slash (as on macOS). This
results in less-friendly path operations for callers who are building
paths based on this value.
`std.fs.path.join()` handles trailing slashes just fine, but it's an
allocating API. For callers who just want to format a path, they have to
assume they need to include their own path separator.
We can make this friendlier by always trimming trailing path separators
from the environment variable values before returning the slice.
This behavior matches "higher-level" languages' standard libraries (I
checked Python, Node, Ruby, and Perl). Other "systems" languages (Go,
Rust) just return the system value as-is, like we were doing before.
Per review feedback, cover the four Windows branches added in the
parent commit:
- bare `cmd.exe` resolves via `%COMSPEC%` (with documented fallback)
- bare non-cmd shell (`pwsh.exe`) is passed through unchanged
- shell value with arguments (`wsl ~`) is split on whitespace
- direct command is passed through without modification
Co-authored-by: Claude <noreply@anthropic.com>
Per review feedback, drop the `if (Discover == Windows)` comptime
branches in SharedGridSet and list_fonts by making every backend's
`init` take a Library and ignore it when unused. Call sites just do
`Discover.init(self.font_lib)` now.
Also adds a discovery test for the Windows backend that looks up
Arial and checks the returned face has the 'A' codepoint.
Co-authored-by: Claude <noreply@anthropic.com>
Pass null for lpApplicationName and put the program as the first
token of lpCommandLine. Per the Windows docs, this makes
CreateProcessW perform the standard program search (parent-app dir,
CWD, system dirs, PATH) and append ".exe" when the name has no
extension.
So a bare command name like `wsl` or `pwsh` from the Ghostty config
now resolves without any PATH/PATHEXT handling on our side. The
child also sees its argv[0] exactly as written rather than replaced
with the resolved absolute path.
Co-authored-by: Claude <noreply@anthropic.com>
Resolve the system font directory from SYSTEMROOT rather than assuming
it lives on C:. If SYSTEMROOT is somehow unset we skip the system
directory instead of falling back to a literal drive letter.
Co-authored-by: Claude <noreply@anthropic.com>