Files
ghostty-rs/crates/ghostty-core/src/global.rs
T
ddidderr 49d4ecefae feat(lib): port libghostty, Ghostty's embedded C API
Ports apprt/embedded.zig, main_c.zig and config/CApi.zig + c_get.zig as
crates/ghostty-lib (cdylib/staticlib/rlib `libghostty`), exporting every
function of the unchanged include/ghostty.h - the ABI Ghostty's Swift
macOS/iOS app links against. The embedded runtime implements the
runtime traits over the host's C callbacks, so the real core drives it.

- ghostty-core selects the renderer per target: Metal on Apple (surface
  NSView/UIView via RtSurface::platform_view), OpenGL elsewhere.
- ghostty_config_get follows Zig's c_get rules via a CValue type (143
  supported keys, checked against the Zig field types).
- The inspector C API is complete; its Metal backend replaces ImGui's
  Metal renderer and has not run.

Verification: generated C ABI probe (sizeof/alignof/offsetof of every
struct/union, 755 enum values, every function linked, nm exports, headers
byte-identical to upstream); a C host on Linux covering init, CLI
actions, config, app, keys, focus and color scheme; an opt-in
test-platform feature creating real surfaces on Linux with headless EGL
and a pty (13 tests). Apple targets are clippy-clean but not run; the
Swift app has not been built against it.

Test Plan:
- cargo test -p ghostty-lib (and --features test-platform)
- cargo clippy --target aarch64-apple-darwin -p ghostty-lib (cross env
  in docs/rewrite/notes/macos-M1.md)
2026-10-12 06:38:33 +02:00

374 lines
12 KiB
Rust

