From 389266720dc3fcf9cf80e6874cfe682fc6f9f6d6 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Mon, 12 Oct 2026 15:56:21 +0200 Subject: [PATCH] feat(font): add the FreeType-only font backend (font-backend-freetype) Ghostty can be built with -Dfont-backend=freetype, which uses FreeType without any font discovery. The Rust port only had fontconfig+freetype (default on Linux) and the CoreText variants. The new cargo feature `font-backend-freetype` on ghostty-font (forwarded by ghostty-cli and the root crate; non-Apple, non-wasm targets) behaves like Zig's mode: font-family discovery, fallback search and codepoint-map overrides are skipped (embedded fonts only); +list-fonts and +show-face print Zig's "built without a font discovery mechanism" message and exit 1; +version reports `.freetype`. The default backend is unchanged. Also registers the Rust-only benchmark tooling extensions (--time, --handler=full, ghostty-synthetic +sgr) in DEVIANT_FIXES.md. Note: `--all-features` now selects this backend for the workspace. Test Plan: - cargo test -p ghostty-font --features font-backend-freetype (198) - cargo build --features font-backend-freetype && ghostty +list-fonts --- Cargo.toml | 3 + crates/ghostty-cli/Cargo.toml | 5 ++ crates/ghostty-cli/src/build_info.rs | 2 +- crates/ghostty-cli/src/list_fonts.rs | 33 ++++++++++ crates/ghostty-cli/src/show_face.rs | 10 +++ crates/ghostty-core/src/global.rs | 5 +- crates/ghostty-font/Cargo.toml | 5 ++ crates/ghostty-font/build.rs | 19 +++++- crates/ghostty-font/src/backend.rs | 65 ++++++++++++++++++- crates/ghostty-font/src/codepoint_resolver.rs | 51 ++++++++++++++- .../src/deferred_face_freetype.rs | 51 +++++++++++++++ crates/ghostty-font/src/discovery.rs | 36 ++++++++++ crates/ghostty-font/src/lib.rs | 8 +++ crates/ghostty-font/src/shaper/mod.rs | 2 +- crates/ghostty-font/src/shaper/test_util.rs | 12 +++- crates/ghostty-font/src/shared_grid_set.rs | 19 +++++- docs/rewrite/DEVIANT_FIXES.md | 1 + docs/rewrite/notes/final-Z19.md | 12 ++++ 18 files changed, 327 insertions(+), 12 deletions(-) create mode 100644 crates/ghostty-font/src/deferred_face_freetype.rs create mode 100644 docs/rewrite/notes/final-Z19.md diff --git a/Cargo.toml b/Cargo.toml index 6d8d760..24d0d95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,9 @@ path = "src/main.rs" sentry = ["ghostty-crash/sentry"] # Mirrors upstream's `-Dsnap` build option (off by default). snap = ["ghostty-os/snap"] +# Mirrors upstream's `-Dfont-backend=freetype`: FreeType rendering with no +# font discovery (fontconfig is not used). Non-Apple, non-wasm targets only. +font-backend-freetype = ["ghostty-cli/font-backend-freetype"] [dependencies] ghostty-core.workspace = true diff --git a/crates/ghostty-cli/Cargo.toml b/crates/ghostty-cli/Cargo.toml index 92c6be6..a235b26 100644 --- a/crates/ghostty-cli/Cargo.toml +++ b/crates/ghostty-cli/Cargo.toml @@ -4,6 +4,11 @@ version.workspace = true edition.workspace = true license.workspace = true +[features] +# Select the FreeType-only font backend (no font discovery), like +# `-Dfont-backend=freetype`. +font-backend-freetype = ["ghostty-font/font-backend-freetype"] + [dependencies] flate2 = "1.1.10" ghostty-build-info.workspace = true diff --git a/crates/ghostty-cli/src/build_info.rs b/crates/ghostty-cli/src/build_info.rs index bfb8bda..24a4c4a 100644 --- a/crates/ghostty-cli/src/build_info.rs +++ b/crates/ghostty-cli/src/build_info.rs @@ -59,7 +59,7 @@ impl Default for BuildInfo { release_channel: ghostty_build_info::RELEASE_CHANNEL.to_string(), build_mode: ghostty_build_info::BUILD_MODE.to_string(), app_runtime: ".gtk".to_string(), - font_backend: ".fontconfig_freetype".to_string(), + font_backend: format!(".{}", ghostty_font::options.backend.name()), renderer: "renderer.generic.Renderer(renderer.OpenGL)".to_string(), gtk_build: None, gtk_runtime: None, diff --git a/crates/ghostty-cli/src/list_fonts.rs b/crates/ghostty-cli/src/list_fonts.rs index c06db32..8f77280 100644 --- a/crates/ghostty-cli/src/list_fonts.rs +++ b/crates/ghostty-cli/src/list_fonts.rs @@ -4,6 +4,11 @@ use ghostty_config::config_struct; use crate::error::Error; +/// Printed (without a trailing newline) by `+list-fonts` and `+show-face` +/// when the build has no font discovery mechanism (Zig `Discover == void`, +/// the `freetype` font backend). +pub(crate) const NO_DISCOVERY_MESSAGE: &str = "Ghostty was built without a font discovery mechanism. This is a compile-time\noption. Please review how Ghostty was built from source, contact the\nmaintainer to enable a font discovery mechanism, and try again."; + config_struct! { pub struct Options { /// The font family to search for. If this is set, then only fonts @@ -51,6 +56,14 @@ pub fn run(args: &[impl AsRef<[u8]>]) -> Result { use std::{collections::HashMap, io::Write}; + // Its possible to build Ghostty without font discovery! + if !ghostty_font::HAS_DISCOVERY { + let mut stderr = crate::zig_writer::ZigWriter::new(crate::zig_writer::stderr_sink(), 1024); + write!(stderr, "{NO_DISCOVERY_MESSAGE}")?; + stderr.flush()?; + return Ok(1); + } + // We'll be putting our fonts into a list categorized by family // so it is easier to read the output. let mut families: Vec = Vec::new(); @@ -118,3 +131,23 @@ pub fn run(args: &[impl AsRef<[u8]>]) -> Result { stdout.flush()?; Ok(0) } + +#[cfg(test)] +mod tests { + /// Without a discovery mechanism (the `freetype` font backend, Zig + /// `Discover == void`) `+list-fonts` prints the compile-time-option + /// message and exits with 1, after parsing the arguments. + #[cfg(feature = "font-backend-freetype")] + #[test] + fn freetype_backend_reports_no_discovery() { + const { assert!(!ghostty_font::HAS_DISCOVERY) }; + assert_eq!(super::run(&[] as &[&[u8]]).expect("run"), 1); + assert!(super::NO_DISCOVERY_MESSAGE.ends_with("try again.")); + } + + #[cfg(not(feature = "font-backend-freetype"))] + #[test] + fn default_backend_has_discovery() { + const { assert!(ghostty_font::HAS_DISCOVERY) }; + } +} diff --git a/crates/ghostty-cli/src/show_face.rs b/crates/ghostty-cli/src/show_face.rs index 9f2ad82..024fc71 100644 --- a/crates/ghostty-cli/src/show_face.rs +++ b/crates/ghostty-cli/src/show_face.rs @@ -86,6 +86,16 @@ cli_options!(Options, help, diagnostics); /// style. Valid options are `text` and `emoji`. If unset, the presentation /// style of a codepoint will be inferred from the Unicode standard. pub fn run(args: &[impl AsRef<[u8]>]) -> Result { + // Its possible to build Ghostty without font discovery! (This is checked + // before the arguments are parsed, as in Zig; the stderr writer is on + // stdout, like the other show-face writers.) + if !ghostty_font::HAS_DISCOVERY { + let mut stderr = ZigWriter::new(stdout_sink(), 1024); + write!(stderr, "{}", crate::list_fonts::NO_DISCOVERY_MESSAGE)?; + stderr.flush()?; + return Ok(1); + } + let mut opts = Options::default(); crate::parse_options(&mut opts, args)?; diff --git a/crates/ghostty-core/src/global.rs b/crates/ghostty-core/src/global.rs index ea88a59..011fa96 100644 --- a/crates/ghostty-core/src/global.rs +++ b/crates/ghostty-core/src/global.rs @@ -146,9 +146,12 @@ pub fn default_crash_options() -> ghostty_crash::Options { .to_owned(), ..ghostty_crash::Options::default() }; + ghostty_font::options + .backend + .name() + .clone_into(&mut o.font_backend); if cfg!(target_vendor = "apple") { "none".clone_into(&mut o.app_runtime); - "coretext".clone_into(&mut o.font_backend); "metal".clone_into(&mut o.renderer); } o diff --git a/crates/ghostty-font/Cargo.toml b/crates/ghostty-font/Cargo.toml index 9dd11e1..8a7c4ca 100644 --- a/crates/ghostty-font/Cargo.toml +++ b/crates/ghostty-font/Cargo.toml @@ -58,6 +58,11 @@ coretext-harfbuzz = ["dep:harfbuzz-sys"] # CoreText for discovery and rendering, no shaping # (Zig `-Dfont-backend=coretext_noshape`; Apple targets only). coretext-noshape = [] +# FreeType for rendering and HarfBuzz for shaping with no font discovery +# (Zig `-Dfont-backend=freetype`; non-Apple, non-wasm targets only; the +# default there is `fontconfig_freetype`). Not enabled by `--all-features` +# in any meaningful sense: it switches the whole build to the backend. +font-backend-freetype = [] # This crate wraps the freetype / harfbuzz / fontconfig C libraries through # their `-sys` crates; unsafe is denied by default and allowed on minimal, diff --git a/crates/ghostty-font/build.rs b/crates/ghostty-font/build.rs index 284c483..a0d16ee 100644 --- a/crates/ghostty-font/build.rs +++ b/crates/ghostty-font/build.rs @@ -3,6 +3,9 @@ //! //! Backends (see `src/backend.rs`): //! * Linux & other non-Apple, non-wasm targets: `fontconfig_freetype`. +//! * Linux & other non-Apple, non-wasm targets with the cargo feature +//! `font-backend-freetype`: `freetype` (FreeType rendering, HarfBuzz +//! shaping, no font discovery; Zig `-Dfont-backend=freetype`). //! * wasm32: `web_canvas`. //! * Apple targets: `coretext` (default), or via cargo features //! `coretext-freetype`, `coretext-harfbuzz`, `coretext-noshape`. @@ -10,6 +13,8 @@ //! Emitted cfgs: //! * `font_freetype` - FreeType face / library are compiled. //! * `font_fontconfig` - fontconfig discovery is compiled. +//! * `font_discovery` - a discovery mechanism (fontconfig or CoreText) exists +//! (Zig: `font.Discover != void`). //! * `font_harfbuzz` - the HarfBuzz shaper is compiled. //! * `font_coretext` - CoreText discovery/deferred face (Apple targets). //! * `font_coretext_face` - the CoreText `Face` is the font face. @@ -21,6 +26,7 @@ fn main() { for c in [ "font_freetype", "font_fontconfig", + "font_discovery", "font_harfbuzz", "font_coretext", "font_coretext_face", @@ -40,6 +46,15 @@ fn main() { let ct_hb = feat("CORETEXT_HARFBUZZ"); let ct_noshape = feat("CORETEXT_NOSHAPE"); + let ft_only = feat("FONT_BACKEND_FREETYPE"); + if ft_only && (wasm || apple) { + println!( + "cargo:warning=feature `font-backend-freetype` only applies to non-Apple, \ + non-wasm targets; ignored" + ); + } + let ft_only = ft_only && !wasm && !apple; + let on = |c: &str, v: bool| { if v { println!("cargo:rustc-cfg={c}"); @@ -49,7 +64,9 @@ fn main() { let freetype = !wasm && (!apple || ct_ft); let harfbuzz = !wasm && (!apple || ct_ft || ct_hb); on("font_freetype", freetype); - on("font_fontconfig", !wasm && !apple); + let fontconfig = !wasm && !apple && !ft_only; + on("font_fontconfig", fontconfig); + on("font_discovery", fontconfig || apple); on("font_harfbuzz", harfbuzz); on("font_coretext", apple); on("font_coretext_face", apple && !ct_ft); diff --git a/crates/ghostty-font/src/backend.rs b/crates/ghostty-font/src/backend.rs index 8e96212..8c2cb64 100644 --- a/crates/ghostty-font/src/backend.rs +++ b/crates/ghostty-font/src/backend.rs @@ -13,6 +13,9 @@ //! (Zig: `-Dfont-backend=...`). "There is no scenario that is the default" //! for those; they're only used by people who want to self-compile Ghostty //! and prefer the freetype aesthetic. +//! * Non-Apple, non-wasm targets with the cargo feature +//! `font-backend-freetype`: [`Backend::Freetype`] (Zig +//! `-Dfont-backend=freetype`: no font discovery). //! * wasm32: [`Backend::WebCanvas`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -50,6 +53,19 @@ impl Backend { } } + /// The Zig tag name (`@tagName`) of the backend. + pub fn name(self) -> &'static str { + match self { + Backend::Freetype => "freetype", + Backend::FontconfigFreetype => "fontconfig_freetype", + Backend::Coretext => "coretext", + Backend::CoretextFreetype => "coretext_freetype", + Backend::CoretextHarfbuzz => "coretext_harfbuzz", + Backend::CoretextNoshape => "coretext_noshape", + Backend::WebCanvas => "web_canvas", + } + } + pub fn has_freetype(self) -> bool { matches!( self, @@ -88,7 +104,20 @@ pub struct Options { pub backend: Backend, } -#[cfg(all(not(target_arch = "wasm32"), not(target_vendor = "apple")))] +#[cfg(all( + not(target_arch = "wasm32"), + not(target_vendor = "apple"), + feature = "font-backend-freetype" +))] +pub const OPTIONS: Options = Options { + backend: Backend::Freetype, +}; + +#[cfg(all( + not(target_arch = "wasm32"), + not(target_vendor = "apple"), + not(feature = "font-backend-freetype") +))] pub const OPTIONS: Options = Options { backend: Backend::FontconfigFreetype, }; @@ -182,13 +211,45 @@ mod tests { } } - #[cfg(all(not(target_arch = "wasm32"), not(target_vendor = "apple")))] + #[test] + fn freetype_capabilities() { + let b = Backend::Freetype; + assert!(b.has_freetype()); + assert!(!b.has_fontconfig()); + assert!(b.has_harfbuzz()); + assert!(!b.has_coretext()); + assert_eq!(b.name(), "freetype"); + } + + #[cfg(all( + not(target_arch = "wasm32"), + not(target_vendor = "apple"), + not(feature = "font-backend-freetype") + ))] #[test] fn linux_backend_capabilities() { let b = OPTIONS.backend; + assert_eq!(b, Backend::FontconfigFreetype); assert!(b.has_freetype()); assert!(b.has_fontconfig()); assert!(b.has_harfbuzz()); assert!(!b.has_coretext()); } + + #[cfg(all( + not(target_arch = "wasm32"), + not(target_vendor = "apple"), + feature = "font-backend-freetype" + ))] + #[test] + fn freetype_only_backend_selected() { + let b = OPTIONS.backend; + assert_eq!(b, Backend::Freetype); + assert!(b.has_freetype()); + assert!(!b.has_fontconfig()); + assert!(b.has_harfbuzz()); + assert!(!b.has_coretext()); + // No discovery mechanism, as in Zig (`Discover == void`). + const { assert!(!crate::HAS_DISCOVERY) }; + } } diff --git a/crates/ghostty-font/src/codepoint_resolver.rs b/crates/ghostty-font/src/codepoint_resolver.rs index fd7fa8c..adc90c9 100644 --- a/crates/ghostty-font/src/codepoint_resolver.rs +++ b/crates/ghostty-font/src/codepoint_resolver.rs @@ -10,6 +10,8 @@ use std::collections::HashMap; +#[cfg(font_discovery)] +use crate::collection::{AddOptions, Entry}; use crate::{ Atlas, CodepointMap, @@ -17,7 +19,7 @@ use crate::{ Presentation, SpriteFace, Style, - collection::{AddOptions, Collection, Entry, GetFaceError, Index, PresentationMode, Special}, + collection::{Collection, GetFaceError, Index, PresentationMode, Special}, discovery::Discover, face::{RenderError as FaceRenderError, RenderOptions}, sprite, @@ -88,6 +90,7 @@ pub struct CodepointResolver { /// The descriptor cache is used to cache the descriptor to font face /// mapping for codepoint maps. + #[cfg_attr(not(font_discovery), allow(dead_code))] descriptor_cache: DescriptorCache, /// Set this to a non-null value to enable sprite glyph drawing. If this @@ -108,6 +111,7 @@ pub struct CodepointResolver { /// /// As in upstream, descriptors are compared by their `hashcode()` only. #[derive(Default)] +#[cfg_attr(not(font_discovery), allow(dead_code))] struct DescriptorCache(HashMap>); impl CodepointResolver { @@ -240,6 +244,27 @@ impl CodepointResolver { .get_index(cp, Style::Regular, PresentationMode::Any) } + /// Without a discovery mechanism (the `freetype` backend, Zig + /// `Discover == void`) there is no fallback search. + #[cfg(not(font_discovery))] + #[allow(clippy::unused_self)] + fn discover_fallback( + &mut self, + _cp: u32, + _style: Style, + _p: PresentationMode, + ) -> Option { + None + } + + /// Without a discovery mechanism, codepoint overrides cannot find fonts. + #[cfg(not(font_discovery))] + #[allow(clippy::unused_self)] + fn get_index_codepoint_override(&mut self, _cp: u32) -> Option { + None + } + + #[cfg(font_discovery)] fn discover_fallback( &mut self, cp: u32, @@ -309,6 +334,7 @@ impl CodepointResolver { /// Checks if the codepoint is in the map of codepoint overrides, /// finds the override font, and returns it. + #[cfg(font_discovery)] fn get_index_codepoint_override(&mut self, cp: u32) -> Option { // Get our codepoint map. If we have no map set then we have no // codepoint overrides and we're done. @@ -459,7 +485,7 @@ mod tests { use crate::{ Face, Library, - collection::{LoadOptions, SizeAdjustment}, + collection::{AddOptions, LoadOptions, SizeAdjustment}, embedded, face::{DesiredSize, Options}, }; @@ -534,6 +560,27 @@ mod tests { assert!(r.get_index(0x1FB00, Style::Regular, None).is_none()); } + /// `freetype` backend (no discovery): codepoint overrides and fallback + /// searches are disabled, so an unmapped codepoint is simply not found. + #[cfg(not(font_discovery))] + #[test] + fn freetype_backend_has_no_discovery_fallback_or_override() { + const { assert!(!crate::HAS_DISCOVERY) }; + let lib = Library::init().unwrap(); + let mut c = Collection::new(); + c.load_options = Some(LoadOptions::new(lib.clone())); + add(&mut c, &lib, embedded::REGULAR, Style::Regular, 12.0); + + let mut r = CodepointResolver::new(c); + assert!(r.discover.is_none()); + assert!(r.get_index_codepoint_override('A' as u32).is_none()); + // Not in the embedded font, and there is no discovery to ask. + assert!(r.get_index(0x4E2D, Style::Regular, None).is_none()); + assert!(r.get_index(0x4E2D, Style::Bold, None).is_none()); + // Embedded codepoints still resolve. + assert!(r.get_index('A' as u32, Style::Regular, None).is_some()); + } + #[test] fn get_index_disabled_font_style() { let lib = Library::init().unwrap(); diff --git a/crates/ghostty-font/src/deferred_face_freetype.rs b/crates/ghostty-font/src/deferred_face_freetype.rs new file mode 100644 index 0000000..6c5f8e8 --- /dev/null +++ b/crates/ghostty-font/src/deferred_face_freetype.rs @@ -0,0 +1,51 @@ +//! A deferred face represents a single font face with all the information +//! necessary to load it, but defers loading the full face until it is +//! needed. +//! +//! Port of `font/DeferredFace.zig` (`freetype` backend: no discovery, so +//! there is no deferred data and every face must already be loaded). + +use crate::{ + Face, + Library, + Presentation, + face::{self, Options}, +}; + +#[derive(Debug, thiserror::Error)] +pub enum LoadError { + #[error(transparent)] + Face(#[from] face::FaceError), +} + +/// With no discovery mechanism there is no data (all fields are `void` in +/// Zig for the `freetype` backend). +#[derive(Default)] +pub struct DeferredFace {} + +impl DeferredFace { + /// Returns the family name of the font. + pub fn family_name(&self) -> Result { + Ok(String::new()) + } + + /// Returns the name of this face. + pub fn name(&self) -> Result { + Ok(String::new()) + } + + /// Load the deferred font face. + pub fn load(&mut self, _lib: &Library, _opts: Options) -> Result { + // Unreachable because we must be already loaded or have the + // proper configuration for one of the other deferred mechanisms. + unreachable!("deferred face without a discovery mechanism"); + } + + /// Returns true if this face can satisfy the given codepoint and + /// presentation. + pub fn has_codepoint(&self, _cp: u32, _p: Option) -> bool { + // This is unreachable because discovery mechanisms terminate, and + // if we're not using a discovery mechanism, the face MUST be loaded. + unreachable!("deferred face without a discovery mechanism"); + } +} diff --git a/crates/ghostty-font/src/discovery.rs b/crates/ghostty-font/src/discovery.rs index ccb1559..6290d73 100644 --- a/crates/ghostty-font/src/discovery.rs +++ b/crates/ghostty-font/src/discovery.rs @@ -10,6 +10,42 @@ pub use fc_impl::*; mod coretext_impl; #[cfg(font_coretext)] pub use coretext_impl::*; + +/// The `freetype` backend has no discovery mechanism: Zig's +/// `Discover = void`. This is an uninhabited stand-in so that generic code +/// type-checks; it can never be constructed, and the callers check +/// [`crate::HAS_DISCOVERY`] (or `cfg(font_discovery)`) first. +#[cfg(not(any(font_fontconfig, font_coretext)))] +pub struct Discover(std::convert::Infallible); + +#[cfg(not(any(font_fontconfig, font_coretext)))] +pub type DiscoverError = std::convert::Infallible; +#[cfg(not(any(font_fontconfig, font_coretext)))] +pub type NextError = std::convert::Infallible; + +#[cfg(not(any(font_fontconfig, font_coretext)))] +pub struct DiscoverIterator(std::convert::Infallible); + +#[cfg(not(any(font_fontconfig, font_coretext)))] +impl Discover { + #[must_use] + pub fn init() -> Discover { + unreachable!("no font discovery mechanism in this build") + } + + pub fn discover(&self, _desc: &Descriptor) -> Result { + match self.0 {} + } +} + +#[cfg(not(any(font_fontconfig, font_coretext)))] +impl DiscoverIterator { + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> Result, NextError> { + match self.0 {} + } +} + /// Descriptor is used to search for fonts (canonical definition in `ghostty-config`). pub use ghostty_config::font_types::Descriptor; diff --git a/crates/ghostty-font/src/lib.rs b/crates/ghostty-font/src/lib.rs index 75392ab..52a5489 100644 --- a/crates/ghostty-font/src/lib.rs +++ b/crates/ghostty-font/src/lib.rs @@ -19,6 +19,9 @@ pub mod config_types; pub mod coretext_util; #[cfg(font_fontconfig)] pub mod deferred_face; +#[cfg(all(not(target_arch = "wasm32"), not(font_fontconfig), not(font_coretext)))] +#[path = "deferred_face_freetype.rs"] +pub mod deferred_face; #[cfg(font_coretext)] #[path = "deferred_face_coretext.rs"] pub mod deferred_face; @@ -48,6 +51,11 @@ pub mod sprite; pub use atlas::Atlas; pub use backend::{Backend, OPTIONS as options}; + +/// Whether a font discovery mechanism exists in this build (Zig: +/// `font.Discover != void`; false for the `freetype` and `web_canvas` +/// backends). +pub const HAS_DISCOVERY: bool = cfg!(font_discovery); pub use codepoint_map::CodepointMap; #[cfg(not(target_arch = "wasm32"))] pub use codepoint_resolver::CodepointResolver; diff --git a/crates/ghostty-font/src/shaper/mod.rs b/crates/ghostty-font/src/shaper/mod.rs index d9096a8..a987011 100644 --- a/crates/ghostty-font/src/shaper/mod.rs +++ b/crates/ghostty-font/src/shaper/mod.rs @@ -19,5 +19,5 @@ pub mod run; pub mod web_canvas; -#[cfg(all(test, font_harfbuzz, font_fontconfig))] +#[cfg(all(test, font_harfbuzz))] pub(crate) mod test_util; diff --git a/crates/ghostty-font/src/shaper/test_util.rs b/crates/ghostty-font/src/shaper/test_util.rs index 12ae50e..6bc16ee 100644 --- a/crates/ghostty-font/src/shaper/test_util.rs +++ b/crates/ghostty-font/src/shaper/test_util.rs @@ -23,7 +23,6 @@ use crate::{ Style, codepoint_resolver::CodepointResolver, collection::{AddOptions, Collection, LoadOptions, SizeAdjustment}, - discovery::{Descriptor, Fontconfig}, embedded, face::{DesiredSize, FontData, Options as FaceOptions}, shape, @@ -102,7 +101,18 @@ pub(crate) fn test_shaper_with_font(font_req: TestFont) -> TestShaper { /// Returns None (the test is skipped, like `error.SkipZigTest`) if the font /// can't be discovered. +#[cfg(not(font_fontconfig))] +pub(crate) fn test_shaper_with_discovered_font(_font_req: &str) -> Option { + // No fontconfig discovery in this backend (for example `freetype`). + None +} + +/// Returns None (the test is skipped, like `error.SkipZigTest`) if the font +/// can't be discovered. +#[cfg(font_fontconfig)] pub(crate) fn test_shaper_with_discovered_font(font_req: &str) -> Option { + use crate::discovery::{Descriptor, Fontconfig}; + let lib = Library::init().unwrap(); let mut c = Collection::new(); c.load_options = Some(LoadOptions::new(lib.clone())); diff --git a/crates/ghostty-font/src/shared_grid_set.rs b/crates/ghostty-font/src/shared_grid_set.rs index ae90572..028534c 100644 --- a/crates/ghostty-font/src/shared_grid_set.rs +++ b/crates/ghostty-font/src/shared_grid_set.rs @@ -27,13 +27,15 @@ use ghostty_config::{ types_b::FreetypeLoadFlags, }; +#[cfg(font_discovery)] +use crate::discovery::Discover; use crate::{ Face, Style, codepoint_map::CodepointMap, codepoint_resolver::{CodepointResolver, StyleStatus}, collection::{self, AddOptions, Collection, LoadOptions, SizeAdjustment}, - discovery::{Descriptor, Discover}, + discovery::Descriptor, embedded, face::{self, DesiredSize, FontData}, library::{Library, LibraryError}, @@ -45,7 +47,9 @@ use crate::{ #[derive(Debug, thiserror::Error)] pub enum RefError { #[error(transparent)] + #[cfg(font_discovery)] Discover(#[from] crate::discovery::DiscoverError), + #[cfg(font_discovery)] #[error("font discovery failed: {0}")] DiscoverNext(#[from] crate::discovery::NextError), #[error(transparent)] @@ -80,6 +84,7 @@ struct Inner { /// Font discovery mechanism. Initialized once and kept for the /// lifetime of the set because some font discovery mechanisms /// (i.e. fontconfig) are unsafe to reinit. + #[cfg(font_discovery)] font_discover: Option, } @@ -98,6 +103,7 @@ impl SharedGridSet { font_lib: Library::init()?, inner: Mutex::new(Inner { map: HashMap::new(), + #[cfg(font_discovery)] font_discover: None, }), }) @@ -158,7 +164,10 @@ impl SharedGridSet { // Init our resolver which just requires setting fields. let mut resolver = CodepointResolver::new(c); resolver.styles = styles; - resolver.discover = Some(Self::discover(&mut inner).clone()); + #[cfg(font_discovery)] + { + resolver.discover = Some(Self::discover(&mut inner).clone()); + } resolver.codepoint_map = Some(key.codepoint_map.clone()); let grid = Arc::new(SharedGrid::new(resolver)?); @@ -174,6 +183,7 @@ impl SharedGridSet { /// Builds the Collection for the given configuration key and /// initial font size. + #[cfg_attr(not(font_discovery), allow(unused_variables))] fn collection( &self, inner: &mut Inner, @@ -191,7 +201,9 @@ impl SharedGridSet { c.load_options = Some(load_options.clone()); c.metric_modifiers.clone_from(&key.metric_modifiers); - // Search for fonts + // Search for fonts. Without a discovery mechanism (the `freetype` + // backend) there is nothing to search with. + #[cfg(font_discovery)] { let disco = Self::discover(inner); for style in Style::ALL { @@ -392,6 +404,7 @@ impl SharedGridSet { /// Initialize once and return the font discovery mechanism. This remains /// initialized throughout the lifetime of the application because some /// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit. + #[cfg(font_discovery)] fn discover(inner: &mut Inner) -> &Discover { inner.font_discover.get_or_insert_with(Discover::init) } diff --git a/docs/rewrite/DEVIANT_FIXES.md b/docs/rewrite/DEVIANT_FIXES.md index a0476dd..779f7b0 100644 --- a/docs/rewrite/DEVIANT_FIXES.md +++ b/docs/rewrite/DEVIANT_FIXES.md @@ -96,6 +96,7 @@ Only entries with an observable behavioural difference are listed. | Config strings | UTF-8 not required in many places | Escaped-bytes representation (`EncodedOs`) so non-UTF-8 survives; behaviour and error names matched to Zig (for example `theme=/etc/passwd/x` gives `error.NotDir`, checked against the oracle) | `notes/review-rv11-codex.md` (item 3) | 672e8a4, 4bbe0d7 | | `+version` / build info | Real build metadata | Placeholder `build_info` values in places; `gtk-opengl-debug` follows `cfg!(debug_assertions)` (the `release` profile has them on, `production` off) | `notes/ghostty-cli.md`, `notes/ghostty-config.md` | 6286e02 | | OSC 52 base64 acceptance | Surface.zig:2149,2163 rules | Was different in both directions (Codex core audit); fixed to match Zig | `notes/review-core-codex.md` | 2f58f02 | +| Benchmark / synthetic tooling extensions | `ghostty-bench` has no `--time` option and only the simplified terminal handler; `ghostty-synthetic` has the actions `ascii`, `osc`, `utf8` | `ghostty-bench --time` prints the measured step time to stderr; `ghostty-bench --handler=full` runs the full `stream_readonly` handler instead of the print-only one; `ghostty-synthetic +sgr` generates SGR-heavy output. Dev/benchmark tooling only; otherwise upstream behaviour is unchanged | `crates/ghostty-bench/src/main.rs`, `src/benches.rs`, `src/bin/ghostty-synthetic.rs`; `notes/review-rv16-codex.md` | 0c4f0ee | ## Maintenance diff --git a/docs/rewrite/notes/final-Z19.md b/docs/rewrite/notes/final-Z19.md new file mode 100644 index 0000000..14e9edb --- /dev/null +++ b/docs/rewrite/notes/final-Z19.md @@ -0,0 +1,12 @@ +# Z19: font-backend=freetype feature, DEVIANT_FIXES additions (RV16) + +## Font backend `freetype` (Zig `-Dfont-backend=freetype`) +- Cargo feature `font-backend-freetype` on `ghostty-font`, forwarded by `ghostty-cli` and the root crate (`cargo build --features font-backend-freetype`). Default (fontconfig_freetype / coretext / web_canvas) unchanged. Non-Apple, non-wasm targets only (build.rs warns and ignores it elsewhere). +- `build.rs` emits `font_discovery` (fontconfig or CoreText exists; Zig `Discover != void`); `font_fontconfig` is off in this mode. `Backend::name()` added; `OPTIONS` = `Backend::Freetype`; `ghostty_font::HAS_DISCOVERY`. +- Zig behaviour matched: `Discover = void` (Rust: uninhabited stub `Discover`, never constructed); `DeferredFace` is empty (family/name "", load/has_codepoint unreachable); SharedGridSet skips the font-family search (primary fonts come from the embedded fallback only) and still adds the embedded emoji fonts; CodepointResolver has no discovery fallback and codepoint-map overrides return None; `+list-fonts` (after arg parsing) and `+show-face` (before parsing, written to stdout like Zig's writers) print the "built without a font discovery mechanism" message (no trailing newline) and exit 1. `+version` shows `.freetype`; crash options use `Backend::name()`. +- Fontconfig sys crate is still linked (target dependency), just unused. +- Tests: `cargo test -p ghostty-font --features font-backend-freetype` (198 pass), `cargo test -p ghostty-cli --features font-backend-freetype list_fonts`. shaper `test_util` now compiles without fontconfig (discovered-font tests skip). Note `--all-features` (just recipes) selects this backend workspace-wide. + +## DEVIANT_FIXES.md +- Added row 33 (maximum-length NOTIFY_SOCKET: Zig `systemd.zig:116` writes `path[108]` after the `108 < len` check; Rust uses rustix `SocketAddrUnix::new`). Introduced in f8c5ce0. New regression test `notify_maximum_length_socket_path_is_rejected_safely` in `crates/ghostty-os/src/systemd.rs` (own hunk, for the orchestrator's fix-deviant commit). +- Added "Benchmark / synthetic tooling extensions" row (`--time`, `--handler=full`, `+sgr`; commit 0c4f0ee) under Other deliberate deviations.