feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)

Ports the macOS-specific Zig code behind cfg(target_os = "macos"):
- renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend
  (all renderer/metal/* files, shaders.metal verbatim, runtime library
  compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga;
  OpenGL remains the backend elsewhere.
- font: CoreText face, shaper and discovery with build.rs backend
  selection (coretext default; coretext-freetype / coretext-harfbuzz /
  coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple.
- input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes.
- os: macos, mach, cf_release_thread, NSProcessInfo args and macOS
  branches; config: macOS defaults, keybinds, Application Support paths.
- termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit
  handling, os_log logging.

Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings
clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot
run here. The embedded runtime/C API (embedded.zig) is the next step.

Also includes the cross-platform integration edits shared by the crash,
verification, Windows and macOS work (Cargo target-gating, thread
crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg
gates), a cfg fix so ghostty-config builds on wasm32, and an allow for
the intentional recursion in the crash overflow test.

Test Plan:
- cargo test --workspace (3224 passed)
- cargo clippy --workspace --all-targets -- -D warnings
- cargo check --target aarch64-apple-darwin -p ghostty-renderer (with
  the documented cross env)
This commit is contained in:
ddidderr committed 2026-10-12 06:02:39 +02:00
1 parent 76771bed0d
commit bab49ee596
124 files changed
+14936 -1242

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
# macOS CoreText font backend (M1 / font)
Crate: `ghostty-font`. Compile-checked with
`cargo check|clippy --target aarch64-apple-darwin|x86_64-apple-darwin -p ghostty-font --all-targets`
(clean, `-D warnings`); never run on real macOS. Linux behaviour unchanged
(`cargo test -p ghostty-font`: 203 pass / 3 ignored, clippy `-D warnings`, nightly fmt).
## Backend selection (`font/backend.zig`)
`build.rs` emits cfgs from target + cargo features: `font_freetype`, `font_fontconfig`,
`font_harfbuzz`, `font_coretext` (Apple), `font_coretext_face`, `font_coretext_shaper`, `font_noshape`.
* Linux/other: fontconfig + FreeType + HarfBuzz (unchanged). wasm32: web_canvas (unchanged).
* Apple targets, default `Backend::Coretext`: CoreText discovery/face/shaper; freetype-sys,
harfbuzz-sys, fontconfig are NOT linked (freetype/harfbuzz are optional deps, `bundled`, apple only).
* Cargo features (apple only): `coretext-freetype` (CT discovery, FreeType face, HarfBuzz shaper),
`coretext-harfbuzz` (CT face + HarfBuzz via `hb_coretext_font_create`), `coretext-noshape`
(no-op shaper). `backend::OPTIONS` follows the features. `face::DEFAULT_DPI` is 72 on macOS.
## Module map (Zig -> Rust, `crates/ghostty-font/src/`)
| Zig | Rust |
|---|---|
| face/coretext.zig | `face/coretext.rs` (CTFont face, CoreGraphics bitmap rasterization, `ColorState`) + `face/coretext_math.rs` (pure math, tested on Linux) |
| shaper/coretext.zig | `shaper/coretext.rs` (CTTypesetter/CTLine/CTRun, feature dicts, per-font attr-dict cache) + `shaper/coretext_cells.rs` (pure cluster/ligature logic, tested) + `shaper/cf_release.rs` |
| discovery.zig CoreText + `toCoreTextDescriptor` | `discovery_coretext.rs` (`CoreText`, `Score`-based sorting, CJK / `CTFontCreateForString` fallbacks) |
| DeferredFace.zig CoreText | `deferred_face_coretext.rs` (+ `coretext_freetype` file-path load) |
| library.zig (noop for CT) | `library.rs` (empty library when no FreeType) |
| SharedGridSet.zig Apple Emoji branch | `shared_grid_set.rs` (Apple Color Emoji via discovery on CT; embedded Noto emoji otherwise) |
| (CF helpers of the `macos` package) | `coretext_util.rs` (`CfBox` Send+Sync wrapper, tables, glyph lookup, variation axes) |
API unification (so collection/grid/resolver stop naming `face::freetype::*`): `face::{Face, FaceError,
RenderError, HbFont}`, `shape::{Shaper, ShapeError, ShapeRunError}`, `discovery::{Discover, NextError}`.
`discover_fallback` now takes `&mut Collection` (CT needs `get_face`); no-op shaper gained `shape_run`,
`ShapeError` and `new -> Result`.
## Tests
Ported & platform-independent (run on Linux): metrics table priority (OS/2 typo vs hhea vs win),
underline/strikethrough fallbacks, synthetic-bold width, glyph bearing/canvas layout (incl. sbix, thicken,
bold), UTF-16/surrogate table, CoreText cell building incl. ligature/out-of-order-cluster/non-LTR heuristics,
discovery `Score` ordering, style fuzzy/exact scoring, variation-axis bold/italic thresholds, backend
capability predicates. macOS-only tests written but not runnable here (`#[cfg(font_coretext...)]`):
face (Monaco ASCII, name, emoji, in-memory, variable, set variation, SVG table, glyphIndex colored vs text),
discovery (coretext, codepoint, sorting), deferred face. The Zig CoreText shaper tests (23, need
Terminal/RenderState + system fonts) are NOT ported; resolver/collection tests adjusted for CT
(`Coretext doesn't support Noto's format`, text-emoji index 1).
## Caveats / TODO(integration)
* Never executed on macOS: rasterization (CGBitmapContext flags, colour spaces), shaping and discovery are
faithful ports but unverified at runtime.
* `coretext-freetype` / `coretext-harfbuzz` cannot be cross-checked here: bundled harfbuzz with CoreText
needs the macOS SDK headers (`ApplicationServices`). `coretext-noshape` and the default are checked.
* `face_metrics` returns `Some(..)` for cap/ex/ascii height always (Zig returns plain values for CT).
* CF release thread is local to the font crate (`shaper/cf_release.rs`, std thread + channel);
switch to `ghostty_os::cf_release_thread` once that exists.
* `Face::init` copies in-memory font data into a CFData (Zig used no-copy).
* Renderer crate must use `ghostty_font::shape::{ShapeError, ShapeRunError}` instead of
`shaper::harfbuzz::{ShapeError, ShapeRunError}` (generic.rs lines ~144-146) to build on macOS.
* Cross-check env: `CC_<target>`/`AR_<target>` zig-cc wrappers (onig_sys C build); `GETTEXT_DIR` dummy.
+92
View File
@@ -0,0 +1,92 @@
# macOS M1 — renderer (Metal backend + GraphicsApi abstraction)
Scope: `crates/ghostty-renderer` only. Zig: `renderer/Metal.zig`, `renderer/metal/*`, the macOS parts of
`generic.zig` / `Thread.zig` / `renderer.zig` / `shadertoy.zig`.
## GraphicsApi abstraction (`graphics_api.rs`)
Zig's `GenericRenderer(comptime GraphicsAPI)` duck-types its API; Rust has the `GraphicsApi` trait with
associated types (`Target`, `Texture`, `Sampler`, `Buffer<T>` (GAT), `BufferRef`, `Pipeline`, `Shaders`, `GpuUniforms`,
`Frame`, `RenderPass<'a>`), associated consts (`CUSTOM_SHADER_TARGET`, `CUSTOM_SHADER_Y_IS_DOWN`, `SWAP_CHAIN_COUNT`) and
operations (`init_*`, `begin_frame`, `surface_size`, `present_last_target`, `draw_frame_start/end`, `loop_enter/exit`, ...).
Helper traits: `GpuBuffer`, `GpuTexture`, `GpuTarget`, `GpuShaders`, `GpuFrame`, `GpuRenderPass`; shared value types
`Attachment`, `AttachmentTarget`, `Draw`, `Step`, `Primitive`, `ImageTextureFormat`, `TextureKind`, `ApiError`.
* `generic::Renderer<G: GraphicsApi = OpenGL>`, `image::{State,Image,Entry}<G>`, `thread::Thread<G = Api>`.
* `lib.rs`: `Api = metal::Metal` on `target_vendor = "apple"`, `opengl::OpenGL` otherwise; `Renderer = generic::Renderer<Api>`
(`Backend::default_for_target` agrees, tested).
* The OpenGL backend implements the trait in `opengl/api.rs` by delegating to its (unchanged) inherent API, so apprt-gtk
and `tests/egl_headless.rs` keep using the GL-typed API. `opengl::render_pass::{Attachment,Step,Draw}` are now aliases of the
generic types; `gl::Primitive` re-exports `graphics_api::Primitive`.
* CPU-side GPU structs shared by every API moved to `shader_types.rs` (`Uniforms` keeps the std140 layout with bit-packed `bools`;
Metal converts via `GraphicsApi::gpu_uniforms` to `MetalUniforms`, whose bools are four bytes like the MSL struct).
* Behaviour changes needed for asynchronous (Metal) frames, equal to Zig's `frame_sema`/`frameCompleted`:
the swap chain hands frame states back from the frame completion callback (`SwapChainInner` + `Condvar`);
`begin_frame(&self)` no longer borrows the API; `GpuFrame::complete(sync, completion)` reports health through a
`FrameCompletion` (inline for GL / sync Metal, on Metal's completion thread otherwise); `display_unrealized` waits for
in-flight frames; font grid changes bump an `atlas_epoch` so in-flight frames resync atlases.
* `Renderer::display_realized<F>(loader)` / `last_frame_pixels` exist only for `Renderer<OpenGL>`; the generic hook is
`display_realized_with(|api| ..)`.
* GL: `OpenGL::last_target` is now `Arc<Mutex<Option<Arc<Target>>>>` (frames present without borrowing the API); `present`,
`present_last_target`, `clear_last_target`, `begin_frame` take `&self`; `last_target()` returns a clone.
## Metal (`src/metal/`, `cfg(target_vendor = "apple")`, objc2 0.6 + objc2-metal 0.3)
| Zig | Rust |
|---|---|
| Metal.zig | `metal/mod.rs` (`Metal`, `ViewInfo`, `Metal::init(blending, &ViewInfo)`) |
| metal/api.zig | `metal_layout.rs` (raw values, plain Rust, unit tested) + `objc2_metal` types |
| metal/buffer.zig | `metal/buffer.rs` |
| metal/Frame.zig | `metal/frame.rs` (command buffer, completion block via `block2::RcBlock`) |
| metal/IOSurfaceLayer.zig | `metal/io_surface_layer.rs` (`define_class!` CALayer subclass `IOSurfaceLayer`: `display`, `actionForKey:`; main-thread `set_surface` via `dispatch2`) |
| metal/Pipeline.zig | `metal/pipeline.rs` (vertex attributes from `MetalVertex::ATTRIBUTES` instead of reflection) |
| metal/RenderPass.zig | `metal/render_pass.rs` (buffer slots: vertex 0, uniforms 1, rest from 2) |
| metal/Sampler.zig, Target.zig, Texture.zig | `metal/{sampler,target,texture}.rs` (Target = IOSurface BGRA + Display P3 colorspace + MTLTexture) |
| metal/shaders.zig | `metal/shaders.rs` (+ `shader_types`, `metal_layout::PIPELINE_DESCS`) |
| shaders/shaders.metal | `shaders/shaders.metal` (verbatim) |
* The metallib: Zig precompiles `shaders.metal` with `xcrun metal`; we embed the source and compile it with
`newLibraryWithSource` at `Shaders::init` (same as post-process shaders already do). Entry points are looked up by name.
* View attachment is done with `msg_send!` on the `NSView`/`UIView` (no AppKit dependency): macOS `setLayer:` then
`setWantsLayer:`, iOS `addSublayer:`. `setClipsToBounds:` is guarded with `respondsToSelector:` (NSView API since macOS 14).
* `Metal::init` expects to run on the main thread with the view the apprt (L1) provides; the apprt builds `Metal` and passes it
to `Renderer::new(opts, api)`; `Thread::draw_on_app_thread` defaults to false on Apple targets.
* iOS: compiled via `target_vendor = "apple"` with the UIView branch; not checked here (only macOS targets are installed).
## macOS parts of generic / thread
* `display_link.rs`: `CVDisplayLink` wrapper (`objc2-core-video`, macOS only). On other platforms `DisplayLink` is uninhabited
and the renderer holds `None` (Zig: `DisplayLink = void`). `Renderer` creates it when `window-vsync` is set (macOS),
`loop_enter(draw_now)` installs the callback and starts it, `loop_exit` stops, `set_focus`/`set_visible` start/stop it,
`set_macos_display_id`, `has_vsync` (= link running).
* `loop_enter` also sets the Metal layer's display callback to `draw_frame(true)` (holding a `Weak<Renderer>`).
* `thread.rs`: `pthread_setname_np("renderer")`, QoS class (`qos_for(visible, focused)`, tested on every platform),
autorelease pool around `drain_mailbox`, `Message::MacosDisplayId`, `thread_enter/exit`, `loop_enter/exit` guards.
* `generic.rs`: with `background-blur = macos-glass-*` the background alpha is forced to 0 (macOS only).
`Renderer::{thread_enter,thread_exit,loop_enter,loop_exit,set_macos_display_id}` are new.
## Shadertoy MSL
`shadertoy::msl_from_compiled` (naga `msl-out`, enabled on all platforms so it is tested on Linux): entry point renamed to
`ghostty_main` (spirv-cross' `main0` becomes `main0_` in naga, which suffixes names ending in a digit), `Globals` at
`[[buffer(1)]]`, `iChannel0` at `[[texture(0)]]` / `[[sampler(0)]]` (what the Metal render pass binds), MSL 2.1.
`Target::Msl` no longer returns `Unsupported`. Known limitation: the unused `iChannelTime` float array has std140 stride 16
in the CPU struct but naga emits tightly packed `float[4]` (spirv-cross emits padded types); no Ghostty shader uses it.
## Tests
* Linux (run): `metal_layout` (resource options / texture usage bits, storage mode, max texture size, surface size clamp,
pixel formats, vertex attribute tables, buffer slots, blend state, `MetalUniforms` offsets == MSL struct, conversion from the
std140 uniforms, shader function names exist in `shaders.metal`), shadertoy `shadertoy_to_msl` (port of "shadertoy to msl",
+focus shader, +file loading), `thread::qos_class_policy`, `backend` tests, `tests/mock_api.rs` (generic renderer over a mock
API with asynchronous triple-buffered completion, health reporting, unrealize waiting for in-flight frames).
Totals: 97 unit + 9 EGL + 2 mock_api, all passing; the 9 EGL tests are unchanged apart from dropping an unneeded `mut`.
* macOS (compile-checked only): `metal/target.rs`, `render_pass.rs`, `frame.rs` unit tests (pixel format fourcc, primitive
mapping, health from command buffer status) compile with `--all-targets`.
* Not ported: Zig has no tests in the Metal files other than `shadertoy to msl` (ported) and the `CellText` size test (covered
by `shader_types` layout tests).
## Deviations / notes
* `Buffer::sync` (Metal) allocates exactly double the required bytes; Zig multiplies by `@sizeOf(T)` again.
* `Texture::replace_region` rejects too-short data (safe fn), Zig does not check.
* `Image` vertex stays 48 bytes (GL layout padded to 16); Metal's stride is `size_of` of that struct, offsets unchanged.
* `ghostty-core`'s `surface/mod.rs` imports `ghostty_renderer::Renderer` (the platform alias) but builds an `OpenGL` API (GTK);
on macOS that alias is `Renderer<Metal>`. Until L1 provides the embedded surface, core should import
`ghostty_renderer::generic::Renderer` (= `Renderer<OpenGL>`, identical type on Linux) so `cargo check --target *-apple-darwin
-p ghostty-core` passes. `Thread` is generic over the API for this reason.
* Cross-check recipe (no macOS SDK): `GETTEXT_DIR=/nonexistent` (skip gettext-sys source build), `CC_<target>` = a wrapper
around `zig cc -target aarch64-macos` for `onig_sys` (drop `-arch`/`--target`/`-mmacosx-version-min`), `CARGO_BUILD_RUSTC_WRAPPER=`
(kache is picked up as cc wrapper), `ZIG_GLOBAL_CACHE_DIR` on a writable fs.
+96
View File
@@ -0,0 +1,96 @@
# macOS port (M1): os, input, config, termio, core, crash glue
Scope of this note: everything of M1 except the two big sub-ports, which have their own notes:
* `macos-M1-font.md` – CoreText face/shaper/discovery, backend selection (`ghostty-font`).
* `macos-M1-renderer.md` – Metal backend, `GraphicsApi` abstraction (`ghostty-renderer`).
Nothing here has ever run on macOS. It is compile-checked (`cargo check|clippy --target
aarch64-apple-darwin|x86_64-apple-darwin`) and every piece of platform-independent logic was
extracted so its macOS variant is unit-tested on Linux (via explicit `darwin: bool` parameters, or
`ghostty_input::platform::with_darwin` in test builds). Linux behaviour is unchanged.
## Cross-checking on Linux (no macOS SDK)
The C build scripts of the dependency tree (onig_sys, gettext-sys, dear-imgui-sys, oslog) cannot
cross-compile without the SDK. For the *check only* (no repo change) use `zig cc`/`zig c++`
wrappers (`-target aarch64-macos`/`x86_64-macos`):
```
export CARGO_BUILD_RUSTC_WRAPPER= # kache breaks cc-rs
export CC_aarch64_apple_darwin=<zcc-arm> CXX_aarch64_apple_darwin=<zcxx-arm> AR_aarch64_apple_darwin=<zig ar>
export GETTEXT_DIR=/nonexistent # makes gettext-sys skip its from-source build
export CFLAGS_<target>=-I<dir with a stub os/log.h> # oslog's wrapper.c needs <os/log.h>
export ZIG_GLOBAL_CACHE_DIR/ZIG_LOCAL_CACHE_DIR=<writable dir>
cargo check --target aarch64-apple-darwin -p ghostty-vt -p ghostty-unicode -p ghostty-datastruct \
-p ghostty-os -p ghostty-input -p ghostty-config -p ghostty-terminal -p ghostty-font \
-p ghostty-renderer -p ghostty-termio -p ghostty-core
```
(the wrappers drop `-arch`, `-mmacosx-version-min` and `--target=` that cc-rs adds). Real macOS builds
need none of this: onig_sys compiles with the system clang, `gettext-rs` builds libintl from source
(like Zig's `pkg/libintl`), `oslog`/imgui use the SDK. Root crate (`ghostty`, GTK) and
`ghostty-apprt-gtk` are Linux-only and are not part of the macOS check (the macOS app links the embedded
`libghostty`, L1's job).
## ghostty-os
| Zig | Rust |
|---|---|
| `os/macos.zig` | `macos.rs`: `QosClass`, `set_qos_class`, `pthread_setname_np`, `is_at_least_version`, `app_support_dir`, `cache_dir` (NSFileManager via objc2-foundation), pure `join_dir`/`QosClass` values tested everywhere |
| `os/mach.zig` | `mach.rs`: `VmTag` (+`make`), `tagged::TaggedPageAllocator` (mmap with VM tag in the fd arg; Rust has no stable allocator API so it is an alloc/unmap/realloc struct) |
| `os/cf_release_thread.zig` | `cf_release_thread.rs`: bounded(64) mailbox + stop channel instead of xev; injectable release fn (tests count releases on Linux); `CFRelease` via `CFRetained` |
| `os/args.zig` IteratorMacOS | `args::iterator()` on macOS reads `NSProcessInfo.arguments` |
| `os/mouse.zig` | `click_interval()` = `NSEvent.doubleClickInterval` ceil ms (`click_interval_ms` tested) |
| `os/desktop.zig` | macOS: ppid == 1 (+ `GHOSTTY_MAC_LAUNCH_SOURCE=app` for the lib artifact: `launched_from_desktop_lib`), `DesktopEnvironment::Macos`, iOS true |
| `os/homedir.zig` | macOS `homeDirectoryForCurrentUser` after the `HOME` check |
| `os/locale.zig` | Darwin `ensure_locale`: LANG from NSLocale (`<lang>_<country>.UTF-8`) + `LANGUAGE` from preferred languages (`preferred_language_value`), setlocale fallback chain (module `darwin`, unsafe allowed there) |
| `os/open.zig` | `open [-t] url`; iOS `Unsupported`; Windows branch kept |
| `os/resourcesdir.zig` | `Contents/Resources` bundle lookup first, `terminfo/78/xterm-ghostty` sentinel |
| `os/pipe.zig` | Darwin: `pipe` + `FD_CLOEXEC` on both ends (no `pipe2`) |
| `os/TempDir.zig` | non-Linux random via `/dev/urandom` (no `getrandom`) |
| `os/systemd.zig`, `passwd.zig` flatpak | Linux/Android only; `launched_by_systemd` false and `notify` no-ops elsewhere |
| `os/i18n.zig` canonicalize | still the pure-Rust approximation on all platforms (the real `_libintl_locale_name_canonicalize` is only linked in Zig's own libintl build) |
`xdg::Error` gained `AppleApiFailed`. Deps (apple only): objc2, objc2-foundation, objc2-app-kit, objc2-core-foundation, libc.
## ghostty-input
* `platform.rs`: `is_darwin()`/`is_macos()` consts with a test-only per-thread override (`with_darwin`).
* Darwin branches enabled: `Mods::translation` (option-as-alt with sides), `ctrl_or_super` (Mods, Key, free fn
+ `ctrl_or_super_for(mods, darwin)`), keycode table column 4 (mac) on Apple (`native_for_code` for tests),
key_encode: kitty `alt_prevents_text`, modifyOtherKeys alt handling, `legacy_alt_prefix` option-as-alt,
"super+text encodes nothing" on macOS. The 7 macOS-only Zig tests + `translation macos-option-as-alt` are ported
and run on Linux through the override.
* `keymap_darwin.rs` (`KeymapDarwin.zig`): Carbon `TISCopyCurrentKeyboardLayoutInputSource`/`UCKeyTranslate`
FFI (macOS only); `CarbonMods`, `carbon_modifier_state`, `utf16_to_utf8` tested on Linux; `keymap_noop.rs`;
`ghostty_input::{Keymap, KeymapState, KeymapTranslation}` pick by platform. Crate lint is now `unsafe_code = "deny"`.
## ghostty-config
macOS defaults: `font-size` 13, `alpha-blending` native, `copy-on-select` (true on Linux/macOS, false elsewhere),
`quick-terminal-autohide` true, `quit-after-last-window-closed` Linux-only, `linux-cgroup` never off Linux.
`Keybinds::init_for(darwin)` is now a faithful port of the whole Zig `init` (platform lists + the
Mac-specific block: cmd+q/k/a, undo/redo, mac windowing/splits/search/inspector, natural text editing);
`init()` selects with the target. Linux output verified identical to the previous dump; macOS tested.
`file_load`: Application Support paths (`default/legacy/preferred_app_support_path`, `preferred_default_file_path`
macOS order), `edit::config_path_candidates`, `load_default_files` loads app-support + XDG and writes the
template to app support; `probable_cli_environment` macOS desktop branch; `load_cli_args` reads
`ghostty_os::args::iterator()` (NSProcessInfo) and the Linux/BSD-only `xdg-terminal-exec` + no-args fast path.
## ghostty-termio / crash / core / terminal
* `pty.rs`: Darwin `openpty()` (libc) + CLOEXEC on the master; `command.rs`: dup2 path clears CLOEXEC on the
source fd, hand-rolled `execvpe` (PATH search like Zig's `execvpeZ`, candidates precomputed pre-fork,
tested), `__error` for errno; `wakeup.rs`: non-blocking pipe instead of eventfd; `exec.rs`: no pidfd (poll
`waitpid`), `login(1)` wrapping (`exec_command_with(command, darwin, passwd)`, `-q` for `.hushlogin`, tests ported +
extra), `XDG_DATA_DIRS`/`MANPATH` bundle env (`darwin_bundle_env`), killpg EPERM ignored on Darwin, terminfo-missing
warnings; `shell_integration.rs`: `/bin/bash` is not integrated on Darwin (`detect_shell_for`).
* `ghostty-crash`: `getentropy` instead of `getrandom` on Apple.
* `ghostty-terminal` search thread: `pthread_setname_np` + `QosClass::Utility` on Apple.
* `ghostty-core`: Darwin child-exit handling (any quick exit is abnormal, no exit code line), macOS
`encode_key_opts` (`macos-option-as-alt` from config or `RtApp::keyboard_layout().detect_option_as_alt()`),
rectangle select = option only, `global::Logging.macos` + `os_log` logger (oslog crate, unified logging, levels
debug/info/err/fault like Zig; `GHOSTTY_LOG=macos` parsed).
* `ghostty-apprt` (tiny): `RtApp::keyboard_layout()` default method (`Unknown`); the embedded runtime (L1) overrides it
with `ghostty_input::Keymap::source_id` → `KeyboardLayout::map_apple_id`.
## Not done / for L1 and later
* apprt embedded.zig macOS bits (NSView/surface plumbing, CVDisplayLink hosting, `keyboardLayout`, app menu actions): L1.
* Render/IO thread names on Darwin rely on `std::thread::Builder::name` (std calls `pthread_setname_np` for the
new thread on Darwin, which is what the Zig explicit call works around). QoS for the renderer thread: see renderer note.
* The font crate keeps a private CF release thread (`shaper/cf_release.rs`); `ghostty_os::cf_release_thread` is available
for the renderer to own one like Zig's `renderer/Thread.zig`.
* Skipped Zig tests: `macos.zig cacheDir paths` runs only on macOS (ported as a pure + `cfg(apple)` test),
KeymapDarwin's empty `test {}`, `Command.zig` `/private/tmp` cwd expectation (Linux test untouched).