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:
1 parent
4787565397
commit
49d4ecefae
47 files changed
+10296
-34
No files matched your search
Generated
+32
@@ -1345,6 +1345,7 @@ dependencies = [
|
|||||||
"ghostty-config",
|
"ghostty-config",
|
||||||
"ghostty-crash",
|
"ghostty-crash",
|
||||||
"ghostty-font",
|
"ghostty-font",
|
||||||
|
"ghostty-helpgen",
|
||||||
"ghostty-input",
|
"ghostty-input",
|
||||||
"ghostty-os",
|
"ghostty-os",
|
||||||
"ghostty-vt",
|
"ghostty-vt",
|
||||||
@@ -1390,6 +1391,7 @@ dependencies = [
|
|||||||
"ghostty-unicode",
|
"ghostty-unicode",
|
||||||
"ghostty-vt",
|
"ghostty-vt",
|
||||||
"log",
|
"log",
|
||||||
|
"objc2-core-foundation",
|
||||||
"onig",
|
"onig",
|
||||||
"oslog",
|
"oslog",
|
||||||
"png",
|
"png",
|
||||||
@@ -1441,10 +1443,15 @@ dependencies = [
|
|||||||
"yeslogic-fontconfig-sys",
|
"yeslogic-fontconfig-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ghostty-helpgen"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ghostty-input"
|
name = "ghostty-input"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ghostty-helpgen",
|
||||||
"ghostty-unicode",
|
"ghostty-unicode",
|
||||||
"ghostty-vt",
|
"ghostty-vt",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@@ -1466,6 +1473,31 @@ dependencies = [
|
|||||||
"log",
|
"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]]
|
[[package]]
|
||||||
name = "ghostty-onig"
|
name = "ghostty-onig"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ ghostty-vt = { path = "crates/ghostty-vt" }
|
|||||||
ghostty-unicode = { path = "crates/ghostty-unicode" }
|
ghostty-unicode = { path = "crates/ghostty-unicode" }
|
||||||
ghostty-terminal = { path = "crates/ghostty-terminal" }
|
ghostty-terminal = { path = "crates/ghostty-terminal" }
|
||||||
ghostty-input = { path = "crates/ghostty-input" }
|
ghostty-input = { path = "crates/ghostty-input" }
|
||||||
|
ghostty-helpgen = { path = "crates/ghostty-helpgen" }
|
||||||
ghostty-config = { path = "crates/ghostty-config" }
|
ghostty-config = { path = "crates/ghostty-config" }
|
||||||
ghostty-font = { path = "crates/ghostty-font" }
|
ghostty-font = { path = "crates/ghostty-font" }
|
||||||
ghostty-renderer = { path = "crates/ghostty-renderer" }
|
ghostty-renderer = { path = "crates/ghostty-renderer" }
|
||||||
@@ -66,6 +67,7 @@ ghostty-bench = { path = "crates/ghostty-bench" }
|
|||||||
ghostty-inspector = { path = "crates/ghostty-inspector" }
|
ghostty-inspector = { path = "crates/ghostty-inspector" }
|
||||||
ghostty-vt-capi = { path = "crates/ghostty-vt-capi" }
|
ghostty-vt-capi = { path = "crates/ghostty-vt-capi" }
|
||||||
ghostty-wasm = { path = "crates/ghostty-wasm" }
|
ghostty-wasm = { path = "crates/ghostty-wasm" }
|
||||||
|
ghostty-lib = { path = "crates/ghostty-lib" }
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
unsafe_code = "forbid"
|
unsafe_code = "forbid"
|
||||||
|
|||||||
@@ -310,6 +310,12 @@ pub trait RtSurface {
|
|||||||
let _ = name;
|
let _ = name;
|
||||||
std::ptr::null()
|
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
|
/// Callback to create a transient systemd scope for the child process
|
||||||
/// (Zig: `apprt/gtk/cgroup.zig` + `post_fork`). Called on the main
|
/// (Zig: `apprt/gtk/cgroup.zig` + `post_fork`). Called on the main
|
||||||
/// thread when the surface is created; the returned closure is called
|
/// thread when the surface is created; the returned closure is called
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
use std::{fmt, io::BufRead};
|
use std::{fmt, io::BufRead};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
c_value::CValue,
|
||||||
comma_splitter::CommaSplitter,
|
comma_splitter::CommaSplitter,
|
||||||
diagnostics::{Diagnostic, DiagnosticList, Location},
|
diagnostics::{Diagnostic, DiagnosticList, Location},
|
||||||
formatter::EntryFormatter,
|
formatter::EntryFormatter,
|
||||||
@@ -113,6 +114,28 @@ pub trait ConfigField: Clone + PartialEq + Default + fmt::Debug {
|
|||||||
|
|
||||||
/// Write this value in the `+show-config` format.
|
/// Write this value in the `+show-config` format.
|
||||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result;
|
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.
|
/// The destination of CLI parsing.
|
||||||
@@ -456,6 +479,10 @@ pub fn parse_int_base0(s: &str) -> Option<i128> {
|
|||||||
impl ConfigField for bool {
|
impl ConfigField for bool {
|
||||||
const KIND: FieldKind = FieldKind::Bool;
|
const KIND: FieldKind = FieldKind::Bool;
|
||||||
|
|
||||||
|
fn c_value(&self) -> CValue {
|
||||||
|
CValue::Bool(*self)
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||||
*self = parse_bool(value.unwrap_or("t"))?;
|
*self = parse_bool(value.unwrap_or("t"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -467,8 +494,13 @@ impl ConfigField for bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! impl_int_field {
|
macro_rules! impl_int_field {
|
||||||
($($t:ty),*) => {$(
|
($($t:ty => $cv:expr),*) => {$(
|
||||||
impl ConfigField for $t {
|
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> {
|
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||||
let n = parse_int_base0(v).ok_or(ArgError::InvalidValue)?;
|
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 {
|
macro_rules! impl_float_field {
|
||||||
($($t:ty),*) => {$(
|
($($t:ty => $cv:expr),*) => {$(
|
||||||
impl ConfigField for $t {
|
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> {
|
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||||
*self = v.parse::<$t>().map_err(|_| ArgError::InvalidValue)?;
|
*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 {
|
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> {
|
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||||
*self = value.ok_or(ArgError::ValueRequired)?.to_string();
|
*self = value.ok_or(ArgError::ValueRequired)?.to_string();
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -519,6 +575,13 @@ impl<T: ConfigField> ConfigField for Option<T> {
|
|||||||
_ => FieldKind::OptionalOther,
|
_ => 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> {
|
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||||
// For optional fields, we just treat it as the child type.
|
// For optional fields, we just treat it as the child type.
|
||||||
if let Some(v) = self {
|
if let Some(v) = self {
|
||||||
|
|||||||
@@ -3810,6 +3810,16 @@ impl Default for Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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`).
|
/// Build the default configuration (Zig: `Config.default`).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new_default() -> Config {
|
pub fn new_default() -> Config {
|
||||||
@@ -5412,4 +5422,85 @@ mod tests {
|
|||||||
assert!(out.contains("\nfont-size = 12\n"));
|
assert!(out.contains("\nfont-size = 12\n"));
|
||||||
assert!(out.contains("\nbackground = #282c34\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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ use std::fmt;
|
|||||||
use ghostty_input::{KeyRemapSet, OptionAsAlt};
|
use ghostty_input::{KeyRemapSet, OptionAsAlt};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
c_value::CValue,
|
||||||
cli_args::{ArgError, ConfigField},
|
cli_args::{ArgError, ConfigField},
|
||||||
formatter::EntryFormatter,
|
formatter::EntryFormatter,
|
||||||
};
|
};
|
||||||
@@ -30,6 +31,15 @@ impl ConfigField for KeyRemapSet {
|
|||||||
impl ConfigField for OptionAsAlt {
|
impl ConfigField for OptionAsAlt {
|
||||||
const VALID_VALUES: Option<&'static [&'static str]> = None;
|
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> {
|
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||||
*self = match v {
|
*self = match v {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
)]
|
)]
|
||||||
|
|
||||||
// Generic CLI parsing infrastructure.
|
// Generic CLI parsing infrastructure.
|
||||||
|
pub mod c_value;
|
||||||
pub mod cli_args;
|
pub mod cli_args;
|
||||||
pub mod comma_splitter;
|
pub mod comma_splitter;
|
||||||
pub mod diagnostics;
|
pub mod diagnostics;
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ pub struct FieldInfo<T> {
|
|||||||
pub eq: fn(&T, &T) -> bool,
|
pub eq: fn(&T, &T) -> bool,
|
||||||
/// Format this field as in `+show-config`.
|
/// Format this field as in `+show-config`.
|
||||||
pub format: fn(&T, &str, &mut dyn fmt::Write) -> fmt::Result,
|
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> {
|
impl<T> FieldInfo<T> {
|
||||||
@@ -147,6 +149,7 @@ macro_rules! config_struct {
|
|||||||
let mut f = $crate::formatter::EntryFormatter::new(name, w);
|
let mut f = $crate::formatter::EntryFormatter::new(name, w);
|
||||||
$crate::cli_args::ConfigField::format_entry(&a.$field, &mut f)
|
$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(())
|
::core::result::Result::Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn c_value(&self) -> $crate::c_value::CValue {
|
||||||
|
$crate::c_value::CValue::EnumName(self.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
fn format_entry(
|
fn format_entry(
|
||||||
&self,
|
&self,
|
||||||
f: &mut $crate::formatter::EntryFormatter<'_>,
|
f: &mut $crate::formatter::EntryFormatter<'_>,
|
||||||
@@ -274,6 +281,20 @@ macro_rules! config_packed {
|
|||||||
const KIND: $crate::cli_args::FieldKind =
|
const KIND: $crate::cli_args::FieldKind =
|
||||||
$crate::cli_args::FieldKind::Packed(Self::NAMES);
|
$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(
|
fn parse_cli(
|
||||||
&mut self,
|
&mut self,
|
||||||
value: ::core::option::Option<&str>,
|
value: ::core::option::Option<&str>,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
c_value::CValue,
|
||||||
cli_args::{ArgError, ConfigField},
|
cli_args::{ArgError, ConfigField},
|
||||||
diagnostics::{Diagnostic, DiagnosticList},
|
diagnostics::{Diagnostic, DiagnosticList},
|
||||||
formatter::EntryFormatter,
|
formatter::EntryFormatter,
|
||||||
@@ -171,6 +172,19 @@ impl Path {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigField for 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.
|
/// Parse CLI option. The value will be overwritten.
|
||||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||||
let Some(item) = Path::parse(input)? else {
|
let Some(item) = Path::parse(input)? else {
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ impl RepeatableStringMap {
|
|||||||
.map(|(_, v)| v.as_str())
|
.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).
|
/// Iterator over the entries in the map (insertion order).
|
||||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
|
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||||
self.map.iter().map(|(k, v)| (k.as_str(), v.as_str()))
|
self.map.iter().map(|(k, v)| (k.as_str(), v.as_str()))
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,6 +7,7 @@ use std::fmt;
|
|||||||
use ghostty_vt::color::{DEFAULT as DEFAULT_PALETTE, Palette as TermPalette, PaletteMask, Rgb};
|
use ghostty_vt::color::{DEFAULT as DEFAULT_PALETTE, Palette as TermPalette, PaletteMask, Rgb};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
c_value::CValue,
|
||||||
cli_args::{ArgError, ConfigField, parse_int_base0, trim_ws},
|
cli_args::{ArgError, ConfigField, parse_int_base0, trim_ws},
|
||||||
clipboard_codepoint_map::{ClipboardCodepointMap, Entry as ClipboardEntry, Replacement},
|
clipboard_codepoint_map::{ClipboardCodepointMap, Entry as ClipboardEntry, Replacement},
|
||||||
config_enum,
|
config_enum,
|
||||||
@@ -35,6 +36,15 @@ impl ConfigField for CursorStyle {
|
|||||||
const VALID_VALUES: Option<&'static [&'static str]> =
|
const VALID_VALUES: Option<&'static [&'static str]> =
|
||||||
Some(&["bar", "block", "underline", "block_hollow"]);
|
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> {
|
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||||
*self = match v {
|
*self = match v {
|
||||||
@@ -290,6 +300,14 @@ impl Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigField for 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> {
|
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||||
*self = Color::from_cli(input)?;
|
*self = Color::from_cli(input)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -394,6 +412,10 @@ pub struct ColorList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigField for 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> {
|
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||||
let input = input.ok_or(ArgError::ValueRequired)?;
|
let input = input.ok_or(ArgError::ValueRequired)?;
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
@@ -454,6 +476,14 @@ impl Default for Palette {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigField 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> {
|
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||||
let value = input.ok_or(ArgError::ValueRequired)?;
|
let value = input.ok_or(ArgError::ValueRequired)?;
|
||||||
let eql_idx = value.find('=').ok_or(ArgError::InvalidValue)?;
|
let eql_idx = value.find('=').ok_or(ArgError::InvalidValue)?;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use std::fmt;
|
|||||||
use ghostty_input::binding::Action;
|
use ghostty_input::binding::Action;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
c_value::{CValue, QuickTerminalSizeC},
|
||||||
cli_args::{ArgError, ConfigField, parse_auto_struct_with, parse_bool, trim_ws},
|
cli_args::{ArgError, ConfigField, parse_auto_struct_with, parse_bool, trim_ws},
|
||||||
config_enum,
|
config_enum,
|
||||||
config_packed,
|
config_packed,
|
||||||
@@ -152,6 +153,15 @@ impl RepeatableCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigField for 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> {
|
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||||
// Unset or empty input clears the list
|
// Unset or empty input clears the list
|
||||||
let input = input.unwrap_or("");
|
let input = input.unwrap_or("");
|
||||||
@@ -604,6 +614,18 @@ impl QuickTerminalSize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigField for 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> {
|
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||||
let input = input.ok_or(ArgError::ValueRequired)?;
|
let input = input.ok_or(ArgError::ValueRequired)?;
|
||||||
let mut it = input.split(',');
|
let mut it = input.split(',');
|
||||||
@@ -786,6 +808,17 @@ impl BackgroundBlur {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigField for 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> {
|
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||||
let Some(input) = input else {
|
let Some(input) = input else {
|
||||||
// Emulate behavior for bools
|
// Emulate behavior for bools
|
||||||
@@ -882,6 +915,10 @@ impl WindowDecoration {
|
|||||||
impl ConfigField for WindowDecoration {
|
impl ConfigField for WindowDecoration {
|
||||||
const VALID_VALUES: Option<&'static [&'static str]> = Some(Self::NAMES);
|
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> {
|
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||||
*self = Self::parse(input)?;
|
*self = Self::parse(input)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1127,6 +1164,10 @@ impl fmt::Display for Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigField 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> {
|
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||||
*self = Duration::parse(input)?;
|
*self = Duration::parse(input)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1267,6 +1308,47 @@ mod tests {
|
|||||||
assert_eq!(list.value.len(), 0);
|
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]
|
#[test]
|
||||||
fn repeatable_command_format_config_empty() {
|
fn repeatable_command_format_config_empty() {
|
||||||
let list = RepeatableCommand::default();
|
let list = RepeatableCommand::default();
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ workspace = true
|
|||||||
|
|
||||||
[target.'cfg(target_vendor = "apple")'.dependencies]
|
[target.'cfg(target_vendor = "apple")'.dependencies]
|
||||||
oslog = { version = "0.2.0", default-features = false }
|
oslog = { version = "0.2.0", default-features = false }
|
||||||
|
objc2-core-foundation = "0.3.2"
|
||||||
@@ -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
|
/// Add an initialized surface. Since we have non-zero surfaces, the quit
|
||||||
/// timer is cancelled.
|
/// timer is cancelled.
|
||||||
pub(crate) fn add_surface(&self, id: SurfaceId, surface: &Rc<Surface>, rt: &Rc<dyn RtApp>) {
|
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_app: Rc<dyn RtApp>,
|
||||||
rt_surface: Rc<dyn RtSurface>,
|
rt_surface: Rc<dyn RtSurface>,
|
||||||
) -> Result<Rc<dyn CoreSurface>, CoreError> {
|
) -> Result<Rc<dyn CoreSurface>, CoreError> {
|
||||||
let surface = Surface::new(
|
Ok(self.create_surface(id, config, rt_app, rt_surface)?)
|
||||||
id,
|
|
||||||
Rc::clone(&self.state),
|
|
||||||
Arc::new(config),
|
|
||||||
Rc::clone(&rt_app),
|
|
||||||
rt_surface,
|
|
||||||
)?;
|
|
||||||
self.add_surface(id, &surface, &rt_app);
|
|
||||||
Ok(surface)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_surface(&self, id: SurfaceId) {
|
fn delete_surface(&self, id: SurfaceId) {
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ impl Logging {
|
|||||||
/// invalid value (including an empty part) yields the default.
|
/// invalid value (including an empty part) yields the default.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn parse(v: &str) -> Logging {
|
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 {
|
match v {
|
||||||
"1" | "t" | "T" | "true" => {
|
"1" | "t" | "T" | "true" => {
|
||||||
return Logging {
|
return Logging {
|
||||||
@@ -53,7 +60,7 @@ impl Logging {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
let mut out = Logging::default();
|
let mut out = default;
|
||||||
for part in v.split(',') {
|
for part in v.split(',') {
|
||||||
let part = part.trim_matches([' ', '\t']);
|
let part = part.trim_matches([' ', '\t']);
|
||||||
let (name, value) = match part.strip_prefix("no-") {
|
let (name, value) = match part.strip_prefix("no-") {
|
||||||
@@ -63,7 +70,7 @@ impl Logging {
|
|||||||
match name {
|
match name {
|
||||||
"stderr" => out.stderr = value,
|
"stderr" => out.stderr = value,
|
||||||
"macos" => out.macos = value,
|
"macos" => out.macos = value,
|
||||||
_ => return Logging::default(),
|
_ => return default,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
@@ -134,8 +141,25 @@ pub fn state() -> &'static GlobalState {
|
|||||||
/// Initialize the global state: logging, rlimits, resources dir and i18n.
|
/// Initialize the global state: logging, rlimits, resources dir and i18n.
|
||||||
/// Call once, early (Zig: `GlobalState.init`).
|
/// Call once, early (Zig: `GlobalState.init`).
|
||||||
pub fn init(opts: InitOptions) -> &'static GlobalState {
|
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(|| {
|
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
|
// If we have an action executing, we disable logging by default
|
||||||
// since we write to stderr we don't want logs messing up our
|
// 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.
|
// Env vars are useful for logging too because they are easy to set.
|
||||||
if let Ok(v) = std::env::var("GHOSTTY_LOG") {
|
if let Ok(v) = std::env::var("GHOSTTY_LOG") {
|
||||||
logging = Logging::parse(&v);
|
logging = Logging::parse_with(&v, default_logging);
|
||||||
}
|
}
|
||||||
|
|
||||||
init_logger(logging);
|
init_logger(logging);
|
||||||
|
|||||||
@@ -89,14 +89,13 @@ use ghostty_font::{
|
|||||||
};
|
};
|
||||||
use ghostty_input::{Mods, MouseButton, MouseButtonState, ScrollMods};
|
use ghostty_input::{Mods, MouseButton, MouseButtonState, ScrollMods};
|
||||||
use ghostty_renderer::{
|
use ghostty_renderer::{
|
||||||
|
Api as GraphicsApi,
|
||||||
Health,
|
Health,
|
||||||
// The generic renderer over the OpenGL API (the GTK runtime's graphics
|
// The renderer over the graphics API selected for the target (Zig:
|
||||||
// API). `ghostty_renderer::Renderer` is the platform default, which is
|
// `renderer.zig`): Metal on Apple targets, OpenGL elsewhere.
|
||||||
// Metal on Apple targets; the embedded (macOS) runtime provides its own
|
Renderer,
|
||||||
// Metal-based surface setup (TODO(integration): L1).
|
generic::Host as RendererHost,
|
||||||
generic::{Host as RendererHost, Renderer},
|
|
||||||
message::{FontGridChange, Message as RendererMessage},
|
message::{FontGridChange, Message as RendererMessage},
|
||||||
opengl::OpenGL,
|
|
||||||
options::Options as RendererOptions,
|
options::Options as RendererOptions,
|
||||||
size::{CellSize, Coordinate, Padding, ScreenSize, Size},
|
size::{CellSize, Coordinate, Padding, ScreenSize, Size},
|
||||||
state::{Preedit, PreeditCodepoint, State as RenderStateData},
|
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.
|
/// Font state of the surface.
|
||||||
pub struct FontState {
|
pub struct FontState {
|
||||||
/// The key of the font grid we hold a reference on.
|
/// 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:
|
// Initialize our renderer with our initialized surface (Zig:
|
||||||
// `Renderer.surfaceInit`): load the GL functions for the current
|
// `Renderer.surfaceInit`): load the GL functions for the current
|
||||||
// context.
|
// context (OpenGL) or attach the Metal layer to the host's view.
|
||||||
let mut api = OpenGL::init(config.alpha_blending);
|
let api = init_graphics_api(&config, rt_surface.as_ref())?;
|
||||||
{
|
|
||||||
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}"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine our DPI configurations so we can properly configure
|
// Determine our DPI configurations so we can properly configure
|
||||||
// font points to pixels and handle other high-DPI scaling factors.
|
// font points to pixels and handle other high-DPI scaling factors.
|
||||||
@@ -708,6 +727,34 @@ impl Surface {
|
|||||||
self.renderer_handle.send(msg);
|
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.
|
/// Current sizes.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn size(&self) -> Size {
|
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
|
/// Write the last presented frame as a PNG. Requires the GL context to be
|
||||||
/// current.
|
/// 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> {
|
pub fn write_screenshot_png(&self, path: &std::path::Path) -> Result<(), CoreError> {
|
||||||
let Some((w, h, px)) = self.renderer.last_frame_pixels() else {
|
let Some((w, h, px)) = self.renderer.last_frame_pixels() else {
|
||||||
return Err("no frame has been presented yet".into());
|
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,
|
/// The pixels of the last presented frame (`width`, `height`, RGBA8,
|
||||||
/// bottom-up). Test/debug hook for headless integration tests; requires
|
/// bottom-up). Test/debug hook for headless integration tests; requires
|
||||||
/// the GL context to be current on the calling thread.
|
/// 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>)> {
|
pub fn frame_pixels(&self) -> Option<(usize, usize, Vec<u8>)> {
|
||||||
self.renderer.last_frame_pixels()
|
self.renderer.last_frame_pixels()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The GLArea was (re)realized; the GL context is current.
|
/// The GLArea was (re)realized; the GL context is current.
|
||||||
|
#[cfg(not(target_vendor = "apple"))]
|
||||||
pub fn renderer_realized(&self) -> Result<(), CoreError> {
|
pub fn renderer_realized(&self) -> Result<(), CoreError> {
|
||||||
let rt = Rc::clone(&self.rt_surface);
|
let rt = Rc::clone(&self.rt_surface);
|
||||||
self.renderer
|
self.renderer
|
||||||
@@ -1844,6 +1901,13 @@ impl Surface {
|
|||||||
Ok(())
|
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.
|
/// The GLArea is being unrealized; the GL context is current.
|
||||||
pub fn renderer_unrealized(&self) {
|
pub fn renderer_unrealized(&self) {
|
||||||
self.renderer.display_unrealized();
|
self.renderer.display_unrealized();
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
//! Keybinding actions end to end (default and configured bindings).
|
//! 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)]
|
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||||
|
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
//! IME: preedit state and rendering, commit, composing key events.
|
//! 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)]
|
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
//! recording (with the pty encoding and triggered bindings) and VT event
|
//! recording (with the pty encoding and triggered bindings) and VT event
|
||||||
//! recording while data is read from the pty.
|
//! 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)]
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::assert_is_empty)]
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
//! Keyboard input end to end: key events -> core Surface -> encoder -> pty ->
|
//! Keyboard input end to end: key events -> core Surface -> encoder -> pty ->
|
||||||
//! a real `cat` child that records the exact bytes it received.
|
//! 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)]
|
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
//! Mouse end to end: selection, copy-on-select, multi-click, mouse reporting
|
//! Mouse end to end: selection, copy-on-select, multi-click, mouse reporting
|
||||||
//! formats, alternate scroll, shift bypass and links.
|
//! 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)]
|
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
//! Terminal <-> runtime events: resize, focus, title, pwd, bell, OSC 52,
|
//! Terminal <-> runtime events: resize, focus, title, pwd, bell, OSC 52,
|
||||||
//! notifications, progress, child exit.
|
//! 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)]
|
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|||||||
@@ -697,6 +697,20 @@ impl Face {
|
|||||||
Ok(util::font_table_bytes(&self.font, tag))
|
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).
|
/// The underlying CTFont (for shapers).
|
||||||
pub(crate) fn ct_font(&self) -> &CTFont {
|
pub(crate) fn ct_font(&self) -> &CTFont {
|
||||||
&self.font
|
&self.font
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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 *
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()) });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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) };
|
||||||
|
}
|
||||||
@@ -1520,6 +1520,12 @@ impl<G: GraphicsApi> Renderer<G> {
|
|||||||
.is_some_and(DisplayLink::is_running)
|
.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`).
|
/// Called by the renderer thread when it begins (`threadEnter`).
|
||||||
pub fn thread_enter(&self) {
|
pub fn thread_enter(&self) {
|
||||||
lock(&self.draw).api.thread_enter();
|
lock(&self.draw).api.thread_enter();
|
||||||
|
|||||||
@@ -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.
|
/// The layer the renderer presents on.
|
||||||
pub fn layer(&self) -> &IOSurfaceLayer {
|
pub fn layer(&self) -> &IOSurfaceLayer {
|
||||||
&self.layer
|
&self.layer
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ dependency (cycle): the core boundary is the trait set in `src/core.rs`.
|
|||||||
# Per-component notes (by sub-agent)
|
# 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)
|
## 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_*`):
|
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.
|
- `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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -95,9 +95,9 @@ Unix-specific existing tests are `cfg(unix)`-gated.
|
|||||||
* `Command::wait(block)` ignores `block` (blocks, like Zig). The thread
|
* `Command::wait(block)` ignores `block` (blocks, like Zig). The thread
|
||||||
handle from `CreateProcessW` is closed (Zig leaks it).
|
handle from `CreateProcessW` is closed (Zig leaks it).
|
||||||
* Fontconfig discovery is kept on Windows (needs a real fontconfig to link).
|
* Fontconfig discovery is kept on Windows (needs a real fontconfig to link).
|
||||||
* Not touched: `cli/ssh-cache/DiskCache` file locking, `cli/boo|list_colors|
|
* (Update GC: `cli/ssh-cache/DiskCache` locking, the `cli/boo|list_colors|
|
||||||
list_keybinds|edit_config` Windows branches (ghostty-cli), `benchmark/
|
list_keybinds|edit_config` Windows branches and `benchmark/CodepointWidth` are
|
||||||
CodepointWidth`, `config/Config.zig` `probableCliEnvironment` env-based
|
now done, see compat-GC*.md.) Not touched: `config/Config.zig` `probableCliEnvironment` env-based
|
||||||
extras. ghostty-core/apprt-gtk are not Windows-checkable (GTK).
|
extras. ghostty-core/apprt-gtk are not Windows-checkable (GTK).
|
||||||
* Unrelated breakage seen while verifying (other agents): `ghostty-config`
|
* Unrelated breakage seen while verifying (other agents): `ghostty-config`
|
||||||
`config.rs` uses `ghostty_os::launched_from_desktop` under `cfg!(macos)`
|
`config.rs` uses `ghostty_os::launched_from_desktop` under `cfg!(macos)`
|
||||||
|
|||||||
Reference in new issue
Block a user