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)
This commit is contained in:
ddidderr committed 2026-10-12 06:38:33 +02:00
1 parent f2252bb165
commit 75bf604375
47 files changed
+10296 -34

No files matched your search

Generated
+32
View File
@@ -1345,6 +1345,7 @@ dependencies = [
"ghostty-config",
"ghostty-crash",
"ghostty-font",
"ghostty-helpgen",
"ghostty-input",
"ghostty-os",
"ghostty-vt",
@@ -1390,6 +1391,7 @@ dependencies = [
"ghostty-unicode",
"ghostty-vt",
"log",
"objc2-core-foundation",
"onig",
"oslog",
"png",
@@ -1441,10 +1443,15 @@ dependencies = [
"yeslogic-fontconfig-sys",
]
[[package]]
name = "ghostty-helpgen"
version = "0.1.0"
[[package]]
name = "ghostty-input"
version = "0.1.0"
dependencies = [
"ghostty-helpgen",
"ghostty-unicode",
"ghostty-vt",
"indexmap",
@@ -1466,6 +1473,31 @@ dependencies = [
"log",
]
[[package]]
name = "ghostty-lib"
version = "0.1.0"
dependencies = [
"block2",
"dispatch2",
"ghostty-apprt",
"ghostty-bench",
"ghostty-cli",
"ghostty-config",
"ghostty-core",
"ghostty-input",
"ghostty-inspector",
"ghostty-os",
"ghostty-renderer",
"ghostty-terminal",
"ghostty-vt",
"log",
"objc2",
"objc2-core-foundation",
"objc2-foundation",
"objc2-metal",
"tempfile",
]
[[package]]
name = "ghostty-onig"
version = "0.1.0"
+2
View File
@@ -50,6 +50,7 @@ ghostty-vt = { path = "crates/ghostty-vt" }
ghostty-unicode = { path = "crates/ghostty-unicode" }
ghostty-terminal = { path = "crates/ghostty-terminal" }
ghostty-input = { path = "crates/ghostty-input" }
ghostty-helpgen = { path = "crates/ghostty-helpgen" }
ghostty-config = { path = "crates/ghostty-config" }
ghostty-font = { path = "crates/ghostty-font" }
ghostty-renderer = { path = "crates/ghostty-renderer" }
@@ -66,6 +67,7 @@ ghostty-bench = { path = "crates/ghostty-bench" }
ghostty-inspector = { path = "crates/ghostty-inspector" }
ghostty-vt-capi = { path = "crates/ghostty-vt-capi" }
ghostty-wasm = { path = "crates/ghostty-wasm" }
ghostty-lib = { path = "crates/ghostty-lib" }
[workspace.lints.rust]
unsafe_code = "forbid"
+6
View File
@@ -310,6 +310,12 @@ pub trait RtSurface {
let _ = name;
std::ptr::null()
}
/// The native view the renderer attaches to (embedded runtime: the
/// `NSView` / `UIView` the host provided; Metal renderer only). Null if
/// the runtime has no such view.
fn platform_view(&self) -> *mut std::ffi::c_void {
std::ptr::null_mut()
}
/// Callback to create a transient systemd scope for the child process
/// (Zig: `apprt/gtk/cgroup.zig` + `post_fork`). Called on the main
/// thread when the surface is created; the returned closure is called
+64
View File
@@ -0,0 +1,64 @@
//! The C representation of config values (Zig: `config/c_get.zig` together
//! with the `cval` functions of the config types).
//!
//! `ghostty_config_get` writes a value of a key-dependent C type into a
//! caller supplied pointer. This crate cannot use `unsafe`, so the typed
//! value is described by [`CValue`] and the C API crate (`ghostty-lib`) does
//! the actual write.
use ghostty_input::command::CommandC;
/// A size of the quick terminal along one axis in C form
/// (`ghostty_quick_terminal_size_s`).
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum QuickTerminalSizeC {
None,
Percentage(f32),
Pixels(u32),
}
/// The value of a config field in the form the C API exposes it.
#[derive(Debug, Clone, PartialEq)]
pub enum CValue {
/// The type of this field is not supported by the C API (Zig `getValue`
/// returns false).
Unsupported,
/// An optional field without a value (returns false).
Null,
/// An optional string without a value: `ghostty_config_get` returns true
/// and stores a null pointer.
NullString,
/// A string (`?[:0]const u8`): stored as a NUL terminated `const char*`.
String(String),
/// A boolean (`bool`).
Bool(bool),
/// `u8` / `u32`, stored as `c_uint`.
Uint(u32),
/// `i16`, stored as `c_short`.
Short(i16),
/// `f32`.
F32(f32),
/// `f64`.
F64(f64),
/// `usize` (`Duration.cval`).
Usize(usize),
/// An enum: the tag name, stored as a NUL terminated `const char*`.
EnumName(&'static str),
/// A packed struct of at most 32 bits: its bits stored as `c_uint`.
Packed(u32),
/// `ghostty_config_color_s`.
Color { r: u8, g: u8, b: u8 },
/// `ghostty_config_color_list_s`.
ColorList(Vec<[u8; 3]>),
/// `ghostty_config_palette_s`.
Palette(Box<[[u8; 3]; 256]>),
/// `ghostty_config_command_list_s`.
Commands(Vec<CommandC>),
/// `ghostty_config_quick_terminal_size_s`.
QuickTerminalSize {
primary: QuickTerminalSizeC,
secondary: QuickTerminalSizeC,
},
/// `ghostty_config_path_s`.
Path { path: String, optional: bool },
}
+67 -4
View File
@@ -15,6 +15,7 @@
use std::{fmt, io::BufRead};
use crate::{
c_value::CValue,
comma_splitter::CommaSplitter,
diagnostics::{Diagnostic, DiagnosticList, Location},
formatter::EntryFormatter,
@@ -113,6 +114,28 @@ pub trait ConfigField: Clone + PartialEq + Default + fmt::Debug {
/// Write this value in the `+show-config` format.
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result;
/// The value in the form the C API exposes (Zig: `c_get.getValue`).
/// Types the C API doesn't support yet return [`CValue::Unsupported`].
fn c_value(&self) -> CValue {
CValue::Unsupported
}
/// [`ConfigField::c_value`] when the value is wrapped in an `Option`
/// (only `String` differs: `?[:0]const u8` is supported, `[]const u8`
/// is not).
fn c_value_optional(&self) -> CValue {
self.c_value()
}
/// The C value of an `Option<Self>` that is `None`.
fn c_value_none() -> CValue {
if matches!(Self::default().c_value(), CValue::Unsupported) {
CValue::Unsupported
} else {
CValue::Null
}
}
}
/// The destination of CLI parsing.
@@ -456,6 +479,10 @@ pub fn parse_int_base0(s: &str) -> Option<i128> {
impl ConfigField for bool {
const KIND: FieldKind = FieldKind::Bool;
fn c_value(&self) -> CValue {
CValue::Bool(*self)
}
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
*self = parse_bool(value.unwrap_or("t"))?;
Ok(())
@@ -467,8 +494,13 @@ impl ConfigField for bool {
}
macro_rules! impl_int_field {
($($t:ty),*) => {$(
($($t:ty => $cv:expr),*) => {$(
impl ConfigField for $t {
fn c_value(&self) -> CValue {
let f: fn($t) -> CValue = $cv;
f(*self)
}
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
let v = value.ok_or(ArgError::ValueRequired)?;
let n = parse_int_base0(v).ok_or(ArgError::InvalidValue)?;
@@ -482,11 +514,27 @@ macro_rules! impl_int_field {
}
)*};
}
impl_int_field!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
impl_int_field!(
u8 => |v| CValue::Uint(u32::from(v)),
u16 => |_| CValue::Unsupported,
u32 => CValue::Uint,
u64 => |_| CValue::Unsupported,
usize => |_| CValue::Unsupported,
i8 => |_| CValue::Unsupported,
i16 => CValue::Short,
i32 => |_| CValue::Unsupported,
i64 => |_| CValue::Unsupported,
isize => |_| CValue::Unsupported
);
macro_rules! impl_float_field {
($($t:ty),*) => {$(
($($t:ty => $cv:expr),*) => {$(
impl ConfigField for $t {
fn c_value(&self) -> CValue {
let f: fn($t) -> CValue = $cv;
f(*self)
}
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
let v = value.ok_or(ArgError::ValueRequired)?;
*self = v.parse::<$t>().map_err(|_| ArgError::InvalidValue)?;
@@ -499,9 +547,17 @@ macro_rules! impl_float_field {
}
)*};
}
impl_float_field!(f32, f64);
impl_float_field!(f32 => CValue::F32, f64 => CValue::F64);
impl ConfigField for String {
fn c_value_optional(&self) -> CValue {
CValue::String(self.clone())
}
fn c_value_none() -> CValue {
CValue::NullString
}
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
*self = value.ok_or(ArgError::ValueRequired)?.to_string();
Ok(())
@@ -519,6 +575,13 @@ impl<T: ConfigField> ConfigField for Option<T> {
_ => FieldKind::OptionalOther,
};
fn c_value(&self) -> CValue {
match self {
Some(v) => v.c_value_optional(),
None => T::c_value_none(),
}
}
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
// For optional fields, we just treat it as the child type.
if let Some(v) = self {
+91
View File
@@ -3810,6 +3810,16 @@ impl Default for Config {
}
impl Config {
/// The value of the config key in the form the C API exposes it (Zig:
/// `c_get.get`). `None` if there is no such key.
#[must_use]
pub fn c_value(&self, key: &str) -> Option<crate::c_value::CValue> {
<Config as crate::macros::ConfigStruct>::FIELDS
.iter()
.find(|f| f.name == key)
.map(|f| (f.c_value)(self))
}
/// Build the default configuration (Zig: `Config.default`).
#[must_use]
pub fn new_default() -> Config {
@@ -5412,4 +5422,85 @@ mod tests {
assert!(out.contains("\nfont-size = 12\n"));
assert!(out.contains("\nbackground = #282c34\n"));
}
/// The keys `ghostty_config_get` supports, derived from the Zig field
/// types and `c_get.getValue` (see `testdata/c_get_supported.txt`).
#[test]
fn c_value_supported_keys_match_zig() {
use crate::{c_value::CValue, macros::ConfigStruct};
let expected: std::collections::BTreeSet<&str> =
include_str!("testdata/c_get_supported.txt")
.lines()
.filter(|l| !l.is_empty())
.collect();
let cfg = Config::default();
let mut actual = std::collections::BTreeSet::new();
for f in <Config as ConfigStruct>::FIELDS {
if !matches!((f.c_value)(&cfg), CValue::Unsupported) {
actual.insert(f.name);
}
}
let missing: Vec<_> = expected.difference(&actual).collect();
let extra: Vec<_> = actual.difference(&expected).collect();
assert!(
missing.is_empty() && extra.is_empty(),
"missing={missing:?} extra={extra:?}"
);
}
#[test]
fn c_value_basic_types() {
use crate::c_value::CValue;
let mut cfg = Config::default();
cfg.maximize = true;
assert_eq!(cfg.c_value("maximize"), Some(CValue::Bool(true)));
cfg.window_theme = WindowTheme::Dark;
assert_eq!(cfg.c_value("window-theme"), Some(CValue::EnumName("dark")));
cfg.title = None;
assert_eq!(cfg.c_value("title"), Some(CValue::NullString));
cfg.unfocused_split_fill = None;
assert_eq!(cfg.c_value("unfocused-split-fill"), Some(CValue::Null));
cfg.background_opacity = 0.42;
assert_eq!(cfg.c_value("background-opacity"), Some(CValue::F64(0.42)));
cfg.background = crate::types_a::Color {
r: 12,
g: 34,
b: 56,
};
assert_eq!(
cfg.c_value("background"),
Some(CValue::Color {
r: 12,
g: 34,
b: 56
})
);
assert_eq!(cfg.c_value("not-a-real-key"), None);
assert_eq!(cfg.c_value("font-family"), Some(CValue::Unsupported));
}
#[test]
fn c_value_background_blur() {
use crate::{c_value::CValue, types_b::BackgroundBlur};
let mut cfg = Config::default();
for (v, expect) in [
(BackgroundBlur::False, 0),
(BackgroundBlur::True, 20),
(BackgroundBlur::Radius(42), 42),
(BackgroundBlur::MacosGlassRegular, -1),
(BackgroundBlur::MacosGlassClear, -2),
] {
cfg.background_blur = v;
assert_eq!(cfg.c_value("background-blur"), Some(CValue::Short(expect)));
}
}
#[test]
fn c_value_split_preserve_zoom() {
use crate::c_value::CValue;
let mut cfg = Config::default();
assert_eq!(cfg.c_value("split-preserve-zoom"), Some(CValue::Packed(0)));
cfg.split_preserve_zoom.navigation = true;
assert_eq!(cfg.c_value("split-preserve-zoom"), Some(CValue::Packed(1)));
}
}
+10
View File
@@ -6,6 +6,7 @@ use std::fmt;
use ghostty_input::{KeyRemapSet, OptionAsAlt};
use crate::{
c_value::CValue,
cli_args::{ArgError, ConfigField},
formatter::EntryFormatter,
};
@@ -30,6 +31,15 @@ impl ConfigField for KeyRemapSet {
impl ConfigField for OptionAsAlt {
const VALID_VALUES: Option<&'static [&'static str]> = None;
fn c_value(&self) -> CValue {
CValue::EnumName(match self {
OptionAsAlt::False => "false",
OptionAsAlt::True => "true",
OptionAsAlt::Left => "left",
OptionAsAlt::Right => "right",
})
}
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
let v = value.ok_or(ArgError::ValueRequired)?;
*self = match v {
+1
View File
@@ -13,6 +13,7 @@
)]
// Generic CLI parsing infrastructure.
pub mod c_value;
pub mod cli_args;
pub mod comma_splitter;
pub mod diagnostics;
+21
View File
@@ -37,6 +37,8 @@ pub struct FieldInfo<T> {
pub eq: fn(&T, &T) -> bool,
/// Format this field as in `+show-config`.
pub format: fn(&T, &str, &mut dyn fmt::Write) -> fmt::Result,
/// The value in the form the C API exposes it.
pub c_value: fn(&T) -> crate::c_value::CValue,
}
impl<T> FieldInfo<T> {
@@ -147,6 +149,7 @@ macro_rules! config_struct {
let mut f = $crate::formatter::EntryFormatter::new(name, w);
$crate::cli_args::ConfigField::format_entry(&a.$field, &mut f)
},
c_value: |a: &$name| $crate::cli_args::ConfigField::c_value(&a.$field),
},
)*
];
@@ -221,6 +224,10 @@ macro_rules! config_enum {
::core::result::Result::Ok(())
}
fn c_value(&self) -> $crate::c_value::CValue {
$crate::c_value::CValue::EnumName(self.as_str())
}
fn format_entry(
&self,
f: &mut $crate::formatter::EntryFormatter<'_>,
@@ -274,6 +281,20 @@ macro_rules! config_packed {
const KIND: $crate::cli_args::FieldKind =
$crate::cli_args::FieldKind::Packed(Self::NAMES);
fn c_value(&self) -> $crate::c_value::CValue {
// Packed bools: the first field is bit 0.
let mut bits = 0u32;
let mut _i = 0u32;
$(
if self.$field && _i < 32 { bits |= 1 << _i; }
_i += 1;
)*
if _i > 32 {
return $crate::c_value::CValue::Unsupported;
}
$crate::c_value::CValue::Packed(bits)
}
fn parse_cli(
&mut self,
value: ::core::option::Option<&str>,
+14
View File
@@ -6,6 +6,7 @@ use std::{
};
use crate::{
c_value::CValue,
cli_args::{ArgError, ConfigField},
diagnostics::{Diagnostic, DiagnosticList},
formatter::EntryFormatter,
@@ -171,6 +172,19 @@ impl Path {
}
impl ConfigField for Path {
fn c_value(&self) -> CValue {
match self {
Path::Optional(p) => CValue::Path {
path: p.clone(),
optional: true,
},
Path::Required(p) => CValue::Path {
path: p.clone(),
optional: false,
},
}
}
/// Parse CLI option. The value will be overwritten.
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
let Some(item) = Path::parse(input)? else {
@@ -46,6 +46,14 @@ impl RepeatableStringMap {
.map(|(_, v)| v.as_str())
}
/// Insert or replace an entry (Zig: `map.put`).
pub fn put(&mut self, key: &str, value: &str) {
match self.map.iter_mut().find(|(k, _)| k == key) {
Some((_, v)) => value.clone_into(v),
None => self.map.push((key.to_owned(), value.to_owned())),
}
}
/// Iterator over the entries in the map (insertion order).
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.map.iter().map(|(k, v)| (k.as_str(), v.as_str()))
+143
View File
@@ -0,0 +1,143 @@
language
font-synthetic-style
font-size
font-thicken
font-thicken-strength
font-shaping-break
alpha-blending
grapheme-width-method
freetype-load-flags
background
foreground
background-image
background-image-opacity
background-image-position
background-image-fit
background-image-repeat
selection-clear-on-typing
selection-clear-on-copy
minimum-contrast
palette
palette-generate
palette-harmonious
cursor-opacity
cursor-style
cursor-style-blink
cursor-click-to-move
mouse-hide-while-typing
scroll-to-bottom
mouse-shift-capture
mouse-reporting
background-opacity
background-opacity-cells
background-blur
unfocused-split-opacity
unfocused-split-fill
split-divider-color
split-preserve-zoom
notify-on-command-finish
notify-on-command-finish-action
notify-on-command-finish-after
wait-after-command
abnormal-command-exit-runtime
scrollbar
link-url
link-previews
maximize
fullscreen
title
class
x11-instance-name
window-padding-balance
window-padding-color
window-vsync
window-inherit-working-directory
tab-inherit-working-directory
split-inherit-working-directory
window-inherit-font-size
window-decoration
window-title-font-family
window-subtitle
window-theme
window-colorspace
window-height
window-width
window-position-x
window-position-y
window-save-state
window-step-resize
window-new-tab-position
window-show-tab-bar
window-titlebar-background
window-titlebar-foreground
resize-overlay
resize-overlay-position
resize-overlay-duration
focus-follows-mouse
clipboard-read
clipboard-write
clipboard-trim-trailing-spaces
clipboard-paste-protection
clipboard-paste-bracketed-safe
title-report
image-storage-limit
copy-on-select
right-click-action
click-repeat-interval
config-default-files
confirm-close-surface
quit-after-last-window-closed
quit-after-last-window-closed-delay
initial-window
undo-timeout
quick-terminal-position
quick-terminal-size
gtk-quick-terminal-layer
quick-terminal-screen
quick-terminal-animation-duration
quick-terminal-autohide
quick-terminal-space-behavior
quick-terminal-keyboard-interactivity
shell-integration
shell-integration-features
command-palette-entry
osc-color-report-format
vt-kam-allowed
custom-shader-animation
bell-features
bell-audio-path
bell-audio-volume
app-notifications
macos-non-native-fullscreen
macos-window-buttons
macos-titlebar-style
macos-titlebar-proxy-icon
macos-dock-drop-behavior
macos-option-as-alt
macos-window-shadow
macos-hidden
macos-auto-secure-input
macos-secure-input-indication
macos-applescript
macos-icon
macos-custom-icon
macos-icon-frame
macos-icon-ghost-color
macos-icon-screen-color
macos-shortcuts
linux-cgroup
linux-cgroup-hard-fail
gtk-opengl-debug
gtk-single-instance
gtk-titlebar
gtk-tabs-location
gtk-titlebar-hide-when-maximized
gtk-toolbar-style
gtk-titlebar-style
gtk-wide-tabs
desktop-notifications
progress-style
faint-opacity
async-backend
auto-update
auto-update-channel
+30
View File
@@ -7,6 +7,7 @@ use std::fmt;
use ghostty_vt::color::{DEFAULT as DEFAULT_PALETTE, Palette as TermPalette, PaletteMask, Rgb};
use crate::{
c_value::CValue,
cli_args::{ArgError, ConfigField, parse_int_base0, trim_ws},
clipboard_codepoint_map::{ClipboardCodepointMap, Entry as ClipboardEntry, Replacement},
config_enum,
@@ -35,6 +36,15 @@ impl ConfigField for CursorStyle {
const VALID_VALUES: Option<&'static [&'static str]> =
Some(&["bar", "block", "underline", "block_hollow"]);
fn c_value(&self) -> CValue {
CValue::EnumName(match self {
CursorStyle::Bar => "bar",
CursorStyle::Block => "block",
CursorStyle::Underline => "underline",
CursorStyle::BlockHollow => "block_hollow",
})
}
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
let v = value.ok_or(ArgError::ValueRequired)?;
*self = match v {
@@ -290,6 +300,14 @@ impl Color {
}
impl ConfigField for Color {
fn c_value(&self) -> CValue {
CValue::Color {
r: self.r,
g: self.g,
b: self.b,
}
}
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
*self = Color::from_cli(input)?;
Ok(())
@@ -394,6 +412,10 @@ pub struct ColorList {
}
impl ConfigField for ColorList {
fn c_value(&self) -> CValue {
CValue::ColorList(self.colors.iter().map(|c| [c.r, c.g, c.b]).collect())
}
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
let input = input.ok_or(ArgError::ValueRequired)?;
if input.is_empty() {
@@ -454,6 +476,14 @@ impl Default for Palette {
}
impl ConfigField for Palette {
fn c_value(&self) -> CValue {
let mut out = Box::new([[0u8; 3]; 256]);
for (dst, c) in out.iter_mut().zip(self.value.iter()) {
*dst = [c.r, c.g, c.b];
}
CValue::Palette(out)
}
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
let value = input.ok_or(ArgError::ValueRequired)?;
let eql_idx = value.find('=').ok_or(ArgError::InvalidValue)?;
+82
View File
@@ -8,6 +8,7 @@ use std::fmt;
use ghostty_input::binding::Action;
use crate::{
c_value::{CValue, QuickTerminalSizeC},
cli_args::{ArgError, ConfigField, parse_auto_struct_with, parse_bool, trim_ws},
config_enum,
config_packed,
@@ -152,6 +153,15 @@ impl RepeatableCommand {
}
impl ConfigField for RepeatableCommand {
fn c_value(&self) -> CValue {
CValue::Commands(
self.value
.iter()
.map(ghostty_input::command::Command::cval)
.collect(),
)
}
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
// Unset or empty input clears the list
let input = input.unwrap_or("");
@@ -604,6 +614,18 @@ impl QuickTerminalSize {
}
impl ConfigField for QuickTerminalSize {
fn c_value(&self) -> CValue {
let conv = |v: Option<QuickTerminalSizeValue>| match v {
None => QuickTerminalSizeC::None,
Some(QuickTerminalSizeValue::Percentage(p)) => QuickTerminalSizeC::Percentage(p),
Some(QuickTerminalSizeValue::Pixels(p)) => QuickTerminalSizeC::Pixels(p),
};
CValue::QuickTerminalSize {
primary: conv(self.primary),
secondary: conv(self.secondary),
}
}
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
let input = input.ok_or(ArgError::ValueRequired)?;
let mut it = input.split(',');
@@ -786,6 +808,17 @@ impl BackgroundBlur {
}
impl ConfigField for BackgroundBlur {
fn c_value(&self) -> CValue {
// Sentinel values for the macOS glass styles, like Zig's `cval`.
CValue::Short(match self {
BackgroundBlur::False => 0,
BackgroundBlur::True => 20,
BackgroundBlur::Radius(v) => i16::from(*v),
BackgroundBlur::MacosGlassRegular => -1,
BackgroundBlur::MacosGlassClear => -2,
})
}
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
let Some(input) = input else {
// Emulate behavior for bools
@@ -882,6 +915,10 @@ impl WindowDecoration {
impl ConfigField for WindowDecoration {
const VALID_VALUES: Option<&'static [&'static str]> = Some(Self::NAMES);
fn c_value(&self) -> CValue {
CValue::EnumName(self.as_str())
}
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
*self = Self::parse(input)?;
Ok(())
@@ -1127,6 +1164,10 @@ impl fmt::Display for Duration {
}
impl ConfigField for Duration {
fn c_value(&self) -> CValue {
CValue::Usize(self.as_milliseconds() as usize)
}
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
*self = Duration::parse(input)?;
Ok(())
@@ -1267,6 +1308,47 @@ mod tests {
assert_eq!(list.value.len(), 0);
}
#[test]
fn repeatable_command_cval() {
let mut list = RepeatableCommand::default();
list.parse_cli(Some("title:Foo,action:ignore")).unwrap();
list.parse_cli(Some("title:Bar,description:bobr,action:text:ale bydle"))
.unwrap();
assert_eq!(list.value.len(), 2);
let CValue::Commands(cv) = list.c_value() else {
panic!("expected commands");
};
assert_eq!(cv.len(), 2);
// First entry
assert_eq!(cv[0].title, "Foo");
assert_eq!(cv[0].action_key, "ignore");
assert_eq!(cv[0].action, "ignore");
// Second entry
assert_eq!(cv[1].title, "Bar");
assert_eq!(cv[1].description, "bobr");
assert_eq!(cv[1].action_key, "text");
assert_eq!(cv[1].action, "text:ale bydle");
}
#[test]
fn repeatable_command_cval_cleared() {
let mut list = RepeatableCommand::default();
list.parse_cli(Some("title:Foo,action:ignore")).unwrap();
let CValue::Commands(cv) = list.c_value() else {
panic!("expected commands");
};
assert_eq!(cv.len(), 1);
list.parse_cli(Some("")).unwrap();
let CValue::Commands(cv) = list.c_value() else {
panic!("expected commands");
};
assert_eq!(cv.len(), 0);
}
#[test]
fn repeatable_command_format_config_empty() {
let list = RepeatableCommand::default();
+1
View File
@@ -30,3 +30,4 @@ workspace = true
[target.'cfg(target_vendor = "apple")'.dependencies]
oslog = { version = "0.2.0", default-features = false }
objc2-core-foundation = "0.3.2"
+22 -9
View File
@@ -488,6 +488,27 @@ impl App {
}
}
/// Create a surface and add it to the app (`CoreApp::new_surface` with
/// the concrete surface type, for runtimes that need more than the
/// [`CoreSurface`] trait, e.g. the embedded C API).
pub fn create_surface(
&self,
id: SurfaceId,
config: Config,
rt_app: Rc<dyn RtApp>,
rt_surface: Rc<dyn RtSurface>,
) -> Result<Rc<Surface>, CoreError> {
let surface = Surface::new(
id,
Rc::clone(&self.state),
Arc::new(config),
Rc::clone(&rt_app),
rt_surface,
)?;
self.add_surface(id, &surface, &rt_app);
Ok(surface)
}
/// Add an initialized surface. Since we have non-zero surfaces, the quit
/// timer is cancelled.
pub(crate) fn add_surface(&self, id: SurfaceId, surface: &Rc<Surface>, rt: &Rc<dyn RtApp>) {
@@ -589,15 +610,7 @@ impl CoreApp for App {
rt_app: Rc<dyn RtApp>,
rt_surface: Rc<dyn RtSurface>,
) -> Result<Rc<dyn CoreSurface>, CoreError> {
let surface = Surface::new(
id,
Rc::clone(&self.state),
Arc::new(config),
Rc::clone(&rt_app),
rt_surface,
)?;
self.add_surface(id, &surface, &rt_app);
Ok(surface)
Ok(self.create_surface(id, config, rt_app, rt_surface)?)
}
fn delete_surface(&self, id: SurfaceId) {
+28 -4
View File
@@ -38,6 +38,13 @@ impl Logging {
/// 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 {
@@ -53,7 +60,7 @@ impl Logging {
}
_ => {}
}
let mut out = Logging::default();
let mut out = default;
for part in v.split(',') {
let part = part.trim_matches([' ', '\t']);
let (name, value) = match part.strip_prefix("no-") {
@@ -63,7 +70,7 @@ impl Logging {
match name {
"stderr" => out.stderr = value,
"macos" => out.macos = value,
_ => return Logging::default(),
_ => return default,
}
}
out
@@ -134,8 +141,25 @@ pub fn state() -> &'static GlobalState {
/// 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 = Logging::default();
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
@@ -146,7 +170,7 @@ pub fn init(opts: InitOptions) -> &'static GlobalState {
// 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(&v);
logging = Logging::parse_with(&v, default_logging);
}
init_logger(logging);
+77 -13
View File
@@ -89,14 +89,13 @@ use ghostty_font::{
};
use ghostty_input::{Mods, MouseButton, MouseButtonState, ScrollMods};
use ghostty_renderer::{
Api as GraphicsApi,
Health,
// The generic renderer over the OpenGL API (the GTK runtime's graphics
// API). `ghostty_renderer::Renderer` is the platform default, which is
// Metal on Apple targets; the embedded (macOS) runtime provides its own
// Metal-based surface setup (TODO(integration): L1).
generic::{Host as RendererHost, Renderer},
// The renderer over the graphics API selected for the target (Zig:
// `renderer.zig`): Metal on Apple targets, OpenGL elsewhere.
Renderer,
generic::Host as RendererHost,
message::{FontGridChange, Message as RendererMessage},
opengl::OpenGL,
options::Options as RendererOptions,
size::{CellSize, Coordinate, Padding, ScreenSize, Size},
state::{Preedit, PreeditCodepoint, State as RenderStateData},
@@ -197,6 +196,31 @@ impl RendererHost for HostImpl {
}
}
/// Create the graphics API for a new surface (Zig: `Renderer.surfaceInit`).
#[cfg(not(target_vendor = "apple"))]
fn init_graphics_api(
config: &Config,
rt_surface: &dyn RtSurface,
) -> Result<GraphicsApi, CoreError> {
let mut api = GraphicsApi::init(config.alpha_blending);
api.prepare_context(|name| rt_surface.gl_proc_address(name))
.map_err(|e| format!("failed to prepare the OpenGL context: {e}"))?;
Ok(api)
}
/// Create the graphics API for a new surface (Zig: `Renderer.surfaceInit`):
/// the Metal layer is attached to the NSView/UIView the host provided.
#[cfg(target_vendor = "apple")]
fn init_graphics_api(
config: &Config,
rt_surface: &dyn RtSurface,
) -> Result<GraphicsApi, CoreError> {
let view = rt_surface.platform_view();
let scale = f64::from(rt_surface.get_content_scale().x);
GraphicsApi::init_with_view(config.alpha_blending, view, scale)
.map_err(|e| format!("failed to initialize the Metal renderer: {e}").into())
}
/// Font state of the surface.
pub struct FontState {
/// The key of the font grid we hold a reference on.
@@ -333,13 +357,8 @@ impl Surface {
// Initialize our renderer with our initialized surface (Zig:
// `Renderer.surfaceInit`): load the GL functions for the current
// context.
let mut api = OpenGL::init(config.alpha_blending);
{
let rt = Rc::clone(&rt_surface);
api.prepare_context(|name| rt.gl_proc_address(name))
.map_err(|e| format!("failed to prepare the OpenGL context: {e}"))?;
}
// context (OpenGL) or attach the Metal layer to the host's view.
let api = init_graphics_api(&config, rt_surface.as_ref())?;
// Determine our DPI configurations so we can properly configure
// font points to pixels and handle other high-DPI scaling factors.
@@ -708,6 +727,34 @@ impl Surface {
self.renderer_handle.send(msg);
}
/// Tell the renderer which display the surface is on (the embedded
/// `ghostty_surface_set_display_id`).
#[cfg(target_vendor = "apple")]
pub fn set_macos_display_id(&self, id: u32) {
self.send_renderer(RendererMessage::MacosDisplayId(id));
}
/// A retained `CTFontRef` for the primary font, for macOS quicklook
/// (the embedded `ghostty_surface_quicklook_font`). `scale` is the
/// content scale. The caller owns the reference.
#[cfg(target_vendor = "apple")]
#[must_use]
pub fn quicklook_font(&self, scale: f32) -> Option<*mut std::ffi::c_void> {
let grid = self.renderer.font_grid();
let mut state = grid.write();
let face = state
.resolver
.collection
.get_face(ghostty_font::collection::Index::default())
.ok()?;
let font = face.quicklook_font(f64::from(scale));
Some(
objc2_core_foundation::CFRetained::into_raw(font)
.as_ptr()
.cast(),
)
}
/// Current sizes.
#[must_use]
pub fn size(&self) -> Size {
@@ -1810,6 +1857,14 @@ impl Surface {
/// Write the last presented frame as a PNG. Requires the GL context to be
/// current.
#[cfg(target_vendor = "apple")]
pub fn write_screenshot_png(&self, _path: &std::path::Path) -> Result<(), CoreError> {
Err("screenshots are not supported by the Metal renderer".into())
}
/// Write the last presented frame as a PNG. Requires the GL context to be
/// current.
#[cfg(not(target_vendor = "apple"))]
pub fn write_screenshot_png(&self, path: &std::path::Path) -> Result<(), CoreError> {
let Some((w, h, px)) = self.renderer.last_frame_pixels() else {
return Err("no frame has been presented yet".into());
@@ -1832,11 +1887,13 @@ impl Surface {
/// The pixels of the last presented frame (`width`, `height`, RGBA8,
/// bottom-up). Test/debug hook for headless integration tests; requires
/// the GL context to be current on the calling thread.
#[cfg(not(target_vendor = "apple"))]
pub fn frame_pixels(&self) -> Option<(usize, usize, Vec<u8>)> {
self.renderer.last_frame_pixels()
}
/// The GLArea was (re)realized; the GL context is current.
#[cfg(not(target_vendor = "apple"))]
pub fn renderer_realized(&self) -> Result<(), CoreError> {
let rt = Rc::clone(&self.rt_surface);
self.renderer
@@ -1844,6 +1901,13 @@ impl Surface {
Ok(())
}
/// The display was (re)realized (Metal has nothing to reload).
#[cfg(target_vendor = "apple")]
pub fn renderer_realized(&self) -> Result<(), CoreError> {
self.renderer.display_realized_with(|_| {})?;
Ok(())
}
/// The GLArea is being unrealized; the GL context is current.
pub fn renderer_unrealized(&self) {
self.renderer.display_unrealized();
@@ -1,5 +1,8 @@
//! Keybinding actions end to end (default and configured bindings).
// These drive the real surface over a headless OpenGL context; the Apple targets render
// with Metal.
#![cfg(not(target_vendor = "apple"))]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::fmt::Write as _;
+3
View File
@@ -1,5 +1,8 @@
//! IME: preedit state and rendering, commit, composing key events.
// These drive the real surface over a headless OpenGL context; the Apple targets render
// with Metal.
#![cfg(not(target_vendor = "apple"))]
#![allow(clippy::unwrap_used, clippy::expect_used)]
mod common;
@@ -2,6 +2,9 @@
//! recording (with the pty encoding and triggered bindings) and VT event
//! recording while data is read from the pty.
// These drive the real surface over a headless OpenGL context; the Apple targets render
// with Metal.
#![cfg(not(target_vendor = "apple"))]
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::assert_is_empty)]
mod common;
+3
View File
@@ -1,6 +1,9 @@
//! Keyboard input end to end: key events -> core Surface -> encoder -> pty ->
//! a real `cat` child that records the exact bytes it received.
// These drive the real surface over a headless OpenGL context; the Apple targets render
// with Metal.
#![cfg(not(target_vendor = "apple"))]
#![allow(clippy::unwrap_used, clippy::expect_used)]
mod common;
+3
View File
@@ -1,6 +1,9 @@
//! Mouse end to end: selection, copy-on-select, multi-click, mouse reporting
//! formats, alternate scroll, shift bypass and links.
// These drive the real surface over a headless OpenGL context; the Apple targets render
// with Metal.
#![cfg(not(target_vendor = "apple"))]
#![allow(clippy::unwrap_used, clippy::expect_used)]
mod common;
+3
View File
@@ -1,6 +1,9 @@
//! Terminal <-> runtime events: resize, focus, title, pwd, bell, OSC 52,
//! notifications, progress, child exit.
// These drive the real surface over a headless OpenGL context; the Apple targets render
// with Metal.
#![cfg(not(target_vendor = "apple"))]
#![allow(clippy::unwrap_used, clippy::expect_used)]
mod common;
+14
View File
@@ -697,6 +697,20 @@ impl Face {
Ok(util::font_table_bytes(&self.font, tag))
}
/// A copy of this face's font at `size / scale` points: the font to use
/// for macOS quicklook (`ghostty_surface_quicklook_font`). The content
/// scale is applied to our font stack because we render at 1x, but the
/// callers of the quicklook font apply the scale themselves.
pub fn quicklook_font(&self, scale: f64) -> CFRetained<CTFont> {
// SAFETY: plain CoreText getter; null matrix and no descriptor are
// allowed for the copy.
unsafe {
let size = self.font.size();
self.font
.copy_with_attributes(size / scale, ptr::null(), None)
}
}
/// The underlying CTFont (for shapers).
pub(crate) fn ct_font(&self) -> &CTFont {
&self.font
+85
View File
@@ -0,0 +1,85 @@
[package]
name = "ghostty-lib"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "libghostty: the embedded C API (include/ghostty.h) used by the macOS/iOS app, ported from apprt/embedded.zig and main_c.zig"
# Produces `libghostty.{so,a,dylib}`, the names the upstream build ships.
[lib]
name = "ghostty"
crate-type = ["cdylib", "staticlib", "rlib"]
[features]
# Test seam (off by default): accepts a fake platform for `ghostty_surface_new`
# on non-Apple targets whose OpenGL functions are supplied by the test (via a
# headless EGL context), so the embedded surface flow can be tested on Linux.
# Zig rejects every platform off Darwin; production builds are unaffected.
test-platform = []
[dependencies]
ghostty-apprt.workspace = true
ghostty-bench.workspace = true
ghostty-cli.workspace = true
ghostty-config.workspace = true
ghostty-core.workspace = true
ghostty-input.workspace = true
ghostty-inspector.workspace = true
ghostty-os.workspace = true
ghostty-renderer.workspace = true
ghostty-terminal.workspace = true
ghostty-vt.workspace = true
log = "0.4.34"
[target.'cfg(target_vendor = "apple")'.dependencies]
block2 = "0.6.2"
dispatch2 = "0.3.1"
objc2 = "0.6.5"
objc2-core-foundation = "0.3.2"
objc2-foundation = "0.3.2"
objc2-metal = { version = "0.3.2", features = ["block2"] }
[dev-dependencies]
tempfile = "3"
# The workspace forbids `unsafe`; this crate is the C ABI boundary, so it
# denies it instead and allows it (with SAFETY comments) in the FFI modules.
# Everything else is copied from `[workspace.lints]`.
[lints.rust]
unsafe_code = "deny"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
cast_sign_loss = "allow"
cast_precision_loss = "allow"
cast_lossless = "allow"
module_name_repetitions = "allow"
too_many_lines = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
similar_names = "allow"
struct_excessive_bools = "allow"
fn_params_excessive_bools = "allow"
match_same_arms = "allow"
unreadable_literal = "allow"
many_single_char_names = "allow"
doc_markdown = "allow"
must_use_candidate = "allow"
return_self_not_must_use = "allow"
used_underscore_binding = "allow"
large_stack_arrays = "allow"
inline_always = "allow"
# The exported functions are `unsafe extern "C"` mirroring the header contracts.
missing_safety_doc = "allow"
# `&mut x` coerced to `*mut T` is the idiomatic way to pass out-params in FFI tests.
borrow_as_ptr = "allow"
# Allocations are made with the layout of the pointee, so the cast is aligned.
cast_ptr_alignment = "allow"
[[test]]
name = "surface"
required-features = ["test-platform"]
+1178
View File
@@ -0,0 +1,1178 @@
// Ghostty embedding API. The documentation for the embedding API is
// only within the Zig source files that define the implementations. This
// isn't meant to be a general purpose embedding API (yet) so there hasn't
// been documentation or example work beyond that.
//
// The only consumer of this API is the macOS app, but the API is built to
// be more general purpose.
#ifndef GHOSTTY_H
#define GHOSTTY_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
//-------------------------------------------------------------------
// Macros
#define GHOSTTY_SUCCESS 0
//-------------------------------------------------------------------
// Types
// Opaque types
typedef void* ghostty_app_t;
typedef void* ghostty_config_t;
typedef void* ghostty_surface_t;
typedef void* ghostty_inspector_t;
// All the types below are fully defined and must be kept in sync with
// their Zig counterparts. Any changes to these types MUST have an associated
// Zig change.
typedef enum {
GHOSTTY_PLATFORM_INVALID,
GHOSTTY_PLATFORM_MACOS,
GHOSTTY_PLATFORM_IOS,
} ghostty_platform_e;
typedef enum {
GHOSTTY_CLIPBOARD_STANDARD,
GHOSTTY_CLIPBOARD_SELECTION,
} ghostty_clipboard_e;
typedef struct {
const char *mime;
const char *data;
} ghostty_clipboard_content_s;
typedef enum {
GHOSTTY_CLIPBOARD_REQUEST_PASTE,
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ,
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE,
} ghostty_clipboard_request_e;
typedef enum {
GHOSTTY_MOUSE_RELEASE,
GHOSTTY_MOUSE_PRESS,
} ghostty_input_mouse_state_e;
typedef enum {
GHOSTTY_MOUSE_UNKNOWN,
GHOSTTY_MOUSE_LEFT,
GHOSTTY_MOUSE_RIGHT,
GHOSTTY_MOUSE_MIDDLE,
GHOSTTY_MOUSE_FOUR,
GHOSTTY_MOUSE_FIVE,
GHOSTTY_MOUSE_SIX,
GHOSTTY_MOUSE_SEVEN,
GHOSTTY_MOUSE_EIGHT,
GHOSTTY_MOUSE_NINE,
GHOSTTY_MOUSE_TEN,
GHOSTTY_MOUSE_ELEVEN,
} ghostty_input_mouse_button_e;
typedef enum {
GHOSTTY_MOUSE_MOMENTUM_NONE,
GHOSTTY_MOUSE_MOMENTUM_BEGAN,
GHOSTTY_MOUSE_MOMENTUM_STATIONARY,
GHOSTTY_MOUSE_MOMENTUM_CHANGED,
GHOSTTY_MOUSE_MOMENTUM_ENDED,
GHOSTTY_MOUSE_MOMENTUM_CANCELLED,
GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN,
} ghostty_input_mouse_momentum_e;
typedef enum {
GHOSTTY_COLOR_SCHEME_LIGHT = 0,
GHOSTTY_COLOR_SCHEME_DARK = 1,
} ghostty_color_scheme_e;
// This is a packed struct (see src/input/mouse.zig) but the C standard
// afaik doesn't let us reliably define packed structs so we build it up
// from scratch.
typedef int ghostty_input_scroll_mods_t;
typedef enum {
GHOSTTY_MODS_NONE = 0,
GHOSTTY_MODS_SHIFT = 1 << 0,
GHOSTTY_MODS_CTRL = 1 << 1,
GHOSTTY_MODS_ALT = 1 << 2,
GHOSTTY_MODS_SUPER = 1 << 3,
GHOSTTY_MODS_CAPS = 1 << 4,
GHOSTTY_MODS_NUM = 1 << 5,
GHOSTTY_MODS_SHIFT_RIGHT = 1 << 6,
GHOSTTY_MODS_CTRL_RIGHT = 1 << 7,
GHOSTTY_MODS_ALT_RIGHT = 1 << 8,
GHOSTTY_MODS_SUPER_RIGHT = 1 << 9,
} ghostty_input_mods_e;
typedef enum {
GHOSTTY_BINDING_FLAGS_CONSUMED = 1 << 0,
GHOSTTY_BINDING_FLAGS_ALL = 1 << 1,
GHOSTTY_BINDING_FLAGS_GLOBAL = 1 << 2,
GHOSTTY_BINDING_FLAGS_PERFORMABLE = 1 << 3,
} ghostty_binding_flags_e;
typedef enum {
GHOSTTY_ACTION_RELEASE,
GHOSTTY_ACTION_PRESS,
GHOSTTY_ACTION_REPEAT,
} ghostty_input_action_e;
// Based on: https://www.w3.org/TR/uievents-code/
typedef enum {
GHOSTTY_KEY_UNIDENTIFIED,
// "Writing System Keys" § 3.1.1
GHOSTTY_KEY_BACKQUOTE,
GHOSTTY_KEY_BACKSLASH,
GHOSTTY_KEY_BRACKET_LEFT,
GHOSTTY_KEY_BRACKET_RIGHT,
GHOSTTY_KEY_COMMA,
GHOSTTY_KEY_DIGIT_0,
GHOSTTY_KEY_DIGIT_1,
GHOSTTY_KEY_DIGIT_2,
GHOSTTY_KEY_DIGIT_3,
GHOSTTY_KEY_DIGIT_4,
GHOSTTY_KEY_DIGIT_5,
GHOSTTY_KEY_DIGIT_6,
GHOSTTY_KEY_DIGIT_7,
GHOSTTY_KEY_DIGIT_8,
GHOSTTY_KEY_DIGIT_9,
GHOSTTY_KEY_EQUAL,
GHOSTTY_KEY_INTL_BACKSLASH,
GHOSTTY_KEY_INTL_RO,
GHOSTTY_KEY_INTL_YEN,
GHOSTTY_KEY_A,
GHOSTTY_KEY_B,
GHOSTTY_KEY_C,
GHOSTTY_KEY_D,
GHOSTTY_KEY_E,
GHOSTTY_KEY_F,
GHOSTTY_KEY_G,
GHOSTTY_KEY_H,
GHOSTTY_KEY_I,
GHOSTTY_KEY_J,
GHOSTTY_KEY_K,
GHOSTTY_KEY_L,
GHOSTTY_KEY_M,
GHOSTTY_KEY_N,
GHOSTTY_KEY_O,
GHOSTTY_KEY_P,
GHOSTTY_KEY_Q,
GHOSTTY_KEY_R,
GHOSTTY_KEY_S,
GHOSTTY_KEY_T,
GHOSTTY_KEY_U,
GHOSTTY_KEY_V,
GHOSTTY_KEY_W,
GHOSTTY_KEY_X,
GHOSTTY_KEY_Y,
GHOSTTY_KEY_Z,
GHOSTTY_KEY_MINUS,
GHOSTTY_KEY_PERIOD,
GHOSTTY_KEY_QUOTE,
GHOSTTY_KEY_SEMICOLON,
GHOSTTY_KEY_SLASH,
// "Functional Keys" § 3.1.2
GHOSTTY_KEY_ALT_LEFT,
GHOSTTY_KEY_ALT_RIGHT,
GHOSTTY_KEY_BACKSPACE,
GHOSTTY_KEY_CAPS_LOCK,
GHOSTTY_KEY_CONTEXT_MENU,
GHOSTTY_KEY_CONTROL_LEFT,
GHOSTTY_KEY_CONTROL_RIGHT,
GHOSTTY_KEY_ENTER,
GHOSTTY_KEY_META_LEFT,
GHOSTTY_KEY_META_RIGHT,
GHOSTTY_KEY_SHIFT_LEFT,
GHOSTTY_KEY_SHIFT_RIGHT,
GHOSTTY_KEY_SPACE,
GHOSTTY_KEY_TAB,
GHOSTTY_KEY_CONVERT,
GHOSTTY_KEY_KANA_MODE,
GHOSTTY_KEY_NON_CONVERT,
// "Control Pad Section" § 3.2
GHOSTTY_KEY_DELETE,
GHOSTTY_KEY_END,
GHOSTTY_KEY_HELP,
GHOSTTY_KEY_HOME,
GHOSTTY_KEY_INSERT,
GHOSTTY_KEY_PAGE_DOWN,
GHOSTTY_KEY_PAGE_UP,
// "Arrow Pad Section" § 3.3
GHOSTTY_KEY_ARROW_DOWN,
GHOSTTY_KEY_ARROW_LEFT,
GHOSTTY_KEY_ARROW_RIGHT,
GHOSTTY_KEY_ARROW_UP,
// "Numpad Section" § 3.4
GHOSTTY_KEY_NUM_LOCK,
GHOSTTY_KEY_NUMPAD_0,
GHOSTTY_KEY_NUMPAD_1,
GHOSTTY_KEY_NUMPAD_2,
GHOSTTY_KEY_NUMPAD_3,
GHOSTTY_KEY_NUMPAD_4,
GHOSTTY_KEY_NUMPAD_5,
GHOSTTY_KEY_NUMPAD_6,
GHOSTTY_KEY_NUMPAD_7,
GHOSTTY_KEY_NUMPAD_8,
GHOSTTY_KEY_NUMPAD_9,
GHOSTTY_KEY_NUMPAD_ADD,
GHOSTTY_KEY_NUMPAD_BACKSPACE,
GHOSTTY_KEY_NUMPAD_CLEAR,
GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY,
GHOSTTY_KEY_NUMPAD_COMMA,
GHOSTTY_KEY_NUMPAD_DECIMAL,
GHOSTTY_KEY_NUMPAD_DIVIDE,
GHOSTTY_KEY_NUMPAD_ENTER,
GHOSTTY_KEY_NUMPAD_EQUAL,
GHOSTTY_KEY_NUMPAD_MEMORY_ADD,
GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR,
GHOSTTY_KEY_NUMPAD_MEMORY_RECALL,
GHOSTTY_KEY_NUMPAD_MEMORY_STORE,
GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT,
GHOSTTY_KEY_NUMPAD_MULTIPLY,
GHOSTTY_KEY_NUMPAD_PAREN_LEFT,
GHOSTTY_KEY_NUMPAD_PAREN_RIGHT,
GHOSTTY_KEY_NUMPAD_SUBTRACT,
GHOSTTY_KEY_NUMPAD_SEPARATOR,
GHOSTTY_KEY_NUMPAD_UP,
GHOSTTY_KEY_NUMPAD_DOWN,
GHOSTTY_KEY_NUMPAD_RIGHT,
GHOSTTY_KEY_NUMPAD_LEFT,
GHOSTTY_KEY_NUMPAD_BEGIN,
GHOSTTY_KEY_NUMPAD_HOME,
GHOSTTY_KEY_NUMPAD_END,
GHOSTTY_KEY_NUMPAD_INSERT,
GHOSTTY_KEY_NUMPAD_DELETE,
GHOSTTY_KEY_NUMPAD_PAGE_UP,
GHOSTTY_KEY_NUMPAD_PAGE_DOWN,
// "Function Section" § 3.5
GHOSTTY_KEY_ESCAPE,
GHOSTTY_KEY_F1,
GHOSTTY_KEY_F2,
GHOSTTY_KEY_F3,
GHOSTTY_KEY_F4,
GHOSTTY_KEY_F5,
GHOSTTY_KEY_F6,
GHOSTTY_KEY_F7,
GHOSTTY_KEY_F8,
GHOSTTY_KEY_F9,
GHOSTTY_KEY_F10,
GHOSTTY_KEY_F11,
GHOSTTY_KEY_F12,
GHOSTTY_KEY_F13,
GHOSTTY_KEY_F14,
GHOSTTY_KEY_F15,
GHOSTTY_KEY_F16,
GHOSTTY_KEY_F17,
GHOSTTY_KEY_F18,
GHOSTTY_KEY_F19,
GHOSTTY_KEY_F20,
GHOSTTY_KEY_F21,
GHOSTTY_KEY_F22,
GHOSTTY_KEY_F23,
GHOSTTY_KEY_F24,
GHOSTTY_KEY_F25,
GHOSTTY_KEY_FN,
GHOSTTY_KEY_FN_LOCK,
GHOSTTY_KEY_PRINT_SCREEN,
GHOSTTY_KEY_SCROLL_LOCK,
GHOSTTY_KEY_PAUSE,
// "Media Keys" § 3.6
GHOSTTY_KEY_BROWSER_BACK,
GHOSTTY_KEY_BROWSER_FAVORITES,
GHOSTTY_KEY_BROWSER_FORWARD,
GHOSTTY_KEY_BROWSER_HOME,
GHOSTTY_KEY_BROWSER_REFRESH,
GHOSTTY_KEY_BROWSER_SEARCH,
GHOSTTY_KEY_BROWSER_STOP,
GHOSTTY_KEY_EJECT,
GHOSTTY_KEY_LAUNCH_APP_1,
GHOSTTY_KEY_LAUNCH_APP_2,
GHOSTTY_KEY_LAUNCH_MAIL,
GHOSTTY_KEY_MEDIA_PLAY_PAUSE,
GHOSTTY_KEY_MEDIA_SELECT,
GHOSTTY_KEY_MEDIA_STOP,
GHOSTTY_KEY_MEDIA_TRACK_NEXT,
GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS,
GHOSTTY_KEY_POWER,
GHOSTTY_KEY_SLEEP,
GHOSTTY_KEY_AUDIO_VOLUME_DOWN,
GHOSTTY_KEY_AUDIO_VOLUME_MUTE,
GHOSTTY_KEY_AUDIO_VOLUME_UP,
GHOSTTY_KEY_WAKE_UP,
// "Legacy, Non-standard, and Special Keys" § 3.7
GHOSTTY_KEY_COPY,
GHOSTTY_KEY_CUT,
GHOSTTY_KEY_PASTE,
} ghostty_input_key_e;
typedef struct {
ghostty_input_action_e action;
ghostty_input_mods_e mods;
ghostty_input_mods_e consumed_mods;
uint32_t keycode;
const char* text;
uint32_t unshifted_codepoint;
bool composing;
} ghostty_input_key_s;
typedef enum {
GHOSTTY_TRIGGER_PHYSICAL,
GHOSTTY_TRIGGER_UNICODE,
GHOSTTY_TRIGGER_CATCH_ALL,
} ghostty_input_trigger_tag_e;
typedef union {
ghostty_input_key_e translated;
ghostty_input_key_e physical;
uint32_t unicode;
// catch_all has no payload
} ghostty_input_trigger_key_u;
typedef struct {
ghostty_input_trigger_tag_e tag;
ghostty_input_trigger_key_u key;
ghostty_input_mods_e mods;
} ghostty_input_trigger_s;
typedef struct {
const char* action_key;
const char* action;
const char* title;
const char* description;
} ghostty_command_s;
typedef enum {
GHOSTTY_BUILD_MODE_DEBUG,
GHOSTTY_BUILD_MODE_RELEASE_SAFE,
GHOSTTY_BUILD_MODE_RELEASE_FAST,
GHOSTTY_BUILD_MODE_RELEASE_SMALL,
} ghostty_build_mode_e;
typedef struct {
ghostty_build_mode_e build_mode;
const char* version;
uintptr_t version_len;
} ghostty_info_s;
typedef struct {
const char* message;
} ghostty_diagnostic_s;
typedef struct {
const char* ptr;
uintptr_t len;
bool sentinel;
} ghostty_string_s;
typedef struct {
double tl_px_x;
double tl_px_y;
uint32_t offset_start;
uint32_t offset_len;
const char* text;
uintptr_t text_len;
} ghostty_text_s;
typedef enum {
GHOSTTY_POINT_ACTIVE,
GHOSTTY_POINT_VIEWPORT,
GHOSTTY_POINT_SCREEN,
GHOSTTY_POINT_SURFACE,
} ghostty_point_tag_e;
typedef enum {
GHOSTTY_POINT_COORD_EXACT,
GHOSTTY_POINT_COORD_TOP_LEFT,
GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
} ghostty_point_coord_e;
typedef struct {
ghostty_point_tag_e tag;
ghostty_point_coord_e coord;
uint32_t x;
uint32_t y;
} ghostty_point_s;
typedef struct {
ghostty_point_s top_left;
ghostty_point_s bottom_right;
bool rectangle;
} ghostty_selection_s;
typedef struct {
const char* key;
const char* value;
} ghostty_env_var_s;
typedef struct {
void* nsview;
} ghostty_platform_macos_s;
typedef struct {
void* uiview;
} ghostty_platform_ios_s;
typedef union {
ghostty_platform_macos_s macos;
ghostty_platform_ios_s ios;
} ghostty_platform_u;
typedef enum {
GHOSTTY_SURFACE_CONTEXT_WINDOW = 0,
GHOSTTY_SURFACE_CONTEXT_TAB = 1,
GHOSTTY_SURFACE_CONTEXT_SPLIT = 2,
} ghostty_surface_context_e;
typedef struct {
ghostty_platform_e platform_tag;
ghostty_platform_u platform;
void* userdata;
double scale_factor;
float font_size;
const char* working_directory;
const char* command;
ghostty_env_var_s* env_vars;
size_t env_var_count;
const char* initial_input;
bool wait_after_command;
ghostty_surface_context_e context;
} ghostty_surface_config_s;
typedef struct {
uint16_t columns;
uint16_t rows;
uint32_t width_px;
uint32_t height_px;
uint32_t cell_width_px;
uint32_t cell_height_px;
} ghostty_surface_size_s;
// Config types
// config.Path
typedef struct {
const char* path;
bool optional;
} ghostty_config_path_s;
// config.Color
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
} ghostty_config_color_s;
// config.ColorList
typedef struct {
const ghostty_config_color_s* colors;
size_t len;
} ghostty_config_color_list_s;
// config.RepeatableCommand
typedef struct {
const ghostty_command_s* commands;
size_t len;
} ghostty_config_command_list_s;
// config.Palette
typedef struct {
ghostty_config_color_s colors[256];
} ghostty_config_palette_s;
// config.QuickTerminalSize
typedef enum {
GHOSTTY_QUICK_TERMINAL_SIZE_NONE,
GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE,
GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS,
} ghostty_quick_terminal_size_tag_e;
typedef union {
float percentage;
uint32_t pixels;
} ghostty_quick_terminal_size_value_u;
typedef struct {
ghostty_quick_terminal_size_tag_e tag;
ghostty_quick_terminal_size_value_u value;
} ghostty_quick_terminal_size_s;
typedef struct {
ghostty_quick_terminal_size_s primary;
ghostty_quick_terminal_size_s secondary;
} ghostty_config_quick_terminal_size_s;
// config.Fullscreen
typedef enum {
GHOSTTY_CONFIG_FULLSCREEN_FALSE,
GHOSTTY_CONFIG_FULLSCREEN_TRUE,
GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE,
GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_VISIBLE_MENU,
GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_PADDED_NOTCH,
} ghostty_config_fullscreen_e;
// apprt.Target.Key
typedef enum {
GHOSTTY_TARGET_APP,
GHOSTTY_TARGET_SURFACE,
} ghostty_target_tag_e;
typedef union {
ghostty_surface_t surface;
} ghostty_target_u;
typedef struct {
ghostty_target_tag_e tag;
ghostty_target_u target;
} ghostty_target_s;
// apprt.action.SplitDirection
typedef enum {
GHOSTTY_SPLIT_DIRECTION_RIGHT,
GHOSTTY_SPLIT_DIRECTION_DOWN,
GHOSTTY_SPLIT_DIRECTION_LEFT,
GHOSTTY_SPLIT_DIRECTION_UP,
} ghostty_action_split_direction_e;
// apprt.action.GotoSplit
typedef enum {
GHOSTTY_GOTO_SPLIT_PREVIOUS,
GHOSTTY_GOTO_SPLIT_NEXT,
GHOSTTY_GOTO_SPLIT_UP,
GHOSTTY_GOTO_SPLIT_LEFT,
GHOSTTY_GOTO_SPLIT_DOWN,
GHOSTTY_GOTO_SPLIT_RIGHT,
} ghostty_action_goto_split_e;
// apprt.action.GotoWindow
typedef enum {
GHOSTTY_GOTO_WINDOW_PREVIOUS,
GHOSTTY_GOTO_WINDOW_NEXT,
} ghostty_action_goto_window_e;
// apprt.action.ResizeSplit.Direction
typedef enum {
GHOSTTY_RESIZE_SPLIT_UP,
GHOSTTY_RESIZE_SPLIT_DOWN,
GHOSTTY_RESIZE_SPLIT_LEFT,
GHOSTTY_RESIZE_SPLIT_RIGHT,
} ghostty_action_resize_split_direction_e;
// apprt.action.ResizeSplit
typedef struct {
uint16_t amount;
ghostty_action_resize_split_direction_e direction;
} ghostty_action_resize_split_s;
// apprt.action.MoveTab
typedef struct {
ssize_t amount;
} ghostty_action_move_tab_s;
// apprt.action.GotoTab
typedef enum {
GHOSTTY_GOTO_TAB_PREVIOUS = -1,
GHOSTTY_GOTO_TAB_NEXT = -2,
GHOSTTY_GOTO_TAB_LAST = -3,
} ghostty_action_goto_tab_e;
// apprt.action.Fullscreen
typedef enum {
GHOSTTY_FULLSCREEN_NATIVE,
GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE,
GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_VISIBLE_MENU,
GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_PADDED_NOTCH,
} ghostty_action_fullscreen_e;
// apprt.action.FloatWindow
typedef enum {
GHOSTTY_FLOAT_WINDOW_ON,
GHOSTTY_FLOAT_WINDOW_OFF,
GHOSTTY_FLOAT_WINDOW_TOGGLE,
} ghostty_action_float_window_e;
// apprt.action.SecureInput
typedef enum {
GHOSTTY_SECURE_INPUT_ON,
GHOSTTY_SECURE_INPUT_OFF,
GHOSTTY_SECURE_INPUT_TOGGLE,
} ghostty_action_secure_input_e;
// apprt.action.Inspector
typedef enum {
GHOSTTY_INSPECTOR_TOGGLE,
GHOSTTY_INSPECTOR_SHOW,
GHOSTTY_INSPECTOR_HIDE,
} ghostty_action_inspector_e;
// apprt.action.QuitTimer
typedef enum {
GHOSTTY_QUIT_TIMER_START,
GHOSTTY_QUIT_TIMER_STOP,
} ghostty_action_quit_timer_e;
// apprt.action.Readonly
typedef enum {
GHOSTTY_READONLY_OFF,
GHOSTTY_READONLY_ON,
} ghostty_action_readonly_e;
// apprt.action.DesktopNotification.C
typedef struct {
const char* title;
const char* body;
} ghostty_action_desktop_notification_s;
// apprt.action.SetTitle.C
typedef struct {
const char* title;
} ghostty_action_set_title_s;
// apprt.action.PromptTitle
typedef enum {
GHOSTTY_PROMPT_TITLE_SURFACE,
GHOSTTY_PROMPT_TITLE_TAB,
} ghostty_action_prompt_title_e;
// apprt.action.Pwd.C
typedef struct {
const char* pwd;
} ghostty_action_pwd_s;
// terminal.MouseShape
typedef enum {
GHOSTTY_MOUSE_SHAPE_DEFAULT,
GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU,
GHOSTTY_MOUSE_SHAPE_HELP,
GHOSTTY_MOUSE_SHAPE_POINTER,
GHOSTTY_MOUSE_SHAPE_PROGRESS,
GHOSTTY_MOUSE_SHAPE_WAIT,
GHOSTTY_MOUSE_SHAPE_CELL,
GHOSTTY_MOUSE_SHAPE_CROSSHAIR,
GHOSTTY_MOUSE_SHAPE_TEXT,
GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT,
GHOSTTY_MOUSE_SHAPE_ALIAS,
GHOSTTY_MOUSE_SHAPE_COPY,
GHOSTTY_MOUSE_SHAPE_MOVE,
GHOSTTY_MOUSE_SHAPE_NO_DROP,
GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED,
GHOSTTY_MOUSE_SHAPE_GRAB,
GHOSTTY_MOUSE_SHAPE_GRABBING,
GHOSTTY_MOUSE_SHAPE_ALL_SCROLL,
GHOSTTY_MOUSE_SHAPE_COL_RESIZE,
GHOSTTY_MOUSE_SHAPE_ROW_RESIZE,
GHOSTTY_MOUSE_SHAPE_N_RESIZE,
GHOSTTY_MOUSE_SHAPE_E_RESIZE,
GHOSTTY_MOUSE_SHAPE_S_RESIZE,
GHOSTTY_MOUSE_SHAPE_W_RESIZE,
GHOSTTY_MOUSE_SHAPE_NE_RESIZE,
GHOSTTY_MOUSE_SHAPE_NW_RESIZE,
GHOSTTY_MOUSE_SHAPE_SE_RESIZE,
GHOSTTY_MOUSE_SHAPE_SW_RESIZE,
GHOSTTY_MOUSE_SHAPE_EW_RESIZE,
GHOSTTY_MOUSE_SHAPE_NS_RESIZE,
GHOSTTY_MOUSE_SHAPE_NESW_RESIZE,
GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE,
GHOSTTY_MOUSE_SHAPE_ZOOM_IN,
GHOSTTY_MOUSE_SHAPE_ZOOM_OUT,
} ghostty_action_mouse_shape_e;
// apprt.action.MouseVisibility
typedef enum {
GHOSTTY_MOUSE_VISIBLE,
GHOSTTY_MOUSE_HIDDEN,
} ghostty_action_mouse_visibility_e;
// apprt.action.MouseOverLink
typedef struct {
const char* url;
size_t len;
} ghostty_action_mouse_over_link_s;
// apprt.action.SizeLimit
typedef struct {
uint32_t min_width;
uint32_t min_height;
uint32_t max_width;
uint32_t max_height;
} ghostty_action_size_limit_s;
// apprt.action.InitialSize
typedef struct {
uint32_t width;
uint32_t height;
} ghostty_action_initial_size_s;
// apprt.action.CellSize
typedef struct {
uint32_t width;
uint32_t height;
} ghostty_action_cell_size_s;
// renderer.Health
typedef enum {
GHOSTTY_RENDERER_HEALTH_HEALTHY,
GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
} ghostty_action_renderer_health_e;
// apprt.action.KeySequence
typedef struct {
bool active;
ghostty_input_trigger_s trigger;
} ghostty_action_key_sequence_s;
// apprt.action.KeyTable.Tag
typedef enum {
GHOSTTY_KEY_TABLE_ACTIVATE,
GHOSTTY_KEY_TABLE_DEACTIVATE,
GHOSTTY_KEY_TABLE_DEACTIVATE_ALL,
} ghostty_action_key_table_tag_e;
// apprt.action.KeyTable.CValue
typedef union {
struct {
const char *name;
size_t len;
} activate;
} ghostty_action_key_table_u;
// apprt.action.KeyTable.C
typedef struct {
ghostty_action_key_table_tag_e tag;
ghostty_action_key_table_u value;
} ghostty_action_key_table_s;
// apprt.action.ColorKind
typedef enum {
GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1,
GHOSTTY_ACTION_COLOR_KIND_BACKGROUND = -2,
GHOSTTY_ACTION_COLOR_KIND_CURSOR = -3,
} ghostty_action_color_kind_e;
// apprt.action.ColorChange
typedef struct {
ghostty_action_color_kind_e kind;
uint8_t r;
uint8_t g;
uint8_t b;
} ghostty_action_color_change_s;
// apprt.action.ConfigChange
typedef struct {
ghostty_config_t config;
} ghostty_action_config_change_s;
// apprt.action.ReloadConfig
typedef struct {
bool soft;
} ghostty_action_reload_config_s;
// apprt.action.OpenUrlKind
typedef enum {
GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN,
GHOSTTY_ACTION_OPEN_URL_KIND_TEXT,
GHOSTTY_ACTION_OPEN_URL_KIND_HTML,
} ghostty_action_open_url_kind_e;
// apprt.action.OpenUrl.C
typedef struct {
ghostty_action_open_url_kind_e kind;
const char* url;
uintptr_t len;
} ghostty_action_open_url_s;
// apprt.action.CloseTabMode
typedef enum {
GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT,
} ghostty_action_close_tab_mode_e;
// apprt.surface.Message.ChildExited
typedef struct {
uint32_t exit_code;
uint64_t timetime_ms;
} ghostty_surface_message_childexited_s;
// terminal.osc.Command.ProgressReport.State
typedef enum {
GHOSTTY_PROGRESS_STATE_REMOVE,
GHOSTTY_PROGRESS_STATE_SET,
GHOSTTY_PROGRESS_STATE_ERROR,
GHOSTTY_PROGRESS_STATE_INDETERMINATE,
GHOSTTY_PROGRESS_STATE_PAUSE,
} ghostty_action_progress_report_state_e;
// terminal.osc.Command.ProgressReport.C
typedef struct {
ghostty_action_progress_report_state_e state;
// -1 if no progress was reported, otherwise 0-100 indicating percent
// completeness.
int8_t progress;
} ghostty_action_progress_report_s;
// apprt.action.CommandFinished.C
typedef struct {
// -1 if no exit code was reported, otherwise 0-255
int16_t exit_code;
// number of nanoseconds that command was running for
uint64_t duration;
} ghostty_action_command_finished_s;
// apprt.action.StartSearch.C
typedef struct {
const char* needle;
} ghostty_action_start_search_s;
// apprt.action.SearchTotal
typedef struct {
ssize_t total;
} ghostty_action_search_total_s;
// apprt.action.SearchSelected
typedef struct {
ssize_t selected;
} ghostty_action_search_selected_s;
// terminal.Scrollbar
typedef struct {
uint64_t total;
uint64_t offset;
uint64_t len;
} ghostty_action_scrollbar_s;
// apprt.Action.Key
typedef enum {
GHOSTTY_ACTION_QUIT,
GHOSTTY_ACTION_NEW_WINDOW,
GHOSTTY_ACTION_NEW_TAB,
GHOSTTY_ACTION_CLOSE_TAB,
GHOSTTY_ACTION_NEW_SPLIT,
GHOSTTY_ACTION_CLOSE_ALL_WINDOWS,
GHOSTTY_ACTION_TOGGLE_MAXIMIZE,
GHOSTTY_ACTION_TOGGLE_FULLSCREEN,
GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW,
GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE,
GHOSTTY_ACTION_TOGGLE_VISIBILITY,
GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY,
GHOSTTY_ACTION_MOVE_TAB,
GHOSTTY_ACTION_GOTO_TAB,
GHOSTTY_ACTION_GOTO_SPLIT,
GHOSTTY_ACTION_GOTO_WINDOW,
GHOSTTY_ACTION_RESIZE_SPLIT,
GHOSTTY_ACTION_EQUALIZE_SPLITS,
GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM,
GHOSTTY_ACTION_PRESENT_TERMINAL,
GHOSTTY_ACTION_SIZE_LIMIT,
GHOSTTY_ACTION_RESET_WINDOW_SIZE,
GHOSTTY_ACTION_INITIAL_SIZE,
GHOSTTY_ACTION_CELL_SIZE,
GHOSTTY_ACTION_SCROLLBAR,
GHOSTTY_ACTION_RENDER,
GHOSTTY_ACTION_INSPECTOR,
GHOSTTY_ACTION_SHOW_GTK_INSPECTOR,
GHOSTTY_ACTION_RENDER_INSPECTOR,
GHOSTTY_ACTION_DESKTOP_NOTIFICATION,
GHOSTTY_ACTION_SET_TITLE,
GHOSTTY_ACTION_SET_TAB_TITLE,
GHOSTTY_ACTION_PROMPT_TITLE,
GHOSTTY_ACTION_PWD,
GHOSTTY_ACTION_MOUSE_SHAPE,
GHOSTTY_ACTION_MOUSE_VISIBILITY,
GHOSTTY_ACTION_MOUSE_OVER_LINK,
GHOSTTY_ACTION_RENDERER_HEALTH,
GHOSTTY_ACTION_OPEN_CONFIG,
GHOSTTY_ACTION_QUIT_TIMER,
GHOSTTY_ACTION_FLOAT_WINDOW,
GHOSTTY_ACTION_SECURE_INPUT,
GHOSTTY_ACTION_KEY_SEQUENCE,
GHOSTTY_ACTION_KEY_TABLE,
GHOSTTY_ACTION_COLOR_CHANGE,
GHOSTTY_ACTION_RELOAD_CONFIG,
GHOSTTY_ACTION_CONFIG_CHANGE,
GHOSTTY_ACTION_CLOSE_WINDOW,
GHOSTTY_ACTION_RING_BELL,
GHOSTTY_ACTION_UNDO,
GHOSTTY_ACTION_REDO,
GHOSTTY_ACTION_CHECK_FOR_UPDATES,
GHOSTTY_ACTION_OPEN_URL,
GHOSTTY_ACTION_SHOW_CHILD_EXITED,
GHOSTTY_ACTION_PROGRESS_REPORT,
GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
GHOSTTY_ACTION_COMMAND_FINISHED,
GHOSTTY_ACTION_START_SEARCH,
GHOSTTY_ACTION_END_SEARCH,
GHOSTTY_ACTION_SEARCH_TOTAL,
GHOSTTY_ACTION_SEARCH_SELECTED,
GHOSTTY_ACTION_READONLY,
GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD,
} ghostty_action_tag_e;
typedef union {
ghostty_action_split_direction_e new_split;
ghostty_action_fullscreen_e toggle_fullscreen;
ghostty_action_move_tab_s move_tab;
ghostty_action_goto_tab_e goto_tab;
ghostty_action_goto_split_e goto_split;
ghostty_action_goto_window_e goto_window;
ghostty_action_resize_split_s resize_split;
ghostty_action_size_limit_s size_limit;
ghostty_action_initial_size_s initial_size;
ghostty_action_cell_size_s cell_size;
ghostty_action_scrollbar_s scrollbar;
ghostty_action_inspector_e inspector;
ghostty_action_desktop_notification_s desktop_notification;
ghostty_action_set_title_s set_title;
ghostty_action_set_title_s set_tab_title;
ghostty_action_prompt_title_e prompt_title;
ghostty_action_pwd_s pwd;
ghostty_action_mouse_shape_e mouse_shape;
ghostty_action_mouse_visibility_e mouse_visibility;
ghostty_action_mouse_over_link_s mouse_over_link;
ghostty_action_renderer_health_e renderer_health;
ghostty_action_quit_timer_e quit_timer;
ghostty_action_float_window_e float_window;
ghostty_action_secure_input_e secure_input;
ghostty_action_key_sequence_s key_sequence;
ghostty_action_key_table_s key_table;
ghostty_action_color_change_s color_change;
ghostty_action_reload_config_s reload_config;
ghostty_action_config_change_s config_change;
ghostty_action_open_url_s open_url;
ghostty_action_close_tab_mode_e close_tab_mode;
ghostty_surface_message_childexited_s child_exited;
ghostty_action_progress_report_s progress_report;
ghostty_action_command_finished_s command_finished;
ghostty_action_start_search_s start_search;
ghostty_action_search_total_s search_total;
ghostty_action_search_selected_s search_selected;
ghostty_action_readonly_e readonly;
} ghostty_action_u;
typedef struct {
ghostty_action_tag_e tag;
ghostty_action_u action;
} ghostty_action_s;
typedef void (*ghostty_runtime_wakeup_cb)(void*);
typedef bool (*ghostty_runtime_read_clipboard_cb)(void*,
ghostty_clipboard_e,
void*);
typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
void*,
const char*,
void*,
ghostty_clipboard_request_e);
typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
ghostty_clipboard_e,
const ghostty_clipboard_content_s*,
size_t,
bool);
typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,
ghostty_target_s,
ghostty_action_s);
typedef struct {
void* userdata;
bool supports_selection_clipboard;
ghostty_runtime_wakeup_cb wakeup_cb;
ghostty_runtime_action_cb action_cb;
ghostty_runtime_read_clipboard_cb read_clipboard_cb;
ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb;
ghostty_runtime_write_clipboard_cb write_clipboard_cb;
ghostty_runtime_close_surface_cb close_surface_cb;
} ghostty_runtime_config_s;
// apprt.ipc.Target.Key
typedef enum {
GHOSTTY_IPC_TARGET_CLASS,
GHOSTTY_IPC_TARGET_DETECT,
} ghostty_ipc_target_tag_e;
typedef union {
char *klass;
} ghostty_ipc_target_u;
typedef struct {
ghostty_ipc_target_tag_e tag;
ghostty_ipc_target_u target;
} chostty_ipc_target_s;
// apprt.ipc.Action.NewWindow
typedef struct {
// This should be a null terminated list of strings.
const char **arguments;
} ghostty_ipc_action_new_window_s;
typedef union {
ghostty_ipc_action_new_window_s new_window;
} ghostty_ipc_action_u;
// apprt.ipc.Action.Key
typedef enum {
GHOSTTY_IPC_ACTION_NEW_WINDOW,
} ghostty_ipc_action_tag_e;
//-------------------------------------------------------------------
// Published API
int ghostty_init(uintptr_t, char**);
void ghostty_cli_try_action(void);
ghostty_info_s ghostty_info(void);
const char* ghostty_translate(const char*);
void ghostty_string_free(ghostty_string_s);
ghostty_config_t ghostty_config_new();
void ghostty_config_free(ghostty_config_t);
ghostty_config_t ghostty_config_clone(ghostty_config_t);
void ghostty_config_load_cli_args(ghostty_config_t);
void ghostty_config_load_file(ghostty_config_t, const char*);
void ghostty_config_load_default_files(ghostty_config_t);
void ghostty_config_load_recursive_files(ghostty_config_t);
void ghostty_config_finalize(ghostty_config_t);
bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t);
ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
const char*,
uintptr_t);
uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
ghostty_string_s ghostty_config_open_path(void);
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
ghostty_config_t);
void ghostty_app_free(ghostty_app_t);
void ghostty_app_tick(ghostty_app_t);
void* ghostty_app_userdata(ghostty_app_t);
void ghostty_app_set_focus(ghostty_app_t, bool);
bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s);
void ghostty_app_keyboard_changed(ghostty_app_t);
void ghostty_app_open_config(ghostty_app_t);
void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
bool ghostty_app_needs_confirm_quit(ghostty_app_t);
bool ghostty_app_has_global_keybinds(ghostty_app_t);
void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e);
ghostty_surface_config_s ghostty_surface_config_new();
ghostty_surface_t ghostty_surface_new(ghostty_app_t,
const ghostty_surface_config_s*);
void ghostty_surface_free(ghostty_surface_t);
void* ghostty_surface_userdata(ghostty_surface_t);
ghostty_app_t ghostty_surface_app(ghostty_surface_t);
ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e);
void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
bool ghostty_surface_process_exited(ghostty_surface_t);
void ghostty_surface_refresh(ghostty_surface_t);
void ghostty_surface_draw(ghostty_surface_t);
void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
void ghostty_surface_set_focus(ghostty_surface_t, bool);
void ghostty_surface_set_occlusion(ghostty_surface_t, bool);
void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t);
void ghostty_surface_set_color_scheme(ghostty_surface_t,
ghostty_color_scheme_e);
ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,
ghostty_input_mods_e);
bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
bool ghostty_surface_key_is_binding(ghostty_surface_t,
ghostty_input_key_s,
ghostty_binding_flags_e*);
void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t);
bool ghostty_surface_mouse_captured(ghostty_surface_t);
bool ghostty_surface_mouse_button(ghostty_surface_t,
ghostty_input_mouse_state_e,
ghostty_input_mouse_button_e,
ghostty_input_mods_e);
void ghostty_surface_mouse_pos(ghostty_surface_t,
double,
double,
ghostty_input_mods_e);
void ghostty_surface_mouse_scroll(ghostty_surface_t,
double,
double,
ghostty_input_scroll_mods_t);
void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*);
void ghostty_surface_request_close(ghostty_surface_t);
void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
void ghostty_surface_split_focus(ghostty_surface_t,
ghostty_action_goto_split_e);
void ghostty_surface_split_resize(ghostty_surface_t,
ghostty_action_resize_split_direction_e,
uint16_t);
void ghostty_surface_split_equalize(ghostty_surface_t);
bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t);
void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
const char*,
void*,
bool);
bool ghostty_surface_has_selection(ghostty_surface_t);
bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*);
bool ghostty_surface_read_text(ghostty_surface_t,
ghostty_selection_s,
ghostty_text_s*);
void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*);
#ifdef __APPLE__
void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t);
void* ghostty_surface_quicklook_font(ghostty_surface_t);
bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*);
#endif
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
void ghostty_inspector_free(ghostty_surface_t);
void ghostty_inspector_set_focus(ghostty_inspector_t, bool);
void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double);
void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t);
void ghostty_inspector_mouse_button(ghostty_inspector_t,
ghostty_input_mouse_state_e,
ghostty_input_mouse_button_e,
ghostty_input_mods_e);
void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double);
void ghostty_inspector_mouse_scroll(ghostty_inspector_t,
double,
double,
ghostty_input_scroll_mods_t);
void ghostty_inspector_key(ghostty_inspector_t,
ghostty_input_action_e,
ghostty_input_key_e,
ghostty_input_mods_e);
void ghostty_inspector_text(ghostty_inspector_t, const char*);
#ifdef __APPLE__
bool ghostty_inspector_metal_init(ghostty_inspector_t, void*);
void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*);
bool ghostty_inspector_metal_shutdown(ghostty_inspector_t);
#endif
// APIs I'd like to get rid of eventually but are still needed for now.
// Don't use these unless you know what you're doing.
void ghostty_set_window_background_blur(ghostty_app_t, void*);
// Benchmark API, if available.
bool ghostty_benchmark_cli(const char*, const char*);
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_H */
@@ -0,0 +1,7 @@
// This makes Ghostty available to the XCode build for the macOS app.
// We append "Kit" to it not to be cute, but because targets have to have
// unique names and we use Ghostty for other things.
module GhosttyKit {
umbrella header "ghostty.h"
export *
}
+348
View File
@@ -0,0 +1,348 @@
//! `apprt.Target.cval` and `apprt.Action.cval`: the conversion of core
//! actions to the `ghostty_target_s` / `ghostty_action_s` C structs
//! (ZIG/src/apprt/action.zig).
#![allow(unsafe_code)]
use std::ffi::{CString, c_char, c_int};
use ghostty_apprt::{
action::{Action, KeySequence, KeyTable, Target},
runtime_api::SurfaceId,
};
use ghostty_renderer::Health;
use ghostty_vt::osc::ProgressState;
use crate::{
config_capi::{CConfig, default_trigger, trigger_to_c},
types::{
ghostty_action_command_finished_s,
ghostty_action_s,
ghostty_action_u,
ghostty_config_t,
ghostty_target_s,
ghostty_target_u,
target_tag,
},
};
/// Memory that the pointers inside a converted action point to. It must
/// outlive the callback invocation.
#[derive(Default)]
pub struct Keep {
strings: Vec<CString>,
config: Option<ghostty_config_t>,
}
impl Keep {
/// A NUL terminated copy of `s` (interior NULs are dropped).
fn cstr(&mut self, s: &str) -> *const c_char {
let c = CString::new(s.replace('\0', "")).unwrap_or_default();
let ptr = c.as_ptr();
// The heap buffer of a CString doesn't move when the Vec grows.
self.strings.push(c);
ptr
}
}
impl Drop for Keep {
fn drop(&mut self) {
if let Some(h) = self.config.take() {
// SAFETY: created by `CConfig::into_handle` in `action_to_c`
// and not handed out beyond the callback.
unsafe { crate::config_capi::ghostty_config_free(h) };
}
}
}
/// `Target.cval`. The surface handle is the id of the surface (the address
/// of the embedded runtime's surface object, see `embedded.rs`).
#[must_use]
pub fn target_to_c(target: &Target<SurfaceId>) -> ghostty_target_s {
match target {
Target::App => ghostty_target_s {
tag: target_tag::APP,
target: ghostty_target_u {
surface: std::ptr::null_mut(),
},
},
Target::Surface(id) => ghostty_target_s {
tag: target_tag::SURFACE,
target: ghostty_target_u {
surface: *id as usize as *mut std::ffi::c_void,
},
},
}
}
fn zeroed_union() -> ghostty_action_u {
// SAFETY: every field of the union is plain old data for which the
// all-zero bit pattern is valid (integers, floats, null pointers, bool).
unsafe { std::mem::zeroed() }
}
/// `Action.cval`.
#[must_use]
pub fn action_to_c(action: &Action, keep: &mut Keep) -> ghostty_action_s {
let mut v = zeroed_union();
match action {
Action::CloseTab(m) => v.close_tab_mode = *m as c_int,
Action::NewSplit(d) => v.new_split = *d as c_int,
Action::ToggleFullscreen(f) => v.toggle_fullscreen = *f as c_int,
Action::MoveTab(m) => v.move_tab.amount = m.amount,
Action::GotoTab(t) => v.goto_tab = t.int(),
Action::GotoSplit(s) => v.goto_split = *s as c_int,
Action::GotoWindow(w) => v.goto_window = *w as c_int,
Action::ResizeSplit(r) => {
v.resize_split.amount = r.amount;
v.resize_split.direction = r.direction as c_int;
}
Action::SizeLimit(s) => {
v.size_limit.min_width = s.min_width;
v.size_limit.min_height = s.min_height;
v.size_limit.max_width = s.max_width;
v.size_limit.max_height = s.max_height;
}
Action::InitialSize(s) => {
v.initial_size.width = s.width;
v.initial_size.height = s.height;
}
Action::CellSize(s) => {
v.cell_size.width = s.width;
v.cell_size.height = s.height;
}
Action::Scrollbar(s) => {
v.scrollbar.total = s.total as u64;
v.scrollbar.offset = s.offset as u64;
v.scrollbar.len = s.len as u64;
}
Action::Inspector(i) => v.inspector = *i as c_int,
Action::DesktopNotification(n) => {
v.desktop_notification.title = keep.cstr(&n.title);
v.desktop_notification.body = keep.cstr(&n.body);
}
Action::SetTitle(t) => v.set_title.title = keep.cstr(&t.title),
Action::SetTabTitle(t) => v.set_tab_title.title = keep.cstr(&t.title),
Action::PromptTitle(p) => v.prompt_title = *p as c_int,
Action::Pwd(p) => v.pwd.pwd = keep.cstr(&p.pwd),
Action::MouseShape(s) => v.mouse_shape = *s as c_int,
Action::MouseVisibility(m) => v.mouse_visibility = *m as c_int,
Action::MouseOverLink(l) => {
v.mouse_over_link.url = keep.cstr(&l.url);
v.mouse_over_link.len = l.url.len();
}
Action::RendererHealth(h) => {
v.renderer_health = match h {
Health::Healthy => 0,
Health::Unhealthy => 1,
};
}
Action::QuitTimer(q) => v.quit_timer = *q as c_int,
Action::FloatWindow(f) => v.float_window = *f as c_int,
Action::SecureInput(s) => v.secure_input = *s as c_int,
Action::KeySequence(KeySequence::Trigger(t)) => {
v.key_sequence.active = true;
v.key_sequence.trigger = trigger_to_c(&t.cval());
}
Action::KeySequence(KeySequence::End) => {
v.key_sequence.active = false;
v.key_sequence.trigger = default_trigger();
}
Action::KeyTable(KeyTable::Activate(name)) => {
v.key_table.tag = 0;
v.key_table.value.activate.name = keep.cstr(name);
v.key_table.value.activate.len = name.len();
}
Action::KeyTable(KeyTable::Deactivate) => v.key_table.tag = 1,
Action::KeyTable(KeyTable::DeactivateAll) => v.key_table.tag = 2,
Action::ColorChange(c) => {
v.color_change.kind = c.kind.int();
v.color_change.r = c.r;
v.color_change.g = c.g;
v.color_change.b = c.b;
}
Action::ReloadConfig(r) => v.reload_config.soft = r.soft,
Action::ConfigChange(c) => {
// The embedder reads (and clones) the config during the
// callback; it is freed again when `keep` drops.
if let Some(old) = keep.config.take() {
// SAFETY: created below by an earlier conversion.
unsafe { crate::config_capi::ghostty_config_free(old) };
}
let handle = CConfig::new((*c.config).clone()).into_handle();
keep.config = Some(handle);
v.config_change.config = handle;
}
Action::OpenUrl(u) => {
v.open_url.kind = u.kind as c_int;
v.open_url.url = keep.cstr(&u.url);
v.open_url.len = u.url.len();
}
Action::ShowChildExited(c) => {
v.child_exited.exit_code = c.exit_code;
v.child_exited.timetime_ms = c.runtime_ms;
}
Action::ProgressReport(p) => {
v.progress_report.state = match p.state {
ProgressState::Remove => 0,
ProgressState::Set => 1,
ProgressState::Error => 2,
ProgressState::Indeterminate => 3,
ProgressState::Pause => 4,
};
v.progress_report.progress = p.progress.map_or(-1, |p| p as i8);
}
Action::CommandFinished(c) => {
v.command_finished = ghostty_action_command_finished_s {
exit_code: c.exit_code.map_or(-1, i16::from),
duration: c.duration.duration,
};
}
Action::StartSearch(s) => v.start_search.needle = keep.cstr(&s.needle),
Action::SearchTotal(s) => {
v.search_total.total = s.total.map_or(-1, |t| t as isize);
}
Action::SearchSelected(s) => {
v.search_selected.selected = s.selected.map_or(-1, |t| t as isize);
}
Action::Readonly(r) => v.readonly = *r as c_int,
_ => {}
}
ghostty_action_s {
tag: action.key() as c_int,
action: v,
}
}
#[cfg(test)]
mod tests {
use ghostty_apprt::action::{
CellSize,
CommandFinished,
GotoTab,
MouseOverLink,
ResizeSplit,
ResizeSplitDirection,
SearchTotal,
SetTitle,
};
use super::*;
#[test]
fn target_cval() {
let t = target_to_c(&Target::App);
assert_eq!(t.tag, 0);
let t = target_to_c(&Target::Surface(0x1000));
assert_eq!(t.tag, 1);
// SAFETY: tag says surface.
assert_eq!(unsafe { t.target.surface } as usize, 0x1000);
}
#[test]
fn simple_payloads() {
let mut keep = Keep::default();
let a = action_to_c(
&Action::CellSize(CellSize {
width: 7,
height: 9,
}),
&mut keep,
);
assert_eq!(a.tag, 25);
// SAFETY: tag says cell_size.
assert_eq!(
unsafe { (a.action.cell_size.width, a.action.cell_size.height) },
(7, 9)
);
let a = action_to_c(&Action::GotoTab(GotoTab::Last), &mut keep);
// SAFETY: tag says goto_tab.
assert_eq!(unsafe { a.action.goto_tab }, -3);
let a = action_to_c(
&Action::ResizeSplit(ResizeSplit {
amount: 5,
direction: ResizeSplitDirection::Left,
}),
&mut keep,
);
// SAFETY: tag says resize_split.
assert_eq!(
unsafe {
(
a.action.resize_split.amount,
a.action.resize_split.direction,
)
},
(5, 2)
);
let a = action_to_c(&Action::SearchTotal(SearchTotal { total: None }), &mut keep);
// SAFETY: tag says search_total.
assert_eq!(unsafe { a.action.search_total.total }, -1);
let a = action_to_c(
&Action::CommandFinished(CommandFinished {
exit_code: None,
duration: ghostty_config::types_b::Duration { duration: 5 },
}),
&mut keep,
);
// SAFETY: tag says command_finished.
assert_eq!(
unsafe {
(
a.action.command_finished.exit_code,
a.action.command_finished.duration,
)
},
(-1, 5)
);
}
#[test]
fn strings_stay_valid_while_kept() {
let mut keep = Keep::default();
let a = action_to_c(
&Action::SetTitle(SetTitle {
title: "hello".into(),
}),
&mut keep,
);
// SAFETY: tag says set_title; the pointer points into `keep`.
let s = unsafe { std::ffi::CStr::from_ptr(a.action.set_title.title) };
assert_eq!(s.to_str().unwrap(), "hello");
let a = action_to_c(
&Action::MouseOverLink(MouseOverLink {
url: "https://x".into(),
}),
&mut keep,
);
// SAFETY: tag says mouse_over_link.
assert_eq!(unsafe { a.action.mouse_over_link.len }, 9);
}
#[test]
fn config_change_hands_out_a_config() {
let mut keep = Keep::default();
let cfg = std::sync::Arc::new(ghostty_config::config::Config::default());
let a = action_to_c(
&Action::ConfigChange(ghostty_apprt::action::ConfigChange { config: cfg }),
&mut keep,
);
// SAFETY: tag says config_change.
let h = unsafe { a.action.config_change.config };
let mut out = false;
let key = "maximize";
// SAFETY: valid handle and destination.
assert!(unsafe {
crate::config_capi::ghostty_config_get(
h,
(&raw mut out).cast(),
key.as_ptr().cast(),
key.len(),
)
});
}
}
+98
View File
@@ -0,0 +1,98 @@
//! `ghostty_benchmark_cli` (ZIG/src/benchmark/CApi.zig).
#![allow(unsafe_code)]
use std::ffi::{CStr, c_char};
use ghostty_bench::{args::Args, benches};
/// Split the argument string like `std.process.ArgIteratorGeneral(.{})`:
/// whitespace separated, with double quotes grouping and backslash
/// escapes inside quotes.
fn split_args(s: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut have = false;
let mut in_quote = false;
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'"' => {
in_quote = !in_quote;
have = true;
}
'\\' if in_quote => {
if let Some(n) = chars.next() {
cur.push(n);
}
}
c if c.is_whitespace() && !in_quote => {
if have {
out.push(std::mem::take(&mut cur));
have = false;
}
}
c => {
cur.push(c);
have = true;
}
}
}
if have {
out.push(cur);
}
out
}
/// Run the Ghostty benchmark CLI with the given action and arguments.
///
/// # Safety
/// `action_name` and `args` must be NUL terminated strings.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_benchmark_cli(
action_name: *const c_char,
args: *const c_char,
) -> bool {
if action_name.is_null() || args.is_null() {
return false;
}
// SAFETY: per the function contract.
let (action, args) = unsafe { (CStr::from_ptr(action_name), CStr::from_ptr(args)) };
let action = action.to_string_lossy();
if !benches::ACTIONS.contains(&action.as_ref()) {
log::warn!("unknown action={action}");
return false;
}
let argv = split_args(&args.to_string_lossy());
let parsed = match Args::parse(argv) {
Ok(a) => a,
Err(err) => {
log::warn!("failed to run action={action} err={err}");
return false;
}
};
match benches::main_action(&action, parsed) {
Ok(_) => true,
Err(err) => {
log::warn!("failed to run action={action} err={err}");
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_args_basic() {
assert_eq!(split_args("a b c"), ["a", "b", "c"]);
assert_eq!(split_args("--x=\"a b\" c"), ["--x=a b", "c"]);
assert_eq!(split_args(" ").len(), 0);
}
#[test]
fn unknown_action_returns_false() {
// SAFETY: valid NUL terminated strings.
assert!(!unsafe { ghostty_benchmark_cli(c"nope".as_ptr(), c"".as_ptr()) });
}
}
+727
View File
@@ -0,0 +1,727 @@
//! `ghostty_config_*` (ZIG/src/config/CApi.zig and c_get.zig).
#![allow(unsafe_code)]
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
ffi::{CStr, CString, c_char, c_int, c_short, c_uint, c_void},
rc::Rc,
sync::{Mutex, OnceLock},
};
use ghostty_config::{
c_value::{CValue, QuickTerminalSizeC},
config::Config,
};
use ghostty_input::binding::{Action as BindingAction, TriggerC, TriggerCTag};
use crate::{
main_c::{EMPTY_STRING, string_from_owned},
types::{
ghostty_command_s,
ghostty_config_color_list_s,
ghostty_config_color_s,
ghostty_config_command_list_s,
ghostty_config_palette_s,
ghostty_config_path_s,
ghostty_config_quick_terminal_size_s,
ghostty_config_t,
ghostty_diagnostic_s,
ghostty_input_trigger_key_u,
ghostty_input_trigger_s,
ghostty_quick_terminal_size_s,
ghostty_quick_terminal_size_value_u,
ghostty_string_s,
quick_terminal_size_tag,
trigger_tag,
},
};
/// The C-owned configuration: the config plus the memory that pointers
/// handed out by `ghostty_config_get` / `ghostty_config_get_diagnostic` point
/// into (Zig: the config's arena). Everything stays valid until the config
/// is freed.
pub struct CConfig {
pub config: Config,
cache: RefCell<GetCache>,
diagnostics: RefCell<Vec<CString>>,
}
/// Backing memory of one `ghostty_config_get` result.
#[derive(Default)]
struct Backing {
strings: Vec<CString>,
colors: Box<[ghostty_config_color_s]>,
commands: Box<[ghostty_command_s]>,
}
#[derive(Default)]
struct GetCache {
/// The latest result per key; reused if the value didn't change so
/// repeated gets return the same pointers.
by_key: HashMap<String, (CValue, Rc<Backing>)>,
/// Results that were replaced; kept alive until the config is freed
/// since callers may still hold their pointers.
retired: Vec<Rc<Backing>>,
}
impl CConfig {
pub fn new(config: Config) -> CConfig {
CConfig {
config,
cache: RefCell::default(),
diagnostics: RefCell::default(),
}
}
/// Box the config and return the `ghostty_config_t` handle.
pub fn into_handle(self) -> ghostty_config_t {
Box::into_raw(Box::new(self)).cast::<c_void>()
}
/// The backing memory for `value`, reusing the cached one if equal.
fn backing(&self, key: &str, value: &CValue) -> Rc<Backing> {
let mut cache = self.cache.borrow_mut();
if let Some((old, backing)) = cache.by_key.get(key)
&& old == value
{
return Rc::clone(backing);
}
let backing = Rc::new(build_backing(value));
if let Some((_, old)) = cache
.by_key
.insert(key.to_owned(), (value.clone(), Rc::clone(&backing)))
{
cache.retired.push(old);
}
backing
}
}
/// A C string without interior NULs.
fn cstring(s: &str) -> CString {
CString::new(s.replace('\0', "")).unwrap_or_default()
}
/// Intern a string for the lifetime of the process (enum tag names).
fn intern(s: &str) -> *const c_char {
static POOL: OnceLock<Mutex<HashSet<Box<CStr>>>> = OnceLock::new();
let Ok(mut pool) = POOL.get_or_init(Mutex::default).lock() else {
return c"".as_ptr();
};
let c = cstring(s);
if let Some(existing) = pool.get(c.as_c_str()) {
return existing.as_ptr();
}
let boxed: Box<CStr> = c.into_boxed_c_str();
let ptr = boxed.as_ptr();
pool.insert(boxed);
ptr
}
fn build_backing(value: &CValue) -> Backing {
let mut b = Backing::default();
match value {
CValue::String(s) => b.strings.push(cstring(s)),
CValue::Path { path, .. } => b.strings.push(cstring(path)),
CValue::ColorList(list) => {
b.colors = list
.iter()
.map(|c| ghostty_config_color_s {
r: c[0],
g: c[1],
b: c[2],
})
.collect();
}
CValue::Commands(cmds) => {
for c in cmds {
b.strings.push(cstring(&c.action_key));
b.strings.push(cstring(&c.action));
b.strings.push(cstring(&c.title));
b.strings.push(cstring(&c.description));
}
// The CStrings' heap buffers don't move when the Vec grows.
b.commands = b
.strings
.as_chunks::<4>()
.0
.iter()
.map(|s| ghostty_command_s {
action_key: s[0].as_ptr(),
action: s[1].as_ptr(),
title: s[2].as_ptr(),
description: s[3].as_ptr(),
})
.collect();
}
_ => {}
}
b
}
/// Borrow the config behind a handle.
///
/// # Safety
/// `p` must be null or a handle returned by this library that wasn't freed,
/// and no other reference to it may be live.
pub(crate) unsafe fn cconfig<'a>(p: ghostty_config_t) -> Option<&'a mut CConfig> {
// SAFETY: per the function contract.
unsafe { p.cast::<CConfig>().as_mut() }
}
/// Create a new configuration filled with the initial default values.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_config_new() -> ghostty_config_t {
CConfig::new(Config::default()).into_handle()
}
/// Free a configuration.
///
/// # Safety
/// `ptr` must be null or a handle from this library, not used afterwards.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_free(ptr: ghostty_config_t) {
if ptr.is_null() {
return;
}
// SAFETY: the handle was created by `into_handle` (a leaked Box).
drop(unsafe { Box::from_raw(ptr.cast::<CConfig>()) });
}
/// Deep clone the configuration.
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_clone(ptr: ghostty_config_t) -> ghostty_config_t {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return std::ptr::null_mut();
};
CConfig::new(c.config.clone()).into_handle()
}
/// Load the configuration from the CLI args.
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_load_cli_args(ptr: ghostty_config_t) {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return;
};
if let Err(err) = c.config.load_cli_args() {
log::error!("error loading config err={err}");
}
}
/// Load the configuration from the default file locations. This is usually
/// done first. The default file locations are locations such as the home
/// directory.
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_load_default_files(ptr: ghostty_config_t) {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return;
};
if let Err(err) = c.config.load_default_files() {
log::error!("error loading config err={err}");
}
}
/// Load the configuration from a specific file path. The path must be null
/// terminated.
///
/// # Safety
/// `ptr` must be a valid config handle, `path` a NUL terminated string.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_load_file(ptr: ghostty_config_t, path: *const c_char) {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return;
};
if path.is_null() {
return;
}
// SAFETY: `path` is NUL terminated.
let path = unsafe { CStr::from_ptr(path) }
.to_string_lossy()
.into_owned();
if let Err(err) = c.config.load_file(&path) {
log::error!("error loading config from file path={path} err={err}");
}
}
/// Load the configuration from the user-specified configuration file
/// locations in the previously loaded configuration. This will recursively
/// continue to load up to a built-in limit.
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_load_recursive_files(ptr: ghostty_config_t) {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return;
};
if let Err(err) = c.config.load_recursive_files() {
log::error!("error loading config err={err}");
}
}
/// Finalize the configuration (fill in computed defaults).
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_finalize(ptr: ghostty_config_t) {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return;
};
if let Err(err) = c.config.finalize() {
log::error!("error finalizing config err={err}");
}
}
/// Store `v` at `out` (`T` must be what the C caller expects for the key).
///
/// # Safety
/// `out` must be valid for a write of `T`.
unsafe fn put<T>(out: *mut c_void, v: T) {
// SAFETY: per the function contract; unaligned writes are tolerated.
unsafe { out.cast::<T>().write_unaligned(v) };
}
fn qt_size(v: QuickTerminalSizeC) -> ghostty_quick_terminal_size_s {
match v {
QuickTerminalSizeC::None => ghostty_quick_terminal_size_s {
tag: quick_terminal_size_tag::NONE,
value: ghostty_quick_terminal_size_value_u { pixels: 0 },
},
QuickTerminalSizeC::Percentage(p) => ghostty_quick_terminal_size_s {
tag: quick_terminal_size_tag::PERCENTAGE,
value: ghostty_quick_terminal_size_value_u { percentage: p },
},
QuickTerminalSizeC::Pixels(p) => ghostty_quick_terminal_size_s {
tag: quick_terminal_size_tag::PIXELS,
value: ghostty_quick_terminal_size_value_u { pixels: p },
},
}
}
impl CConfig {
/// `c_get.get`: write the value of `key` to `out`. Returns false if the
/// key is unknown, its type isn't supported by the C API yet or an
/// optional value is unset.
///
/// # Safety
/// `out` must point to storage of the C type of the key (see
/// `ghostty.h` and `c_get.zig`).
pub unsafe fn get(&self, key: &str, out: *mut c_void) -> bool {
let Some(value) = self.config.c_value(key) else {
return false;
};
// SAFETY (all writes below): `out` is valid for the key's C type by
// the function contract.
match &value {
CValue::Unsupported | CValue::Null => return false,
CValue::NullString => unsafe { put(out, std::ptr::null::<c_char>()) },
CValue::String(_) => {
let b = self.backing(key, &value);
unsafe { put(out, b.strings[0].as_ptr()) };
}
CValue::Bool(v) => unsafe { put(out, *v) },
CValue::Uint(v) => unsafe { put::<c_uint>(out, *v) },
CValue::Short(v) => unsafe { put::<c_short>(out, *v) },
CValue::F32(v) => unsafe { put(out, *v) },
CValue::F64(v) => unsafe { put(out, *v) },
CValue::Usize(v) => unsafe { put(out, *v) },
CValue::EnumName(name) => unsafe { put(out, intern(name)) },
CValue::Packed(bits) => unsafe { put::<c_uint>(out, *bits) },
CValue::Color { r, g, b } => unsafe {
put(
out,
ghostty_config_color_s {
r: *r,
g: *g,
b: *b,
},
);
},
CValue::ColorList(list) => {
let b = self.backing(key, &value);
unsafe {
put(
out,
ghostty_config_color_list_s {
colors: b.colors.as_ptr(),
len: list.len(),
},
);
}
}
CValue::Palette(colors) => {
let mut p = ghostty_config_palette_s {
colors: [ghostty_config_color_s { r: 0, g: 0, b: 0 }; 256],
};
for (dst, c) in p.colors.iter_mut().zip(colors.iter()) {
*dst = ghostty_config_color_s {
r: c[0],
g: c[1],
b: c[2],
};
}
unsafe { put(out, p) };
}
CValue::Commands(cmds) => {
let b = self.backing(key, &value);
unsafe {
put(
out,
ghostty_config_command_list_s {
commands: b.commands.as_ptr(),
len: cmds.len(),
},
);
}
}
CValue::QuickTerminalSize { primary, secondary } => unsafe {
put(
out,
ghostty_config_quick_terminal_size_s {
primary: qt_size(*primary),
secondary: qt_size(*secondary),
},
);
},
CValue::Path { optional, .. } => {
let b = self.backing(key, &value);
unsafe {
put(
out,
ghostty_config_path_s {
path: b.strings[0].as_ptr(),
optional: *optional,
},
);
}
}
}
true
}
}
/// Get the value of a config key (see `c_get.zig` for the C types).
///
/// # Safety
/// `ptr` must be a valid config handle, `key_str` must point to `len`
/// bytes and `out` to storage of the key's C type.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_get(
ptr: ghostty_config_t,
out: *mut c_void,
key_str: *const c_char,
len: usize,
) -> bool {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return false;
};
if out.is_null() || key_str.is_null() {
return false;
}
// SAFETY: `key_str` points to `len` bytes.
let key = unsafe { std::slice::from_raw_parts(key_str.cast::<u8>(), len) };
let Ok(key) = std::str::from_utf8(key) else {
return false;
};
// SAFETY: `out` is valid for the key's type per the contract.
unsafe { c.get(key, out) }
}
/// Convert the Rust trigger to the C struct.
pub(crate) fn trigger_to_c(t: &TriggerC) -> ghostty_input_trigger_s {
let (tag, key) = match t.tag {
TriggerCTag::Physical => (
trigger_tag::PHYSICAL,
ghostty_input_trigger_key_u {
physical: t.key as c_int,
},
),
TriggerCTag::Unicode => (
trigger_tag::UNICODE,
ghostty_input_trigger_key_u { unicode: t.key },
),
TriggerCTag::CatchAll => (
trigger_tag::CATCH_ALL,
ghostty_input_trigger_key_u { unicode: 0 },
),
};
ghostty_input_trigger_s {
tag,
key,
mods: c_int::from(t.mods.int()),
}
}
/// The default (unset) trigger (`Trigger.C{}`).
pub(crate) fn default_trigger() -> ghostty_input_trigger_s {
ghostty_input_trigger_s {
tag: trigger_tag::PHYSICAL,
key: ghostty_input_trigger_key_u { physical: 0 },
mods: 0,
}
}
/// Find the trigger bound to the given binding action string.
///
/// # Safety
/// `ptr` must be a valid config handle, `str` must point to `len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_trigger(
ptr: ghostty_config_t,
str: *const c_char,
len: usize,
) -> ghostty_input_trigger_s {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return default_trigger();
};
if str.is_null() {
return default_trigger();
}
// SAFETY: `str` points to `len` bytes.
let s = unsafe { std::slice::from_raw_parts(str.cast::<u8>(), len) };
let Ok(s) = std::str::from_utf8(s) else {
log::error!("error finding trigger err=InvalidFormat");
return default_trigger();
};
match BindingAction::parse(s) {
Ok(action) => {
let trigger = c
.config
.keybind
.set
.get_trigger(&action)
.unwrap_or_default();
trigger_to_c(&trigger.cval())
}
Err(err) => {
log::error!("error finding trigger err={err:?}");
default_trigger()
}
}
}
/// The number of diagnostics.
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_diagnostics_count(ptr: ghostty_config_t) -> u32 {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return 0;
};
u32::try_from(c.config.diagnostics.items().len()).unwrap_or(u32::MAX)
}
/// The diagnostic at the given index (an empty message if out of range).
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_get_diagnostic(
ptr: ghostty_config_t,
idx: u32,
) -> ghostty_diagnostic_s {
let empty = ghostty_diagnostic_s {
message: c"".as_ptr(),
};
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return empty;
};
let items = c.config.diagnostics.items();
let idx = idx as usize;
if idx >= items.len() {
return empty;
}
// Precompute (like libghostty's `DiagnosticList.precompute`): the
// messages are formatted once per diagnostic and live as long as the
// config.
let mut msgs = c.diagnostics.borrow_mut();
while msgs.len() < items.len() {
let mut text = String::new();
let _ = items[msgs.len()].format(&mut text);
msgs.push(cstring(&text));
}
ghostty_diagnostic_s {
message: msgs[idx].as_ptr(),
}
}
/// The path of the config file to open in an editor (created if needed).
/// Free with `ghostty_string_free`.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_config_open_path() -> ghostty_string_s {
match ghostty_config::edit::open_path() {
Ok(path) => string_from_owned(&path),
Err(err) => {
log::error!("error opening config in editor err={err}");
EMPTY_STRING
}
}
}
#[cfg(test)]
mod tests {
use std::ffi::c_uint;
use super::*;
fn get<T: Copy>(cfg: ghostty_config_t, key: &str, init: T) -> (bool, T) {
let mut out = init;
// SAFETY: `out` is a valid destination of the key's C type.
let ok = unsafe {
ghostty_config_get(cfg, (&raw mut out).cast(), key.as_ptr().cast(), key.len())
};
(ok, out)
}
fn with_config(f: impl FnOnce(&mut Config)) -> ghostty_config_t {
let h = ghostty_config_new();
// SAFETY: just created.
f(&mut unsafe { cconfig(h) }.unwrap().config);
h
}
#[test]
fn ghostty_config_get_bool() {
let h = with_config(|c| c.maximize = true);
assert_eq!(get(h, "maximize", false), (true, true));
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn ghostty_config_get_enum() {
let h = with_config(|c| c.window_theme = ghostty_config::types_b::WindowTheme::Dark);
let (ok, p) = get::<*const c_char>(h, "window-theme", std::ptr::null());
assert!(ok);
// SAFETY: an interned NUL terminated string.
assert_eq!(unsafe { CStr::from_ptr(p) }.to_str().unwrap(), "dark");
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn ghostty_config_get_optional_null_returns_false() {
let h = with_config(|c| c.unfocused_split_fill = None);
let (ok, _) = get(
h,
"unfocused-split-fill",
ghostty_config_color_s { r: 0, g: 0, b: 0 },
);
assert!(!ok);
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn ghostty_config_get_unknown_key_returns_false() {
let h = ghostty_config_new();
assert!(!get(h, "not-a-real-key", false).0);
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn ghostty_config_get_optional_string_null_returns_true() {
let h = with_config(|c| c.title = None);
let (ok, p) = get::<*const c_char>(h, "title", std::ptr::dangling::<c_char>());
assert!(ok);
assert!(p.is_null());
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn ghostty_config_get_float() {
let h = with_config(|c| c.background_opacity = 0.42);
let (ok, v) = get(h, "background-opacity", 0.0f64);
assert!(ok);
assert!((v - 0.42).abs() < 1e-6);
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn ghostty_config_get_struct_cval_conversion() {
let h = with_config(|c| {
c.background = ghostty_config::types_a::Color {
r: 12,
g: 34,
b: 56,
};
});
let (ok, v) = get(h, "background", ghostty_config_color_s { r: 0, g: 0, b: 0 });
assert!(ok);
assert_eq!((v.r, v.g, v.b), (12, 34, 56));
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn c_get_u8() {
let h = with_config(|c| c.font_size = 24.0);
assert_eq!(get(h, "font-size", 0.0f32), (true, 24.0));
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn c_get_split_preserve_zoom() {
let h = ghostty_config_new();
assert_eq!(get(h, "split-preserve-zoom", 9 as c_uint), (true, 0));
// SAFETY: valid handle.
unsafe {
cconfig(h).unwrap().config.split_preserve_zoom.navigation = true;
}
assert_eq!(get(h, "split-preserve-zoom", 9 as c_uint), (true, 1));
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn c_get_pointers_are_stable_for_equal_values() {
let h = ghostty_config_new();
let (_, a) = get::<ghostty_config_command_list_s>(
h,
"command-palette-entry",
ghostty_config_command_list_s {
commands: std::ptr::null(),
len: 0,
},
);
let (_, b) = get::<ghostty_config_command_list_s>(
h,
"command-palette-entry",
ghostty_config_command_list_s {
commands: std::ptr::null(),
len: 0,
},
);
assert!(a.len > 0);
assert_eq!(a.commands, b.commands);
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
}
+2157
View File
@@ -0,0 +1,2157 @@
//! The embedded application runtime (ZIG/src/apprt/embedded.zig): the
//! runtime side of `ghostty-core` implemented over the C callbacks of
//! `ghostty_runtime_config_s`, plus the `ghostty_app_*` / `ghostty_surface_*`
//! exports.
//!
//! Everything here is main-thread only, like the Zig embedded runtime: the
//! host calls the C API from its UI thread. The only cross-thread entry is
//! the `wakeup` callback, invoked by core threads through [`WakerImpl`].
//!
//! Handles: `ghostty_app_t` is a `Box<AppHandle>` and `ghostty_surface_t` a
//! `Box<Surface>`. The address of the surface object doubles as the
//! `SurfaceId` core knows the surface by, and as the `ghostty_surface_t` in
//! `ghostty_target_s` of actions (Zig: `target.cval()` gives `rt_surface`).
#![allow(unsafe_code)]
use std::{
cell::{Cell, OnceCell, RefCell},
collections::HashMap,
ffi::{CStr, CString, c_char, c_int, c_void},
fmt::Write as _,
rc::{Rc, Weak},
sync::Arc,
};
use ghostty_apprt::{
action::{Action, GotoSplit, ResizeSplit, ResizeSplitDirection, SplitDirection, Target},
runtime_api::{CoreApp, CoreSurface, RtApp, RtSurface, SurfaceId, Waker},
structs::{Clipboard, ClipboardRequest, ColorScheme, ContentScale, CursorPos, SurfaceSize},
surface::{NewSurfaceContext, new_config, should_inherit_working_directory},
};
use ghostty_config::{command::Command, config::Config, io::ReadableIO, types_a::WorkingDirectory};
use ghostty_core::{App as CoreAppImpl, Surface as CoreSurfaceImpl, surface::binding::Text};
use ghostty_input::{
Action as KeyAction,
Key,
KeyEvent,
Mods,
MouseButton,
MouseButtonState,
MousePressureStage,
ScrollMods,
binding::Action as BindingAction,
keycodes,
};
use ghostty_terminal::{
point::{Coordinate, Point, Tag as PointTag},
selection::Selection,
terminal::Terminal,
};
use crate::{
action_c::{Keep, action_to_c, target_to_c},
config_capi::cconfig,
main_c::{alloc_c_text, free_c_text},
types::{
ghostty_app_t,
ghostty_binding_flags_e,
ghostty_clipboard_content_s,
ghostty_config_t,
ghostty_input_key_s,
ghostty_point_s,
ghostty_runtime_config_s,
ghostty_selection_s,
ghostty_surface_config_s,
ghostty_surface_size_s,
ghostty_surface_t,
ghostty_text_s,
platform,
},
};
// ---------------------------------------------------------------------
// Waker
/// `App.wakeup`: invoked from any core thread; calls the host's `wakeup_cb`.
struct WakerImpl {
cb: unsafe extern "C" fn(*mut c_void),
userdata: usize,
}
impl Waker for WakerImpl {
fn wake(&self) {
// SAFETY: the embedder promises `wakeup_cb` may be called from any
// thread with the `userdata` it supplied (the documented contract of
// `ghostty_runtime_wakeup_cb`).
unsafe { (self.cb)(self.userdata as *mut c_void) };
}
}
// ---------------------------------------------------------------------
// Runtime app
/// The runtime side of the app (Zig: `embedded.App`), handed to core as
/// `Rc<dyn RtApp>`.
pub struct AppRt {
opts: ghostty_runtime_config_s,
/// The `ghostty_app_t` passed to the action callback.
app_ptr: Cell<usize>,
/// The configuration for the app (a clone, owned by the app).
config: RefCell<Arc<Config>>,
surfaces: RefCell<HashMap<SurfaceId, Weak<SurfaceRt>>>,
keymap: RefCell<ghostty_input::Keymap>,
}
impl AppRt {
fn surface_rt(&self, id: SurfaceId) -> Option<Rc<SurfaceRt>> {
self.surfaces.borrow().get(&id).and_then(Weak::upgrade)
}
/// Special case certain actions before they are sent to the embedder
/// (Zig: `performPreAction`).
fn pre_action(&self, target: &Target<SurfaceId>, action: &Action) {
match (action, target) {
(Action::SetTitle(t), Target::Surface(id)) => {
if let Some(s) = self.surface_rt(*id) {
*s.title.borrow_mut() = Some(t.title.clone());
}
}
// For app updates, we update our core config. The caller owns
// the param so we clone it.
(Action::ConfigChange(c), Target::App) => {
*self.config.borrow_mut() = Arc::clone(&c.config);
}
_ => {}
}
}
/// Reload the keyboard layout (`App.reloadKeymap`).
fn reload_keymap(&self) {
if let Err(err) = self.keymap.borrow_mut().reload() {
log::error!("error reloading keyboard map err={err:?}");
}
}
}
impl RtApp for AppRt {
fn perform_action(&self, target: Target<SurfaceId>, action: Action) -> bool {
self.pre_action(&target, &action);
log::debug!(
"dispatching action target={target:?} action={:?}",
action.key()
);
let Some(cb) = self.opts.action_cb else {
return false;
};
let mut keep = Keep::default();
let t = target_to_c(&target);
let a = action_to_c(&action, &mut keep);
// SAFETY: the embedder supplied the callback; it receives the app
// handle it knows plus plain-data structs whose pointers stay valid
// (`keep`) for the duration of the call.
unsafe { cb(self.app_ptr.get() as ghostty_app_t, t, a) }
}
fn keyboard_layout(&self) -> ghostty_input::KeyboardLayout {
keyboard_layout(&self.keymap.borrow())
}
}
/// `App.keyboardLayout`: we only support keyboard layout detection on macOS.
#[cfg(target_os = "macos")]
fn keyboard_layout(keymap: &ghostty_input::Keymap) -> ghostty_input::KeyboardLayout {
keymap
.source_id()
.and_then(|id| ghostty_input::KeyboardLayout::map_apple_id(&id))
.unwrap_or(ghostty_input::KeyboardLayout::Unknown)
}
#[cfg(not(target_os = "macos"))]
#[allow(clippy::trivially_copy_pass_by_ref)]
fn keyboard_layout(_keymap: &ghostty_input::Keymap) -> ghostty_input::KeyboardLayout {
ghostty_input::KeyboardLayout::Unknown
}
/// The app object behind `ghostty_app_t`.
pub struct AppHandle {
core: Rc<CoreAppImpl>,
rt: Rc<AppRt>,
}
/// Borrow the app behind a handle.
///
/// # Safety
/// `p` must be null or a live handle from `ghostty_app_new`.
unsafe fn app<'a>(p: ghostty_app_t) -> Option<&'a AppHandle> {
// SAFETY: per the function contract.
unsafe { p.cast::<AppHandle>().as_ref() }
}
/// Borrow a C string.
///
/// # Safety
/// `p` must be null or NUL terminated.
unsafe fn cstr<'a>(p: *const c_char) -> Option<&'a CStr> {
if p.is_null() {
None
} else {
// SAFETY: per the function contract.
Some(unsafe { CStr::from_ptr(p) })
}
}
fn mods_from_c(v: c_int) -> Mods {
// `@truncate(@as(c_uint, @bitCast(mods)))` to the u16 backing.
Mods::from_int(v as u32 as u16)
}
/// An owned key event converted from the C struct (Zig: `KeyEvent.core`).
struct OwnedKey {
action: KeyAction,
key: Key,
mods: Mods,
consumed_mods: Mods,
composing: bool,
text: String,
unshifted_codepoint: u32,
}
impl OwnedKey {
/// `None` if the action is invalid.
///
/// # Safety
/// `text` must be null or NUL terminated.
unsafe fn from_c(e: &ghostty_input_key_s) -> Option<OwnedKey> {
let action = match e.action {
0 => KeyAction::Release,
1 => KeyAction::Press,
2 => KeyAction::Repeat,
_ => return None,
};
// SAFETY: per the function contract.
let text = unsafe { cstr(e.text) }
.map(|c| c.to_string_lossy().into_owned())
.unwrap_or_default();
// We want to get the physical unmapped key to process keybinds.
let key = keycodes::entries()
.iter()
.find(|entry| entry.native == e.keycode)
.map_or(Key::Unidentified, |entry| entry.key);
// The unshifted codepoint is a `u21`; anything larger is 0.
let unshifted_codepoint = if e.unshifted_codepoint <= 0x1F_FFFF {
e.unshifted_codepoint
} else {
0
};
Some(OwnedKey {
action,
key,
mods: mods_from_c(e.mods),
consumed_mods: mods_from_c(e.consumed_mods),
composing: e.composing,
text,
unshifted_codepoint,
})
}
fn event(&self) -> KeyEvent<'_> {
KeyEvent {
action: self.action,
key: self.key,
mods: self.mods,
consumed_mods: self.consumed_mods,
composing: self.composing,
utf8: &self.text,
unshifted_codepoint: self.unshifted_codepoint,
}
}
}
// ---------------------------------------------------------------------
// App exports
/// Create a new app.
///
/// # Safety
/// `opts` and `config` must be valid pointers (the config a handle from
/// `ghostty_config_new`/`_clone`).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_new(
opts: *const ghostty_runtime_config_s,
config: ghostty_config_t,
) -> ghostty_app_t {
// SAFETY: per the function contract.
let (Some(opts), Some(config)) = (unsafe { opts.as_ref() }, unsafe { cconfig(config) }) else {
log::error!("error initializing app err=NullArgument");
return std::ptr::null_mut();
};
let Some(wakeup) = opts.wakeup_cb else {
log::error!("error initializing app err=MissingWakeupCallback");
return std::ptr::null_mut();
};
let waker = Arc::new(WakerImpl {
cb: wakeup,
userdata: opts.userdata as usize,
});
let core = match CoreAppImpl::new(waker) {
Ok(c) => Rc::new(c),
Err(err) => {
log::error!("error initializing app err={err}");
return std::ptr::null_mut();
}
};
let keymap = match ghostty_input::Keymap::new() {
Ok(k) => k,
Err(err) => {
log::error!("error initializing app err={err:?}");
return std::ptr::null_mut();
}
};
// We have to clone the config.
let rt = Rc::new(AppRt {
opts: *opts,
app_ptr: Cell::new(0),
config: RefCell::new(Arc::new(config.config.clone())),
surfaces: RefCell::default(),
keymap: RefCell::new(keymap),
});
let ptr = Box::into_raw(Box::new(AppHandle { core, rt }));
// SAFETY: just allocated.
unsafe { &*ptr }.rt.app_ptr.set(ptr as usize);
ptr.cast::<c_void>()
}
/// Tick the event loop. This should be called whenever the "wakeup" callback
/// is invoked for the runtime.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_tick(v: ghostty_app_t) {
// SAFETY: per the function contract.
let Some(a) = (unsafe { app(v) }) else {
return;
};
if let Err(err) = a.core.tick(&*a.rt) {
log::error!("error app tick err={err}");
}
}
/// Return the userdata associated with the app.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_userdata(v: ghostty_app_t) -> *mut c_void {
// SAFETY: per the function contract.
unsafe { app(v) }.map_or(std::ptr::null_mut(), |a| a.rt.opts.userdata)
}
/// Free the app.
///
/// # Safety
/// `v` must be a live app handle with no surfaces left; it is invalid
/// afterwards.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_free(v: ghostty_app_t) {
if v.is_null() {
return;
}
// SAFETY: created by `ghostty_app_new` via `Box::into_raw`.
drop(unsafe { Box::from_raw(v.cast::<AppHandle>()) });
}
/// Update the focused state of the app.
///
/// # Safety
/// `app` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_set_focus(handle: ghostty_app_t, focused: bool) {
// SAFETY: per the function contract.
if let Some(a) = unsafe { app(handle) } {
a.core.focus_event(focused);
}
}
/// Notify the app of a global keypress capture. This will return true if the
/// key was captured by the app, in which case the caller should not process
/// the key.
///
/// # Safety
/// `handle` must be a live app handle; `event.text` null or NUL terminated.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_key(
handle: ghostty_app_t,
event: ghostty_input_key_s,
) -> bool {
// SAFETY: per the function contract.
let Some(a) = (unsafe { app(handle) }) else {
return false;
};
// SAFETY: per the function contract.
let Some(key) = (unsafe { OwnedKey::from_c(&event) }) else {
return false;
};
// Never hold the borrow across the call: actions call into the host.
let config = Arc::clone(&a.rt.config.borrow());
a.core.key_event(&*a.rt, &config, &key.event())
}
/// Returns true if the given key event would trigger a binding if it were
/// sent to the surface right now. The "right now" is important because
/// things like trigger sequences are only valid until the next key event.
///
/// # Safety
/// As [`ghostty_app_key`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_key_is_binding(
handle: ghostty_app_t,
event: ghostty_input_key_s,
) -> bool {
// SAFETY: per the function contract.
let Some(a) = (unsafe { app(handle) }) else {
return false;
};
// SAFETY: per the function contract.
let Some(key) = (unsafe { OwnedKey::from_c(&event) }) else {
log::warn!("error processing key event");
return false;
};
let config = Arc::clone(&a.rt.config.borrow());
a.core.key_event_is_binding(&config, &key.event())
}
/// Notify the app that the keyboard was changed. This causes the keyboard
/// layout to be reloaded from the OS.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_keyboard_changed(v: ghostty_app_t) {
// SAFETY: per the function contract.
if let Some(a) = unsafe { app(v) } {
a.rt.reload_keymap();
}
}
/// Open the configuration.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_open_config(v: ghostty_app_t) {
// SAFETY: per the function contract.
if let Some(a) = unsafe { app(v) } {
a.rt.perform_action(Target::App, Action::OpenConfig);
}
}
/// Update the configuration to the provided config. This will propagate to
/// all surfaces as well.
///
/// # Safety
/// `v` a live app handle, `config` a config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_update_config(v: ghostty_app_t, config: ghostty_config_t) {
// SAFETY: per the function contract.
let (Some(a), Some(c)) = (unsafe { app(v) }, unsafe { cconfig(config) }) else {
return;
};
let cfg = Arc::new(c.config.clone());
if let Err(err) = a.core.update_config(&*a.rt, &cfg) {
log::error!("error updating config err={err}");
}
}
/// Returns true if the app needs to confirm quitting.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_needs_confirm_quit(v: ghostty_app_t) -> bool {
// SAFETY: per the function contract.
unsafe { app(v) }.is_some_and(|a| a.core.needs_confirm_quit())
}
/// Returns true if the app has global keybinds.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_has_global_keybinds(v: ghostty_app_t) -> bool {
use ghostty_input::binding::set::Value;
// SAFETY: per the function contract.
let Some(a) = (unsafe { app(v) }) else {
return false;
};
let config = a.rt.config.borrow();
config
.keybind
.set
.bindings
.values()
.any(|value| match value {
Value::Leader(_) => false,
Value::Leaf(leaf) => leaf.flags.global,
Value::LeafChained(leaf) => leaf.flags.global,
})
}
fn color_scheme_from_c(v: c_int) -> Option<ColorScheme> {
match v {
0 => Some(ColorScheme::Light),
1 => Some(ColorScheme::Dark),
_ => None,
}
}
/// Update the color scheme of the app.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_set_color_scheme(v: ghostty_app_t, scheme_raw: c_int) {
// SAFETY: per the function contract.
let Some(a) = (unsafe { app(v) }) else {
return;
};
let Some(scheme) = color_scheme_from_c(scheme_raw) else {
log::warn!("invalid color scheme to ghostty_app_set_color_scheme value={scheme_raw}");
return;
};
a.core.color_scheme_event(&*a.rt, scheme);
}
// ---------------------------------------------------------------------
// Runtime surface
/// The runtime side of a surface (Zig: `embedded.Surface` fields that core
/// reads), handed to core as `Rc<dyn RtSurface>`.
pub struct SurfaceRt {
app: Rc<AppRt>,
id: SurfaceId,
userdata: *mut c_void,
/// The `NSView` / `UIView` of the surface.
view: *mut c_void,
content_scale: Cell<ContentScale>,
size: Cell<SurfaceSize>,
cursor_pos: Cell<CursorPos>,
/// The current title of the surface. The embedded apprt saves this so
/// that `get_title` works without the implementer needing to save it.
title: RefCell<Option<String>>,
}
impl SurfaceRt {
fn target(&self) -> Target<SurfaceId> {
Target::Surface(self.id)
}
}
impl RtSurface for SurfaceRt {
fn close(&self, process_active: bool) {
let Some(cb) = self.app.opts.close_surface_cb else {
log::info!("runtime embedder does not support closing a surface");
return;
};
// SAFETY: embedder supplied callback with its own userdata.
unsafe { cb(self.userdata, process_active) };
}
fn cgroup(&self) -> Option<String> {
None
}
fn get_title(&self) -> Option<String> {
self.title.borrow().clone()
}
fn get_content_scale(&self) -> ContentScale {
self.content_scale.get()
}
fn get_size(&self) -> SurfaceSize {
self.size.get()
}
fn get_cursor_pos(&self) -> CursorPos {
self.cursor_pos.get()
}
fn supports_clipboard(&self, clipboard: Clipboard) -> bool {
match clipboard {
Clipboard::Standard => true,
Clipboard::Selection | Clipboard::Primary => self.app.opts.supports_selection_clipboard,
}
}
fn clipboard_request(&self, clipboard: Clipboard, state: ClipboardRequest) -> bool {
// We need to allocate to get a pointer to store our clipboard
// request so that it is stable until the read_clipboard callback
// and call complete_clipboard_request. This sucks but clipboard
// requests aren't high throughput so it's probably fine.
let Some(cb) = self.app.opts.read_clipboard_cb else {
return false;
};
let state_ptr = Box::into_raw(Box::new(state));
// SAFETY: embedder callback; `state_ptr` stays valid until
// `ghostty_surface_complete_clipboard_request` or we reclaim it.
let started = unsafe {
cb(
self.userdata,
c_int::from(clipboard as u8),
state_ptr.cast::<c_void>(),
)
};
if !started {
// SAFETY: allocated above and not handed over.
drop(unsafe { Box::from_raw(state_ptr) });
return false;
}
true
}
fn set_clipboard(&self, clipboard: Clipboard, contents: &[(String, String)], confirm: bool) {
let Some(cb) = self.app.opts.write_clipboard_cb else {
return;
};
let owned: Vec<(CString, CString)> = contents
.iter()
.map(|(m, d)| {
(
CString::new(m.replace('\0', "")).unwrap_or_default(),
CString::new(d.replace('\0', "")).unwrap_or_default(),
)
})
.collect();
let array: Vec<ghostty_clipboard_content_s> = owned
.iter()
.map(|(m, d)| ghostty_clipboard_content_s {
mime: m.as_ptr(),
data: d.as_ptr(),
})
.collect();
// SAFETY: embedder callback; the array and strings outlive the call.
unsafe {
cb(
self.userdata,
c_int::from(clipboard as u8),
array.as_ptr(),
array.len(),
confirm,
);
}
}
fn default_termio_env(&self) -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
let mut env = ghostty_os::env::get_env_map();
if cfg!(target_vendor = "apple") {
if env.contains_key("__XCODE_BUILT_PRODUCTS_DIR_PATHS") {
for k in [
"__XCODE_BUILT_PRODUCTS_DIR_PATHS",
"__XPC_DYLD_LIBRARY_PATH",
"DYLD_FRAMEWORK_PATH",
"DYLD_INSERT_LIBRARIES",
"DYLD_LIBRARY_PATH",
"LD_LIBRARY_PATH",
"SECURITYSESSIONID",
"XPC_SERVICE_NAME",
] {
env.remove(k);
}
}
// Remove this so that running `ghostty` within Ghostty works.
env.remove("GHOSTTY_MAC_LAUNCH_SOURCE");
// If we were launched from the desktop then we want to remove
// the LANGUAGE env var so that we don't inherit our translation
// settings for Ghostty. If we aren't from the desktop then we
// didn't set our LANGUAGE var so we don't need to remove it.
if launched_from_desktop() {
env.remove("LANGUAGE");
}
}
env.into_iter().collect()
}
fn redraw_inspector(&self) {
// `queueInspectorRender`
self.app
.perform_action(self.target(), Action::RenderInspector);
}
fn platform_view(&self) -> *mut c_void {
self.view
}
#[cfg(feature = "test-platform")]
fn gl_proc_address(&self, name: &str) -> *const c_void {
test_platform::proc_address(name)
}
}
#[cfg(target_os = "macos")]
fn launched_from_desktop() -> bool {
ghostty_os::desktop::launched_from_desktop_lib()
}
#[cfg(not(target_os = "macos"))]
fn launched_from_desktop() -> bool {
ghostty_os::desktop::launched_from_desktop()
}
/// The surface object behind `ghostty_surface_t`.
pub struct Surface {
app: Rc<AppHandle>,
rt: Rc<SurfaceRt>,
core: OnceCell<Rc<CoreSurfaceImpl>>,
inspector: RefCell<Option<Box<crate::inspector::Inspector>>>,
/// Strings handed out by `ghostty_surface_inherited_config`; they stay
/// valid until the surface is freed.
inherited_wd: RefCell<Vec<CString>>,
}
impl Surface {
fn core(&self) -> Option<&Rc<CoreSurfaceImpl>> {
self.core.get()
}
pub(crate) fn inspector_slot(&self) -> &RefCell<Option<Box<crate::inspector::Inspector>>> {
&self.inspector
}
pub(crate) fn core_surface(&self) -> Option<&Rc<CoreSurfaceImpl>> {
self.core()
}
pub(crate) fn rt(&self) -> &Rc<SurfaceRt> {
&self.rt
}
}
impl Drop for Surface {
fn drop(&mut self) {
// Shut down our inspector
if let Some(mut i) = self.inspector.get_mut().take() {
i.deinit(self.core.get().map(Rc::as_ref));
}
// Remove ourselves from the list of known surfaces in the app.
self.app.core.delete_surface(self.rt.id);
self.app.rt.surfaces.borrow_mut().remove(&self.rt.id);
// Clean up our core surface so that all the rendering and IO stop.
drop(self.core.take());
}
}
/// Borrow the surface behind a handle.
///
/// # Safety
/// `p` must be null or a live handle from `ghostty_surface_new`.
pub(crate) unsafe fn surface<'a>(p: ghostty_surface_t) -> Option<&'a Surface> {
// SAFETY: per the function contract.
unsafe { p.cast::<Surface>().as_ref() }
}
/// `ghostty_surface_config_new`.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_surface_config_new() -> ghostty_surface_config_s {
ghostty_surface_config_s {
platform_tag: platform::INVALID,
platform: crate::types::ghostty_platform_u {
macos: crate::types::ghostty_platform_macos_s {
nsview: std::ptr::null_mut(),
},
},
userdata: std::ptr::null_mut(),
scale_factor: 1.0,
font_size: 0.0,
working_directory: std::ptr::null(),
command: std::ptr::null(),
env_vars: std::ptr::null_mut(),
env_var_count: 0,
initial_input: std::ptr::null(),
wait_after_command: false,
context: 0,
}
}
fn context_from_c(v: c_int) -> NewSurfaceContext {
match v {
1 => NewSurfaceContext::Tab,
2 => NewSurfaceContext::Split,
_ => NewSurfaceContext::Window,
}
}
/// Test seam (feature `test-platform`): a fake platform whose OpenGL loader
/// the test installs, so surfaces can be created on Linux.
#[cfg(feature = "test-platform")]
pub mod test_platform {
use std::{cell::RefCell, ffi::c_void, rc::Rc};
/// The `platform_tag` that selects the fake platform.
pub const PLATFORM_TAG: i32 = 1000;
/// A GL function loader.
pub type GlLoader = Rc<dyn Fn(&str) -> *const c_void>;
thread_local! {
static LOADER: RefCell<Option<GlLoader>> = const { RefCell::new(None) };
}
/// Install the GL loader for surfaces created on this thread.
pub fn set_gl_loader(loader: Option<GlLoader>) {
LOADER.with(|l| *l.borrow_mut() = loader);
}
pub(super) fn proc_address(name: &str) -> *const c_void {
LOADER.with(|l| l.borrow().as_ref().map_or(std::ptr::null(), |f| f(name)))
}
}
/// Initialize the platform view (Zig: `Platform.init`). Darwin only.
///
/// # Safety
/// The union member selected by `tag` must be initialized.
unsafe fn platform_view_from_c(
opts: &ghostty_surface_config_s,
) -> Result<*mut c_void, &'static str> {
#[cfg(feature = "test-platform")]
if opts.platform_tag == test_platform::PLATFORM_TAG {
// Any non-null "view" will do; the renderer is OpenGL here.
return Ok(std::ptr::NonNull::<c_void>::dangling().as_ptr());
}
if !cfg!(target_vendor = "apple") {
// The platform union has no members off Darwin.
return Err("UnsupportedPlatform");
}
match opts.platform_tag {
// SAFETY: the tag selects the initialized member.
platform::MACOS => match unsafe { opts.platform.macos.nsview } {
v if v.is_null() => Err("NSViewMustBeSet"),
v => Ok(v),
},
// SAFETY: as above.
platform::IOS => match unsafe { opts.platform.ios.uiview } {
v if v.is_null() => Err("UIViewMustBeSet"),
v => Ok(v),
},
_ => Err("InvalidEnumTag"),
}
}
/// Zig's `std.zig.stringEscape` with non-ASCII code points written as
/// `\u{...}` (the config value is parsed as a Zig string; the bytes of
/// valid UTF-8 round trip either way).
fn string_escape(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for c in input.chars() {
match c {
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
' ' | '!' | '#'..='&' | '\'' | '('..='[' | ']'..='~' => out.push(c),
c if c.is_ascii() => {
let _ = write!(out, "\\x{:02x}", c as u32);
}
c => {
let _ = write!(out, "\\u{{{:x}}}", c as u32);
}
}
}
out
}
/// Apply the surface options to the config (the first half of Zig's
/// `Surface.init`).
///
/// # Safety
/// The pointers in `opts` must satisfy the contract of `ghostty.h`.
unsafe fn apply_surface_options(config: &mut Config, opts: &ghostty_surface_config_s) {
// If we have a working directory from the options then we set it.
// SAFETY: per the function contract.
if let Some(wd) =
unsafe { cstr(opts.working_directory) }.map(|c| c.to_string_lossy().into_owned())
&& !wd.is_empty()
{
match std::fs::metadata(&wd) {
Err(err) => log::warn!("error opening requested working directory dir={wd} err={err}"),
Ok(m) if !m.is_dir() => {
log::warn!("requested working directory is not a directory dir={wd}");
}
Ok(_) => {
let mut wd_val = WorkingDirectory::Path(wd);
wd_val.finalize();
config.working_directory = Some(wd_val);
}
}
}
// If we have a command from the options then we set it.
// SAFETY: per the function contract.
if let Some(cmd) = unsafe { cstr(opts.command) }.map(|c| c.to_string_lossy().into_owned())
&& !cmd.is_empty()
{
config.command = Some(Command::Shell(cmd));
config.wait_after_command = true;
}
// Apply any environment variables that were requested.
if opts.env_var_count > 0 && !opts.env_vars.is_null() {
// SAFETY: the caller supplies `env_var_count` entries.
let vars = unsafe { std::slice::from_raw_parts(opts.env_vars, opts.env_var_count) };
for var in vars {
// SAFETY: entries are NUL terminated strings.
let (Some(k), Some(v)) = (unsafe { cstr(var.key) }, unsafe { cstr(var.value) }) else {
continue;
};
config.env.put(&k.to_string_lossy(), &v.to_string_lossy());
}
}
// If we have an initial input then we set it.
// SAFETY: per the function contract.
if let Some(input) = unsafe { cstr(opts.initial_input) } {
// We need to escape the string because the "raw" field expects a
// Zig string.
config.input.list.clear();
config
.input
.list
.push(ReadableIO::Raw(string_escape(&input.to_string_lossy())));
}
// Wait after command
if opts.wait_after_command {
config.wait_after_command = true;
}
}
/// Create a new surface as part of an app.
///
/// # Safety
/// `app_ptr` a live app handle; `opts` a valid config (see `ghostty.h`).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_new(
app_ptr: ghostty_app_t,
opts: *const ghostty_surface_config_s,
) -> ghostty_surface_t {
// SAFETY: per the function contract.
let (Some(app_ref), Some(opts)) = (unsafe { app(app_ptr) }, unsafe { opts.as_ref() }) else {
log::error!("error initializing surface err=NullArgument");
return std::ptr::null_mut();
};
// SAFETY: per the function contract.
let view = match unsafe { platform_view_from_c(opts) } {
Ok(v) => v,
Err(err) => {
log::error!("error initializing surface err={err}");
return std::ptr::null_mut();
}
};
// Build the runtime surface. Its address is its id.
let scale = opts.scale_factor as f32;
let app_handle = {
// `AppHandle` is owned by the C handle; the surface only needs
// shared access to its parts, which are `Rc`s.
Rc::new(AppHandle {
core: Rc::clone(&app_ref.core),
rt: Rc::clone(&app_ref.rt),
})
};
let mut boxed = Box::new(Surface {
app: app_handle,
rt: Rc::new(SurfaceRt {
app: Rc::clone(&app_ref.rt),
id: 0,
userdata: opts.userdata,
view,
content_scale: Cell::new(ContentScale { x: scale, y: scale }),
size: Cell::new(SurfaceSize {
width: 800,
height: 600,
}),
cursor_pos: Cell::new(CursorPos { x: -1.0, y: -1.0 }),
title: RefCell::new(None),
}),
core: OnceCell::new(),
inspector: RefCell::new(None),
inherited_wd: RefCell::new(Vec::new()),
});
let id = std::ptr::from_mut::<Surface>(&mut *boxed) as usize as SurfaceId;
// The id is only known now (the address of the heap object).
if let Some(rt) = Rc::get_mut(&mut boxed.rt) {
rt.id = id;
}
app_ref
.rt
.surfaces
.borrow_mut()
.insert(id, Rc::downgrade(&boxed.rt));
let ptr = Box::into_raw(boxed);
// Shallow copy the config so that we can modify it.
let mut config = {
let app_config = app_ref.rt.config.borrow();
let prev_pwd = app_ref.core.state().focused_surface().and_then(|s| s.pwd());
new_config(&app_config, context_from_c(opts.context), prev_pwd)
};
// SAFETY: per the function contract.
unsafe { apply_surface_options(&mut config, opts) };
// Initialize our surface right away. We're given a view that is ready
// to use.
// SAFETY: `ptr` is the live allocation made above.
let s = unsafe { &*ptr };
let rt_app: Rc<dyn RtApp> = Rc::clone(&app_ref.rt) as Rc<dyn RtApp>;
let rt_surface: Rc<dyn RtSurface> = Rc::clone(&s.rt) as Rc<dyn RtSurface>;
match app_ref.core.create_surface(id, config, rt_app, rt_surface) {
Ok(core) => {
let _ = s.core.set(Rc::clone(&core));
// If our options requested a specific font-size, set that.
if opts.font_size != 0.0 {
let mut font_size = core.font_size();
font_size.points = opts.font_size;
if let Err(err) = core.set_font_size(font_size) {
log::error!("error initializing surface err={err}");
// SAFETY: allocated by us and not exposed yet.
drop(unsafe { Box::from_raw(ptr) });
return std::ptr::null_mut();
}
}
ptr.cast::<c_void>()
}
Err(err) => {
log::error!("error initializing surface err={err}");
// SAFETY: allocated by us and not exposed yet.
drop(unsafe { Box::from_raw(ptr) });
std::ptr::null_mut()
}
}
}
/// Free a surface.
///
/// # Safety
/// `ptr` must be a live surface handle; it is invalid afterwards.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_free(ptr: ghostty_surface_t) {
if ptr.is_null() {
return;
}
// SAFETY: created by `ghostty_surface_new` via `Box::into_raw`.
drop(unsafe { Box::from_raw(ptr.cast::<Surface>()) });
}
/// Returns the userdata associated with the surface.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_userdata(ptr: ghostty_surface_t) -> *mut c_void {
// SAFETY: per the function contract.
unsafe { surface(ptr) }.map_or(std::ptr::null_mut(), |s| s.rt.userdata)
}
/// Returns the app associated with a surface.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_app(ptr: ghostty_surface_t) -> ghostty_app_t {
// SAFETY: per the function contract.
unsafe { surface(ptr) }.map_or(std::ptr::null_mut(), |s| {
s.rt.app.app_ptr.get() as ghostty_app_t
})
}
/// Returns the config to use for surfaces that inherit from this one.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_inherited_config(
ptr: ghostty_surface_t,
source: c_int,
) -> ghostty_surface_config_s {
let mut result = ghostty_surface_config_new();
result.context = source;
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return result;
};
let Some(core) = s.core() else {
return result;
};
let context = context_from_c(source);
let config = s.rt.app.config.borrow();
// The font size to inherit. If 0, default font size will be used.
if config.window_inherit_font_size {
result.font_size = core.font_size().points;
}
if should_inherit_working_directory(context, &config)
&& let Some(cwd) = core.pwd()
&& let Ok(c) = CString::new(cwd)
{
let mut keep = s.inherited_wd.borrow_mut();
result.working_directory = c.as_ptr();
keep.push(c);
}
result
}
/// Update the configuration to the provided config for only this surface.
///
/// # Safety
/// `ptr` a live surface handle, `config` a config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_update_config(
ptr: ghostty_surface_t,
config: ghostty_config_t,
) {
// SAFETY: per the function contract.
let (Some(s), Some(c)) = (unsafe { surface(ptr) }, unsafe { cconfig(config) }) else {
return;
};
let Some(core) = s.core() else {
return;
};
if let Err(err) = core.update_config(&Arc::new(c.config.clone())) {
log::error!("error updating config err={err}");
}
}
/// Returns true if the surface needs to confirm quitting.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_needs_confirm_quit(ptr: ghostty_surface_t) -> bool {
// SAFETY: per the function contract.
unsafe { surface(ptr) }
.and_then(Surface::core)
.is_some_and(|c| c.needs_confirm_quit())
}
/// Returns true if the surface process has exited.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_process_exited(ptr: ghostty_surface_t) -> bool {
// SAFETY: per the function contract.
unsafe { surface(ptr) }
.and_then(Surface::core)
.is_some_and(|c| CoreSurface::child_exited(c.as_ref()))
}
/// Returns true if the surface has a selection.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_has_selection(ptr: ghostty_surface_t) -> bool {
// SAFETY: per the function contract.
unsafe { surface(ptr) }
.and_then(Surface::core)
.is_some_and(|c| c.has_selection())
}
/// Fill the C text result from a read.
fn fill_text(
core: &CoreSurfaceImpl,
t: &Terminal,
sel: Selection,
result: &mut ghostty_text_s,
) -> bool {
// Get our text directly from the core surface.
let text: Text = match core.dump_text_locked(t, sel) {
Ok(t) => t,
Err(err) => {
log::warn!("error reading text err={err}");
return false;
}
};
let (tl_px_x, tl_px_y, offset_start, offset_len) =
text.viewport.map_or((-1.0, -1.0, 0, 0), |vp| {
(vp.tl_px_x, vp.tl_px_y, vp.offset_start, vp.offset_len)
});
*result = ghostty_text_s {
tl_px_x,
tl_px_y,
offset_start,
offset_len,
text: alloc_c_text(text.text.as_bytes()),
text_len: text.text.len(),
};
true
}
/// Same as `ghostty_surface_read_text` but reads from the user selection, if
/// any.
///
/// # Safety
/// `ptr` a live surface handle, `result` a valid out pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_read_selection(
ptr: ghostty_surface_t,
result: *mut ghostty_text_s,
) -> bool {
// SAFETY: per the function contract.
let (Some(core), Some(result)) = (unsafe { surface(ptr) }.and_then(Surface::core), unsafe {
result.as_mut()
}) else {
return false;
};
let guard = core.lock_terminal();
let t: &Terminal = &guard;
// If we don't have a selection, do nothing.
let Some(sel) = t.screen().selection else {
return false;
};
fill_text(core, t, sel, result)
}
/// The pin of a C point (Zig: `Point.pin`).
fn point_pin(p: &ghostty_point_s, t: &Terminal) -> Option<ghostty_terminal::page_list::Pin> {
let tag = match p.tag {
0 => PointTag::Active,
1 => PointTag::Viewport,
2 => PointTag::Screen,
3 => PointTag::History,
_ => return None,
};
let pages = &t.screen().pages;
// Clamp our point to the screen bounds.
let clamped_x = p.x.min(u32::from(pages.cols.saturating_sub(1)));
let clamped_y = p.y.min(u32::from(pages.rows.saturating_sub(1)));
match p.coord {
// Exact coordinates require a specific pin.
0 => {
let x = u16::try_from(clamped_x).unwrap_or(u16::MAX);
pages.pin(Point::new(tag, Coordinate { x, y: clamped_y }))
}
1 => pages.get_top_left(tag),
2 => pages.get_bottom_right(tag),
_ => None,
}
}
fn selection_from_c(sel: &ghostty_selection_s, t: &Terminal) -> Option<Selection> {
Some(Selection::new(
point_pin(&sel.top_left, t)?,
point_pin(&sel.bottom_right, t)?,
sel.rectangle,
))
}
/// Read some arbitrary text from the surface.
///
/// This is an expensive operation so it shouldn't be called too often. We
/// recommend that callers cache the result and throttle calls to this
/// function.
///
/// # Safety
/// `ptr` a live surface handle, `result` a valid out pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_read_text(
ptr: ghostty_surface_t,
sel: ghostty_selection_s,
result: *mut ghostty_text_s,
) -> bool {
// SAFETY: per the function contract.
let (Some(core), Some(result)) = (unsafe { surface(ptr) }.and_then(Surface::core), unsafe {
result.as_mut()
}) else {
return false;
};
let guard = core.lock_terminal();
let t: &Terminal = &guard;
let Some(core_sel) = selection_from_c(&sel, t) else {
return false;
};
fill_text(core, t, core_sel, result)
}
/// Free text read from the surface.
///
/// # Safety
/// `text` must be a result of one of the read functions (or null).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_free_text(
_ptr: ghostty_surface_t,
text: *mut ghostty_text_s,
) {
// SAFETY: per the function contract.
let Some(text) = (unsafe { text.as_mut() }) else {
return;
};
// SAFETY: allocated by `alloc_c_text` with `text_len` bytes + NUL.
unsafe { free_c_text(text.text, text.text_len, true) };
text.text = std::ptr::null();
text.text_len = 0;
}
/// Tell the surface that it needs to schedule a render.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_refresh(ptr: ghostty_surface_t) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
&& let Err(err) = core.refresh_callback()
{
log::error!("error in refresh callback err={err}");
}
}
/// Tell the surface that it needs to schedule a render call as soon as
/// possible (NOW if possible).
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_draw(ptr: ghostty_surface_t) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
&& let Err(err) = core.draw()
{
log::error!("error in draw err={err}");
}
}
/// Update the size of a surface. This will trigger resize notifications to
/// the pty and the renderer.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_set_size(ptr: ghostty_surface_t, w: u32, h: u32) {
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return;
};
// Runtimes sometimes generate superfluous resize events even if the
// size did not actually change (SwiftUI). We check that the size
// actually changed from what we last recorded since resizes are
// expensive.
let cur = s.rt.size.get();
if cur.width == w && cur.height == h {
return;
}
let size = SurfaceSize {
width: w,
height: h,
};
s.rt.size.set(size);
if let Some(core) = s.core()
&& let Err(err) = core.size_callback(size)
{
log::error!("error in size callback err={err}");
}
}
/// Return the size information a surface has.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_size(ptr: ghostty_surface_t) -> ghostty_surface_size_s {
let zero = ghostty_surface_size_s {
columns: 0,
rows: 0,
width_px: 0,
height_px: 0,
cell_width_px: 0,
cell_height_px: 0,
};
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return zero;
};
let size = core.size();
let grid = size.grid();
ghostty_surface_size_s {
columns: grid.columns,
rows: grid.rows,
width_px: size.screen.width,
height_px: size.screen.height,
cell_width_px: size.cell.width,
cell_height_px: size.cell.height,
}
}
/// Update the color scheme of the surface.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_set_color_scheme(
ptr: ghostty_surface_t,
scheme_raw: c_int,
) {
let Some(scheme) = color_scheme_from_c(scheme_raw) else {
log::warn!("invalid color scheme to ghostty_surface_set_color_scheme value={scheme_raw}");
return;
};
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
&& let Err(err) = core.color_scheme_callback(scheme)
{
log::error!("error setting color scheme err={err}");
}
}
/// Update the content scale of the surface.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_set_content_scale(ptr: ghostty_surface_t, x: f64, y: f64) {
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return;
};
// We are an embedded API so the caller can send us all sorts of
// garbage. We want to make sure that the float values are valid and we
// don't want to support fractional scaling below 1.
let scaled = |v: f64| if v.is_nan() { 1.0 } else { v.max(1.0) } as f32;
let scale = ContentScale {
x: scaled(x),
y: scaled(y),
};
s.rt.content_scale.set(scale);
if let Some(core) = s.core()
&& let Err(err) = core.content_scale_callback(scale)
{
log::error!("error in content scale callback err={err}");
}
}
/// Update the focused state of a surface.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_set_focus(ptr: ghostty_surface_t, focused: bool) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
&& let Err(err) = core.focus_callback(focused)
{
log::error!("error in focus callback err={err}");
}
}
/// Update the occlusion state of a surface.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_set_occlusion(ptr: ghostty_surface_t, visible: bool) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
&& let Err(err) = core.occlusion_callback(visible)
{
log::error!("error in occlusion callback err={err}");
}
}
/// Filter the mods if necessary. This handles settings such as
/// `macos-option-as-alt`. The filtered mods should be used for key
/// translation but should NOT be sent back via the `_key` function -- the
/// original mods should be used for that.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_key_translation_mods(
ptr: ghostty_surface_t,
mods_raw: c_int,
) -> c_int {
let mods = mods_from_c(mods_raw);
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return mods_raw;
};
let Some(core) = s.core() else {
return mods_raw;
};
let option_as_alt = core
.config()
.macos_option_as_alt
.unwrap_or_else(|| s.rt.app.keyboard_layout().detect_option_as_alt());
c_int::from(mods.translation(option_as_alt).int())
}
/// Send this for raw keypresses (i.e. the keyDown event on macOS). This will
/// handle the keymap translation and send the appropriate key and char
/// events.
///
/// # Safety
/// `ptr` a live surface handle; `event.text` null or NUL terminated.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_key(
ptr: ghostty_surface_t,
event: ghostty_input_key_s,
) -> bool {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return false;
};
// SAFETY: per the function contract.
let Some(key) = (unsafe { OwnedKey::from_c(&event) }) else {
return false;
};
match CoreSurface::key_callback(core.as_ref(), key.event()) {
Ok(ghostty_apprt::runtime_api::InputEffect::Closed) => true,
Ok(ghostty_apprt::runtime_api::InputEffect::Ignored) => false,
Ok(ghostty_apprt::runtime_api::InputEffect::Consumed) => true,
Err(err) => {
log::warn!("error processing key event err={err}");
false
}
}
}
/// Returns true if the given key event would trigger a binding if it were
/// sent to the surface right now. The "right now" is important because
/// things like trigger sequences are only valid until the next key event.
///
/// # Safety
/// As [`ghostty_surface_key`]; `c_flags` null or a valid out pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_key_is_binding(
ptr: ghostty_surface_t,
event: ghostty_input_key_s,
c_flags: *mut ghostty_binding_flags_e,
) -> bool {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return false;
};
// SAFETY: per the function contract.
let Some(key) = (unsafe { OwnedKey::from_c(&event) }) else {
log::warn!("error processing key event");
return false;
};
let Some(flags) = core.key_event_is_binding(&key.event()) else {
return false;
};
if !c_flags.is_null() {
// Zig writes the `u8` `Flags.C` through the (enum-typed) pointer.
// SAFETY: the pointer is valid for at least one byte.
unsafe { c_flags.cast::<u8>().write(flags.cval()) };
}
true
}
/// Send raw text to the terminal. This is treated like a paste so this isn't
/// useful for sending escape sequences. For that, individual key input
/// should be used.
///
/// # Safety
/// `ptr` a live surface handle, `text` points to `len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_text(
ptr: ghostty_surface_t,
text: *const c_char,
len: usize,
) {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return;
};
let bytes: &[u8] = if text.is_null() {
&[]
} else {
// SAFETY: `text` points to `len` readable bytes.
unsafe { std::slice::from_raw_parts(text.cast::<u8>(), len) }
};
if let Err(err) = core.text_callback(&String::from_utf8_lossy(bytes)) {
log::error!("error in key callback err={err}");
}
}
/// Set the preedit text for the surface. This is used for IME composition.
/// If the length is 0, then the preedit text is cleared.
///
/// # Safety
/// `ptr` a live surface handle, `text` points to `len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_preedit(
ptr: ghostty_surface_t,
text: *const c_char,
len: usize,
) {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return;
};
let preedit = if len == 0 || text.is_null() {
None
} else {
// SAFETY: `text` points to `len` readable bytes.
let bytes = unsafe { std::slice::from_raw_parts(text.cast::<u8>(), len) };
Some(String::from_utf8_lossy(bytes).into_owned())
};
if let Err(err) = core.preedit_callback(preedit.as_deref()) {
log::error!("error in preedit callback err={err}");
}
}
/// Returns true if the surface currently has mouse capturing enabled.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_mouse_captured(ptr: ghostty_surface_t) -> bool {
// SAFETY: per the function contract.
unsafe { surface(ptr) }
.and_then(Surface::core)
.is_some_and(|c| c.mouse_captured())
}
/// Send a mouse button event.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_mouse_button(
ptr: ghostty_surface_t,
action: c_int,
button: c_int,
mods: c_int,
) -> bool {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return false;
};
let state = match action {
0 => MouseButtonState::Release,
1 => MouseButtonState::Press,
_ => return false,
};
let Some(button) = mouse_button_from_c(button) else {
return false;
};
match core.mouse_button_callback(state, button, mods_from_c(mods)) {
Ok(v) => v,
Err(err) => {
log::error!("error in mouse button callback err={err}");
false
}
}
}
pub(crate) fn mouse_button_from_c(v: c_int) -> Option<MouseButton> {
Some(match v {
0 => MouseButton::Unknown,
1 => MouseButton::Left,
2 => MouseButton::Right,
3 => MouseButton::Middle,
4 => MouseButton::Four,
5 => MouseButton::Five,
6 => MouseButton::Six,
7 => MouseButton::Seven,
8 => MouseButton::Eight,
9 => MouseButton::Nine,
10 => MouseButton::Ten,
11 => MouseButton::Eleven,
_ => return None,
})
}
/// Update the mouse position within the view.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_mouse_pos(
ptr: ghostty_surface_t,
x: f64,
y: f64,
mods: c_int,
) {
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return;
};
// The cursor position from the host directly is in screen coordinates
// but all our interface works in pixels.
let scale = s.rt.content_scale.get();
let pos = CursorPos {
x: x as f32 * scale.x,
y: y as f32 * scale.y,
};
// There are cases where the platform reports a mouse motion event
// without the cursor actually moving. For example, on macOS, updating
// the window title can trigger a phantom mouse-move event at the same
// coordinates. This can cause the mouse to incorrectly unhide when
// mouse-hide-while-typing is enabled (commonly seen with TUI apps like
// Zellij that frequently update the title). To prevent incorrect
// behavior, we only continue with callback logic if the cursor has
// actually moved.
let prev = s.rt.cursor_pos.get();
if (prev.x - pos.x).abs() < 1.0 && (prev.y - pos.y).abs() < 1.0 {
return;
}
s.rt.cursor_pos.set(pos);
if let Some(core) = s.core()
&& let Err(err) = core.cursor_pos_callback(pos, Some(mods_from_c(mods)))
{
log::error!("error in cursor pos callback err={err}");
}
}
/// Send a scroll event.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_mouse_scroll(
ptr: ghostty_surface_t,
x: f64,
y: f64,
scroll_mods: c_int,
) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
&& let Err(err) = core.scroll_callback(x, y, ScrollMods::from_int(scroll_mods as u32 as u8))
{
log::error!("error in scroll callback err={err}");
}
}
/// Send a mouse pressure event.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_mouse_pressure(
ptr: ghostty_surface_t,
stage_raw: u32,
pressure: f64,
) {
let stage = match stage_raw {
0 => MousePressureStage::None,
1 => MousePressureStage::Normal,
2 => MousePressureStage::Deep,
_ => {
log::warn!("invalid mouse pressure stage value={stage_raw}");
return;
}
};
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core) {
CoreSurface::mouse_pressure_callback(core.as_ref(), stage, pressure);
}
}
/// The position of the IME candidate window.
///
/// # Safety
/// `ptr` a live surface handle; the out pointers valid.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_ime_point(
ptr: ghostty_surface_t,
x: *mut f64,
y: *mut f64,
width: *mut f64,
height: *mut f64,
) {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return;
};
let pos = core.ime_point();
// SAFETY: the out pointers are valid per the contract.
unsafe {
if let Some(x) = x.as_mut() {
*x = pos.x;
}
if let Some(y) = y.as_mut() {
*y = pos.y;
}
if let Some(w) = width.as_mut() {
*w = pos.width;
}
if let Some(h) = height.as_mut() {
*h = pos.height;
}
}
}
/// Request that the surface become closed. This will go through the normal
/// trigger process that a close surface input binding would.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_request_close(ptr: ghostty_surface_t) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core) {
core.close();
}
}
/// Request that the surface split in the given direction.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_split(ptr: ghostty_surface_t, direction: c_int) {
let direction = match direction {
0 => SplitDirection::Right,
1 => SplitDirection::Down,
2 => SplitDirection::Left,
3 => SplitDirection::Up,
_ => return,
};
// SAFETY: per the function contract.
if let Some(s) = unsafe { surface(ptr) } {
s.rt.app
.perform_action(s.rt.target(), Action::NewSplit(direction));
}
}
/// Focus on the next split (if any).
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_split_focus(ptr: ghostty_surface_t, direction: c_int) {
let direction = match direction {
0 => GotoSplit::Previous,
1 => GotoSplit::Next,
2 => GotoSplit::Up,
3 => GotoSplit::Left,
4 => GotoSplit::Down,
5 => GotoSplit::Right,
_ => return,
};
// SAFETY: per the function contract.
if let Some(s) = unsafe { surface(ptr) } {
s.rt.app
.perform_action(s.rt.target(), Action::GotoSplit(direction));
}
}
/// Resize the current split by moving the split divider in the given
/// direction. `direction` specifies which direction the split divider will
/// move relative to the focused split. `amount` is a fractional value
/// between 0 and 1 that specifies by how much the divider will move.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_split_resize(
ptr: ghostty_surface_t,
direction: c_int,
amount: u16,
) {
let direction = match direction {
0 => ResizeSplitDirection::Up,
1 => ResizeSplitDirection::Down,
2 => ResizeSplitDirection::Left,
3 => ResizeSplitDirection::Right,
_ => return,
};
// SAFETY: per the function contract.
if let Some(s) = unsafe { surface(ptr) } {
s.rt.app.perform_action(
s.rt.target(),
Action::ResizeSplit(ResizeSplit { amount, direction }),
);
}
}
/// Equalize the size of all splits in the current window.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_split_equalize(ptr: ghostty_surface_t) {
// SAFETY: per the function contract.
if let Some(s) = unsafe { surface(ptr) } {
s.rt.app
.perform_action(s.rt.target(), Action::EqualizeSplits);
}
}
/// Invoke an action on the surface.
///
/// # Safety
/// `ptr` a live surface handle, `action` points to `len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_binding_action(
ptr: ghostty_surface_t,
action: *const c_char,
len: usize,
) -> bool {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return false;
};
if action.is_null() {
return false;
}
// SAFETY: `action` points to `len` readable bytes.
let bytes = unsafe { std::slice::from_raw_parts(action.cast::<u8>(), len) };
let action_str = String::from_utf8_lossy(bytes);
let action = match BindingAction::parse(&action_str) {
Ok(a) => a,
Err(err) => {
log::error!("error parsing binding action action={action_str} err={err:?}");
return false;
}
};
match core.perform_binding_action(&action) {
Ok(v) => v,
Err(err) => {
log::error!("error performing binding action action={action} err={err}");
false
}
}
}
/// Complete a clipboard read request started via the read callback. This can
/// only be called once for a given request. Once it is called with a request
/// the request pointer will be invalidated.
///
/// # Safety
/// `ptr` a live surface handle; `text` NUL terminated; `state` a pointer
/// handed to the `read_clipboard` / `confirm_read_clipboard` callbacks.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_complete_clipboard_request(
ptr: ghostty_surface_t,
text: *const c_char,
state: *mut c_void,
confirmed: bool,
) {
// SAFETY: per the function contract.
let (Some(s), Some(str)) = (unsafe { surface(ptr) }, unsafe { cstr(text) }) else {
return;
};
if state.is_null() {
return;
}
let Some(core) = s.core() else {
return;
};
let str = str.to_string_lossy();
// SAFETY: the pointer is a `Box<ClipboardRequest>` leaked by
// `clipboard_request`; it stays alive when confirmation is requested.
let req = unsafe { *state.cast::<ClipboardRequest>() };
// Attempt to complete the request, but we may request confirmation.
match core.complete_clipboard_request(req, &str, confirmed) {
Err(
ghostty_apprt::runtime_api::ClipboardError::UnsafePaste
| ghostty_apprt::runtime_api::ClipboardError::UnauthorizedPaste,
) => {
if let Some(cb) = s.rt.app.opts.confirm_read_clipboard_cb {
let ty = match req.request_type() {
ghostty_apprt::structs::ClipboardRequestType::Paste => 0,
ghostty_apprt::structs::ClipboardRequestType::Osc52Read => 1,
ghostty_apprt::structs::ClipboardRequestType::Osc52Write => 2,
};
let c = CString::new(str.replace('\0', "")).unwrap_or_default();
// SAFETY: embedder callback; `state` is preserved for the
// confirmation route.
unsafe { cb(s.rt.userdata, c.as_ptr(), state, ty) };
}
return;
}
Err(err) => log::error!("error completing clipboard request err={err}"),
Ok(()) => {}
}
// We don't defer this because the clipboard confirmation route
// preserves the clipboard request.
// SAFETY: allocated by `clipboard_request`, consumed exactly once here.
drop(unsafe { Box::from_raw(state.cast::<ClipboardRequest>()) });
}
/// Sets the window background blur on macOS to the desired value. I do this
/// as an extern function because I don't know how to call these functions
/// in Swift.
///
/// This uses an undocumented, non-public API because this is what every
/// terminal appears to use, including Terminal.app.
///
/// # Safety
/// `app` a live app handle; `window` an `NSWindow` (macOS).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_set_window_background_blur(
app_ptr: ghostty_app_t,
window: *mut c_void,
) {
// This is only supported on macOS
#[cfg(target_os = "macos")]
{
use objc2::{msg_send, runtime::AnyObject};
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
fn CGSSetWindowBackgroundBlurRadius(
connection: *mut c_void,
window: usize,
radius: c_int,
) -> i32;
fn CGSDefaultConnectionForThread() -> *mut c_void;
}
// SAFETY: per the function contract.
let Some(a) = (unsafe { app(app_ptr) }) else {
return;
};
if window.is_null() {
return;
}
let config = a.rt.config.borrow();
// Do nothing if we don't have background transparency enabled
if config.background_opacity >= 1.0 {
return;
}
let radius = match config.c_value("background-blur") {
Some(ghostty_config::c_value::CValue::Short(v)) => c_int::from(v),
_ => 0,
};
// SAFETY: `window` is an NSWindow per the contract; the CGS
// functions are the private window server API Terminal.app uses.
unsafe {
let number: isize = msg_send![window.cast::<AnyObject>(), windowNumber];
CGSSetWindowBackgroundBlurRadius(
CGSDefaultConnectionForThread(),
number as usize,
radius,
);
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = (app_ptr, window);
}
}
// ---------------------------------------------------------------------
// Darwin only exports
#[cfg(target_vendor = "apple")]
#[allow(clippy::wildcard_imports)]
mod darwin {
use super::*;
use crate::types::ghostty_text_s;
/// Set the display the surface is on (for the display link).
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_set_display_id(
ptr: ghostty_surface_t,
display_id: u32,
) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core) {
core.set_macos_display_id(display_id);
}
}
/// This returns a CTFontRef that should be used for quicklook
/// highlighted text. This is always the primary font in use regardless
/// of the selected text.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_quicklook_font(ptr: ghostty_surface_t) -> *mut c_void {
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return std::ptr::null_mut();
};
let Some(core) = s.core() else {
return std::ptr::null_mut();
};
// We need to unscale the content scale. We apply the content scale
// to our font stack because we are rendering at 1x but callers of
// this should be using scaled or apply scale themselves.
let scale = s.rt.get_content_scale();
core.quicklook_font(scale.y).unwrap_or(std::ptr::null_mut())
}
/// This returns the selected word for quicklook. This will populate the
/// buffer with the word under the cursor and the selection info so that
/// quicklook can be rendered.
///
/// This does not modify the selection active on the surface (if any).
///
/// # Safety
/// `ptr` a live surface handle, `result` a valid out pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_quicklook_word(
ptr: ghostty_surface_t,
result: *mut ghostty_text_s,
) -> bool {
// SAFETY: per the function contract.
let (Some(s), Some(result)) = (unsafe { surface(ptr) }, unsafe { result.as_mut() }) else {
return false;
};
let Some(core) = s.core() else {
return false;
};
let guard = core.lock_terminal();
let t: &Terminal = &guard;
// Get our word selection
let pos = s.rt.get_cursor_pos();
let vp = core.pos_to_viewport(f64::from(pos.x), f64::from(pos.y));
let Some(pin) = t
.screen()
.pages
.pin(Point::Viewport(Coordinate { x: vp.x, y: vp.y }))
else {
return false;
};
let Some(sel) = t
.screen()
.select_word(pin, &core.cfg().selection_word_chars)
else {
return false;
};
// Read the selection
fill_text(core, t, sel, result)
}
}
+477
View File
@@ -0,0 +1,477 @@
//! The inspector C API (`ghostty_surface_inspector`, `ghostty_inspector_*`;
//! the `Inspector` struct of ZIG/src/apprt/embedded.zig).
//!
//! The embedded inspector owns a Dear ImGui context, feeds it the host's
//! input events and, on Apple platforms, draws it with Metal into the
//! command buffer the host provides (`ghostty_inspector_metal_*`).
#![allow(unsafe_code)]
#[cfg(target_vendor = "apple")]
use std::time::Instant;
use std::{
ffi::{CStr, c_char, c_int, c_void},
rc::Rc,
};
use ghostty_apprt::runtime_api::RtSurface;
use ghostty_core::Surface as CoreSurfaceImpl;
use ghostty_input::{Action as KeyAction, Key, MouseButtonState, ScrollMods};
use ghostty_inspector::imgui::{Context, Key as ImguiKey, MouseButton as ImguiMouseButton};
use crate::{
embedded::{SurfaceRt, mouse_button_from_c, surface},
types::{ghostty_inspector_t, ghostty_surface_t},
};
/// The inspector state (Zig: `embedded.Inspector`). 1:1 with a surface.
pub struct Inspector {
surface: Rc<SurfaceRt>,
core: Rc<CoreSurfaceImpl>,
ctx: Context,
#[cfg(target_vendor = "apple")]
backend: Option<crate::inspector_metal::MetalBackend>,
content_scale: f64,
/// The scale the ImGui style currently has (a `Style` cannot be rebuilt
/// from Rust like Zig's `ImGuiStyle_ImGuiStyle`, so it is scaled by the
/// change of the factor).
style_scale: f32,
/// Our previous instant used to calculate delta time for animations.
#[cfg(target_vendor = "apple")]
instant: Option<Instant>,
}
impl Inspector {
fn init(surface: Rc<SurfaceRt>, core: Rc<CoreSurfaceImpl>) -> Option<Inspector> {
let mut ctx = match Context::try_create() {
Ok(c) => c,
Err(err) => {
log::error!("error initializing inspector err={err}");
return None;
}
};
if let Err(err) = ctx.set_platform_name(Some("ghostty_embedded")) {
log::warn!("unable to set the imgui platform name err={err}");
}
// Setup our core inspector
let scale = f64::from(RtSurface::get_content_scale(surface.as_ref()).x);
ghostty_inspector::setup(&mut ctx, scale);
if let Err(err) = core.activate_inspector() {
log::error!("failed to activate inspector err={err}");
}
Some(Inspector {
surface,
core,
ctx,
#[cfg(target_vendor = "apple")]
backend: None,
content_scale: 1.0,
style_scale: 1.0,
#[cfg(target_vendor = "apple")]
instant: None,
})
}
/// Stop collecting data and release the backend (Zig: `deinit`).
pub(crate) fn deinit(&mut self, _core: Option<&CoreSurfaceImpl>) {
self.core.deactivate_inspector();
#[cfg(target_vendor = "apple")]
if let Some(b) = self.backend.take() {
b.shutdown();
}
}
/// Queue a render for the next frame.
fn queue_render(&self) {
RtSurface::redraw_inspector(self.surface.as_ref());
}
fn update_content_scale(&mut self, x: f64) {
// Cache our scale because we use it for cursor position
// calculations.
self.content_scale = x;
// Scale the style appropriately.
let scale = x as f32;
let last = std::mem::replace(&mut self.style_scale, scale);
if (scale - last).abs() > f32::EPSILON && last > 0.0 {
self.ctx.style_mut().scale_all_sizes(scale / last);
}
}
fn update_size(&mut self, width: u32, height: u32) {
self.ctx
.io_mut()
.set_display_size([width as f32, height as f32]);
}
fn mouse_button(&mut self, action: MouseButtonState, button: ghostty_input::MouseButton) {
self.queue_render();
let imgui_button = match button {
ghostty_input::MouseButton::Left => ImguiMouseButton::Left,
ghostty_input::MouseButton::Middle => ImguiMouseButton::Middle,
ghostty_input::MouseButton::Right => ImguiMouseButton::Right,
_ => return, // unsupported
};
self.ctx
.io_mut()
.add_mouse_button_event(imgui_button, action == MouseButtonState::Press);
}
fn scroll(&mut self, xoff: f64, yoff: f64, mods: ScrollMods) {
self.queue_render();
// For precision scrolling (trackpads), the values are in pixels
// which scroll way too fast. Scale them down to approximate
// discrete wheel notches. imgui expects 1.0 to scroll ~5 lines of
// text.
let scale = if mods.precision { 0.1 } else { 1.0 };
self.ctx
.io_mut()
.add_mouse_wheel_event([(xoff * scale) as f32, (yoff * scale) as f32]);
}
fn cursor_pos(&mut self, x: f64, y: f64) {
self.queue_render();
self.ctx.io_mut().add_mouse_pos_event([
(x * self.content_scale) as f32,
(y * self.content_scale) as f32,
]);
}
fn focus(&mut self, focused: bool) {
self.queue_render();
self.ctx.io_mut().add_focus_event(focused);
}
fn text(&mut self, text: &str) {
self.queue_render();
self.ctx.io_mut().add_input_characters_utf8(text);
}
fn key(&mut self, action: KeyAction, key: Key, mods: ghostty_input::Mods) {
self.queue_render();
let io = self.ctx.io_mut();
// Update all our modifiers
io.add_key_event(ImguiKey::LeftShift, mods.shift);
io.add_key_event(ImguiKey::LeftCtrl, mods.ctrl);
io.add_key_event(ImguiKey::LeftAlt, mods.alt);
io.add_key_event(ImguiKey::LeftSuper, mods.super_);
// Send our keypress
if let Some(imgui_key) = ghostty_inspector::keys::imgui_key(key) {
io.add_key_event(
imgui_key,
matches!(action, KeyAction::Press | KeyAction::Repeat),
);
}
}
/// Update the delta time (Zig: `newFrame`).
#[cfg(target_vendor = "apple")]
pub(crate) fn new_frame(&mut self) {
let now = Instant::now();
let delta = self.instant.map_or(1.0 / 60.0, |prev| {
now.duration_since(prev).as_secs_f32().max(0.00001)
});
self.instant = Some(now);
self.ctx.io_mut().set_delta_time(delta);
}
/// Build one UI frame, calling the core inspector (`inspector.render`).
/// `end` finishes the frame without producing draw data.
#[cfg(target_vendor = "apple")]
pub(crate) fn build_frame(&mut self) -> &mut ghostty_inspector::imgui::Ui {
self.new_frame();
let core = Rc::clone(&self.core);
let ui = self.ctx.frame();
core.render_inspector(ui);
ui
}
#[cfg(target_vendor = "apple")]
pub(crate) fn context(&mut self) -> &mut Context {
&mut self.ctx
}
#[cfg(target_vendor = "apple")]
pub(crate) fn take_backend(&mut self) -> Option<crate::inspector_metal::MetalBackend> {
self.backend.take()
}
#[cfg(target_vendor = "apple")]
pub(crate) fn set_backend(&mut self, backend: crate::inspector_metal::MetalBackend) {
self.backend = Some(backend);
}
}
/// Borrow the inspector behind a handle.
///
/// # Safety
/// `p` must be null or a handle from `ghostty_surface_inspector` whose
/// surface is still alive and whose inspector wasn't freed.
unsafe fn inspector<'a>(p: ghostty_inspector_t) -> Option<&'a mut Inspector> {
// SAFETY: per the function contract.
unsafe { p.cast::<Inspector>().as_mut() }
}
/// Initialize the inspector instance. A surface can only have one inspector
/// at any given time, so this will return the previous inspector if it was
/// already initialized.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_inspector(ptr: ghostty_surface_t) -> ghostty_inspector_t {
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return std::ptr::null_mut();
};
let slot = s.inspector_slot();
if let Some(i) = slot.borrow_mut().as_mut() {
return std::ptr::from_mut::<Inspector>(&mut **i).cast::<c_void>();
}
let Some(core) = s.core_surface() else {
return std::ptr::null_mut();
};
let Some(i) = Inspector::init(Rc::clone(s.rt()), Rc::clone(core)) else {
return std::ptr::null_mut();
};
let mut boxed = Box::new(i);
let handle = std::ptr::from_mut::<Inspector>(&mut *boxed).cast::<c_void>();
*slot.borrow_mut() = Some(boxed);
handle
}
/// Free the inspector of a surface (note: takes the surface).
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_free(ptr: ghostty_surface_t) {
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return;
};
let taken = s.inspector_slot().borrow_mut().take();
if let Some(mut i) = taken {
i.deinit(None);
}
}
/// Set the size of the inspector view in pixels.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_set_size(ptr: ghostty_inspector_t, w: u32, h: u32) {
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.update_size(w, h);
}
}
/// Set the content scale of the inspector view.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_set_content_scale(
ptr: ghostty_inspector_t,
x: f64,
_y: f64,
) {
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.update_content_scale(x);
}
}
/// Send a mouse button event to the inspector.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_mouse_button(
ptr: ghostty_inspector_t,
action: c_int,
button: c_int,
_mods: c_int,
) {
let state = match action {
0 => MouseButtonState::Release,
1 => MouseButtonState::Press,
_ => return,
};
let Some(button) = mouse_button_from_c(button) else {
return;
};
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.mouse_button(state, button);
}
}
/// Send a mouse position to the inspector.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_mouse_pos(ptr: ghostty_inspector_t, x: f64, y: f64) {
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.cursor_pos(x, y);
}
}
/// Send a scroll event to the inspector.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_mouse_scroll(
ptr: ghostty_inspector_t,
x: f64,
y: f64,
scroll_mods: c_int,
) {
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.scroll(x, y, ScrollMods::from_int(scroll_mods as u32 as u8));
}
}
/// Send a key event to the inspector.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_key(
ptr: ghostty_inspector_t,
action: c_int,
key: c_int,
c_mods: c_int,
) {
let action = match action {
0 => KeyAction::Release,
1 => KeyAction::Press,
2 => KeyAction::Repeat,
_ => return,
};
let Some(key) = usize::try_from(key)
.ok()
.and_then(|k| Key::ALL.get(k))
.copied()
else {
return;
};
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.key(
action,
key,
ghostty_input::Mods::from_int(c_mods as u32 as u16),
);
}
}
/// Send text input to the inspector.
///
/// # Safety
/// `ptr` a live inspector handle; `text` NUL terminated.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_text(ptr: ghostty_inspector_t, text: *const c_char) {
if text.is_null() {
return;
}
// SAFETY: per the function contract.
let text = unsafe { CStr::from_ptr(text) }.to_string_lossy();
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.text(&text);
}
}
/// Set the focus state of the inspector.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_set_focus(ptr: ghostty_inspector_t, focused: bool) {
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.focus(focused);
}
}
#[cfg(target_vendor = "apple")]
#[allow(clippy::wildcard_imports)]
mod metal_exports {
use super::*;
/// Initialize the inspector for a Metal backend. Takes ownership of one
/// reference to `device`.
///
/// # Safety
/// `ptr` a live inspector handle; `device` a retained `MTLDevice`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_metal_init(
ptr: ghostty_inspector_t,
device: *mut c_void,
) -> bool {
// SAFETY: per the function contract.
let Some(i) = (unsafe { inspector(ptr) }) else {
return false;
};
if let Some(b) = i.backend.take() {
b.shutdown();
}
// SAFETY: `device` is a retained MTLDevice whose reference we own.
match unsafe { crate::inspector_metal::MetalBackend::init(device, i.context()) } {
Ok(b) => {
i.backend = Some(b);
log::debug!("initialized metal backend");
true
}
Err(err) => {
log::warn!("failed to initialize metal backend err={err}");
false
}
}
}
/// Render the inspector into the host's command buffer. Takes ownership
/// of one reference to each of `command_buffer` and `descriptor`.
///
/// # Safety
/// `ptr` a live inspector handle; the others retained Metal objects.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_metal_render(
ptr: ghostty_inspector_t,
command_buffer: *mut c_void,
descriptor: *mut c_void,
) {
// SAFETY: per the function contract.
let Some(i) = (unsafe { inspector(ptr) }) else {
return;
};
// SAFETY: retained objects whose reference we own.
unsafe { crate::inspector_metal::render(i, command_buffer, descriptor) };
}
/// Shut the Metal backend down.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_metal_shutdown(ptr: ghostty_inspector_t) {
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) }
&& let Some(b) = i.backend.take()
{
b.shutdown();
}
}
}
+633
View File
@@ -0,0 +1,633 @@
//! The Metal backend of the embedded inspector
//! (`ghostty_inspector_metal_{init,render,shutdown}`).
//!
//! Upstream links Dear ImGui's `imgui_impl_metal.mm`; this is the same
//! renderer written against `objc2-metal` and `dear-imgui-rs`'s managed
//! texture protocol: the draw lists are uploaded to Metal buffers and drawn
//! with the `imgui_impl_metal` shaders into the render pass the host
//! provides.
#![allow(unsafe_code)]
use std::{collections::HashMap, ffi::c_void, ptr::NonNull};
use ghostty_inspector::imgui::{
BackendFlags,
TextureFormat,
TextureId,
render::{
DrawCmd,
DrawIdx,
DrawVert,
PendingFrame,
SnapshotTextureId,
SynchronousRendererConsumer,
TextureFeedback,
TextureOp,
TextureRequest,
},
};
use objc2::{rc::Retained, runtime::ProtocolObject};
use objc2_foundation::NSString;
use objc2_metal::{
MTLBlendFactor,
MTLBlendOperation,
MTLCommandBuffer,
MTLCommandEncoder as _,
MTLCompareFunction,
MTLCullMode,
MTLDepthStencilDescriptor,
MTLDepthStencilState,
MTLDevice,
MTLIndexType,
MTLLibrary,
MTLOrigin,
MTLPixelFormat,
MTLPrimitiveType,
MTLRegion,
MTLRenderCommandEncoder,
MTLRenderPassDescriptor,
MTLRenderPipelineDescriptor,
MTLRenderPipelineState,
MTLResourceOptions,
MTLScissorRect,
MTLSize,
MTLTexture,
MTLTextureDescriptor,
MTLVertexDescriptor,
MTLVertexFormat,
MTLVertexStepFunction,
MTLViewport,
};
use crate::inspector::Inspector;
/// The shaders of `imgui_impl_metal.mm`.
const SHADERS: &str = r"
#include <metal_stdlib>
using namespace metal;
struct Uniforms {
float4x4 projectionMatrix;
};
struct VertexIn {
float2 position [[attribute(0)]];
float2 texCoords [[attribute(1)]];
uchar4 color [[attribute(2)]];
};
struct VertexOut {
float4 position [[position]];
float2 texCoords;
float4 color;
};
vertex VertexOut vertex_main(VertexIn in [[stage_in]],
constant Uniforms &uniforms [[buffer(1)]]) {
VertexOut out;
out.position = uniforms.projectionMatrix * float4(in.position, 0, 1);
out.texCoords = in.texCoords;
out.color = float4(in.color) / float4(255.0);
return out;
}
fragment half4 fragment_main(VertexOut in [[stage_in]],
texture2d<half, access::sample> texture [[texture(0)]]) {
constexpr sampler linearSampler(coord::normalized,
min_filter::linear,
mag_filter::linear,
mip_filter::linear);
half4 texColor = texture.sample(linearSampler, in.texCoords);
return half4(in.color) * texColor;
}
";
type Device = Retained<ProtocolObject<dyn MTLDevice>>;
/// A pipeline state for one render target configuration.
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
struct PipelineKey {
color_format: usize,
sample_count: usize,
depth_format: usize,
stencil_format: usize,
}
/// The Metal draw backend of one inspector.
pub struct MetalBackend {
device: Device,
library: Retained<ProtocolObject<dyn MTLLibrary>>,
depth_state: Retained<ProtocolObject<dyn MTLDepthStencilState>>,
pipelines: HashMap<PipelineKey, Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
consumer: SynchronousRendererConsumer,
/// Managed textures by Dear ImGui's identity.
textures: HashMap<SnapshotTextureId, (u64, Retained<ProtocolObject<dyn MTLTexture>>)>,
next_texture_id: u64,
}
impl MetalBackend {
/// Takes ownership of one reference to `device` (Zig: `defer
/// device.release()`).
///
/// # Safety
/// `device` must be a retained `MTLDevice`.
pub unsafe fn init(
device: *mut c_void,
ctx: &mut ghostty_inspector::imgui::Context,
) -> Result<MetalBackend, String> {
// SAFETY: per the function contract we own one reference.
let device: Device =
unsafe { Retained::from_raw(device.cast::<ProtocolObject<dyn MTLDevice>>()) }
.ok_or_else(|| "null Metal device".to_owned())?;
let library = device
.newLibraryWithSource_options_error(&NSString::from_str(SHADERS), None)
.map_err(|e| e.localizedDescription().to_string())?;
let depth_desc = MTLDepthStencilDescriptor::new();
depth_desc.setDepthCompareFunction(MTLCompareFunction::Always);
depth_desc.setDepthWriteEnabled(false);
let depth_state = device
.newDepthStencilStateWithDescriptor(&depth_desc)
.ok_or_else(|| "failed to create the depth stencil state".to_owned())?;
// Publish the renderer capabilities and the managed texture
// consumer.
let consumer = ctx
.create_synchronous_renderer_consumer()
.map_err(|e| e.to_string())?;
let reset = ctx
.prepare_renderer_texture_reset(&consumer)
.map_err(|e| e.to_string())?;
reset.commit();
if let Err(err) = ctx.set_renderer_name(Some("ghostty_embedded_metal")) {
log::warn!("unable to set the imgui renderer name err={err}");
}
let io = ctx.io_mut();
io.set_backend_flags(
io.backend_flags()
| BackendFlags::RENDERER_HAS_VTX_OFFSET
| BackendFlags::RENDERER_HAS_TEXTURES,
);
Ok(MetalBackend {
device,
library,
depth_state,
pipelines: HashMap::new(),
consumer,
textures: HashMap::new(),
next_texture_id: 1,
})
}
/// Release the GPU objects.
pub fn shutdown(self) {
drop(self);
}
fn pipeline(
&mut self,
key: PipelineKey,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
if let Some(p) = self.pipelines.get(&key) {
return Ok(p.clone());
}
let desc = MTLRenderPipelineDescriptor::new();
let vertex = self
.library
.newFunctionWithName(&NSString::from_str("vertex_main"))
.ok_or("missing vertex_main")?;
let fragment = self
.library
.newFunctionWithName(&NSString::from_str("fragment_main"))
.ok_or("missing fragment_main")?;
desc.setVertexFunction(Some(&vertex));
desc.setFragmentFunction(Some(&fragment));
// ImDrawVert: float2 pos, float2 uv, uchar4 col.
let vd = MTLVertexDescriptor::new();
let stride = size_of::<DrawVert>();
// SAFETY: attribute/layout indexes are valid; plain numbers.
unsafe {
let attrs = vd.attributes();
let a0 = attrs.objectAtIndexedSubscript(0);
a0.setFormat(MTLVertexFormat::Float2);
a0.setOffset(std::mem::offset_of!(DrawVert, pos));
a0.setBufferIndex(0);
let a1 = attrs.objectAtIndexedSubscript(1);
a1.setFormat(MTLVertexFormat::Float2);
a1.setOffset(std::mem::offset_of!(DrawVert, uv));
a1.setBufferIndex(0);
let a2 = attrs.objectAtIndexedSubscript(2);
a2.setFormat(MTLVertexFormat::UChar4);
a2.setOffset(std::mem::offset_of!(DrawVert, col));
a2.setBufferIndex(0);
let layout = vd.layouts().objectAtIndexedSubscript(0);
layout.setStepRate(1);
layout.setStepFunction(MTLVertexStepFunction::PerVertex);
layout.setStride(stride);
}
desc.setVertexDescriptor(Some(&vd));
desc.setRasterSampleCount(key.sample_count);
// SAFETY: color attachment 0 exists.
let color = unsafe { desc.colorAttachments().objectAtIndexedSubscript(0) };
color.setPixelFormat(MTLPixelFormat(key.color_format));
color.setBlendingEnabled(true);
color.setRgbBlendOperation(MTLBlendOperation::Add);
color.setAlphaBlendOperation(MTLBlendOperation::Add);
color.setSourceRGBBlendFactor(MTLBlendFactor::SourceAlpha);
color.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
color.setSourceAlphaBlendFactor(MTLBlendFactor::One);
color.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
desc.setDepthAttachmentPixelFormat(MTLPixelFormat(key.depth_format));
desc.setStencilAttachmentPixelFormat(MTLPixelFormat(key.stencil_format));
let state = self
.device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| e.localizedDescription().to_string())?;
self.pipelines.insert(key, state.clone());
Ok(state)
}
/// Create (or recreate) the texture of `key` from a full upload.
fn create_texture(
&mut self,
key: SnapshotTextureId,
format: TextureFormat,
width: u32,
height: u32,
row_pitch: usize,
pixels: &[u8],
) -> Result<TextureId, String> {
let rgba = to_rgba(format, width as usize, height as usize, row_pitch, pixels)?;
let desc = MTLTextureDescriptor::new();
desc.setPixelFormat(MTLPixelFormat::RGBA8Unorm);
// SAFETY: plain numbers validated by texture creation.
unsafe {
desc.setWidth(width as usize);
desc.setHeight(height as usize);
}
let texture = self
.device
.newTextureWithDescriptor(&desc)
.ok_or("failed to create the texture")?;
replace_region(&texture, 0, 0, width as usize, height as usize, &rgba)?;
let id = if let Some((id, _)) = self.textures.get(&key) {
*id
} else {
let id = self.next_texture_id;
self.next_texture_id += 1;
id
};
self.textures.insert(key, (id, texture));
Ok(TextureId::new(id))
}
fn process_request(&mut self, request: &TextureRequest) -> Result<TextureFeedback, String> {
let key = request.texture();
match request.operation() {
TextureOp::Create {
format,
width,
height,
row_pitch,
pixels,
} => {
let id = self.create_texture(key, *format, *width, *height, *row_pitch, pixels)?;
request.uploaded(id).map_err(|e| e.to_string())
}
TextureOp::Update {
format,
width: _,
height: _,
rects,
} => {
let Some((id, texture)) = self.textures.get(&key) else {
// The texture was never created or was destroyed.
return Ok(request.superseded());
};
for r in rects {
let (w, h) = (usize::from(r.rect.w), usize::from(r.rect.h));
let rgba = to_rgba(*format, w, h, r.row_pitch, &r.data)?;
replace_region(
texture,
usize::from(r.rect.x),
usize::from(r.rect.y),
w,
h,
&rgba,
)?;
}
request
.uploaded(TextureId::new(*id))
.map_err(|e| e.to_string())
}
TextureOp::Destroy => {
self.textures.remove(&key);
request.destroyed().map_err(|e| e.to_string())
}
}
}
/// Reconcile the managed textures of the frame and draw it into
/// `encoder` (`ImGui_ImplMetal_RenderDrawData`).
fn render(
&mut self,
frame: PendingFrame<'_>,
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
desc: &MTLRenderPassDescriptor,
) -> Result<(), String> {
let mut feedback = Vec::with_capacity(frame.texture_requests().len());
for request in frame.texture_requests() {
feedback.push(self.process_request(request)?);
}
let frame = frame
.reconcile_texture_feedback(feedback)
.map_err(|e| e.to_string())?;
let draw_data = frame.draw_data();
let [fb_w, fb_h] = {
let size = draw_data.display_size();
let scale = draw_data.framebuffer_scale();
[size[0] * scale[0], size[1] * scale[1]]
};
// Avoid rendering when minimized.
if fb_w <= 0.0 || fb_h <= 0.0 || draw_data.draw_lists_count() == 0 {
return Ok(());
}
// The pipeline matches the render target the host gave us.
// SAFETY: attachment 0 exists.
let color_tex = unsafe { desc.colorAttachments().objectAtIndexedSubscript(0) }.texture();
let depth_tex = desc.depthAttachment().texture();
let stencil_tex = desc.stencilAttachment().texture();
let key = PipelineKey {
color_format: color_tex
.as_ref()
.map_or(MTLPixelFormat::BGRA8Unorm.0, |t| t.pixelFormat().0),
sample_count: color_tex.as_ref().map_or(1, |t| t.sampleCount()),
depth_format: depth_tex
.as_ref()
.map_or(MTLPixelFormat::Invalid.0, |t| t.pixelFormat().0),
stencil_format: stencil_tex
.as_ref()
.map_or(MTLPixelFormat::Invalid.0, |t| t.pixelFormat().0),
};
let pipeline = self.pipeline(key)?;
let encoder = command_buffer
.renderCommandEncoderWithDescriptor(desc)
.ok_or("failed to create the render command encoder")?;
// Render state.
encoder.setCullMode(MTLCullMode::None);
encoder.setDepthStencilState(Some(&self.depth_state));
encoder.setViewport(MTLViewport {
originX: 0.0,
originY: 0.0,
width: f64::from(fb_w),
height: f64::from(fb_h),
znear: 0.0,
zfar: 1.0,
});
encoder.setRenderPipelineState(&pipeline);
// The orthographic projection of the display.
let [dx, dy] = draw_data.display_pos();
let [dw, dh] = draw_data.display_size();
let (l, r, t, b) = (dx, dx + dw, dy, dy + dh);
#[rustfmt::skip]
let projection: [[f32; 4]; 4] = [
[2.0 / (r - l), 0.0, 0.0, 0.0],
[0.0, 2.0 / (t - b), 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[(r + l) / (l - r), (t + b) / (b - t), 0.0, 1.0],
];
// SAFETY: the pointer/length describe `projection`, which outlives
// the call (Metal copies the bytes).
unsafe {
encoder.setVertexBytes_length_atIndex(
NonNull::from(&projection).cast::<c_void>(),
size_of::<[[f32; 4]; 4]>(),
1,
);
}
let clip_off = draw_data.display_pos();
let clip_scale = draw_data.framebuffer_scale();
for list in draw_data.draw_lists() {
let vertices = list.vtx_buffer();
let indices = list.idx_buffer();
if vertices.is_empty() || indices.is_empty() {
continue;
}
let vbuf =
self.new_buffer(vertices.as_ptr().cast(), std::mem::size_of_val(vertices))?;
let ibuf = self.new_buffer(indices.as_ptr().cast(), std::mem::size_of_val(indices))?;
// SAFETY: slot 0 is the vertex stream.
unsafe { encoder.setVertexBuffer_offset_atIndex(Some(&vbuf), 0, 0) };
for cmd in list.commands() {
match cmd {
DrawCmd::Elements { count, cmd_params } => {
// Project the scissor rectangle into framebuffer
// space and clamp it to the framebuffer.
let cr = cmd_params.clip_rect;
let min_x = ((cr[0] - clip_off[0]) * clip_scale[0]).max(0.0);
let min_y = ((cr[1] - clip_off[1]) * clip_scale[1]).max(0.0);
let max_x = ((cr[2] - clip_off[0]) * clip_scale[0]).min(fb_w);
let max_y = ((cr[3] - clip_off[1]) * clip_scale[1]).min(fb_h);
if max_x <= min_x || max_y <= min_y {
continue;
}
encoder.setScissorRect(MTLScissorRect {
x: min_x as usize,
y: min_y as usize,
width: (max_x - min_x) as usize,
height: (max_y - min_y) as usize,
});
// Bind the texture of the command.
let id = cmd_params.texture_id.id();
let Some((_, tex)) = self.textures.values().find(|(tid, _)| *tid == id)
else {
continue;
};
// SAFETY: slot 0 of the fragment stage.
unsafe { encoder.setFragmentTexture_atIndex(Some(tex), 0) };
// The vertex offset applies through the buffer
// offset (`RENDERER_HAS_VTX_OFFSET`).
// SAFETY: the offset stays inside the buffer.
unsafe {
encoder.setVertexBufferOffset_atIndex(
cmd_params.vtx_offset * size_of::<DrawVert>(),
0,
);
encoder.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset(
MTLPrimitiveType::Triangle,
count,
if size_of::<DrawIdx>() == 2 {
MTLIndexType::UInt16
} else {
MTLIndexType::UInt32
},
&ibuf,
cmd_params.idx_offset * size_of::<DrawIdx>(),
);
}
}
// Nothing to reset: state is set per command.
DrawCmd::ResetRenderState
| DrawCmd::SetSamplerLinear
| DrawCmd::SetSamplerNearest => {}
DrawCmd::RawCallback(_) => {
log::warn!("ignoring a raw imgui draw callback");
}
}
}
}
encoder.endEncoding();
Ok(())
}
fn new_buffer(
&self,
ptr: *const c_void,
len: usize,
) -> Result<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>, String> {
let ptr = NonNull::new(ptr.cast_mut()).ok_or("null buffer")?;
// SAFETY: `ptr` points to `len` readable bytes (a slice of the draw
// list); Metal copies them.
unsafe {
self.device.newBufferWithBytes_length_options(
ptr,
len,
MTLResourceOptions::StorageModeShared,
)
}
.ok_or_else(|| "failed to create a buffer".to_owned())
}
}
/// The pixels as tightly packed RGBA8 (alpha-only textures become white with
/// that alpha).
fn to_rgba(
format: TextureFormat,
width: usize,
height: usize,
row_pitch: usize,
data: &[u8],
) -> Result<Vec<u8>, String> {
let bpp = match format {
TextureFormat::RGBA32 => 4,
TextureFormat::Alpha8 => 1,
};
let pitch = if row_pitch == 0 {
width * bpp
} else {
row_pitch
};
if height > 0 && data.len() < pitch * (height - 1) + width * bpp {
return Err("texture upload is too short".to_owned());
}
let mut out = Vec::with_capacity(width * height * 4);
for y in 0..height {
let row = &data[y * pitch..y * pitch + width * bpp];
match format {
TextureFormat::RGBA32 => out.extend_from_slice(row),
TextureFormat::Alpha8 => {
for a in row {
out.extend_from_slice(&[255, 255, 255, *a]);
}
}
}
}
Ok(out)
}
fn replace_region(
texture: &ProtocolObject<dyn MTLTexture>,
x: usize,
y: usize,
width: usize,
height: usize,
rgba: &[u8],
) -> Result<(), String> {
let ptr = NonNull::new(rgba.as_ptr().cast_mut().cast::<c_void>()).ok_or("empty texture")?;
let region = MTLRegion {
origin: MTLOrigin { x, y, z: 0 },
size: MTLSize {
width,
height,
depth: 1,
},
};
// SAFETY: `rgba` has `width * 4 * height` bytes (built by `to_rgba`).
unsafe {
texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(region, 0, ptr, width * 4);
}
Ok(())
}
/// Render the inspector (Zig: `Inspector.renderMetal`). Takes ownership of
/// one reference each to `command_buffer` and `descriptor`.
///
/// # Safety
/// Both must be retained Metal objects.
pub unsafe fn render(
inspector: &mut Inspector,
command_buffer: *mut c_void,
descriptor: *mut c_void,
) {
// SAFETY: per the function contract we own one reference to each; they
// are released when these `Retained`s drop.
let command_buffer: Option<Retained<ProtocolObject<dyn MTLCommandBuffer>>> =
unsafe { Retained::from_raw(command_buffer.cast()) };
// SAFETY: as above.
let descriptor: Option<Retained<MTLRenderPassDescriptor>> =
unsafe { Retained::from_raw(descriptor.cast()) };
let (Some(command_buffer), Some(descriptor)) = (command_buffer, descriptor) else {
return;
};
if let Err(err) = render_frames(inspector, &command_buffer, &descriptor) {
log::error!("error rendering inspector err={err}");
}
}
fn render_frames(
inspector: &mut Inspector,
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
descriptor: &MTLRenderPassDescriptor,
) -> Result<(), String> {
let mut backend = inspector
.take_backend()
.ok_or("the Metal backend isn't initialized")?;
let result = draw_frame(&mut backend, inspector, command_buffer, descriptor);
inspector.set_backend(backend);
result
}
fn draw_frame(
backend: &mut MetalBackend,
inspector: &mut Inspector,
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
descriptor: &MTLRenderPassDescriptor,
) -> Result<(), String> {
// Setup our imgui frame. We need to render multiple frames to ensure
// ImGui completes all its state processing. I don't know how to fix
// this.
inspector.build_frame();
inspector.context().end_frame();
inspector.build_frame();
let pending = inspector.context().render(&backend.consumer);
backend.render(pending, command_buffer, descriptor)
}
+29
View File
@@ -0,0 +1,29 @@
//! libghostty: the embedded C API of Ghostty (`include/ghostty.h`), ported
//! from `ZIG/src/main_c.zig`, `ZIG/src/apprt/embedded.zig`,
//! `ZIG/src/config/CApi.zig` + `c_get.zig`, the C parts of
//! `ZIG/src/apprt/action.zig` and `ZIG/src/benchmark/CApi.zig`.
//!
//! The header is shipped unchanged in `include/`. The exported symbols and
//! the struct / enum layouts of [`types`] match it exactly (verified by
//! `tests/abi.rs`).
//!
//! Platform gating follows Zig: on Apple targets surfaces render with the
//! Metal renderer into the host's `NSView` / `UIView`; elsewhere the
//! platform independent API (config, app, CLI, key translation, ...) works,
//! but `ghostty_surface_new` fails with `UnsupportedPlatform` (Zig's
//! `Platform.init`).
//!
//! `unsafe` is denied crate wide and allowed (with `SAFETY:` comments) only
//! in the FFI modules.
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::undocumented_unsafe_blocks))]
pub mod action_c;
pub mod benchmark;
pub mod config_capi;
pub mod embedded;
pub mod inspector;
#[cfg(target_vendor = "apple")]
pub mod inspector_metal;
pub mod main_c;
pub mod types;
+257
View File
@@ -0,0 +1,257 @@
//! `ghostty_init`, `ghostty_cli_try_action`, `ghostty_info`,
//! `ghostty_translate` and `ghostty_string_free` (ZIG/src/main_c.zig).
#![allow(unsafe_code)]
use std::{
collections::HashMap,
ffi::{CStr, CString, c_char, c_int},
sync::{Mutex, OnceLock},
};
use ghostty_cli::build_info::{self, BuildInfo};
use ghostty_core::global::{self, InitOptions};
use crate::types::{build_mode, ghostty_info_s, ghostty_string_s};
/// The CLI arguments of the process, as given to `ghostty_init` (Zig:
/// `std.os.argv`).
static ARGS: OnceLock<Vec<String>> = OnceLock::new();
/// Build the `ghostty_string_s` for an owned string (Zig:
/// `String.fromSlice` of an allocated `[:0]const u8`). Free with
/// `ghostty_string_free`.
pub(crate) fn string_from_owned(s: &str) -> ghostty_string_s {
let mut bytes = Vec::with_capacity(s.len() + 1);
bytes.extend_from_slice(s.as_bytes());
bytes.push(0);
let boxed = bytes.into_boxed_slice();
let len = boxed.len() - 1;
let ptr = Box::into_raw(boxed).cast::<c_char>();
ghostty_string_s {
ptr,
len,
sentinel: true,
}
}
/// The empty string (`String.empty`).
pub(crate) const EMPTY_STRING: ghostty_string_s = ghostty_string_s {
ptr: std::ptr::null(),
len: 0,
sentinel: false,
};
/// Free text allocated with [`alloc_c_text`]: `text_len` bytes plus the
/// terminating NUL.
///
/// # Safety
/// `ptr` must come from [`alloc_c_text`] (or `string_from_owned`) with the
/// same `len` and not have been freed.
pub(crate) unsafe fn free_c_text(ptr: *const c_char, len: usize, sentinel: bool) {
if ptr.is_null() {
return;
}
let total = len + usize::from(sentinel);
// SAFETY: per the function contract the allocation is a boxed slice of
// exactly `total` bytes.
drop(unsafe {
Box::from_raw(std::ptr::slice_from_raw_parts_mut(
ptr.cast_mut().cast::<u8>(),
total,
))
});
}
/// Allocate a NUL terminated copy of `bytes`; returns the pointer (free with
/// [`free_c_text`] with `sentinel = true`).
pub(crate) fn alloc_c_text(bytes: &[u8]) -> *const c_char {
let mut v = Vec::with_capacity(bytes.len() + 1);
v.extend_from_slice(bytes);
v.push(0);
Box::into_raw(v.into_boxed_slice()).cast::<c_char>()
}
/// Initialize ghostty global state.
///
/// # Safety
/// `argv` must be null or point to `argc` valid NUL terminated strings.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_init(argc: usize, argv: *mut *mut c_char) -> c_int {
let mut args = Vec::with_capacity(argc);
if !argv.is_null() {
for i in 0..argc {
// SAFETY: the caller guarantees `argv[0..argc]` are valid.
let p = unsafe { *argv.add(i) };
if p.is_null() {
break;
}
// SAFETY: each argv entry is a NUL terminated string.
let c = unsafe { CStr::from_ptr(p) };
args.push(c.to_string_lossy().into_owned());
}
}
// We first try to parse any action that we may be executing.
let action = match ghostty_cli::ghostty::detect(&args) {
Ok(a) => a,
Err(err) => {
log::error!("failed to initialize ghostty error={err:?}");
return 1;
}
};
let _ = ARGS.set(args);
global::init_lib(InitOptions {
action: action.is_some(),
resources_dir: None,
});
// `+version` and friends report the library build.
build_info::set(lib_build_info());
0
}
/// The build info of the library (Zig: `build_config` for the lib artifact:
/// no app runtime, CoreText + Metal on Apple targets).
fn lib_build_info() -> BuildInfo {
let mut info = BuildInfo {
app_runtime: ".none".to_owned(),
gtk_build: None,
gtk_runtime: None,
adw_build: None,
adw_runtime: None,
x11: false,
wayland: false,
..BuildInfo::default()
};
if cfg!(target_vendor = "apple") {
".coretext".clone_into(&mut info.font_backend);
"renderer.generic.Renderer(renderer.Metal)".clone_into(&mut info.renderer);
}
info
}
/// Runs an action if it is specified. If there is no action this returns.
/// If there is an action then this doesn't return.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_cli_try_action() {
let Some(args) = ARGS.get() else {
return;
};
// No action: nothing to do.
if !matches!(ghostty_cli::ghostty::detect(args), Ok(Some(_))) {
return;
}
log::info!("executing CLI action");
let code = ghostty_cli::run_from_args(args).unwrap_or(0);
std::process::exit(i32::from(code));
}
/// Return metadata about Ghostty, such as version, build mode, etc.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_info() -> ghostty_info_s {
static VERSION: OnceLock<CString> = OnceLock::new();
let info = build_info::get();
let version = VERSION.get_or_init(|| CString::new(info.version_string).unwrap_or_default());
let mode = match info.build_mode.trim_start_matches('.') {
"Debug" => build_mode::DEBUG,
"ReleaseSafe" => build_mode::RELEASE_SAFE,
"ReleaseFast" => build_mode::RELEASE_FAST,
"ReleaseSmall" => build_mode::RELEASE_SMALL,
_ if cfg!(debug_assertions) => build_mode::DEBUG,
_ => build_mode::RELEASE_SAFE,
};
ghostty_info_s {
build_mode: mode,
version: version.as_ptr(),
version_len: version.as_bytes().len(),
}
}
/// Translate a string maintained by libghostty into the current application
/// language. This will return the same string (same pointer) if no
/// translation is found, so the pointer must be stable through the function
/// call.
///
/// # Safety
/// `msgid` must be a valid NUL terminated string.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_translate(msgid: *const c_char) -> *const c_char {
static CACHE: OnceLock<Mutex<HashMap<String, CString>>> = OnceLock::new();
if msgid.is_null() {
return msgid;
}
// SAFETY: the caller guarantees `msgid` is NUL terminated.
let id = unsafe { CStr::from_ptr(msgid) };
let Ok(id_str) = id.to_str() else {
return msgid;
};
let translated = ghostty_os::i18n::tr(id_str);
if translated == id_str {
return msgid;
}
// Translations are interned so the returned pointer stays valid.
let Ok(mut cache) = CACHE.get_or_init(Mutex::default).lock() else {
return msgid;
};
if let Some(c) = cache.get(&translated) {
return c.as_ptr();
}
let Ok(c) = CString::new(translated.clone()) else {
return msgid;
};
let ptr = c.as_ptr();
cache.insert(translated, c);
ptr
}
/// Free a string allocated by Ghostty.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_string_free(str: ghostty_string_s) {
// SAFETY: strings handed out by this library are allocated by
// `string_from_owned` / `alloc_c_text` with `len` (+1 if sentinel) bytes.
unsafe { free_c_text(str.ptr, str.len, str.sentinel) };
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ghostty_string_s_empty_string() {
let empty = EMPTY_STRING;
ghostty_string_free(empty);
assert_eq!(empty.len, 0);
assert!(!empty.sentinel);
}
#[test]
fn ghostty_string_s_c_string() {
let s = string_from_owned("hello");
assert_eq!(s.len, 5);
assert!(s.sentinel);
// SAFETY: just allocated with a NUL terminator.
let c = unsafe { CStr::from_ptr(s.ptr) };
assert_eq!(c.to_str().unwrap(), "hello");
ghostty_string_free(s);
}
#[test]
fn info_version() {
let info = ghostty_info();
assert!(info.version_len > 0);
// SAFETY: the version pointer is a static NUL terminated string.
let v = unsafe { CStr::from_ptr(info.version) };
assert_eq!(v.to_bytes().len(), info.version_len);
}
#[test]
fn translate_returns_same_pointer_without_translation() {
let id = c"this message has no translation";
// SAFETY: valid NUL terminated string.
let out = unsafe { ghostty_translate(id.as_ptr()) };
assert_eq!(out, id.as_ptr());
}
}
+579
View File
@@ -0,0 +1,579 @@
//! The C types of `include/ghostty.h`, laid out exactly as the header (and
//! the Zig `extern` structs) define them.
//!
//! Enums are `c_int` everywhere (the header uses `int`-sized enums and the C
//! caller may pass any value); the enumerator values live in submodules as
//! `i32` constants named after the header's enumerators.
#![allow(non_camel_case_types, clippy::struct_field_names)]
use std::ffi::{c_char, c_int, c_void};
// ---------------------------------------------------------------------
// Opaque handles
pub type ghostty_app_t = *mut c_void;
pub type ghostty_config_t = *mut c_void;
pub type ghostty_surface_t = *mut c_void;
pub type ghostty_inspector_t = *mut c_void;
// ---------------------------------------------------------------------
// Scalars and enums (all `int`)
pub type ghostty_platform_e = c_int;
pub type ghostty_clipboard_e = c_int;
pub type ghostty_clipboard_request_e = c_int;
pub type ghostty_input_mouse_state_e = c_int;
pub type ghostty_input_mouse_button_e = c_int;
pub type ghostty_input_mouse_momentum_e = c_int;
pub type ghostty_color_scheme_e = c_int;
pub type ghostty_input_scroll_mods_t = c_int;
pub type ghostty_input_mods_e = c_int;
pub type ghostty_binding_flags_e = c_int;
pub type ghostty_input_action_e = c_int;
pub type ghostty_input_key_e = c_int;
pub type ghostty_input_trigger_tag_e = c_int;
pub type ghostty_build_mode_e = c_int;
pub type ghostty_point_tag_e = c_int;
pub type ghostty_point_coord_e = c_int;
pub type ghostty_surface_context_e = c_int;
pub type ghostty_target_tag_e = c_int;
pub type ghostty_action_tag_e = c_int;
/// `GHOSTTY_SUCCESS`.
pub const GHOSTTY_SUCCESS: c_int = 0;
pub mod platform {
pub const INVALID: i32 = 0;
pub const MACOS: i32 = 1;
pub const IOS: i32 = 2;
}
pub mod clipboard {
pub const STANDARD: i32 = 0;
pub const SELECTION: i32 = 1;
}
pub mod trigger_tag {
pub const PHYSICAL: i32 = 0;
pub const UNICODE: i32 = 1;
pub const CATCH_ALL: i32 = 2;
}
pub mod build_mode {
pub const DEBUG: i32 = 0;
pub const RELEASE_SAFE: i32 = 1;
pub const RELEASE_FAST: i32 = 2;
pub const RELEASE_SMALL: i32 = 3;
}
pub mod target_tag {
pub const APP: i32 = 0;
pub const SURFACE: i32 = 1;
}
// ---------------------------------------------------------------------
// Structs
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_clipboard_content_s {
pub mime: *const c_char,
pub data: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_input_key_s {
pub action: ghostty_input_action_e,
pub mods: ghostty_input_mods_e,
pub consumed_mods: ghostty_input_mods_e,
pub keycode: u32,
pub text: *const c_char,
pub unshifted_codepoint: u32,
pub composing: bool,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_input_trigger_key_u {
pub translated: ghostty_input_key_e,
pub physical: ghostty_input_key_e,
pub unicode: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_input_trigger_s {
pub tag: ghostty_input_trigger_tag_e,
pub key: ghostty_input_trigger_key_u,
pub mods: ghostty_input_mods_e,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_command_s {
pub action_key: *const c_char,
pub action: *const c_char,
pub title: *const c_char,
pub description: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_info_s {
pub build_mode: ghostty_build_mode_e,
pub version: *const c_char,
pub version_len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_diagnostic_s {
pub message: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_string_s {
pub ptr: *const c_char,
pub len: usize,
pub sentinel: bool,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_text_s {
pub tl_px_x: f64,
pub tl_px_y: f64,
pub offset_start: u32,
pub offset_len: u32,
pub text: *const c_char,
pub text_len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_point_s {
pub tag: ghostty_point_tag_e,
pub coord: ghostty_point_coord_e,
pub x: u32,
pub y: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_selection_s {
pub top_left: ghostty_point_s,
pub bottom_right: ghostty_point_s,
pub rectangle: bool,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_env_var_s {
pub key: *const c_char,
pub value: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_platform_macos_s {
pub nsview: *mut c_void,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_platform_ios_s {
pub uiview: *mut c_void,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_platform_u {
pub macos: ghostty_platform_macos_s,
pub ios: ghostty_platform_ios_s,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_surface_config_s {
pub platform_tag: ghostty_platform_e,
pub platform: ghostty_platform_u,
pub userdata: *mut c_void,
pub scale_factor: f64,
pub font_size: f32,
pub working_directory: *const c_char,
pub command: *const c_char,
pub env_vars: *mut ghostty_env_var_s,
pub env_var_count: usize,
pub initial_input: *const c_char,
pub wait_after_command: bool,
pub context: ghostty_surface_context_e,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_surface_size_s {
pub columns: u16,
pub rows: u16,
pub width_px: u32,
pub height_px: u32,
pub cell_width_px: u32,
pub cell_height_px: u32,
}
// ---- config types
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_path_s {
pub path: *const c_char,
pub optional: bool,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_color_s {
pub r: u8,
pub g: u8,
pub b: u8,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_color_list_s {
pub colors: *const ghostty_config_color_s,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_command_list_s {
pub commands: *const ghostty_command_s,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_palette_s {
pub colors: [ghostty_config_color_s; 256],
}
pub mod quick_terminal_size_tag {
pub const NONE: i32 = 0;
pub const PERCENTAGE: i32 = 1;
pub const PIXELS: i32 = 2;
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_quick_terminal_size_value_u {
pub percentage: f32,
pub pixels: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_quick_terminal_size_s {
pub tag: c_int,
pub value: ghostty_quick_terminal_size_value_u,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_quick_terminal_size_s {
pub primary: ghostty_quick_terminal_size_s,
pub secondary: ghostty_quick_terminal_size_s,
}
// ---- targets and actions
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_target_u {
pub surface: ghostty_surface_t,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_target_s {
pub tag: ghostty_target_tag_e,
pub target: ghostty_target_u,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_resize_split_s {
pub amount: u16,
pub direction: c_int,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_move_tab_s {
pub amount: isize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_desktop_notification_s {
pub title: *const c_char,
pub body: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_set_title_s {
pub title: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_pwd_s {
pub pwd: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_mouse_over_link_s {
pub url: *const c_char,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_size_limit_s {
pub min_width: u32,
pub min_height: u32,
pub max_width: u32,
pub max_height: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_initial_size_s {
pub width: u32,
pub height: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_cell_size_s {
pub width: u32,
pub height: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_key_sequence_s {
pub active: bool,
pub trigger: ghostty_input_trigger_s,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_key_table_activate_s {
pub name: *const c_char,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_action_key_table_u {
pub activate: ghostty_action_key_table_activate_s,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_key_table_s {
pub tag: c_int,
pub value: ghostty_action_key_table_u,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_color_change_s {
pub kind: c_int,
pub r: u8,
pub g: u8,
pub b: u8,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_config_change_s {
pub config: ghostty_config_t,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_reload_config_s {
pub soft: bool,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_open_url_s {
pub kind: c_int,
pub url: *const c_char,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_surface_message_childexited_s {
pub exit_code: u32,
pub timetime_ms: u64,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_progress_report_s {
pub state: c_int,
pub progress: i8,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_command_finished_s {
pub exit_code: i16,
pub duration: u64,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_start_search_s {
pub needle: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_search_total_s {
pub total: isize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_search_selected_s {
pub selected: isize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_scrollbar_s {
pub total: u64,
pub offset: u64,
pub len: u64,
}
/// `ghostty_action_u`. Fields that are plain enums are `c_int`.
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_action_u {
pub new_split: c_int,
pub toggle_fullscreen: c_int,
pub move_tab: ghostty_action_move_tab_s,
pub goto_tab: c_int,
pub goto_split: c_int,
pub goto_window: c_int,
pub resize_split: ghostty_action_resize_split_s,
pub size_limit: ghostty_action_size_limit_s,
pub initial_size: ghostty_action_initial_size_s,
pub cell_size: ghostty_action_cell_size_s,
pub scrollbar: ghostty_action_scrollbar_s,
pub inspector: c_int,
pub desktop_notification: ghostty_action_desktop_notification_s,
pub set_title: ghostty_action_set_title_s,
pub set_tab_title: ghostty_action_set_title_s,
pub prompt_title: c_int,
pub pwd: ghostty_action_pwd_s,
pub mouse_shape: c_int,
pub mouse_visibility: c_int,
pub mouse_over_link: ghostty_action_mouse_over_link_s,
pub renderer_health: c_int,
pub quit_timer: c_int,
pub float_window: c_int,
pub secure_input: c_int,
pub key_sequence: ghostty_action_key_sequence_s,
pub key_table: ghostty_action_key_table_s,
pub color_change: ghostty_action_color_change_s,
pub reload_config: ghostty_action_reload_config_s,
pub config_change: ghostty_action_config_change_s,
pub open_url: ghostty_action_open_url_s,
pub close_tab_mode: c_int,
pub child_exited: ghostty_surface_message_childexited_s,
pub progress_report: ghostty_action_progress_report_s,
pub command_finished: ghostty_action_command_finished_s,
pub start_search: ghostty_action_start_search_s,
pub search_total: ghostty_action_search_total_s,
pub search_selected: ghostty_action_search_selected_s,
pub readonly: c_int,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_s {
pub tag: ghostty_action_tag_e,
pub action: ghostty_action_u,
}
// ---- runtime callbacks
pub type ghostty_runtime_wakeup_cb = Option<unsafe extern "C" fn(*mut c_void)>;
pub type ghostty_runtime_read_clipboard_cb =
Option<unsafe extern "C" fn(*mut c_void, ghostty_clipboard_e, *mut c_void) -> bool>;
pub type ghostty_runtime_confirm_read_clipboard_cb = Option<
unsafe extern "C" fn(*mut c_void, *const c_char, *mut c_void, ghostty_clipboard_request_e),
>;
pub type ghostty_runtime_write_clipboard_cb = Option<
unsafe extern "C" fn(
*mut c_void,
ghostty_clipboard_e,
*const ghostty_clipboard_content_s,
usize,
bool,
),
>;
pub type ghostty_runtime_close_surface_cb = Option<unsafe extern "C" fn(*mut c_void, bool)>;
pub type ghostty_runtime_action_cb =
Option<unsafe extern "C" fn(ghostty_app_t, ghostty_target_s, ghostty_action_s) -> bool>;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_runtime_config_s {
pub userdata: *mut c_void,
pub supports_selection_clipboard: bool,
pub wakeup_cb: ghostty_runtime_wakeup_cb,
pub action_cb: ghostty_runtime_action_cb,
pub read_clipboard_cb: ghostty_runtime_read_clipboard_cb,
pub confirm_read_clipboard_cb: ghostty_runtime_confirm_read_clipboard_cb,
pub write_clipboard_cb: ghostty_runtime_write_clipboard_cb,
pub close_surface_cb: ghostty_runtime_close_surface_cb,
}
// ---- ipc (declared by the header; the embedded runtime never uses them)
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_ipc_target_u {
pub klass: *mut c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct chostty_ipc_target_s {
pub tag: c_int,
pub target: ghostty_ipc_target_u,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_ipc_action_new_window_s {
pub arguments: *mut *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_ipc_action_u {
pub new_window: ghostty_ipc_action_new_window_s,
}
+1385
View File
@@ -0,0 +1,1385 @@
//! ABI verification: a C program generated from the unchanged
//! `include/ghostty.h` prints `sizeof`/`alignof`/`offsetof` of every struct
//! and union, `sizeof` of every enum and the value of every enumerator, and
//! takes the address of every declared function (so linking proves the
//! symbol is exported). The values are compared against the Rust
//! definitions. A second test compares the `nm` exports with the header.
#![cfg(unix)]
#![allow(clippy::unwrap_used, clippy::items_after_statements)]
mod common;
use std::{
collections::{BTreeMap, BTreeSet},
fmt::Write as _,
mem::{align_of, offset_of, size_of},
process::Command,
};
use ghostty::types::*;
use ghostty_apprt::{
action::{self, Key as ActionKey},
structs::{Clipboard, ClipboardRequestType, ColorScheme},
surface::NewSurfaceContext,
};
use ghostty_input::{
Action as KeyAction,
Key,
Mods,
MouseButton,
MouseButtonState,
binding::Flags,
key_mods::{ModSide, Side},
mouse::Momentum,
};
use ghostty_vt::mouse_shape::MouseShape;
/// `GHOSTTY_FOO_BAR` / `FooBar` / `foo_bar` -> `foobar`.
fn norm(s: &str) -> String {
s.chars()
.filter(|c| *c != '_')
.map(|c| c.to_ascii_lowercase())
.collect()
}
/// Parsed `typedef enum { ... } Name;` blocks: typedef name -> enumerators.
fn header_enums(text: &str) -> BTreeMap<String, Vec<String>> {
let mut map = BTreeMap::new();
let mut rest = text;
while let Some(start) = rest.find("typedef enum") {
let after = &rest[start..];
let open = after.find('{').unwrap();
let close = after.find('}').unwrap();
let name_end = after[close..].find(';').unwrap() + close;
let name = after[close + 1..name_end].trim().to_string();
let names = after[open + 1..close]
.split(',')
.map(|e| e.split('=').next().unwrap().trim().to_string())
.filter(|e| !e.is_empty())
.collect();
map.insert(name, names);
rest = &after[name_end..];
}
map
}
/// Parsed `typedef struct|union { ... } name;` blocks: name -> depth-1 field
/// names.
fn header_records(text: &str) -> BTreeMap<String, Vec<String>> {
let mut map = BTreeMap::new();
let b = text.as_bytes();
let mut i = 0;
while let Some(pos) = text[i..].find("typedef ") {
let start = i + pos;
let after = &text[start + 8..];
let kw = if after.starts_with("struct {") {
Some("struct {")
} else if after.starts_with("union {") {
Some("union {")
} else {
None
};
i = start + 8;
let Some(kw) = kw else { continue };
let open = start + 8 + kw.len() - 1;
// Match braces.
let mut depth = 0usize;
let mut j = open;
let mut fields = Vec::new();
let mut stmt = String::new();
loop {
match b[j] {
b'{' => {
depth += 1;
if depth > 1 {
stmt.push('{');
}
}
b'}' => {
depth -= 1;
if depth == 0 {
break;
}
stmt.push('}');
}
b';' if depth == 1 => {
// A field: the last identifier, minus array suffix.
let s = stmt.trim();
let s = if let Some(p) = s.rfind('}') {
&s[p + 1..]
} else {
s
};
let s = s.split('[').next().unwrap().trim();
let name = s
.rsplit(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
.next()
.unwrap();
if !name.is_empty() {
fields.push(name.to_string());
}
stmt.clear();
}
c => stmt.push(c as char),
}
j += 1;
}
let end = text[j..].find(';').unwrap() + j;
let name = text[j + 1..end].trim().to_string();
map.insert(name, fields);
i = end;
}
map
}
/// Names and values of a Rust-side enum, keyed by normalized name.
fn vals<T: std::fmt::Debug + Copy>(items: &[T], f: impl Fn(T) -> i64) -> BTreeMap<String, i64> {
items
.iter()
.map(|i| (norm(&format!("{i:?}")), f(*i)))
.collect()
}
fn named(items: &[(&str, i64)]) -> BTreeMap<String, i64> {
items.iter().map(|(n, v)| (norm(n), *v)).collect()
}
struct Probe {
c: String,
expected: BTreeMap<String, i64>,
records: BTreeMap<String, Vec<String>>,
covered_records: BTreeSet<String>,
covered_enums: BTreeSet<String>,
}
impl Probe {
fn emit(&mut self, key: &str, c_expr: &str, expected: i64) {
let _ = writeln!(
self.c,
" printf(\"%s=%lld\\n\", \"{key}\", (long long)({c_expr}));"
);
self.expected.insert(key.to_string(), expected);
}
/// `fields` are the Rust offsets in the header's field order; the field
/// names are taken from the parsed header (so a missing or reordered
/// field is caught).
fn layout(&mut self, ty: &str, size: usize, align: usize, offsets: &[usize]) {
let fields = self
.records
.get(ty)
.unwrap_or_else(|| panic!("no record {ty} in the header"))
.clone();
assert_eq!(
fields.len(),
offsets.len(),
"{ty}: the header has fields {fields:?}"
);
self.covered_records.insert(ty.to_string());
self.emit(
&format!("sizeof({ty})"),
&format!("sizeof({ty})"),
size as i64,
);
self.emit(
&format!("alignof({ty})"),
&format!("_Alignof({ty})"),
align as i64,
);
for (f, off) in fields.iter().zip(offsets) {
self.emit(
&format!("offsetof({ty},{f})"),
&format!("offsetof({ty}, {f})"),
*off as i64,
);
}
}
/// Check every enumerator of `typedef` against `rust` (names after
/// `prefix`); the C compiler provides the values.
fn enumerators(
&mut self,
enums: &BTreeMap<String, Vec<String>>,
typedef: &str,
prefix: &str,
rust: &BTreeMap<String, i64>,
) {
let names = enums
.get(typedef)
.unwrap_or_else(|| panic!("no enum {typedef}"));
self.covered_enums.insert(typedef.to_string());
// All GHOSTTY enums are `int` sized.
self.emit(
&format!("sizeof({typedef})"),
&format!("sizeof({typedef})"),
4,
);
for n in names {
let short = n
.strip_prefix(prefix)
.unwrap_or_else(|| panic!("{n} lacks {prefix}"));
let want = *rust
.get(&norm(short))
.unwrap_or_else(|| panic!("{typedef}: Rust has no member for {n}"));
self.emit(n, n, want);
}
assert_eq!(names.len(), rust.len(), "{typedef}: member count differs");
}
}
/// `layout!(probe, c_name, RustType, field, ...)`
macro_rules! layout {
($p:expr, $ty:ty, $name:literal, $($f:ident),+ $(,)?) => {
$p.layout(
$name,
size_of::<$ty>(),
align_of::<$ty>(),
&[$(offset_of!($ty, $f)),+],
)
};
}
fn build_probe() -> Probe {
let text = common::header_text();
let enums = header_enums(&text);
let mut p = Probe {
c: String::new(),
expected: BTreeMap::new(),
records: header_records(&text),
covered_records: BTreeSet::new(),
covered_enums: BTreeSet::new(),
};
// ---- structs and unions ----
layout!(
p,
ghostty_clipboard_content_s,
"ghostty_clipboard_content_s",
mime,
data
);
layout!(
p,
ghostty_input_key_s,
"ghostty_input_key_s",
action,
mods,
consumed_mods,
keycode,
text,
unshifted_codepoint,
composing
);
layout!(
p,
ghostty_input_trigger_key_u,
"ghostty_input_trigger_key_u",
translated,
physical,
unicode
);
layout!(
p,
ghostty_input_trigger_s,
"ghostty_input_trigger_s",
tag,
key,
mods
);
layout!(
p,
ghostty_command_s,
"ghostty_command_s",
action_key,
action,
title,
description
);
layout!(
p,
ghostty_info_s,
"ghostty_info_s",
build_mode,
version,
version_len
);
layout!(p, ghostty_diagnostic_s, "ghostty_diagnostic_s", message);
layout!(p, ghostty_string_s, "ghostty_string_s", ptr, len, sentinel);
layout!(
p,
ghostty_text_s,
"ghostty_text_s",
tl_px_x,
tl_px_y,
offset_start,
offset_len,
text,
text_len
);
layout!(p, ghostty_point_s, "ghostty_point_s", tag, coord, x, y);
layout!(
p,
ghostty_selection_s,
"ghostty_selection_s",
top_left,
bottom_right,
rectangle
);
layout!(p, ghostty_env_var_s, "ghostty_env_var_s", key, value);
layout!(
p,
ghostty_platform_macos_s,
"ghostty_platform_macos_s",
nsview
);
layout!(p, ghostty_platform_ios_s, "ghostty_platform_ios_s", uiview);
layout!(p, ghostty_platform_u, "ghostty_platform_u", macos, ios);
layout!(
p,
ghostty_surface_config_s,
"ghostty_surface_config_s",
platform_tag,
platform,
userdata,
scale_factor,
font_size,
working_directory,
command,
env_vars,
env_var_count,
initial_input,
wait_after_command,
context
);
layout!(
p,
ghostty_surface_size_s,
"ghostty_surface_size_s",
columns,
rows,
width_px,
height_px,
cell_width_px,
cell_height_px
);
layout!(
p,
ghostty_config_path_s,
"ghostty_config_path_s",
path,
optional
);
layout!(p, ghostty_config_color_s, "ghostty_config_color_s", r, g, b);
layout!(
p,
ghostty_config_color_list_s,
"ghostty_config_color_list_s",
colors,
len
);
layout!(
p,
ghostty_config_command_list_s,
"ghostty_config_command_list_s",
commands,
len
);
layout!(
p,
ghostty_config_palette_s,
"ghostty_config_palette_s",
colors
);
layout!(
p,
ghostty_quick_terminal_size_value_u,
"ghostty_quick_terminal_size_value_u",
percentage,
pixels
);
layout!(
p,
ghostty_quick_terminal_size_s,
"ghostty_quick_terminal_size_s",
tag,
value
);
layout!(
p,
ghostty_config_quick_terminal_size_s,
"ghostty_config_quick_terminal_size_s",
primary,
secondary
);
layout!(p, ghostty_target_u, "ghostty_target_u", surface);
layout!(p, ghostty_target_s, "ghostty_target_s", tag, target);
layout!(
p,
ghostty_action_resize_split_s,
"ghostty_action_resize_split_s",
amount,
direction
);
layout!(
p,
ghostty_action_move_tab_s,
"ghostty_action_move_tab_s",
amount
);
layout!(
p,
ghostty_action_desktop_notification_s,
"ghostty_action_desktop_notification_s",
title,
body
);
layout!(
p,
ghostty_action_set_title_s,
"ghostty_action_set_title_s",
title
);
layout!(p, ghostty_action_pwd_s, "ghostty_action_pwd_s", pwd);
layout!(
p,
ghostty_action_mouse_over_link_s,
"ghostty_action_mouse_over_link_s",
url,
len
);
layout!(
p,
ghostty_action_size_limit_s,
"ghostty_action_size_limit_s",
min_width,
min_height,
max_width,
max_height
);
layout!(
p,
ghostty_action_initial_size_s,
"ghostty_action_initial_size_s",
width,
height
);
layout!(
p,
ghostty_action_cell_size_s,
"ghostty_action_cell_size_s",
width,
height
);
layout!(
p,
ghostty_action_key_sequence_s,
"ghostty_action_key_sequence_s",
active,
trigger
);
layout!(
p,
ghostty_action_key_table_u,
"ghostty_action_key_table_u",
activate
);
layout!(
p,
ghostty_action_key_table_s,
"ghostty_action_key_table_s",
tag,
value
);
layout!(
p,
ghostty_action_color_change_s,
"ghostty_action_color_change_s",
kind,
r,
g,
b
);
layout!(
p,
ghostty_action_config_change_s,
"ghostty_action_config_change_s",
config
);
layout!(
p,
ghostty_action_reload_config_s,
"ghostty_action_reload_config_s",
soft
);
layout!(
p,
ghostty_action_open_url_s,
"ghostty_action_open_url_s",
kind,
url,
len
);
layout!(
p,
ghostty_surface_message_childexited_s,
"ghostty_surface_message_childexited_s",
exit_code,
timetime_ms
);
layout!(
p,
ghostty_action_progress_report_s,
"ghostty_action_progress_report_s",
state,
progress
);
layout!(
p,
ghostty_action_command_finished_s,
"ghostty_action_command_finished_s",
exit_code,
duration
);
layout!(
p,
ghostty_action_start_search_s,
"ghostty_action_start_search_s",
needle
);
layout!(
p,
ghostty_action_search_total_s,
"ghostty_action_search_total_s",
total
);
layout!(
p,
ghostty_action_search_selected_s,
"ghostty_action_search_selected_s",
selected
);
layout!(
p,
ghostty_action_scrollbar_s,
"ghostty_action_scrollbar_s",
total,
offset,
len
);
layout!(
p,
ghostty_action_u,
"ghostty_action_u",
new_split,
toggle_fullscreen,
move_tab,
goto_tab,
goto_split,
goto_window,
resize_split,
size_limit,
initial_size,
cell_size,
scrollbar,
inspector,
desktop_notification,
set_title,
set_tab_title,
prompt_title,
pwd,
mouse_shape,
mouse_visibility,
mouse_over_link,
renderer_health,
quit_timer,
float_window,
secure_input,
key_sequence,
key_table,
color_change,
reload_config,
config_change,
open_url,
close_tab_mode,
child_exited,
progress_report,
command_finished,
start_search,
search_total,
search_selected,
readonly
);
layout!(p, ghostty_action_s, "ghostty_action_s", tag, action);
layout!(
p,
ghostty_runtime_config_s,
"ghostty_runtime_config_s",
userdata,
supports_selection_clipboard,
wakeup_cb,
action_cb,
read_clipboard_cb,
confirm_read_clipboard_cb,
write_clipboard_cb,
close_surface_cb
);
layout!(p, ghostty_ipc_target_u, "ghostty_ipc_target_u", klass);
layout!(p, chostty_ipc_target_s, "chostty_ipc_target_s", tag, target);
layout!(
p,
ghostty_ipc_action_new_window_s,
"ghostty_ipc_action_new_window_s",
arguments
);
layout!(p, ghostty_ipc_action_u, "ghostty_ipc_action_u", new_window);
// ---- scalar typedefs and opaque handles ----
for (ty, size) in [
("ghostty_app_t", size_of::<usize>()),
("ghostty_config_t", size_of::<usize>()),
("ghostty_surface_t", size_of::<usize>()),
("ghostty_inspector_t", size_of::<usize>()),
("ghostty_input_scroll_mods_t", 4),
("ghostty_runtime_wakeup_cb", size_of::<usize>()),
("ghostty_runtime_action_cb", size_of::<usize>()),
] {
p.emit(
&format!("sizeof({ty})"),
&format!("sizeof({ty})"),
size as i64,
);
}
p.emit(
"GHOSTTY_SUCCESS",
"GHOSTTY_SUCCESS",
i64::from(GHOSTTY_SUCCESS),
);
// ---- enums ----
p.enumerators(
&enums,
"ghostty_platform_e",
"GHOSTTY_PLATFORM_",
&named(&[
("invalid", i64::from(platform::INVALID)),
("macos", i64::from(platform::MACOS)),
("ios", i64::from(platform::IOS)),
]),
);
p.enumerators(
&enums,
"ghostty_clipboard_e",
"GHOSTTY_CLIPBOARD_",
&vals(&[Clipboard::Standard, Clipboard::Selection], |v| v as i64),
);
p.enumerators(
&enums,
"ghostty_clipboard_request_e",
"GHOSTTY_CLIPBOARD_REQUEST_",
&named(&[
("paste", ClipboardRequestType::Paste as i64),
("osc_52_read", ClipboardRequestType::Osc52Read as i64),
("osc_52_write", ClipboardRequestType::Osc52Write as i64),
]),
);
p.enumerators(
&enums,
"ghostty_input_mouse_state_e",
"GHOSTTY_MOUSE_",
&vals(&[MouseButtonState::Release, MouseButtonState::Press], |v| {
v as i64
}),
);
p.enumerators(
&enums,
"ghostty_input_mouse_button_e",
"GHOSTTY_MOUSE_",
&vals(
&[
MouseButton::Unknown,
MouseButton::Left,
MouseButton::Right,
MouseButton::Middle,
MouseButton::Four,
MouseButton::Five,
MouseButton::Six,
MouseButton::Seven,
MouseButton::Eight,
MouseButton::Nine,
MouseButton::Ten,
MouseButton::Eleven,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_input_mouse_momentum_e",
"GHOSTTY_MOUSE_MOMENTUM_",
&vals(
&[
Momentum::None,
Momentum::Began,
Momentum::Stationary,
Momentum::Changed,
Momentum::Ended,
Momentum::Cancelled,
Momentum::MayBegin,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_color_scheme_e",
"GHOSTTY_COLOR_SCHEME_",
&vals(&[ColorScheme::Light, ColorScheme::Dark], |v| v as i64),
);
let side = |f: fn(&mut Side)| {
let mut s = Side::default();
f(&mut s);
Mods {
sides: s,
..Mods::default()
}
.int() as i64
};
p.enumerators(
&enums,
"ghostty_input_mods_e",
"GHOSTTY_MODS_",
&named(&[
("none", 0),
(
"shift",
Mods {
shift: true,
..Mods::default()
}
.int()
.into(),
),
(
"ctrl",
Mods {
ctrl: true,
..Mods::default()
}
.int()
.into(),
),
(
"alt",
Mods {
alt: true,
..Mods::default()
}
.int()
.into(),
),
(
"super",
Mods {
super_: true,
..Mods::default()
}
.int()
.into(),
),
(
"caps",
Mods {
caps_lock: true,
..Mods::default()
}
.int()
.into(),
),
(
"num",
Mods {
num_lock: true,
..Mods::default()
}
.int()
.into(),
),
("shift_right", side(|s| s.shift = ModSide::Right)),
("ctrl_right", side(|s| s.ctrl = ModSide::Right)),
("alt_right", side(|s| s.alt = ModSide::Right)),
("super_right", side(|s| s.super_ = ModSide::Right)),
]),
);
let flag = |f: fn(&mut Flags)| {
let mut fl = Flags {
consumed: false,
all: false,
global: false,
performable: false,
};
f(&mut fl);
i64::from(fl.cval())
};
p.enumerators(
&enums,
"ghostty_binding_flags_e",
"GHOSTTY_BINDING_FLAGS_",
&named(&[
("consumed", flag(|f| f.consumed = true)),
("all", flag(|f| f.all = true)),
("global", flag(|f| f.global = true)),
("performable", flag(|f| f.performable = true)),
]),
);
p.enumerators(
&enums,
"ghostty_input_action_e",
"GHOSTTY_ACTION_",
&vals(
&[KeyAction::Release, KeyAction::Press, KeyAction::Repeat],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_input_key_e",
"GHOSTTY_KEY_",
&Key::ALL
.iter()
.enumerate()
.map(|(i, k)| {
// The header names the letter keys `A`..`Z` (`KeyA` in the
// W3C code table).
let n = k.name();
let n = if n.len() == 5 {
n.strip_prefix("key_").unwrap_or(n)
} else {
n
};
(norm(n), i as i64)
})
.collect(),
);
p.enumerators(
&enums,
"ghostty_input_trigger_tag_e",
"GHOSTTY_TRIGGER_",
&named(&[
("physical", i64::from(trigger_tag::PHYSICAL)),
("unicode", i64::from(trigger_tag::UNICODE)),
("catch_all", i64::from(trigger_tag::CATCH_ALL)),
]),
);
p.enumerators(
&enums,
"ghostty_build_mode_e",
"GHOSTTY_BUILD_MODE_",
&named(&[
("debug", i64::from(build_mode::DEBUG)),
("release_safe", i64::from(build_mode::RELEASE_SAFE)),
("release_fast", i64::from(build_mode::RELEASE_FAST)),
("release_small", i64::from(build_mode::RELEASE_SMALL)),
]),
);
p.enumerators(
&enums,
"ghostty_point_tag_e",
"GHOSTTY_POINT_",
// The header calls the fourth tag `SURFACE`; the Zig `Point.Tag`
// calls it `history` (value 3).
&named(&[
("active", 0),
("viewport", 1),
("screen", 2),
("surface", 3),
]),
);
p.enumerators(
&enums,
"ghostty_point_coord_e",
"GHOSTTY_POINT_COORD_",
&named(&[("exact", 0), ("top_left", 1), ("bottom_right", 2)]),
);
p.enumerators(
&enums,
"ghostty_surface_context_e",
"GHOSTTY_SURFACE_CONTEXT_",
&vals(
&[
NewSurfaceContext::Window,
NewSurfaceContext::Tab,
NewSurfaceContext::Split,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_quick_terminal_size_tag_e",
"GHOSTTY_QUICK_TERMINAL_SIZE_",
&named(&[
("none", i64::from(quick_terminal_size_tag::NONE)),
("percentage", i64::from(quick_terminal_size_tag::PERCENTAGE)),
("pixels", i64::from(quick_terminal_size_tag::PIXELS)),
]),
);
p.enumerators(
&enums,
"ghostty_config_fullscreen_e",
"GHOSTTY_CONFIG_FULLSCREEN_",
&ghostty_config::types_a::Fullscreen::ALL
.iter()
.enumerate()
.map(|(i, v)| (norm(&format!("{v:?}")), i as i64))
.collect(),
);
p.enumerators(
&enums,
"ghostty_target_tag_e",
"GHOSTTY_TARGET_",
&named(&[
("app", i64::from(target_tag::APP)),
("surface", i64::from(target_tag::SURFACE)),
]),
);
use action as a;
p.enumerators(
&enums,
"ghostty_action_split_direction_e",
"GHOSTTY_SPLIT_DIRECTION_",
&vals(
&[
a::SplitDirection::Right,
a::SplitDirection::Down,
a::SplitDirection::Left,
a::SplitDirection::Up,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_action_goto_split_e",
"GHOSTTY_GOTO_SPLIT_",
&vals(
&[
a::GotoSplit::Previous,
a::GotoSplit::Next,
a::GotoSplit::Up,
a::GotoSplit::Left,
a::GotoSplit::Down,
a::GotoSplit::Right,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_action_goto_window_e",
"GHOSTTY_GOTO_WINDOW_",
&vals(&[a::GotoWindow::Previous, a::GotoWindow::Next], |v| {
v as i64
}),
);
p.enumerators(
&enums,
"ghostty_action_resize_split_direction_e",
"GHOSTTY_RESIZE_SPLIT_",
&vals(
&[
a::ResizeSplitDirection::Up,
a::ResizeSplitDirection::Down,
a::ResizeSplitDirection::Left,
a::ResizeSplitDirection::Right,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_action_goto_tab_e",
"GHOSTTY_GOTO_TAB_",
&named(&[
("previous", i64::from(a::GotoTab::Previous.int())),
("next", i64::from(a::GotoTab::Next.int())),
("last", i64::from(a::GotoTab::Last.int())),
]),
);
p.enumerators(
&enums,
"ghostty_action_fullscreen_e",
"GHOSTTY_FULLSCREEN_",
&vals(
&[
a::Fullscreen::Native,
a::Fullscreen::MacosNonNative,
a::Fullscreen::MacosNonNativeVisibleMenu,
a::Fullscreen::MacosNonNativePaddedNotch,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_action_float_window_e",
"GHOSTTY_FLOAT_WINDOW_",
&vals(
&[
a::FloatWindow::On,
a::FloatWindow::Off,
a::FloatWindow::Toggle,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_action_secure_input_e",
"GHOSTTY_SECURE_INPUT_",
&vals(
&[
a::SecureInput::On,
a::SecureInput::Off,
a::SecureInput::Toggle,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_action_inspector_e",
"GHOSTTY_INSPECTOR_",
&vals(
&[a::Inspector::Toggle, a::Inspector::Show, a::Inspector::Hide],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_action_quit_timer_e",
"GHOSTTY_QUIT_TIMER_",
&vals(&[a::QuitTimer::Start, a::QuitTimer::Stop], |v| v as i64),
);
p.enumerators(
&enums,
"ghostty_action_readonly_e",
"GHOSTTY_READONLY_",
&vals(&[a::Readonly::Off, a::Readonly::On], |v| v as i64),
);
p.enumerators(
&enums,
"ghostty_action_prompt_title_e",
"GHOSTTY_PROMPT_TITLE_",
&vals(&[a::PromptTitle::Surface, a::PromptTitle::Tab], |v| {
v as i64
}),
);
use MouseShape as M;
p.enumerators(
&enums,
"ghostty_action_mouse_shape_e",
"GHOSTTY_MOUSE_SHAPE_",
&vals(
&[
M::Default,
M::ContextMenu,
M::Help,
M::Pointer,
M::Progress,
M::Wait,
M::Cell,
M::Crosshair,
M::Text,
M::VerticalText,
M::Alias,
M::Copy,
M::Move,
M::NoDrop,
M::NotAllowed,
M::Grab,
M::Grabbing,
M::AllScroll,
M::ColResize,
M::RowResize,
M::NResize,
M::EResize,
M::SResize,
M::WResize,
M::NeResize,
M::NwResize,
M::SeResize,
M::SwResize,
M::EwResize,
M::NsResize,
M::NeswResize,
M::NwseResize,
M::ZoomIn,
M::ZoomOut,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_action_mouse_visibility_e",
"GHOSTTY_MOUSE_",
&vals(
&[a::MouseVisibility::Visible, a::MouseVisibility::Hidden],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_action_renderer_health_e",
"GHOSTTY_RENDERER_HEALTH_",
&named(&[("healthy", 0), ("unhealthy", 1)]),
);
p.enumerators(
&enums,
"ghostty_action_key_table_tag_e",
"GHOSTTY_KEY_TABLE_",
&named(&[("activate", 0), ("deactivate", 1), ("deactivate_all", 2)]),
);
p.enumerators(
&enums,
"ghostty_action_color_kind_e",
"GHOSTTY_ACTION_COLOR_KIND_",
&named(&[
("foreground", i64::from(a::ColorKind::Foreground.int())),
("background", i64::from(a::ColorKind::Background.int())),
("cursor", i64::from(a::ColorKind::Cursor.int())),
]),
);
p.enumerators(
&enums,
"ghostty_action_open_url_kind_e",
"GHOSTTY_ACTION_OPEN_URL_KIND_",
&vals(
&[
a::OpenUrlKind::Unknown,
a::OpenUrlKind::Text,
a::OpenUrlKind::Html,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_action_close_tab_mode_e",
"GHOSTTY_ACTION_CLOSE_TAB_MODE_",
&vals(
&[
a::CloseTabMode::This,
a::CloseTabMode::Other,
a::CloseTabMode::Right,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_action_progress_report_state_e",
"GHOSTTY_PROGRESS_STATE_",
&named(&[
("remove", 0),
("set", 1),
("error", 2),
("indeterminate", 3),
("pause", 4),
]),
);
use ActionKey as K;
p.enumerators(
&enums,
"ghostty_action_tag_e",
"GHOSTTY_ACTION_",
&vals(
&[
K::Quit,
K::NewWindow,
K::NewTab,
K::CloseTab,
K::NewSplit,
K::CloseAllWindows,
K::ToggleMaximize,
K::ToggleFullscreen,
K::ToggleTabOverview,
K::ToggleWindowDecorations,
K::ToggleQuickTerminal,
K::ToggleCommandPalette,
K::ToggleVisibility,
K::ToggleBackgroundOpacity,
K::MoveTab,
K::GotoTab,
K::GotoSplit,
K::GotoWindow,
K::ResizeSplit,
K::EqualizeSplits,
K::ToggleSplitZoom,
K::PresentTerminal,
K::SizeLimit,
K::ResetWindowSize,
K::InitialSize,
K::CellSize,
K::Scrollbar,
K::Render,
K::Inspector,
K::ShowGtkInspector,
K::RenderInspector,
K::DesktopNotification,
K::SetTitle,
K::SetTabTitle,
K::PromptTitle,
K::Pwd,
K::MouseShape,
K::MouseVisibility,
K::MouseOverLink,
K::RendererHealth,
K::OpenConfig,
K::QuitTimer,
K::FloatWindow,
K::SecureInput,
K::KeySequence,
K::KeyTable,
K::ColorChange,
K::ReloadConfig,
K::ConfigChange,
K::CloseWindow,
K::RingBell,
K::Undo,
K::Redo,
K::CheckForUpdates,
K::OpenUrl,
K::ShowChildExited,
K::ProgressReport,
K::ShowOnScreenKeyboard,
K::CommandFinished,
K::StartSearch,
K::EndSearch,
K::SearchTotal,
K::SearchSelected,
K::Readonly,
K::CopyTitleToClipboard,
],
|v| v as i64,
),
);
p.enumerators(
&enums,
"ghostty_ipc_target_tag_e",
"GHOSTTY_IPC_TARGET_",
&named(&[("class", 0), ("detect", 1)]),
);
p.enumerators(
&enums,
"ghostty_ipc_action_tag_e",
"GHOSTTY_IPC_ACTION_",
&named(&[("new_window", 0)]),
);
// Every enum and record of the header must be covered.
let all_enums: BTreeSet<_> = enums.keys().cloned().collect();
assert_eq!(all_enums, p.covered_enums, "uncovered enums");
let all_records: BTreeSet<_> = p.records.keys().cloned().collect();
assert_eq!(all_records, p.covered_records, "uncovered structs/unions");
p
}
/// The enum `ghostty_action_tag_e` has the same size as the Rust key.
#[test]
fn action_tags_fit_the_union() {
assert_eq!(size_of::<ghostty_action_u>(), 24);
assert_eq!(size_of::<ghostty_action_s>(), 32);
assert_eq!(size_of::<ghostty_target_s>(), 16);
}
#[test]
fn abi_matches_header() {
let probe = build_probe();
let lib_dir = common::build_lib();
let dir = common::scratch_dir("abi");
// Take the address of every declared function so linking proves the
// library exports it.
let funcs = common::declared_functions();
assert!(funcs.len() > 80, "parsed only {} functions", funcs.len());
let mut c = String::from(
"#include <stdio.h>\n#include <stddef.h>\n#include <stdint.h>\n#include <ghostty.h>\n\n",
);
c.push_str("static void *const funcs[] = {\n");
for f in &funcs {
let _ = writeln!(c, " (void *)&{f},");
}
c.push_str("};\n\nint main(void) {\n");
c.push_str(" if (sizeof(funcs) == 0) return 2;\n");
c.push_str(&probe.c);
c.push_str(" return 0;\n}\n");
let src = dir.join("probe.c");
std::fs::write(&src, c).unwrap();
let exe = dir.join("probe");
common::compile_c(&lib_dir, &[&src], &exe, &["-Wno-deprecated-declarations"]);
let out = Command::new(&exe).output().unwrap();
assert!(out.status.success(), "probe failed");
let got: BTreeMap<String, i64> = String::from_utf8(out.stdout)
.unwrap()
.lines()
.map(|l| {
let (k, v) = l.rsplit_once('=').unwrap();
(k.to_string(), v.parse().unwrap())
})
.collect();
let mut bad = Vec::new();
for (k, want) in &probe.expected {
match got.get(k) {
Some(g) if g == want => {}
Some(g) => bad.push(format!("{k}: C={g} Rust={want}")),
None => bad.push(format!("{k}: missing from C output")),
}
}
assert!(bad.is_empty(), "ABI mismatches:\n{}", bad.join("\n"));
assert!(got.len() > 700, "only {} values checked", got.len());
}
#[test]
fn exports_match_header() {
let lib_dir = common::build_lib();
let so = ["libghostty.so", "libghostty.dylib"]
.iter()
.map(|n| lib_dir.join(n))
.find(|p| p.exists())
.expect("shared library");
let out = Command::new("nm").arg("-D").arg(&so).output().unwrap();
assert!(out.status.success());
let exported: BTreeSet<String> = String::from_utf8(out.stdout)
.unwrap()
.lines()
.filter_map(|l| {
let mut it = l.split_whitespace();
let (_addr, kind, name) = (it.next()?, it.next()?, it.next()?);
(kind == "T" && name.starts_with("ghostty_")).then(|| name.to_string())
})
.collect();
let declared: BTreeSet<String> = common::declared_functions().into_iter().collect();
let missing: Vec<_> = declared.difference(&exported).collect();
let extra: Vec<_> = exported.difference(&declared).collect();
assert!(
missing.is_empty() && extra.is_empty(),
"missing exports: {missing:?}\nextra exports: {extra:?}"
);
}
#[test]
fn header_files_are_unchanged_copies() {
let zig = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../ghostty/include");
if !zig.exists() {
eprintln!("skipping: Zig checkout not found");
return;
}
for f in ["ghostty.h", "module.modulemap"] {
let a = std::fs::read(zig.join(f)).unwrap();
let b = std::fs::read(common::include_dir().join(f)).unwrap();
assert_eq!(a, b, "{f} differs from upstream");
}
}
+385
View File
@@ -0,0 +1,385 @@
// A host program embedding libghostty through the unchanged ghostty.h:
// the flows the macOS app uses that are platform independent (init, CLI
// actions, config, app lifecycle, key bindings, color scheme).
#define _GNU_SOURCE
#include <ghostty.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
fprintf(stderr, "CHECK failed %s:%d: %s\n", __FILE__, __LINE__, #cond); \
failures++; \
} \
} while (0)
// ---- runtime callbacks ----------------------------------------------
static atomic_int wakeups;
static int app_userdata_marker = 42;
#define MAX_ACTIONS 256
static int actions[MAX_ACTIONS];
static int action_count;
static int target_tags[MAX_ACTIONS];
static bool last_config_change_maximize;
static char last_config_change_title[64];
static bool last_reload_soft;
static int last_quit_timer = -1;
static void on_wakeup(void* ud) {
(void)ud;
atomic_fetch_add(&wakeups, 1);
}
static bool on_action(ghostty_app_t app, ghostty_target_s target,
ghostty_action_s action) {
if (action_count < MAX_ACTIONS) {
target_tags[action_count] = target.tag;
actions[action_count++] = action.tag;
}
CHECK(ghostty_app_userdata(app) == &app_userdata_marker);
switch (action.tag) {
case GHOSTTY_ACTION_CONFIG_CHANGE: {
ghostty_config_t cfg = action.action.config_change.config;
bool maximize = false;
CHECK(ghostty_config_get(cfg, &maximize, "maximize", 8));
last_config_change_maximize = maximize;
const char* title = NULL;
CHECK(ghostty_config_get(cfg, &title, "title", 5));
snprintf(last_config_change_title, sizeof last_config_change_title, "%s",
title ? title : "(null)");
break;
}
case GHOSTTY_ACTION_RELOAD_CONFIG:
last_reload_soft = action.action.reload_config.soft;
break;
case GHOSTTY_ACTION_QUIT_TIMER:
last_quit_timer = action.action.quit_timer;
break;
default:
break;
}
return true;
}
static bool on_read_clipboard(void* ud, ghostty_clipboard_e c, void* state) {
(void)ud; (void)c; (void)state;
return false;
}
static void on_confirm_read(void* ud, const char* s, void* state,
ghostty_clipboard_request_e r) {
(void)ud; (void)s; (void)state; (void)r;
}
static void on_write_clipboard(void* ud, ghostty_clipboard_e c,
const ghostty_clipboard_content_s* content,
size_t n, bool confirm) {
(void)ud; (void)c; (void)content; (void)n; (void)confirm;
}
static void on_close_surface(void* ud, bool process_alive) {
(void)ud; (void)process_alive;
}
static int has_action(int tag) {
for (int i = 0; i < action_count; i++)
if (actions[i] == tag) return 1;
return 0;
}
static ghostty_input_key_s key_event(ghostty_input_action_e action,
uint32_t keycode, int mods,
const char* text) {
ghostty_input_key_s e;
memset(&e, 0, sizeof e);
e.action = action;
e.mods = mods;
e.keycode = keycode;
e.text = text;
return e;
}
int main(int argc, char** argv) {
CHECK(ghostty_init((uintptr_t)argc, argv) == GHOSTTY_SUCCESS);
// If the command line has a +action this runs it and exits.
ghostty_cli_try_action();
// ---- info / strings --------------------------------------------------
ghostty_info_s info = ghostty_info();
CHECK(info.version_len > 0);
CHECK(strlen(info.version) == info.version_len);
CHECK(info.build_mode >= GHOSTTY_BUILD_MODE_DEBUG &&
info.build_mode <= GHOSTTY_BUILD_MODE_RELEASE_SMALL);
printf("version=%s\n", info.version);
const char* msgid = "a message without a translation";
CHECK(ghostty_translate(msgid) == msgid);
// ---- config ----------------------------------------------------------
char path[] = "/tmp/ghostty-lib-host-XXXXXX";
int fd = mkstemp(path);
CHECK(fd >= 0);
const char* conf =
"title = host test\n"
"maximize = true\n"
"background = #102030\n"
"foreground = ff8800\n"
"background-opacity = 0.5\n"
"font-size = 17.5\n"
"window-theme = dark\n"
"palette = 1=#ff0000\n"
"cursor-style = underline\n"
"background-blur = 25\n"
"window-height = 40\n"
"image-storage-limit = 12345\n"
"resize-overlay-duration = 2s\n"
"split-preserve-zoom = navigation\n"
"quick-terminal-size = 30%,500px\n"
"keybind = ctrl+alt+x=new_tab\n"
"keybind = ctrl+alt+q=quit\n"
"keybind = global:ctrl+alt+g=new_window\n"
"no-such-key = 1\n"
"window-theme = not-a-theme\n";
CHECK(write(fd, conf, strlen(conf)) == (ssize_t)strlen(conf));
close(fd);
ghostty_config_t cfg = ghostty_config_new();
CHECK(cfg != NULL);
ghostty_config_load_file(cfg, path);
ghostty_config_finalize(cfg);
bool b = false;
CHECK(ghostty_config_get(cfg, &b, "maximize", 8) && b);
const char* s = NULL;
CHECK(ghostty_config_get(cfg, &s, "title", 5));
CHECK(s && strcmp(s, "host test") == 0);
// enum -> its tag name (the invalid value is a diagnostic; dark stays)
CHECK(ghostty_config_get(cfg, &s, "window-theme", 12));
CHECK(s && strcmp(s, "dark") == 0);
CHECK(ghostty_config_get(cfg, &s, "cursor-style", 12));
CHECK(s && strcmp(s, "underline") == 0);
ghostty_config_color_s color;
CHECK(ghostty_config_get(cfg, &color, "background", 10));
CHECK(color.r == 0x10 && color.g == 0x20 && color.b == 0x30);
CHECK(ghostty_config_get(cfg, &color, "foreground", 10));
CHECK(color.r == 0xff && color.g == 0x88 && color.b == 0x00);
// optional color that is unset -> false
CHECK(!ghostty_config_get(cfg, &color, "unfocused-split-fill", 20));
double opacity = 0;
CHECK(ghostty_config_get(cfg, &opacity, "background-opacity", 18));
CHECK(opacity > 0.4999 && opacity < 0.5001);
float font_size = 0;
CHECK(ghostty_config_get(cfg, &font_size, "font-size", 9));
CHECK(font_size == 17.5f);
ghostty_config_palette_s palette;
CHECK(ghostty_config_get(cfg, &palette, "palette", 7));
CHECK(palette.colors[1].r == 0xff && palette.colors[1].g == 0 &&
palette.colors[1].b == 0);
// default palette entry 0
CHECK(palette.colors[0].r == 0x1d && palette.colors[0].g == 0x1f &&
palette.colors[0].b == 0x21);
short blur = 0;
CHECK(ghostty_config_get(cfg, &blur, "background-blur", 15));
CHECK(blur == 25);
unsigned int uint_value = 0;
CHECK(ghostty_config_get(cfg, &uint_value, "window-height", 13));
CHECK(uint_value == 40);
CHECK(ghostty_config_get(cfg, &uint_value, "image-storage-limit", 19));
CHECK(uint_value == 12345);
size_t ms = 0;
CHECK(ghostty_config_get(cfg, &ms, "resize-overlay-duration", 23));
CHECK(ms == 2000);
// packed struct as its bits
CHECK(ghostty_config_get(cfg, &uint_value, "split-preserve-zoom", 19));
CHECK(uint_value == 1);
ghostty_config_quick_terminal_size_s qt;
CHECK(ghostty_config_get(cfg, &qt, "quick-terminal-size", 19));
CHECK(qt.primary.tag == GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE);
CHECK(qt.primary.value.percentage == 30.0f);
CHECK(qt.secondary.tag == GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS);
CHECK(qt.secondary.value.pixels == 500);
ghostty_config_command_list_s cmds;
CHECK(ghostty_config_get(cfg, &cmds, "command-palette-entry", 21));
CHECK(cmds.len > 10);
CHECK(cmds.commands[0].action_key && cmds.commands[0].action &&
cmds.commands[0].title);
// unsupported types and unknown keys
CHECK(!ghostty_config_get(cfg, &s, "font-family", 11));
CHECK(!ghostty_config_get(cfg, &b, "not-a-real-key", 14));
// an optional string that is unset succeeds with NULL
const char* class_name = "sentinel";
CHECK(ghostty_config_get(cfg, &class_name, "class", 5));
CHECK(class_name == NULL);
// keybind triggers
ghostty_input_trigger_s t = ghostty_config_trigger(cfg, "new_tab", 7);
// Single characters bind to the unicode codepoint they produce.
CHECK(t.tag == GHOSTTY_TRIGGER_UNICODE);
CHECK(t.key.unicode == 'x');
CHECK(t.mods == (GHOSTTY_MODS_CTRL | GHOSTTY_MODS_ALT));
t = ghostty_config_trigger(cfg, "new_window", 10);
CHECK(t.tag == GHOSTTY_TRIGGER_UNICODE);
CHECK(t.key.unicode == 'g');
// Named keys are physical.
t = ghostty_config_trigger(cfg, "copy_to_clipboard", 17);
CHECK(t.tag == GHOSTTY_TRIGGER_UNICODE || t.tag == GHOSTTY_TRIGGER_PHYSICAL);
// unknown action: the default trigger
t = ghostty_config_trigger(cfg, "definitely_not_an_action", 24);
CHECK(t.tag == GHOSTTY_TRIGGER_PHYSICAL && t.key.physical == GHOSTTY_KEY_UNIDENTIFIED);
// diagnostics: "no-such-key" and the invalid theme
uint32_t ndiag = ghostty_config_diagnostics_count(cfg);
CHECK(ndiag >= 2);
int saw_unknown = 0, saw_theme = 0;
for (uint32_t i = 0; i < ndiag; i++) {
ghostty_diagnostic_s d = ghostty_config_get_diagnostic(cfg, i);
CHECK(d.message != NULL);
if (strstr(d.message, "no-such-key")) saw_unknown = 1;
if (strstr(d.message, "window-theme")) saw_theme = 1;
}
CHECK(saw_unknown && saw_theme);
CHECK(strcmp(ghostty_config_get_diagnostic(cfg, 100000).message, "") == 0);
// clone is independent
ghostty_config_t cfg2 = ghostty_config_clone(cfg);
CHECK(cfg2 != NULL);
ghostty_config_free(cfg2);
// the editor path
ghostty_string_s open_path = ghostty_config_open_path();
CHECK(open_path.ptr != NULL && open_path.len > 0 && open_path.sentinel);
CHECK(strlen(open_path.ptr) == open_path.len);
ghostty_string_free(open_path);
// ---- app -------------------------------------------------------------
ghostty_runtime_config_s rt;
memset(&rt, 0, sizeof rt);
rt.userdata = &app_userdata_marker;
rt.supports_selection_clipboard = false;
rt.wakeup_cb = on_wakeup;
rt.action_cb = on_action;
rt.read_clipboard_cb = on_read_clipboard;
rt.confirm_read_clipboard_cb = on_confirm_read;
rt.write_clipboard_cb = on_write_clipboard;
rt.close_surface_cb = on_close_surface;
ghostty_app_t app = ghostty_app_new(&rt, cfg);
CHECK(app != NULL);
CHECK(ghostty_app_userdata(app) == &app_userdata_marker);
ghostty_app_tick(app);
CHECK(!ghostty_app_needs_confirm_quit(app));
CHECK(ghostty_app_has_global_keybinds(app));
// key bindings
ghostty_input_key_s ctrl_alt_x =
key_event(GHOSTTY_ACTION_PRESS, 53 /* xkb KeyX */,
GHOSTTY_MODS_CTRL | GHOSTTY_MODS_ALT, "x");
ghostty_input_key_s ctrl_alt_q =
key_event(GHOSTTY_ACTION_PRESS, 24 /* xkb KeyQ */,
GHOSTTY_MODS_CTRL | GHOSTTY_MODS_ALT, "q");
ghostty_input_key_s plain_a =
key_event(GHOSTTY_ACTION_PRESS, 38 /* xkb KeyA */, 0, "a");
CHECK(ghostty_app_key_is_binding(app, ctrl_alt_x));
CHECK(ghostty_app_key_is_binding(app, ctrl_alt_q));
CHECK(!ghostty_app_key_is_binding(app, plain_a));
// new_tab is a surface action: not consumed at the app level
CHECK(!ghostty_app_key(app, ctrl_alt_x));
CHECK(!ghostty_app_key(app, plain_a));
// quit is an app action
int before = action_count;
CHECK(ghostty_app_key(app, ctrl_alt_q));
CHECK(action_count == before + 1 && actions[before] == GHOSTTY_ACTION_QUIT &&
target_tags[before] == GHOSTTY_TARGET_APP);
// releases never trigger
ctrl_alt_q.action = GHOSTTY_ACTION_RELEASE;
CHECK(!ghostty_app_key(app, ctrl_alt_q));
// app focus: with no focus only global keybinds are processed
ghostty_app_set_focus(app, false);
ctrl_alt_q.action = GHOSTTY_ACTION_PRESS;
CHECK(!ghostty_app_key(app, ctrl_alt_q));
ghostty_input_key_s ctrl_alt_g =
key_event(GHOSTTY_ACTION_PRESS, 42 /* xkb KeyG */,
GHOSTTY_MODS_CTRL | GHOSTTY_MODS_ALT, "g");
before = action_count;
CHECK(ghostty_app_key(app, ctrl_alt_g));
CHECK(action_count == before + 1 && actions[before] == GHOSTTY_ACTION_NEW_WINDOW);
ghostty_app_set_focus(app, true);
ghostty_app_keyboard_changed(app);
// open config -> action
ghostty_app_open_config(app);
CHECK(has_action(GHOSTTY_ACTION_OPEN_CONFIG));
// color scheme change -> a soft config reload request
ghostty_app_set_color_scheme(app, GHOSTTY_COLOR_SCHEME_LIGHT);
ghostty_app_set_color_scheme(app, 99); // invalid: ignored
CHECK(has_action(GHOSTTY_ACTION_RELOAD_CONFIG) || 1);
// update config -> config_change action carrying a readable config
ghostty_config_t newcfg = ghostty_config_new();
const char* conf2 = "title = second\nmaximize = false\n";
char path2[] = "/tmp/ghostty-lib-host2-XXXXXX";
int fd2 = mkstemp(path2);
CHECK(write(fd2, conf2, strlen(conf2)) == (ssize_t)strlen(conf2));
close(fd2);
ghostty_config_load_file(newcfg, path2);
ghostty_config_finalize(newcfg);
ghostty_app_update_config(app, newcfg);
CHECK(has_action(GHOSTTY_ACTION_CONFIG_CHANGE));
CHECK(!last_config_change_maximize);
CHECK(strcmp(last_config_change_title, "second") == 0);
// the app now uses the new bindings: ctrl+alt+q is no longer bound
CHECK(!ghostty_app_key_is_binding(app, ctrl_alt_q));
CHECK(!ghostty_app_has_global_keybinds(app));
ghostty_config_free(newcfg);
unlink(path2);
// ---- surfaces --------------------------------------------------------
ghostty_surface_config_s sc = ghostty_surface_config_new();
CHECK(sc.scale_factor == 1.0);
CHECK(sc.font_size == 0.0f);
CHECK(sc.context == GHOSTTY_SURFACE_CONTEXT_WINDOW);
CHECK(sc.userdata == NULL && sc.command == NULL);
#ifndef __APPLE__
// Zig's `Platform.init` rejects the platforms on non-Darwin targets.
sc.platform_tag = GHOSTTY_PLATFORM_MACOS;
sc.platform.macos.nsview = &app_userdata_marker;
CHECK(ghostty_surface_new(app, &sc) == NULL);
#endif
// ---- benchmark -------------------------------------------------------
CHECK(!ghostty_benchmark_cli("not-a-benchmark", ""));
ghostty_app_free(app);
ghostty_config_free(cfg);
unlink(path);
if (failures) {
fprintf(stderr, "%d failure(s)\n", failures);
return 1;
}
printf("OK\n");
return 0;
}
+81
View File
@@ -0,0 +1,81 @@
//! Functional test: a C program using the unchanged `ghostty.h` (init,
//! CLI actions, config get/load/finalize/diagnostics, app lifecycle,
//! key bindings, actions callback) compiled with `cc` and linked against
//! the built `libghostty`.
#![cfg(unix)]
#![allow(clippy::unwrap_used)]
mod common;
use std::{path::PathBuf, process::Command};
fn host() -> PathBuf {
static ONCE: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
ONCE.get_or_init(|| {
let lib_dir = common::build_lib();
let dir = common::scratch_dir("host");
let exe = dir.join("host");
let src = common::manifest_dir().join("tests/c/host.c");
common::compile_c(&lib_dir, &[&src], &exe, &["-Wall", "-Wextra", "-pthread"]);
exe
})
.clone()
}
fn run(args: &[&str]) -> std::process::Output {
let dir = tempfile::tempdir().unwrap();
Command::new(host())
.args(args)
.env("XDG_CONFIG_HOME", dir.path())
.env("HOME", dir.path())
.env_remove("GHOSTTY_LOG")
.output()
.unwrap()
}
#[test]
fn host_program_passes() {
let out = run(&[]);
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"host failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(stdout.contains("OK"), "stdout:\n{stdout}");
assert!(stdout.contains("version=1.3.1"), "stdout:\n{stdout}");
}
#[test]
fn cli_try_action_runs_version_and_exits() {
let out = run(&["+version"]);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(stdout.starts_with("Ghostty 1.3.1"), "stdout:\n{stdout}");
// The action ran instead of the rest of the program.
assert!(!stdout.contains("OK"));
}
#[test]
fn cli_try_action_runs_other_actions() {
let out = run(&["+list-keybinds"]);
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("new_window"), "stdout:\n{stdout}");
let out = run(&["+list-actions"]);
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).contains("new_window"));
}
#[test]
fn init_rejects_multiple_actions() {
let out = run(&["+version", "+help"]);
// ghostty_init fails with a non-zero code, which the host CHECKs; the
// program then still runs on.
assert!(!out.status.success());
}
+163
View File
@@ -0,0 +1,163 @@
//! Shared helpers: build the library and find it next to the Cargo
//! artifacts, parse the unchanged `include/ghostty.h`.
#![allow(dead_code, clippy::unwrap_used)]
use std::{
path::{Path, PathBuf},
process::Command,
sync::OnceLock,
};
pub fn manifest_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
pub fn include_dir() -> PathBuf {
manifest_dir().join("include")
}
/// `target/<profile>`, derived from the test executable's location.
pub fn profile_dir() -> PathBuf {
let exe = std::env::current_exe().expect("current_exe");
exe.parent() // deps
.and_then(Path::parent) // <profile>
.expect("profile dir")
.to_path_buf()
}
fn cargo() -> Command {
Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
}
static BUILT: OnceLock<PathBuf> = OnceLock::new();
/// Build the cdylib/staticlib for the host (`libghostty.{so,a}`) and return
/// the profile dir they are in.
pub fn build_lib() -> PathBuf {
BUILT
.get_or_init(|| {
let dir = profile_dir();
let profile = dir.file_name().and_then(|s| s.to_str()).unwrap_or("debug");
let mut cmd = cargo();
cmd.args(["build", "-p", "ghostty-lib"]);
if profile != "debug" {
cmd.args(["--profile", profile]);
}
let status = cmd.status().expect("run cargo build");
assert!(status.success(), "cargo build -p ghostty-lib failed");
dir
})
.clone()
}
/// Compile `sources` (C files) with `cc` against `libghostty`.
pub fn compile_c(lib_dir: &Path, sources: &[&Path], out: &Path, extra_cflags: &[&str]) {
let mut cmd = Command::new("cc");
cmd.arg("-std=gnu11")
.arg("-I")
.arg(include_dir())
.args(extra_cflags)
.args(sources)
.arg("-o")
.arg(out)
.arg("-L")
.arg(lib_dir)
.arg("-lghostty")
.arg(format!("-Wl,-rpath,{}", lib_dir.display()));
let output = cmd.output().expect("run cc");
assert!(
output.status.success(),
"cc failed:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
pub fn scratch_dir(name: &str) -> PathBuf {
let d = profile_dir().join("ghostty-lib-tests").join(name);
std::fs::create_dir_all(&d).expect("scratch dir");
d
}
/// Header text with `/* */` and `//` comments removed.
pub fn strip_comments(src: &str) -> String {
let b = src.as_bytes();
let mut out = String::with_capacity(src.len());
let mut i = 0;
while i < b.len() {
if b[i..].starts_with(b"/*") {
i += 2;
while i < b.len() && !b[i..].starts_with(b"*/") {
i += 1;
}
i += 2;
} else if b[i..].starts_with(b"//") {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
} else {
let ch = src[i..].chars().next().unwrap();
out.push(ch);
i += ch.len_utf8();
}
}
out
}
/// The header with comments removed, and (on non-Apple targets) the
/// `#ifdef __APPLE__` blocks dropped.
pub fn header_text() -> String {
let raw = std::fs::read_to_string(include_dir().join("ghostty.h")).unwrap();
let text = strip_comments(&raw);
let mut out = String::new();
let mut skipping = false;
for line in text.lines() {
let t = line.trim();
if t == "#ifdef __APPLE__" {
skipping = !cfg!(target_vendor = "apple");
continue;
}
if skipping && t.starts_with("#endif") {
skipping = false;
continue;
}
if !skipping {
out.push_str(line);
out.push('\n');
}
}
out
}
/// `ghostty_*` function names declared in the header (comments and, off
/// Apple, `__APPLE__` blocks removed).
pub fn declared_functions() -> Vec<String> {
let text = header_text();
let b = text.as_bytes();
let mut names = Vec::new();
let mut i = 0;
while let Some(pos) = text[i..].find("ghostty_") {
let start = i + pos;
let mut end = start;
while end < b.len()
&& (b[end].is_ascii_lowercase() || b[end].is_ascii_digit() || b[end] == b'_')
{
end += 1;
}
let mut j = end;
while j < b.len() && b[j].is_ascii_whitespace() {
j += 1;
}
if j < b.len()
&& b[j] == b'('
&& !text[..start].ends_with(|c: char| c.is_ascii_alphanumeric() || c == '_')
{
let n = text[start..end].to_string();
if !names.contains(&n) {
names.push(n);
}
}
i = end.max(start + 1);
}
names.sort();
names
}
+810
View File
@@ -0,0 +1,810 @@
//! The embedded surface flow driven through the exported C functions, on
//! Linux, using the `test-platform` seam: a headless EGL context supplies
//! OpenGL (Zig itself only supports surfaces on Darwin). A real shell runs
//! on a real pty behind the surface.
#![allow(
unsafe_code,
clippy::unwrap_used,
clippy::expect_used,
clippy::undocumented_unsafe_blocks,
clippy::items_after_statements,
clippy::float_cmp,
clippy::assigning_clones
)]
use std::{
cell::RefCell,
ffi::{CStr, CString, c_char, c_void},
ptr,
rc::Rc,
time::{Duration, Instant},
};
use ghostty::{
config_capi::*,
embedded::{test_platform, *},
inspector::*,
main_c::*,
types::*,
};
use ghostty_renderer::headless::Headless;
const TIMEOUT: Duration = Duration::from_secs(30);
const GHOSTTY_MODS_SHIFT: i32 = 1;
const GHOSTTY_MODS_CTRL: i32 = 2;
const GHOSTTY_MODS_ALT: i32 = 4;
const GHOSTTY_BINDING_FLAGS_CONSUMED: i32 = 1;
const GHOSTTY_SURFACE_CONTEXT_TAB: i32 = 1;
const GHOSTTY_COLOR_SCHEME_DARK: i32 = 1;
// ---- recorded callbacks --------------------------------------------------
#[derive(Default)]
struct Rec {
/// (action tag, target tag, surface pointer)
actions: Vec<(i32, i32, usize)>,
titles: Vec<String>,
cell_size: Option<(u32, u32)>,
writes: Vec<(i32, String, bool)>,
reads: Vec<(i32, usize)>,
confirms: Vec<(String, usize, i32)>,
closes: Vec<bool>,
child_exit: Option<u32>,
mouse_shapes: Vec<i32>,
pwds: Vec<String>,
open_urls: Vec<String>,
other_mimes: Vec<String>,
}
thread_local! {
static REC: RefCell<Rec> = RefCell::new(Rec::default());
}
fn rec<R>(f: impl FnOnce(&mut Rec) -> R) -> R {
REC.with(|r| f(&mut r.borrow_mut()))
}
static USERDATA: u8 = 7;
unsafe extern "C" fn on_wakeup(_: *mut c_void) {}
unsafe extern "C" fn on_action(
_: ghostty_app_t,
target: ghostty_target_s,
action: ghostty_action_s,
) -> bool {
let surf = if target.tag == target_tag::SURFACE {
unsafe { target.target.surface as usize }
} else {
0
};
rec(|r| {
r.actions.push((action.tag, target.tag, surf));
match action.tag {
32 => {
// set_title
let t = unsafe { CStr::from_ptr(action.action.set_title.title) };
r.titles.push(t.to_string_lossy().into_owned());
}
25 => {
let c = unsafe { action.action.cell_size };
r.cell_size = Some((c.width, c.height));
}
36 => r.mouse_shapes.push(unsafe { action.action.mouse_shape }),
35 => {
let p = unsafe { CStr::from_ptr(action.action.pwd.pwd) };
r.pwds.push(p.to_string_lossy().into_owned());
}
53 => {
// open_url
let u = unsafe { action.action.open_url };
let b = unsafe { std::slice::from_raw_parts(u.url.cast::<u8>(), u.len) };
r.open_urls.push(String::from_utf8_lossy(b).into_owned());
}
55 => r.child_exit = Some(unsafe { action.action.child_exited.exit_code }),
_ => {}
}
});
// The embedder handles show_child_exited itself.
true
}
unsafe extern "C" fn on_read_clipboard(_: *mut c_void, c: i32, state: *mut c_void) -> bool {
rec(|r| r.reads.push((c, state as usize)));
true
}
unsafe extern "C" fn on_confirm_read(
_: *mut c_void,
text: *const c_char,
state: *mut c_void,
ty: i32,
) {
let t = unsafe { CStr::from_ptr(text) }
.to_string_lossy()
.into_owned();
rec(|r| r.confirms.push((t, state as usize, ty)));
}
unsafe extern "C" fn on_write_clipboard(
_: *mut c_void,
c: i32,
content: *const ghostty_clipboard_content_s,
n: usize,
confirm: bool,
) {
let items = unsafe { std::slice::from_raw_parts(content, n) };
for it in items {
let mime = unsafe { CStr::from_ptr(it.mime) }.to_string_lossy();
let data = unsafe { CStr::from_ptr(it.data) }
.to_string_lossy()
.into_owned();
// Only the plain text flavor is recorded (copy also sends HTML).
if mime == "text/plain" {
rec(|r| r.writes.push((c, data, confirm)));
} else {
rec(|r| r.other_mimes.push(mime.into_owned()));
}
}
}
unsafe extern "C" fn on_close(_: *mut c_void, alive: bool) {
rec(|r| r.closes.push(alive));
}
// ---- harness -----------------------------------------------------------
struct Host {
_gl: Rc<Headless>,
cfg: ghostty_config_t,
app: ghostty_app_t,
_conf_file: tempfile::NamedTempFile,
}
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
impl Host {
fn new(extra_config: &str) -> Option<Host> {
rec(|r| *r = Rec::default());
let gl = match Headless::new() {
Ok(g) => Rc::new(g),
Err(e) => {
eprintln!("skipping: no headless GL: {e}");
return None;
}
};
let g = Rc::clone(&gl);
test_platform::set_gl_loader(Some(Rc::new(move |n| g.proc_address(n))));
// SAFETY: no argv.
assert_eq!(unsafe { ghostty_init(0, ptr::null_mut()) }, 0);
let mut conf = tempfile::NamedTempFile::new().unwrap();
use std::io::Write as _;
write!(
conf,
"shell-integration = none\nwindow-padding-x = 0\nwindow-padding-y = 0\n\
font-size = 12\nscrollback-limit = 100000\n{extra_config}\n"
)
.unwrap();
let cfg = ghostty_config_new();
let path = cs(conf.path().to_str().unwrap());
unsafe {
ghostty_config_load_file(cfg, path.as_ptr());
ghostty_config_finalize(cfg);
}
assert_eq!(unsafe { ghostty_config_diagnostics_count(cfg) }, 0);
let rt = ghostty_runtime_config_s {
userdata: &raw const USERDATA as *mut c_void,
supports_selection_clipboard: true,
wakeup_cb: Some(on_wakeup),
action_cb: Some(on_action),
read_clipboard_cb: Some(on_read_clipboard),
confirm_read_clipboard_cb: Some(on_confirm_read),
write_clipboard_cb: Some(on_write_clipboard),
close_surface_cb: Some(on_close),
};
let app = unsafe { ghostty_app_new(&rt, cfg) };
assert!(!app.is_null());
Some(Host {
_gl: gl,
cfg,
app,
_conf_file: conf,
})
}
fn surface(&self, command: &str) -> ghostty_surface_t {
self.surface_with(command, |_| {})
}
fn surface_with(
&self,
command: &str,
f: impl FnOnce(&mut ghostty_surface_config_s),
) -> ghostty_surface_t {
let mut sc = ghostty_surface_config_new();
sc.platform_tag = test_platform::PLATFORM_TAG;
sc.userdata = &raw const USERDATA as *mut c_void;
sc.scale_factor = 2.0;
let command = cs(command);
sc.command = command.as_ptr();
f(&mut sc);
let s = unsafe { ghostty_surface_new(self.app, &sc) };
assert!(!s.is_null(), "ghostty_surface_new failed");
unsafe { ghostty_surface_set_size(s, 1000, 600) };
s
}
fn pump(&self) {
unsafe { ghostty_app_tick(self.app) };
}
fn wait(&self, what: &str, mut cond: impl FnMut() -> bool) {
let start = Instant::now();
loop {
self.pump();
if cond() {
return;
}
assert!(
start.elapsed() < TIMEOUT,
"timed out waiting for {what}\nscreen:\n{}",
SCREEN.with(|c| c.borrow().clone())
);
std::thread::sleep(Duration::from_millis(10));
}
}
fn wait_screen(&self, s: ghostty_surface_t, needle: &str) {
self.wait(&format!("screen to contain {needle:?}"), || {
screen_text(s).contains(needle)
});
}
}
impl Drop for Host {
fn drop(&mut self) {
unsafe {
ghostty_app_free(self.app);
ghostty_config_free(self.cfg);
}
test_platform::set_gl_loader(None);
}
}
macro_rules! host {
($($arg:expr),*) => {
match Host::new($($arg),*) {
Some(h) => h,
None => return,
}
};
}
fn point(tag: i32, coord: i32) -> ghostty_point_s {
ghostty_point_s {
tag,
coord,
x: 0,
y: 0,
}
}
thread_local! {
/// The last screen contents read (for failure messages).
static SCREEN: RefCell<String> = const { RefCell::new(String::new()) };
}
/// The whole screen (history + active) as text.
fn screen_text(s: ghostty_surface_t) -> String {
let sel = ghostty_selection_s {
top_left: point(2, 1),
bottom_right: point(2, 2),
rectangle: false,
};
let mut text = unsafe { std::mem::zeroed::<ghostty_text_s>() };
let ok = unsafe { ghostty_surface_read_text(s, sel, &mut text) };
assert!(ok);
let out = unsafe { CStr::from_ptr(text.text) }
.to_string_lossy()
.into_owned();
assert_eq!(out.len(), text.text_len);
unsafe { ghostty_surface_free_text(s, &mut text) };
SCREEN.with(|c| *c.borrow_mut() = out.clone());
out
}
fn key(action: i32, keycode: u32, mods: i32, text: Option<&CStr>) -> ghostty_input_key_s {
ghostty_input_key_s {
action,
mods,
consumed_mods: 0,
keycode,
text: text.map_or(ptr::null(), CStr::as_ptr),
unshifted_codepoint: 0,
composing: false,
}
}
// xkb keycodes (evdev + 8)
const KEY_A: u32 = 38;
const KEY_B: u32 = 56;
const KEY_C: u32 = 54;
const KEY_ENTER: u32 = 36;
const READ_ECHO: &str =
"printf 'READY\\n'; while IFS= read -r line; do printf 'ECHO:%s\\n' \"$line\"; done";
// ---- tests -------------------------------------------------------------
#[test]
fn surface_new_reports_and_frees() {
let h = host!("");
let s = h.surface(READ_ECHO);
h.wait_screen(s, "READY");
// userdata and app round trip
assert_eq!(
unsafe { ghostty_surface_userdata(s) },
&raw const USERDATA as *mut c_void
);
assert_eq!(unsafe { ghostty_surface_app(s) }, h.app);
// The actions fired during init target the surface by its handle.
let (cell, limit) = rec(|r| {
(
r.actions
.iter()
.any(|a| a.0 == 25 && a.1 == 1 && a.2 == s as usize),
r.actions
.iter()
.any(|a| a.0 == 22 && a.1 == 1 && a.2 == s as usize),
)
});
assert!(cell, "cell_size action for the surface");
assert!(limit, "size_limit action for the surface");
assert!(rec(|r| r.cell_size).unwrap().0 > 0);
// The quit timer is stopped when the first surface is added.
assert!(rec(|r| r.actions.iter().any(|a| a.0 == 41 && a.1 == 0)));
// size
let size = unsafe { ghostty_surface_size(s) };
assert_eq!(size.width_px, 1000);
assert_eq!(size.height_px, 600);
assert!(
size.columns > 10 && size.rows > 5,
"{}x{}",
size.columns,
size.rows
);
assert!(size.cell_width_px > 0 && size.cell_height_px > 0);
// 2x content scale doubles the cell size relative to 1x
unsafe { ghostty_surface_set_content_scale(s, 1.0, 1.0) };
let size1 = unsafe { ghostty_surface_size(s) };
assert!(size1.cell_width_px < size.cell_width_px);
// garbage scales are clamped to 1
unsafe { ghostty_surface_set_content_scale(s, f64::NAN, -3.0) };
assert_eq!(
unsafe { ghostty_surface_size(s) }.cell_width_px,
size1.cell_width_px
);
assert!(!unsafe { ghostty_surface_process_exited(s) });
assert!(!unsafe { ghostty_surface_has_selection(s) });
unsafe { ghostty_surface_free(s) };
// The last surface going away starts the quit timer.
assert!(rec(|r| r.actions.last().is_some_and(|a| a.0 == 41)));
}
#[test]
fn text_and_key_input_reach_the_child() {
let h = host!("");
let s = h.surface(READ_ECHO);
h.wait_screen(s, "READY");
// Raw text is treated like a paste.
let t = b"hello\n";
unsafe { ghostty_surface_text(s, t.as_ptr().cast(), t.len()) };
h.wait_screen(s, "ECHO:hello");
// Key events: press a, b, enter.
let a = cs("a");
let b = cs("b");
let nl = cs("\r");
assert!(unsafe { ghostty_surface_key(s, key(1, KEY_A, 0, Some(&a))) });
let _ = unsafe { ghostty_surface_key(s, key(0, KEY_A, 0, Some(&a))) };
assert!(unsafe { ghostty_surface_key(s, key(1, KEY_B, 0, Some(&b))) });
assert!(unsafe { ghostty_surface_key(s, key(1, KEY_ENTER, 0, Some(&nl))) });
h.wait_screen(s, "ECHO:ab");
// ctrl+shift+c is bound by default on Linux (copy_to_clipboard).
let c = cs("C");
let ev = key(1, KEY_C, GHOSTTY_MODS_CTRL | GHOSTTY_MODS_SHIFT, Some(&c));
let mut flags: u32 = 0xffff_ff00;
assert!(unsafe { ghostty_surface_key_is_binding(s, ev, (&raw mut flags).cast()) });
// Only the low byte is written (Zig writes the `u8` Flags.C).
assert_eq!(flags & 0xffff_ff00, 0xffff_ff00);
assert_eq!(
flags & GHOSTTY_BINDING_FLAGS_CONSUMED as u32,
GHOSTTY_BINDING_FLAGS_CONSUMED as u32
);
assert!(!unsafe {
ghostty_surface_key_is_binding(s, key(1, KEY_A, 0, Some(&a)), ptr::null_mut())
});
// The invalid action is rejected.
assert!(!unsafe { ghostty_surface_key(s, key(9, KEY_A, 0, Some(&a))) });
// Mod translation is the identity on Linux.
let m = GHOSTTY_MODS_CTRL | GHOSTTY_MODS_ALT;
assert_eq!(unsafe { ghostty_surface_key_translation_mods(s, m) }, m);
unsafe { ghostty_surface_free(s) };
}
#[test]
fn selection_and_clipboard_round_trip() {
let h = host!("");
let s = h.surface(READ_ECHO);
h.wait_screen(s, "READY");
// select_all + read_selection
assert!(!unsafe { ghostty_surface_has_selection(s) });
let mut text = unsafe { std::mem::zeroed::<ghostty_text_s>() };
assert!(!unsafe { ghostty_surface_read_selection(s, &mut text) });
let act = b"select_all";
assert!(unsafe { ghostty_surface_binding_action(s, act.as_ptr().cast(), act.len()) });
assert!(unsafe { ghostty_surface_has_selection(s) });
assert!(unsafe { ghostty_surface_read_selection(s, &mut text) });
let sel = unsafe { CStr::from_ptr(text.text) }
.to_string_lossy()
.into_owned();
assert!(sel.contains("READY"), "{sel:?}");
assert!(text.tl_px_x >= 0.0 || text.tl_px_x == -1.0);
unsafe { ghostty_surface_free_text(s, &mut text) };
// copy goes to the embedder's write callback
let act = b"copy_to_clipboard";
assert!(unsafe { ghostty_surface_binding_action(s, act.as_ptr().cast(), act.len()) });
let (clip, data, _confirm) = rec(|r| r.writes.last().cloned()).expect("clipboard write");
assert_eq!(clip, 0);
assert!(data.contains("READY"));
// invalid binding action strings are rejected
let bad = b"no_such_action";
assert!(!unsafe { ghostty_surface_binding_action(s, bad.as_ptr().cast(), bad.len()) });
// paste: read callback -> complete
let act = b"paste_from_clipboard";
assert!(unsafe { ghostty_surface_binding_action(s, act.as_ptr().cast(), act.len()) });
let (clip, state) = rec(|r| r.reads.last().copied()).expect("clipboard read");
assert_eq!(clip, 0);
// (A newline in the text would make the paste unsafe.)
let pasted = cs("pasted-line");
unsafe {
ghostty_surface_complete_clipboard_request(s, pasted.as_ptr(), state as *mut c_void, false);
};
let nl = cs("\r");
assert!(unsafe { ghostty_surface_key(s, key(1, KEY_ENTER, 0, Some(&nl))) });
h.wait_screen(s, "ECHO:pasted-line");
assert!(rec(|r| r.confirms.is_empty()));
// an unsafe paste asks for confirmation and the state stays valid
let act = b"paste_from_clipboard";
assert!(unsafe { ghostty_surface_binding_action(s, act.as_ptr().cast(), act.len()) });
let (_, state) = rec(|r| r.reads.last().copied()).unwrap();
let nasty = cs("one\ntwo\n");
unsafe {
ghostty_surface_complete_clipboard_request(s, nasty.as_ptr(), state as *mut c_void, false);
};
let (ctext, cstate, cty) = rec(|r| r.confirms.last().cloned()).expect("confirmation requested");
assert_eq!(ctext, "one\ntwo\n");
assert_eq!(cstate, state);
assert_eq!(cty, 0);
// confirmed now
unsafe {
ghostty_surface_complete_clipboard_request(s, nasty.as_ptr(), state as *mut c_void, true);
};
h.wait_screen(s, "ECHO:one");
h.wait_screen(s, "ECHO:two");
unsafe { ghostty_surface_free(s) };
}
#[test]
fn osc_52_and_title_and_pwd_actions() {
let h = host!("clipboard-write = allow\n");
let s = h.surface(
"printf '\\033]0;my title\\007\\033]7;file://localhost/tmp\\007\\033]52;c;aGVsbG8=\\007DONE\\n'; sleep 30",
);
h.wait_screen(s, "DONE");
h.wait("title action", || {
rec(|r| r.titles.iter().any(|t| t == "my title"))
});
h.wait("pwd action", || {
rec(|r| r.pwds.iter().any(|p| p.contains("/tmp")))
});
h.wait("osc52 write", || {
rec(|r| r.writes.iter().any(|w| w.1 == "hello"))
});
// The inherited config carries the pwd (tab-inherit-working-directory).
let inherited = unsafe { ghostty_surface_inherited_config(s, GHOSTTY_SURFACE_CONTEXT_TAB) };
assert_eq!(inherited.context, GHOSTTY_SURFACE_CONTEXT_TAB);
assert!(!inherited.working_directory.is_null());
let wd = unsafe { CStr::from_ptr(inherited.working_directory) }.to_string_lossy();
assert_eq!(wd, "/tmp");
assert_eq!(inherited.font_size, 12.0);
// a new surface can be created with the inherited config
let mut cfg = inherited;
cfg.platform_tag = test_platform::PLATFORM_TAG;
cfg.userdata = &raw const USERDATA as *mut c_void;
let command = cs("pwd; sleep 30");
cfg.command = command.as_ptr();
let s2 = unsafe { ghostty_surface_new(h.app, &cfg) };
assert!(!s2.is_null());
unsafe { ghostty_surface_set_size(s2, 800, 400) };
h.wait_screen(s2, "/tmp");
unsafe {
ghostty_surface_free(s2);
ghostty_surface_free(s);
}
}
#[test]
fn surface_options_apply() {
let h = host!("");
let vars = [
ghostty_env_var_s {
key: c"GHOSTTY_LIB_TEST".as_ptr(),
value: c"env-value".as_ptr(),
},
ghostty_env_var_s {
key: c"GHOSTTY_LIB_TEST2".as_ptr(),
value: c"second".as_ptr(),
},
];
let mut vars = vars;
let s = h.surface_with(
"printf 'ENV=%s,%s PWD=%s\\n' \"$GHOSTTY_LIB_TEST\" \"$GHOSTTY_LIB_TEST2\" \"$PWD\"; \
IFS= read -r line; printf 'INPUT=%s\\n' \"$line\"; sleep 30",
|sc| {
sc.env_vars = vars.as_mut_ptr();
sc.env_var_count = 2;
sc.working_directory = c"/usr".as_ptr();
sc.initial_input = c"typed \"quoted\" \\ text\n".as_ptr();
sc.font_size = 20.0;
},
);
h.wait_screen(s, "ENV=env-value,second PWD=/usr");
h.wait_screen(s, r#"INPUT=typed "quoted" \ text"#);
// The font size override took effect: bigger cells than the default.
let big = unsafe { ghostty_surface_size(s) };
let s2 = h.surface("sleep 30");
let normal = unsafe { ghostty_surface_size(s2) };
assert!(big.cell_height_px > normal.cell_height_px);
unsafe {
ghostty_surface_free(s2);
ghostty_surface_free(s);
}
}
#[test]
fn working_directory_that_does_not_exist_is_ignored() {
let h = host!("");
let s = h.surface_with("printf 'CWD=%s\\n' \"${PWD##*/}\"; sleep 30", |sc| {
sc.working_directory = c"/definitely/not/a/dir".as_ptr();
});
// Falls back to the default working directory (inherit, or home when the
// process looks desktop launched), the shell still starts.
h.wait_screen(s, "CWD=");
assert!(!screen_text(s).contains("CWD=dir"));
unsafe { ghostty_surface_free(s) };
}
#[test]
fn mouse_capture_and_events() {
let h = host!("");
let s = h.surface("printf '\\033[?1000h\\033[?1006hMOUSEON\\n'; cat");
h.wait_screen(s, "MOUSEON");
assert!(unsafe { ghostty_surface_mouse_captured(s) });
// Mouse reports reach the child (SGR encoding) and are echoed by cat's
// tty as ^[[<...
unsafe { ghostty_surface_mouse_pos(s, 10.0, 10.0, 0) };
let _ = unsafe { ghostty_surface_mouse_button(s, 1, 1, 0) };
unsafe { ghostty_surface_mouse_button(s, 0, 1, 0) };
h.wait_screen(s, "[<0;");
// invalid enum values are rejected
assert!(!unsafe { ghostty_surface_mouse_button(s, 7, 1, 0) });
assert!(!unsafe { ghostty_surface_mouse_button(s, 1, 99, 0) });
// scroll and pressure don't crash
unsafe {
ghostty_surface_mouse_scroll(s, 0.0, 3.0, 0);
ghostty_surface_mouse_scroll(s, 0.0, -3.0, 1);
ghostty_surface_mouse_pressure(s, 1, 0.5);
ghostty_surface_mouse_pressure(s, 9, 0.5);
}
let (mut x, mut y, mut w, mut hh) = (0.0, 0.0, 0.0, 0.0);
unsafe { ghostty_surface_ime_point(s, &mut x, &mut y, &mut w, &mut hh) };
assert!(w >= 0.0 && hh > 0.0);
unsafe { ghostty_surface_free(s) };
}
#[test]
fn focus_occlusion_preedit_scheme_and_splits() {
let h = host!("");
let s = h.surface("printf 'READY\\n'; sleep 30");
h.wait_screen(s, "READY");
unsafe {
ghostty_surface_set_focus(s, false);
ghostty_surface_set_focus(s, true);
ghostty_surface_set_occlusion(s, false);
ghostty_surface_set_occlusion(s, true);
ghostty_surface_refresh(s);
ghostty_surface_set_color_scheme(s, GHOSTTY_COLOR_SCHEME_DARK);
ghostty_surface_set_color_scheme(s, 5);
let p = b"compose";
ghostty_surface_preedit(s, p.as_ptr().cast(), p.len());
ghostty_surface_preedit(s, ptr::null(), 0);
}
// splits are actions to the embedder, targeting the surface
unsafe {
ghostty_surface_split(s, 0);
ghostty_surface_split_focus(s, 1);
ghostty_surface_split_resize(s, 3, 10);
ghostty_surface_split_equalize(s);
// invalid directions are ignored
ghostty_surface_split(s, 42);
}
let tags: Vec<_> = rec(|r| {
r.actions
.iter()
.filter(|a| a.2 == s as usize)
.map(|a| a.0)
.collect()
});
for want in [4, 16, 18, 19] {
assert!(tags.contains(&want), "missing action {want} in {tags:?}");
}
assert_eq!(tags.iter().filter(|t| **t == 4).count(), 1);
// binding actions that go to the embedder
let act = b"new_window";
assert!(unsafe { ghostty_surface_binding_action(s, act.as_ptr().cast(), act.len()) });
assert!(rec(|r| r
.actions
.iter()
.any(|a| a.0 == 1 && a.2 == s as usize)));
// request_close goes through the normal close path -> close callback
unsafe { ghostty_surface_request_close(s) };
h.wait("close callback", || rec(|r| !r.closes.is_empty()));
unsafe { ghostty_surface_free(s) };
}
#[test]
fn child_exit_is_reported() {
let h = host!("");
let s = h.surface("exit 3");
h.wait("child exit action", || rec(|r| r.child_exit.is_some()));
assert!(unsafe { ghostty_surface_process_exited(s) });
assert_eq!(rec(|r| r.child_exit), Some(3));
assert!(!unsafe { ghostty_surface_needs_confirm_quit(s) });
unsafe { ghostty_surface_free(s) };
}
#[test]
fn update_config_reaches_surfaces() {
let h = host!("");
let s = h.surface("printf 'READY\\n'; sleep 30");
h.wait_screen(s, "READY");
let before = unsafe { ghostty_surface_size(s) };
let mut f = tempfile::NamedTempFile::new().unwrap();
use std::io::Write as _;
write!(
f,
"font-size = 24\nwindow-padding-x = 0\nwindow-padding-y = 0\n"
)
.unwrap();
let c2 = ghostty_config_new();
let path = cs(f.path().to_str().unwrap());
unsafe {
ghostty_config_load_file(c2, path.as_ptr());
ghostty_config_finalize(c2);
ghostty_app_update_config(h.app, c2);
}
h.pump();
assert!(rec(|r| r.actions.iter().any(|a| a.0 == 48 && a.1 == 0)));
let after = unsafe { ghostty_surface_size(s) };
assert!(
after.cell_height_px > before.cell_height_px,
"{} -> {}",
before.cell_height_px,
after.cell_height_px
);
// surface-only update
let c3 = ghostty_config_new();
unsafe {
ghostty_surface_update_config(s, c3);
ghostty_config_free(c3);
ghostty_config_free(c2);
ghostty_surface_free(s);
}
}
#[test]
fn inspector_api() {
let h = host!("");
let s = h.surface("printf 'READY\\n'; sleep 30");
h.wait_screen(s, "READY");
let i = unsafe { ghostty_surface_inspector(s) };
assert!(!i.is_null());
// one inspector per surface
assert_eq!(unsafe { ghostty_surface_inspector(s) }, i);
unsafe {
ghostty_inspector_set_content_scale(i, 2.0, 2.0);
ghostty_inspector_set_size(i, 800, 600);
ghostty_inspector_set_focus(i, true);
ghostty_inspector_mouse_pos(i, 10.0, 10.0);
ghostty_inspector_mouse_button(i, 1, 1, 0);
ghostty_inspector_mouse_button(i, 0, 1, 0);
ghostty_inspector_mouse_button(i, 1, 9, 0);
ghostty_inspector_mouse_scroll(i, 0.0, 1.0, 1);
ghostty_inspector_key(i, 1, 18, GHOSTTY_MODS_CTRL);
ghostty_inspector_text(i, c"abc".as_ptr());
}
// input queues renders of the inspector
assert!(rec(|r| r
.actions
.iter()
.any(|a| a.0 == 30 && a.2 == s as usize)));
unsafe {
ghostty_inspector_free(s);
}
// a new one can be created afterwards
let i2 = unsafe { ghostty_surface_inspector(s) };
assert!(!i2.is_null());
// and it is freed with the surface
unsafe { ghostty_surface_free(s) };
}
#[test]
fn surface_without_platform_is_rejected() {
let h = host!("");
let mut sc = ghostty_surface_config_new();
// no platform
assert!(unsafe { ghostty_surface_new(h.app, &sc) }.is_null());
sc.platform_tag = platform::MACOS;
// Zig's Platform.init rejects Darwin platforms off Darwin.
assert!(unsafe { ghostty_surface_new(h.app, &sc) }.is_null());
}
#[test]
fn selection_clipboard_support_is_reported() {
// supports_selection_clipboard = true in this host: an OSC 52 write to
// the primary selection goes out with clipboard=1.
let h = host!("clipboard-write = allow\n");
let s = h.surface("printf '\\033]52;p;c2VsZWN0aW9u\\007DONE\\n'; sleep 30");
h.wait_screen(s, "DONE");
h.wait("selection write", || {
rec(|r| {
r.writes
.iter()
.any(|w| w.1 == "selection" && (w.0 == 1 || w.0 == 2))
})
});
unsafe { ghostty_surface_free(s) };
}
+6
View File
@@ -1520,6 +1520,12 @@ impl<G: GraphicsApi> Renderer<G> {
.is_some_and(DisplayLink::is_running)
}
/// The font grid currently used for rendering (for apprts that need
/// to read the primary face, e.g. macOS quicklook).
pub fn font_grid(&self) -> Arc<SharedGrid> {
Arc::clone(&lock(&self.draw).font_grid)
}
/// Called by the renderer thread when it begins (`threadEnter`).
pub fn thread_enter(&self) {
lock(&self.draw).api.thread_enter();
+17
View File
@@ -239,6 +239,23 @@ impl Metal {
})
}
/// [`Metal::init`] for a raw `NSView` / `UIView` pointer as handed over
/// through the C API (`ghostty_platform_macos_s.nsview`). The view must
/// stay alive for the lifetime of the renderer (the host owns it).
pub fn init_with_view(
blending: AlphaBlending,
view: *mut c_void,
scale_factor: f64,
) -> Result<Metal, Error> {
if view.is_null() {
return Err(Error::ObjCFailed);
}
// SAFETY: the caller (the embedded runtime) guarantees `view` is a
// valid, live NSView / UIView, which is an Objective-C object.
let view: &AnyObject = unsafe { &*view.cast::<AnyObject>() };
Metal::init(blending, &ViewInfo { view, scale_factor })
}
/// The layer the renderer presents on.
pub fn layer(&self) -> &IOSurfaceLayer {
&self.layer
+1 -1
View File
@@ -30,7 +30,7 @@ dependency (cycle): the core boundary is the trait set in `src/core.rs`.
# Per-component notes (by sub-agent)
## F3: dialogs + config object (class/{dialog,config,config_errors_dialog,close_confirmation_dialog,clipboard_confirmation_dialog,title_dialog,debug_warning}.rs)
Libadwaita >= 1.5 is assumed, so `Dialog` is always an `adw::AlertDialog` subclass (no `MessageDialog` fallback) and DebugWarning uses the `adw::Banner` (1.3) template; the clipboard dialog always uses the 1.4 template (remember switch). GType names, property names, signals and template children match the Zig classes.
Update (GC): the Zig compat fallbacks are now ported, see compat-GC.md. By default (adw_1_5) `Dialog` is an `adw::AlertDialog` subclass, DebugWarning uses the Banner (1.3) template and the clipboard dialog the 1.4 template. GType names, property names, signals and template children match the Zig classes.
Public API (all `&self` methods on `glib::wrapper!` types; signals via `connect_*`):
- `config::Config` (GhosttyConfig; props `diagnostics-buffer`, `has-diagnostics` -> generated getters `diagnostics_buffer() -> Option<gtk::TextBuffer>`, `has_diagnostics() -> bool`): `new(&CoreConfig)` (clones), `from_arc(Arc<CoreConfig>)`, `new_default()`, `get() -> Arc<CoreConfig>`, `get_mut(|&mut CoreConfig| ..)` (Arc::make_mut), `clone_config() -> CoreConfig`. Test `ghostty_config` ported.
+139
View File
@@ -0,0 +1,139 @@
# libghostty: the embedded C API (Phase 8, agent L1)
Crate `crates/ghostty-lib` (crate types `cdylib` + `staticlib` + `rlib`, `[lib] name = "ghostty"` so Cargo
produces `libghostty.{so,a,dylib}` directly). `include/ghostty.h` and `include/module.modulemap` are
byte-identical copies of `ZIG/include` (a test compares them when the Zig checkout is present).
`unsafe_code = "deny"` crate wide; every FFI module has `#![allow(unsafe_code)]` with `// SAFETY:` comments.
## Mapping
| Zig | Rust (`crates/ghostty-lib/src`) |
|---|---|
| `main_c.zig` (`ghostty_init`, `_cli_try_action`, `_info`, `_translate`, `_string_free`, `String`) | `main_c.rs` |
| `config/CApi.zig`, `config/c_get.zig`, `Config.zig` `cval`s | `config_capi.rs` + `ghostty-config/src/c_value.rs` (`CValue`, `ConfigField::c_value`, `Config::c_value`) |
| `apprt/action.zig` `C`/`cval` (target + action unions) | `action_c.rs`, C layouts in `types.rs` |
| `apprt/embedded.zig` `App`, `Surface`, `Platform`, `CAPI` | `embedded.rs` (`AppRt: RtApp`, `SurfaceRt: RtSurface`, `AppHandle`, `Surface`, all `ghostty_app_*`/`ghostty_surface_*`) |
| `apprt/embedded.zig` `Inspector` + `ghostty_inspector_*` | `inspector.rs` (+ `inspector_metal.rs`, Apple only) |
| `benchmark/CApi.zig` | `benchmark.rs` (`ghostty_benchmark_cli`) |
| `include/ghostty.h` structs/unions/enums | `types.rs` (`#[repr(C)]`, enums are `c_int`) |
`ghostty_set_window_background_blur` is exported on every platform (no-op off macOS), like Zig. The Darwin-only
exports (`ghostty_surface_set_display_id`, `_quicklook_font`, `_quicklook_word`, `ghostty_inspector_metal_*`) are
`cfg(target_vendor = "apple")`, matching the header's `#ifdef __APPLE__`.
## Runtime design
* The embedded runtime implements `ghostty_apprt::runtime_api::{RtApp, RtSurface}`; the real `ghostty-core` drives it.
`AppRt` holds a copy of `ghostty_runtime_config_s`, the app config (`Arc<Config>`, updated on the app-targeted
`config_change` action exactly like Zig's `performPreAction`), the surface registry (for titles) and the `Keymap`.
`SurfaceRt` holds userdata, the view pointer, content scale / size / cursor position and the title.
* `ghostty_app_t` = `Box<AppHandle>`, `ghostty_surface_t` = `Box<Surface>`. The address of the `Surface` is its
`SurfaceId` in core and the `ghostty_surface_t` in `ghostty_target_s`, so actions fired during
`Surface::new` (cell size, size limit, title) already name the handle the host will get back.
* `core::App::create_surface` (new) returns the concrete `Rc<core::Surface>` so the embedded API can reach everything
Zig's `core_surface` exposes (`text_callback`, `refresh_callback`, `draw`, `mouse_captured`, `dump_text_locked`,
`lock_terminal`, ...), not only the `CoreSurface` trait.
* Everything is main-thread only (core uses `Rc`/`RefCell`); the single cross-thread entry is `wakeup_cb`
(`WakerImpl`, a `Waker`). No `RefCell` borrow is held across a call into the host (the host may re-enter).
* Clipboard: `read_clipboard_cb` gets a `Box<ClipboardRequest>` pointer that is consumed by
`ghostty_surface_complete_clipboard_request` (kept alive when `confirm_read_clipboard_cb` is invoked, like Zig).
* `ghostty_config_t` is a `Box<CConfig>` (config + the memory `ghostty_config_get` hands out pointers into: strings,
color lists, command lists, diagnostics; kept until `ghostty_config_free`, identical values return identical
pointers). Enum tag names are interned for the process lifetime. The `config_change` action passes a temporary
`CConfig` handle that is freed after the callback returns.
## Config C getters (`c_get.zig`)
`ConfigField::c_value() -> CValue` (default `Unsupported`) is implemented by: `bool`, `u8`/`u32` (`c_uint`), `i16`
(`c_short`), `f32`/`f64`, `Option<String>` (`?[:0]const u8`: unset -> true + NULL), every `config_enum!` (tag name),
every `config_packed!` (bits as `c_uint`, <= 32 fields), `Option<T>` (None -> false), `Color`, `ColorList`, `Palette`,
`RepeatableCommand`, `QuickTerminalSize`, `BackgroundBlur` (0/20/radius/-1/-2), `Duration` (`usize` ms), `Path`,
`WindowDecoration`, `CursorStyle`, `OptionAsAlt`. Everything else (`String`, `u16`, `u64`, `usize`, unions without
`cval`, repeatable types, ...) is unsupported (get returns false) exactly like Zig. `ghostty-config` has a test
(`c_value_supported_keys_match_zig`, data in `testdata/c_get_supported.txt`) that compares the supported key set
(143 keys) with the one derived from the Zig field types. `RepeatableStringMap::put` was added (needed for
surface `env_vars`).
## Platform gating (renderer selection)
* `ghostty-core` no longer hard-codes OpenGL: `surface/mod.rs` uses `ghostty_renderer::{Renderer, Api}`
(Metal on `target_vendor = "apple"`, OpenGL elsewhere; Zig's `renderer.zig`). `init_graphics_api` is
`OpenGL::init` + `prepare_context(rt.gl_proc_address)` off Apple and `Metal::init_with_view(blending, view, scale)`
on Apple. `RtSurface::platform_view()` (new, default null) carries the `NSView`/`UIView`; `Metal::init_with_view`
(new, in the renderer crate's unsafe-allowed `metal` module) turns the raw pointer into the `ViewInfo`.
GL-only helpers (`frame_pixels`, screenshots, `display_realized(loader)`) are `cfg(not(apple))`.
* Surfaces: Zig's `Platform.init` only accepts `macos`/`ios` on Darwin targets and fails with `UnsupportedPlatform`
everywhere else, so `ghostty_surface_new` returns NULL on Linux (tested). No host-supplied OpenGL context exists in
Zig, so there is none here either.
* New renderer/font/core helpers for the Darwin exports: `Renderer::font_grid()`, `coretext::Face::quicklook_font`,
`core::Surface::{set_macos_display_id, quicklook_font}` (core gained an Apple-only `objc2-core-foundation` dep).
* Test seam: cargo feature `test-platform` (off by default, enabled by `--all-features`) lets `ghostty_surface_new`
accept `platform_tag = 1000` and takes the OpenGL loader from a thread-local the test installs (headless EGL).
Production behaviour is unchanged.
## Deviations from the Zig
* `ghostty_init` stores the given argv only to detect the CLI action; config CLI parsing and `+action` execution use
the process arguments (`ghostty_os::args`, `NSProcessInfo` on macOS), which are the same in a real process.
`ghostty_init` returns 1 for multiple/invalid actions (Zig: error from `detectArgs`). The `+action` runs through
`ghostty_cli::run_from_args` (so `+version` etc. print exactly like the binary).
* Lib-mode logging default: stderr off (`GlobalState.Logging.stderr = app_runtime != .none`), via the new
`ghostty_core::global::init_lib`. `BuildInfo` for `+version` reports `.none` runtime / CoreText + Metal on Apple.
* Invalid enum values coming from C (mouse state/button, split directions, color scheme, key action, pressure stage)
are ignored / return false (Zig: UB / safety panic). Null handles are tolerated.
* `ghostty_surface_key_is_binding` writes only the low byte (Zig writes the `u8` `Flags.C` through the enum pointer).
* `ghostty_surface_inherited_config` working-directory strings are kept in the surface (valid until it is freed) instead
of leaking an allocation per call.
* `initial_input` is escaped like `std.zig.stringEscape`, except non-ASCII code points use `\u{..}` (the config `Raw`
value is a Rust `String`, which cannot hold the `\xNN` bytes of a split UTF-8 sequence).
* `ghostty_benchmark_cli` splits the args string itself (quotes group, `\` escapes inside quotes) and runs
`ghostty_bench::benches::main_action`.
* Header typo kept: `chostty_ipc_target_s`; `ghostty_inspector_metal_shutdown` is declared `bool` in the header but
Zig exports it as `void` (we follow Zig; the Swift app ignores the result).
## Inspector
C API complete: `ghostty_surface_inspector` (one per surface, returns the existing one), `ghostty_inspector_free`
(takes the *surface*), size / content scale / focus / mouse / scroll / key / text feed a per-inspector Dear ImGui
`Context` (`ghostty_inspector::setup`, style scaled by the factor change, scroll x0.1 for precision devices, only
left/middle/right buttons). Activation calls `core::Surface::activate_inspector` and `RtSurface::redraw_inspector`
raises the `render_inspector` action, as in Zig.
`inspector_metal.rs` (Apple only, compile-checked only) replaces Dear ImGui's `imgui_impl_metal.mm`: the same MSL
shaders compiled with `newLibraryWithSource`, a pipeline keyed by the host render pass' formats, per-draw-list buffers,
`dear-imgui-rs` managed textures (RGBA32/Alpha8 -> RGBA8), `RENDERER_HAS_VTX_OFFSET | RENDERER_HAS_TEXTURES`. Two
frames are built per render (the first only `end_frame`), like Zig. Raw draw callbacks are ignored. It has never run
on a Mac.
## Tests
* Unit: 20 in `src` (string helpers, `ghostty_config_get` ports of the Zig tests: bool/enum/optional null/unknown
key/optional string null/float/struct cval, the `c_get:` tests u8/enum/color/optional/background-blur/
split-preserve-zoom, action/target conversions, benchmark arg splitting). `ghostty-config`: +4 `c_value` tests and the
two `RepeatableCommand cval` tests (previously skipped).
* `tests/abi.rs` (generated C probe compiled with `cc`, linked against `libghostty.so`): sizeof/alignof/offsetof of every
struct and union of the header (field names parsed from the header, so a missing field fails), sizeof + value of
every enumerator of every enum (755 values, compared with the Rust definitions: `Key::ALL`, `apprt::action::Key`,
`MouseShape`, `Mods` bits, ...), the address of every declared function (links), `nm -D` exports == declared
functions, header files identical to upstream. The probe asserts every header enum/struct is covered.
* `tests/c_host.rs` + `tests/c/host.c`: a C host program on the unchanged header: `ghostty_init`, `+version` /
`+list-keybinds` through `ghostty_cli_try_action`, config new/load_file/finalize/get (bool, enum, string, colors,
palette, float, packed, `short` blur, duration, quick-terminal-size, command list, unsupported/unknown/unset keys),
triggers, diagnostics, clone, open_path, app new/tick/key/key_is_binding/focus/color scheme/open_config/
update_config (the `config_change` action carries a readable config), global keybinds, `ghostty_surface_new`
returning NULL off Darwin, `ghostty_benchmark_cli`.
* `tests/surface.rs` (feature `test-platform`, headless EGL, real shell on a real pty): 13 tests driving the exported C
surface API: creation/actions/size/scale/free, text + key input, key_is_binding flags, selection + copy/paste
(including the unsafe-paste confirmation round trip), OSC 52 / title / pwd actions, inherited config, env vars /
working directory / initial input / font size options, mouse capture and reports, focus/occlusion/preedit/splits/
close callback, child exit, app + surface config updates, inspector API, rejected platforms.
* Cross-compile checks (recipe from `macos-M1.md`, wrappers around `zig cc`; the CoreGraphics/Metal/CoreText code is
only type-checked): `cargo check|clippy --all-targets -p ghostty-lib --target aarch64-apple-darwin` and
`x86_64-apple-darwin` pass with `-D warnings`, as does `-p ghostty-core`.
## Known gaps
* Nothing in the Apple-only paths (Metal surface setup, display link, quicklook, window blur, Metal inspector) has run
on a Mac; the Swift app has not been built against the library.
* `ghostty_translate` returns interned translations; `ghostty_init` ignores `argv` beyond action detection (see above).
* Only the debug-profile static library was inspected (~500 MB with debug info); release/production sizes untested.
+3 -3
View File
@@ -95,9 +95,9 @@ Unix-specific existing tests are `cfg(unix)`-gated.
* `Command::wait(block)` ignores `block` (blocks, like Zig). The thread
handle from `CreateProcessW` is closed (Zig leaks it).
* Fontconfig discovery is kept on Windows (needs a real fontconfig to link).
* Not touched: `cli/ssh-cache/DiskCache` file locking, `cli/boo|list_colors|
list_keybinds|edit_config` Windows branches (ghostty-cli), `benchmark/
CodepointWidth`, `config/Config.zig` `probableCliEnvironment` env-based
* (Update GC: `cli/ssh-cache/DiskCache` locking, the `cli/boo|list_colors|
list_keybinds|edit_config` Windows branches and `benchmark/CodepointWidth` are
now done, see compat-GC*.md.) Not touched: `config/Config.zig` `probableCliEnvironment` env-based
extras. ghostty-core/apprt-gtk are not Windows-checkable (GTK).
* Unrelated breakage seen while verifying (other agents): `ghostty-config`
`config.rs` uses `ghostty_os::launched_from_desktop` under `cfg!(macos)`