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
This commit is contained in:
ddidderr committed 2026-10-12 15:56:21 +02:00
1 parent 798a31ccac
commit 389266720d
18 files changed
+326 -11

No files matched your search

+3
View File
@@ -14,6 +14,9 @@ path = "src/main.rs"
sentry = ["ghostty-crash/sentry"] sentry = ["ghostty-crash/sentry"]
# Mirrors upstream's `-Dsnap` build option (off by default). # Mirrors upstream's `-Dsnap` build option (off by default).
snap = ["ghostty-os/snap"] 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] [dependencies]
ghostty-core.workspace = true ghostty-core.workspace = true
+5
View File
@@ -4,6 +4,11 @@ version.workspace = true
edition.workspace = true edition.workspace = true
license.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] [dependencies]
flate2 = "1.1.10" flate2 = "1.1.10"
ghostty-build-info.workspace = true ghostty-build-info.workspace = true
+1 -1
View File
@@ -59,7 +59,7 @@ impl Default for BuildInfo {
release_channel: ghostty_build_info::RELEASE_CHANNEL.to_string(), release_channel: ghostty_build_info::RELEASE_CHANNEL.to_string(),
build_mode: ghostty_build_info::BUILD_MODE.to_string(), build_mode: ghostty_build_info::BUILD_MODE.to_string(),
app_runtime: ".gtk".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(), renderer: "renderer.generic.Renderer(renderer.OpenGL)".to_string(),
gtk_build: None, gtk_build: None,
gtk_runtime: None, gtk_runtime: None,
+33
View File
@@ -4,6 +4,11 @@ use ghostty_config::config_struct;
use crate::error::Error; 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! { config_struct! {
pub struct Options { pub struct Options {
/// The font family to search for. If this is set, then only fonts /// 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<u8, Error> {
use std::{collections::HashMap, io::Write}; 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 // We'll be putting our fonts into a list categorized by family
// so it is easier to read the output. // so it is easier to read the output.
let mut families: Vec<String> = Vec::new(); let mut families: Vec<String> = Vec::new();
@@ -118,3 +131,23 @@ pub fn run(args: &[impl AsRef<[u8]>]) -> Result<u8, Error> {
stdout.flush()?; stdout.flush()?;
Ok(0) 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) };
}
}
+10
View File
@@ -86,6 +86,16 @@ cli_options!(Options, help, diagnostics);
/// style. Valid options are `text` and `emoji`. If unset, the presentation /// style. Valid options are `text` and `emoji`. If unset, the presentation
/// style of a codepoint will be inferred from the Unicode standard. /// style of a codepoint will be inferred from the Unicode standard.
pub fn run(args: &[impl AsRef<[u8]>]) -> Result<u8, Error> { pub fn run(args: &[impl AsRef<[u8]>]) -> Result<u8, Error> {
// 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(); let mut opts = Options::default();
crate::parse_options(&mut opts, args)?; crate::parse_options(&mut opts, args)?;
+4 -1
View File
@@ -146,9 +146,12 @@ pub fn default_crash_options() -> ghostty_crash::Options {
.to_owned(), .to_owned(),
..ghostty_crash::Options::default() ..ghostty_crash::Options::default()
}; };
ghostty_font::options
.backend
.name()
.clone_into(&mut o.font_backend);
if cfg!(target_vendor = "apple") { if cfg!(target_vendor = "apple") {
"none".clone_into(&mut o.app_runtime); "none".clone_into(&mut o.app_runtime);
"coretext".clone_into(&mut o.font_backend);
"metal".clone_into(&mut o.renderer); "metal".clone_into(&mut o.renderer);
} }
o o
+5
View File
@@ -58,6 +58,11 @@ coretext-harfbuzz = ["dep:harfbuzz-sys"]
# CoreText for discovery and rendering, no shaping # CoreText for discovery and rendering, no shaping
# (Zig `-Dfont-backend=coretext_noshape`; Apple targets only). # (Zig `-Dfont-backend=coretext_noshape`; Apple targets only).
coretext-noshape = [] 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 # This crate wraps the freetype / harfbuzz / fontconfig C libraries through
# their `-sys` crates; unsafe is denied by default and allowed on minimal, # their `-sys` crates; unsafe is denied by default and allowed on minimal,
+18 -1
View File
@@ -3,6 +3,9 @@
//! //!
//! Backends (see `src/backend.rs`): //! Backends (see `src/backend.rs`):
//! * Linux & other non-Apple, non-wasm targets: `fontconfig_freetype`. //! * 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`. //! * wasm32: `web_canvas`.
//! * Apple targets: `coretext` (default), or via cargo features //! * Apple targets: `coretext` (default), or via cargo features
//! `coretext-freetype`, `coretext-harfbuzz`, `coretext-noshape`. //! `coretext-freetype`, `coretext-harfbuzz`, `coretext-noshape`.
@@ -10,6 +13,8 @@
//! Emitted cfgs: //! Emitted cfgs:
//! * `font_freetype` - FreeType face / library are compiled. //! * `font_freetype` - FreeType face / library are compiled.
//! * `font_fontconfig` - fontconfig discovery is 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_harfbuzz` - the HarfBuzz shaper is compiled.
//! * `font_coretext` - CoreText discovery/deferred face (Apple targets). //! * `font_coretext` - CoreText discovery/deferred face (Apple targets).
//! * `font_coretext_face` - the CoreText `Face` is the font face. //! * `font_coretext_face` - the CoreText `Face` is the font face.
@@ -21,6 +26,7 @@ fn main() {
for c in [ for c in [
"font_freetype", "font_freetype",
"font_fontconfig", "font_fontconfig",
"font_discovery",
"font_harfbuzz", "font_harfbuzz",
"font_coretext", "font_coretext",
"font_coretext_face", "font_coretext_face",
@@ -40,6 +46,15 @@ fn main() {
let ct_hb = feat("CORETEXT_HARFBUZZ"); let ct_hb = feat("CORETEXT_HARFBUZZ");
let ct_noshape = feat("CORETEXT_NOSHAPE"); 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| { let on = |c: &str, v: bool| {
if v { if v {
println!("cargo:rustc-cfg={c}"); println!("cargo:rustc-cfg={c}");
@@ -49,7 +64,9 @@ fn main() {
let freetype = !wasm && (!apple || ct_ft); let freetype = !wasm && (!apple || ct_ft);
let harfbuzz = !wasm && (!apple || ct_ft || ct_hb); let harfbuzz = !wasm && (!apple || ct_ft || ct_hb);
on("font_freetype", freetype); 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_harfbuzz", harfbuzz);
on("font_coretext", apple); on("font_coretext", apple);
on("font_coretext_face", apple && !ct_ft); on("font_coretext_face", apple && !ct_ft);
+63 -2
View File
@@ -13,6 +13,9 @@
//! (Zig: `-Dfont-backend=...`). "There is no scenario that is the default" //! (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 //! for those; they're only used by people who want to self-compile Ghostty
//! and prefer the freetype aesthetic. //! 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`]. //! * wasm32: [`Backend::WebCanvas`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[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 { pub fn has_freetype(self) -> bool {
matches!( matches!(
self, self,
@@ -88,7 +104,20 @@ pub struct Options {
pub backend: Backend, 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 { pub const OPTIONS: Options = Options {
backend: Backend::FontconfigFreetype, 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] #[test]
fn linux_backend_capabilities() { fn linux_backend_capabilities() {
let b = OPTIONS.backend; let b = OPTIONS.backend;
assert_eq!(b, Backend::FontconfigFreetype);
assert!(b.has_freetype()); assert!(b.has_freetype());
assert!(b.has_fontconfig()); assert!(b.has_fontconfig());
assert!(b.has_harfbuzz()); assert!(b.has_harfbuzz());
assert!(!b.has_coretext()); 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) };
}
} }
+49 -2
View File
@@ -10,6 +10,8 @@
use std::collections::HashMap; use std::collections::HashMap;
#[cfg(font_discovery)]
use crate::collection::{AddOptions, Entry};
use crate::{ use crate::{
Atlas, Atlas,
CodepointMap, CodepointMap,
@@ -17,7 +19,7 @@ use crate::{
Presentation, Presentation,
SpriteFace, SpriteFace,
Style, Style,
collection::{AddOptions, Collection, Entry, GetFaceError, Index, PresentationMode, Special}, collection::{Collection, GetFaceError, Index, PresentationMode, Special},
discovery::Discover, discovery::Discover,
face::{RenderError as FaceRenderError, RenderOptions}, face::{RenderError as FaceRenderError, RenderOptions},
sprite, sprite,
@@ -88,6 +90,7 @@ pub struct CodepointResolver {
/// The descriptor cache is used to cache the descriptor to font face /// The descriptor cache is used to cache the descriptor to font face
/// mapping for codepoint maps. /// mapping for codepoint maps.
#[cfg_attr(not(font_discovery), allow(dead_code))]
descriptor_cache: DescriptorCache, descriptor_cache: DescriptorCache,
/// Set this to a non-null value to enable sprite glyph drawing. If this /// 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. /// As in upstream, descriptors are compared by their `hashcode()` only.
#[derive(Default)] #[derive(Default)]
#[cfg_attr(not(font_discovery), allow(dead_code))]
struct DescriptorCache(HashMap<u64, Option<Index>>); struct DescriptorCache(HashMap<u64, Option<Index>>);
impl CodepointResolver { impl CodepointResolver {
@@ -240,6 +244,27 @@ impl CodepointResolver {
.get_index(cp, Style::Regular, PresentationMode::Any) .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<Index> {
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<Index> {
None
}
#[cfg(font_discovery)]
fn discover_fallback( fn discover_fallback(
&mut self, &mut self,
cp: u32, cp: u32,
@@ -309,6 +334,7 @@ impl CodepointResolver {
/// Checks if the codepoint is in the map of codepoint overrides, /// Checks if the codepoint is in the map of codepoint overrides,
/// finds the override font, and returns it. /// finds the override font, and returns it.
#[cfg(font_discovery)]
fn get_index_codepoint_override(&mut self, cp: u32) -> Option<Index> { fn get_index_codepoint_override(&mut self, cp: u32) -> Option<Index> {
// Get our codepoint map. If we have no map set then we have no // Get our codepoint map. If we have no map set then we have no
// codepoint overrides and we're done. // codepoint overrides and we're done.
@@ -459,7 +485,7 @@ mod tests {
use crate::{ use crate::{
Face, Face,
Library, Library,
collection::{LoadOptions, SizeAdjustment}, collection::{AddOptions, LoadOptions, SizeAdjustment},
embedded, embedded,
face::{DesiredSize, Options}, face::{DesiredSize, Options},
}; };
@@ -534,6 +560,27 @@ mod tests {
assert!(r.get_index(0x1FB00, Style::Regular, None).is_none()); 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] #[test]
fn get_index_disabled_font_style() { fn get_index_disabled_font_style() {
let lib = Library::init().unwrap(); let lib = Library::init().unwrap();
@@ -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<String, LoadError> {
Ok(String::new())
}
/// Returns the name of this face.
pub fn name(&self) -> Result<String, LoadError> {
Ok(String::new())
}
/// Load the deferred font face.
pub fn load(&mut self, _lib: &Library, _opts: Options) -> Result<Face, LoadError> {
// 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<Presentation>) -> 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");
}
}
+36
View File
@@ -10,6 +10,42 @@ pub use fc_impl::*;
mod coretext_impl; mod coretext_impl;
#[cfg(font_coretext)] #[cfg(font_coretext)]
pub use coretext_impl::*; 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<DiscoverIterator, DiscoverError> {
match self.0 {}
}
}
#[cfg(not(any(font_fontconfig, font_coretext)))]
impl DiscoverIterator {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<Option<crate::deferred_face::DeferredFace>, NextError> {
match self.0 {}
}
}
/// Descriptor is used to search for fonts (canonical definition in `ghostty-config`). /// Descriptor is used to search for fonts (canonical definition in `ghostty-config`).
pub use ghostty_config::font_types::Descriptor; pub use ghostty_config::font_types::Descriptor;
+8
View File
@@ -19,6 +19,9 @@ pub mod config_types;
pub mod coretext_util; pub mod coretext_util;
#[cfg(font_fontconfig)] #[cfg(font_fontconfig)]
pub mod deferred_face; 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)] #[cfg(font_coretext)]
#[path = "deferred_face_coretext.rs"] #[path = "deferred_face_coretext.rs"]
pub mod deferred_face; pub mod deferred_face;
@@ -48,6 +51,11 @@ pub mod sprite;
pub use atlas::Atlas; pub use atlas::Atlas;
pub use backend::{Backend, OPTIONS as options}; 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; pub use codepoint_map::CodepointMap;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub use codepoint_resolver::CodepointResolver; pub use codepoint_resolver::CodepointResolver;
+1 -1
View File
@@ -19,5 +19,5 @@ pub mod run;
pub mod web_canvas; pub mod web_canvas;
#[cfg(all(test, font_harfbuzz, font_fontconfig))] #[cfg(all(test, font_harfbuzz))]
pub(crate) mod test_util; pub(crate) mod test_util;
+11 -1
View File
@@ -23,7 +23,6 @@ use crate::{
Style, Style,
codepoint_resolver::CodepointResolver, codepoint_resolver::CodepointResolver,
collection::{AddOptions, Collection, LoadOptions, SizeAdjustment}, collection::{AddOptions, Collection, LoadOptions, SizeAdjustment},
discovery::{Descriptor, Fontconfig},
embedded, embedded,
face::{DesiredSize, FontData, Options as FaceOptions}, face::{DesiredSize, FontData, Options as FaceOptions},
shape, 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 /// Returns None (the test is skipped, like `error.SkipZigTest`) if the font
/// can't be discovered. /// can't be discovered.
#[cfg(not(font_fontconfig))]
pub(crate) fn test_shaper_with_discovered_font(_font_req: &str) -> Option<TestShaper> {
// 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<TestShaper> { pub(crate) fn test_shaper_with_discovered_font(font_req: &str) -> Option<TestShaper> {
use crate::discovery::{Descriptor, Fontconfig};
let lib = Library::init().unwrap(); let lib = Library::init().unwrap();
let mut c = Collection::new(); let mut c = Collection::new();
c.load_options = Some(LoadOptions::new(lib.clone())); c.load_options = Some(LoadOptions::new(lib.clone()));
+15 -2
View File
@@ -27,13 +27,15 @@ use ghostty_config::{
types_b::FreetypeLoadFlags, types_b::FreetypeLoadFlags,
}; };
#[cfg(font_discovery)]
use crate::discovery::Discover;
use crate::{ use crate::{
Face, Face,
Style, Style,
codepoint_map::CodepointMap, codepoint_map::CodepointMap,
codepoint_resolver::{CodepointResolver, StyleStatus}, codepoint_resolver::{CodepointResolver, StyleStatus},
collection::{self, AddOptions, Collection, LoadOptions, SizeAdjustment}, collection::{self, AddOptions, Collection, LoadOptions, SizeAdjustment},
discovery::{Descriptor, Discover}, discovery::Descriptor,
embedded, embedded,
face::{self, DesiredSize, FontData}, face::{self, DesiredSize, FontData},
library::{Library, LibraryError}, library::{Library, LibraryError},
@@ -45,7 +47,9 @@ use crate::{
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum RefError { pub enum RefError {
#[error(transparent)] #[error(transparent)]
#[cfg(font_discovery)]
Discover(#[from] crate::discovery::DiscoverError), Discover(#[from] crate::discovery::DiscoverError),
#[cfg(font_discovery)]
#[error("font discovery failed: {0}")] #[error("font discovery failed: {0}")]
DiscoverNext(#[from] crate::discovery::NextError), DiscoverNext(#[from] crate::discovery::NextError),
#[error(transparent)] #[error(transparent)]
@@ -80,6 +84,7 @@ struct Inner {
/// Font discovery mechanism. Initialized once and kept for the /// Font discovery mechanism. Initialized once and kept for the
/// lifetime of the set because some font discovery mechanisms /// lifetime of the set because some font discovery mechanisms
/// (i.e. fontconfig) are unsafe to reinit. /// (i.e. fontconfig) are unsafe to reinit.
#[cfg(font_discovery)]
font_discover: Option<Discover>, font_discover: Option<Discover>,
} }
@@ -98,6 +103,7 @@ impl SharedGridSet {
font_lib: Library::init()?, font_lib: Library::init()?,
inner: Mutex::new(Inner { inner: Mutex::new(Inner {
map: HashMap::new(), map: HashMap::new(),
#[cfg(font_discovery)]
font_discover: None, font_discover: None,
}), }),
}) })
@@ -158,7 +164,10 @@ impl SharedGridSet {
// Init our resolver which just requires setting fields. // Init our resolver which just requires setting fields.
let mut resolver = CodepointResolver::new(c); let mut resolver = CodepointResolver::new(c);
resolver.styles = styles; resolver.styles = styles;
#[cfg(font_discovery)]
{
resolver.discover = Some(Self::discover(&mut inner).clone()); resolver.discover = Some(Self::discover(&mut inner).clone());
}
resolver.codepoint_map = Some(key.codepoint_map.clone()); resolver.codepoint_map = Some(key.codepoint_map.clone());
let grid = Arc::new(SharedGrid::new(resolver)?); let grid = Arc::new(SharedGrid::new(resolver)?);
@@ -174,6 +183,7 @@ impl SharedGridSet {
/// Builds the Collection for the given configuration key and /// Builds the Collection for the given configuration key and
/// initial font size. /// initial font size.
#[cfg_attr(not(font_discovery), allow(unused_variables))]
fn collection( fn collection(
&self, &self,
inner: &mut Inner, inner: &mut Inner,
@@ -191,7 +201,9 @@ impl SharedGridSet {
c.load_options = Some(load_options.clone()); c.load_options = Some(load_options.clone());
c.metric_modifiers.clone_from(&key.metric_modifiers); 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); let disco = Self::discover(inner);
for style in Style::ALL { for style in Style::ALL {
@@ -392,6 +404,7 @@ impl SharedGridSet {
/// Initialize once and return the font discovery mechanism. This remains /// Initialize once and return the font discovery mechanism. This remains
/// initialized throughout the lifetime of the application because some /// initialized throughout the lifetime of the application because some
/// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit. /// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit.
#[cfg(font_discovery)]
fn discover(inner: &mut Inner) -> &Discover { fn discover(inner: &mut Inner) -> &Discover {
inner.font_discover.get_or_insert_with(Discover::init) inner.font_discover.get_or_insert_with(Discover::init)
} }
+1
View File
@@ -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 | | 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 | | `+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 | | 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 ## Maintenance
+12
View File
@@ -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.