//! Global process state (port of `global.zig`).
//!
//! The Zig `GlobalState` also owns the allocator and the parsed CLI action;
//! in Rust the allocator is the global one and CLI action dispatch happens in
//! the root binary before this is initialized, so what remains is: logging,
//! signal setup, rlimits and the resources directory.
use std::{io::Write as _, sync::OnceLock};
use ghostty_os::{file::Rlimit, resourcesdir::ResourcesDir};
/// Where logging should go (Zig: `GlobalState.Logging`, a packed struct
/// parsed from `GHOSTTY_LOG`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Logging {
/// Whether to log to stderr.
pub stderr: bool,
/// Whether to log to macOS's unified logging. Enabled by default on
/// macOS.
pub macos: bool,
}
impl Default for Logging {
fn default() -> Self {
Logging {
stderr: true,
macos: cfg!(target_vendor = "apple"),
}
}
}
impl Logging {
/// Parse the `GHOSTTY_LOG` value (Zig: `cli.args.parsePackedStruct`):
/// a standalone boolean (`1/t/T/true/0/f/F/false`) sets every field,
/// otherwise a comma separated list of field names where a `no-` prefix
/// disables. Fields not mentioned keep their default (stderr on). Any
/// invalid value (including an empty part) yields the default.
#[must_use]
pub fn parse(v: &str) -> Logging {
Logging::parse_with(v, Logging::default())
}
/// [`Logging::parse`] with an explicit default (lib mode defaults to no
/// stderr logging).
#[must_use]
pub fn parse_with(v: &str, default: Logging) -> Logging {
match v {
"1" | "t" | "T" | "true" => {
return Logging {
stderr: true,
macos: true,
};
}
"0" | "f" | "F" | "false" => {
return Logging {
stderr: false,
macos: false,
};
}
_ => {}
}
let mut out = default;
for part in v.split(',') {
let part = part.trim_matches([' ', '\t']);
let (name, value) = match part.strip_prefix("no-") {
Some(n) => (n, false),
None => (part, true),
};
match name {
"stderr" => out.stderr = value,
"macos" => out.macos = value,
_ => return default,
}
}
out
}
}
/// Maintains the Unix resource limits that we set for our process. This can
/// be used to restore the limits to their original values.
#[derive(Debug, Clone, Copy, Default)]
pub struct ResourceLimits {
pub nofile: Option<Rlimit>,
}
impl ResourceLimits {
#[must_use]
pub fn init() -> ResourceLimits {
ResourceLimits {
// Maximize the number of file descriptors we can have open
// because we can consume a lot of them if we make many terminals.
nofile: ghostty_os::file::fix_max_files(),
}
}
/// Restore the original limits (the child does this after fork).
pub fn restore(&self) {
if let Some(lim) = self.nofile {
ghostty_os::file::restore_max_files(lim);
}
}
}
/// The global state (initialized once by [`init`]).
#[derive(Debug)]
pub struct GlobalState {
pub logging: Logging,
pub rlimits: ResourceLimits,
/// The app resources directory, equivalent to zig-out/share when we
/// build from source. Empty if we can't detect it.
pub resources_dir: ResourcesDir,
}
static STATE: OnceLock<GlobalState> = OnceLock::new();
/// Options for [`init`].
#[derive(Clone, Copy, Default)]
pub struct InitOptions {
/// A CLI action is being executed: disable stderr logging by default
/// since the action writes to stderr/stdout itself.
pub action: bool,
/// The runtime-specific resources directory discovery (Zig:
/// `apprt.runtime.resourcesDir`, e.g. GTK's Flatpak host path lookup).
/// `None` uses the generic `ghostty_os::resourcesdir::resources_dir`.
pub resources_dir: Option<fn() -> std::io::Result<ResourcesDir>>,
}
/// The global state. If [`init`] was never called, a default state (no
/// resources dir, no rlimit changes) is created.
pub fn state() -> &'static GlobalState {
STATE.get_or_init(|| GlobalState {
logging: Logging::default(),
rlimits: ResourceLimits::default(),
resources_dir: ResourcesDir::default(),
})
}
/// Initialize the global state: logging, rlimits, resources dir and i18n.
/// Call once, early (Zig: `GlobalState.init`).
pub fn init(opts: InitOptions) -> &'static GlobalState {
init_with(opts, Logging::default())
}
/// [`init`] for the embedded library (`ghostty_init`): Zig's
/// `GlobalState.Logging.stderr` defaults to `build_config.app_runtime !=
/// .none`, i.e. off for the lib artifact.
pub fn init_lib(opts: InitOptions) -> &'static GlobalState {
init_with(
opts,
Logging {
stderr: false,
..Logging::default()
},
)
}
fn init_with(opts: InitOptions, default_logging: Logging) -> &'static GlobalState {
STATE.get_or_init(|| {
let mut logging = default_logging;
// If we have an action executing, we disable logging by default
// since we write to stderr we don't want logs messing up our
// output.
if opts.action {
logging.stderr = false;
}
// Env vars are useful for logging too because they are easy to set.
if let Ok(v) = std::env::var("GHOSTTY_LOG") {
logging = Logging::parse_with(&v, default_logging);
}
init_logger(logging);
// SIGPIPE is ignored by the Rust runtime already (it sets SIG_IGN
// before main), which is what Zig's `initSignals` does.
// As early as possible, initialize our resource limits.
let rlimits = ResourceLimits::init();
// Initialize our crash reporting.
if let Err(err) = ghostty_crash::init(&ghostty_crash::Options {
release: ghostty_termio::stream_handler::VERSION_STRING.to_owned(),
..ghostty_crash::Options::default()
}) {
log::warn!("sentry init failed, no crash capture available err={err}");
}
// Find our resources directory once for the app so every launch
// hereafter can use this cached value.
let discover = opts
.resources_dir
.unwrap_or(ghostty_os::resourcesdir::resources_dir);
let resources_dir = match discover() {
Ok(d) => d,
Err(err) => {
log::warn!("failed to find resources dir err={err}");
ResourcesDir::default()
}
};
// Setup i18n
if let Some(v) = resources_dir.app()
&& let Err(err) = ghostty_os::i18n::init(v)
{
log::warn!("failed to init i18n, translations will not be available err={err}");
}
GlobalState {
logging,
rlimits,
resources_dir,
}
})
}
/// The `log` backend, formatted like Zig's `logFn`:
/// `info(scope): message` on stderr.
struct Logger {
stderr: bool,
/// macOS unified logging (`os_log`), only effective on Apple targets.
macos: bool,
max: log::LevelFilter,
}
/// The scope shown in log output: the crate name without the `ghostty_`
/// prefix (Zig scopes are `@tagName(scope)`).
fn log_scope(target: &str) -> &str {
let target = target.strip_prefix("ghostty_").unwrap_or(target);
target.split("::").next().unwrap_or(target)
}
/// The prefix used for macOS unified logging messages: nothing for the
/// default scope, `scope: ` otherwise.
#[cfg_attr(not(any(target_vendor = "apple", test)), allow(dead_code))]
fn macos_message(scope: &str, args: &std::fmt::Arguments<'_>) -> String {
if scope.is_empty() || scope == "default" {
format!("{args}")
} else {
format!("{scope}: {args}")
}
}
/// Bundle id used as the unified logging subsystem
/// (`build_config.bundle_id`).
#[cfg(target_vendor = "apple")]
const BUNDLE_ID: &str = ghostty_os::i18n::BUNDLE_ID;
#[cfg(target_vendor = "apple")]
fn log_macos(record: &log::Record<'_>) {
// On Mac, we use unified logging. To view this:
//
// sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'
//
// macOS logging is thread safe so no need for locks/mutexes.
//
// Convert our levels to Mac levels.
let level = match record.level() {
log::Level::Debug | log::Level::Trace => oslog::Level::Debug,
log::Level::Info => oslog::Level::Info,
log::Level::Warn => oslog::Level::Error,
log::Level::Error => oslog::Level::Fault,
};
let scope = log_scope(record.target());
// Initialize a logger. This is slow to do on every operation but we
// shouldn't be logging too much.
let logger = oslog::OsLog::new(BUNDLE_ID, scope);
logger.with_level(level, &macos_message(scope, record.args()));
}
impl log::Log for Logger {
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
(self.stderr || self.macos) && metadata.level() <= self.max
}
fn log(&self, record: &log::Record<'_>) {
if !self.enabled(record.metadata()) {
return;
}
#[cfg(target_vendor = "apple")]
if self.macos {
log_macos(record);
}
// skip if we are not logging to stderr
if !self.stderr {
return;
}
let level = match record.level() {
log::Level::Error => "error",
log::Level::Warn => "warning",
log::Level::Info => "info",
log::Level::Debug | log::Level::Trace => "debug",
};
let scope = log_scope(record.target());
let _ = writeln!(
std::io::stderr().lock(),
"{level}({scope}): {}",
record.args()
);
}
fn flush(&self) {}
}
fn init_logger(logging: Logging) {
// Our log level is always at least info in every build mode; debug
// builds (or RUST_LOG=debug) also show debug messages.
let max = match std::env::var("RUST_LOG").ok().as_deref() {
Some("trace") => log::LevelFilter::Trace,
Some("debug") => log::LevelFilter::Debug,
Some("warn") => log::LevelFilter::Warn,
Some("error") => log::LevelFilter::Error,
Some("info") => log::LevelFilter::Info,
_ if cfg!(debug_assertions) => log::LevelFilter::Debug,
_ => log::LevelFilter::Info,
};
let logger = Box::new(Logger {
stderr: logging.stderr,
macos: logging.macos,
max,
});
if log::set_boxed_logger(logger).is_ok() {
log::set_max_level(max);
}
}
/// Cleans up the global state. This doesn't _need_ to be called (Zig:
/// `GlobalState.deinit`); it deactivates crash capture ("flush crash logs").
pub fn deinit() {
ghostty_crash::deinit();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn macos_log_message_prefix() {
assert_eq!(
macos_message("font", &format_args!("hi {}", 1)),
"font: hi 1"
);
assert_eq!(macos_message("default", &format_args!("hi")), "hi");
assert_eq!(log_scope("ghostty_font::collection"), "font");
assert_eq!(log_scope("other::x"), "other");
}
#[test]
fn logging_parse() {
assert!(Logging::parse("stderr").stderr);
assert!(!Logging::parse("no-stderr").stderr);
// Unmentioned fields keep their default (stderr on).
assert!(Logging::parse("macos").stderr);
assert!(!Logging::parse("no-macos").macos);
assert!(Logging::parse("macos").macos);
assert!(Logging::parse("true").macos);
assert!(!Logging::parse("0").macos);
assert!(!Logging::parse("false").stderr);
assert!(!Logging::parse("0").stderr);
assert!(Logging::parse("true").stderr);
// Empty / invalid values yield the default.
assert!(Logging::parse("").stderr);
assert!(Logging::parse("stderr,").stderr);
assert_eq!(Logging::parse("garbage"), Logging::default());
}
}