feat(lib): port libghostty, Ghostty's embedded C API

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

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

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

Test Plan:
- cargo test -p ghostty-lib (and --features test-platform)
- cargo clippy --target aarch64-apple-darwin -p ghostty-lib (cross env
  in docs/rewrite/notes/macos-M1.md)
This commit is contained in:
ddidderr committed 2026-10-12 06:38:33 +02:00
1 parent f2252bb165
commit 75bf604375
47 files changed
+10296 -34

No files matched your search

+348
View File
@@ -0,0 +1,348 @@
//! `apprt.Target.cval` and `apprt.Action.cval`: the conversion of core
//! actions to the `ghostty_target_s` / `ghostty_action_s` C structs
//! (ZIG/src/apprt/action.zig).
#![allow(unsafe_code)]
use std::ffi::{CString, c_char, c_int};
use ghostty_apprt::{
action::{Action, KeySequence, KeyTable, Target},
runtime_api::SurfaceId,
};
use ghostty_renderer::Health;
use ghostty_vt::osc::ProgressState;
use crate::{
config_capi::{CConfig, default_trigger, trigger_to_c},
types::{
ghostty_action_command_finished_s,
ghostty_action_s,
ghostty_action_u,
ghostty_config_t,
ghostty_target_s,
ghostty_target_u,
target_tag,
},
};
/// Memory that the pointers inside a converted action point to. It must
/// outlive the callback invocation.
#[derive(Default)]
pub struct Keep {
strings: Vec<CString>,
config: Option<ghostty_config_t>,
}
impl Keep {
/// A NUL terminated copy of `s` (interior NULs are dropped).
fn cstr(&mut self, s: &str) -> *const c_char {
let c = CString::new(s.replace('\0', "")).unwrap_or_default();
let ptr = c.as_ptr();
// The heap buffer of a CString doesn't move when the Vec grows.
self.strings.push(c);
ptr
}
}
impl Drop for Keep {
fn drop(&mut self) {
if let Some(h) = self.config.take() {
// SAFETY: created by `CConfig::into_handle` in `action_to_c`
// and not handed out beyond the callback.
unsafe { crate::config_capi::ghostty_config_free(h) };
}
}
}
/// `Target.cval`. The surface handle is the id of the surface (the address
/// of the embedded runtime's surface object, see `embedded.rs`).
#[must_use]
pub fn target_to_c(target: &Target<SurfaceId>) -> ghostty_target_s {
match target {
Target::App => ghostty_target_s {
tag: target_tag::APP,
target: ghostty_target_u {
surface: std::ptr::null_mut(),
},
},
Target::Surface(id) => ghostty_target_s {
tag: target_tag::SURFACE,
target: ghostty_target_u {
surface: *id as usize as *mut std::ffi::c_void,
},
},
}
}
fn zeroed_union() -> ghostty_action_u {
// SAFETY: every field of the union is plain old data for which the
// all-zero bit pattern is valid (integers, floats, null pointers, bool).
unsafe { std::mem::zeroed() }
}
/// `Action.cval`.
#[must_use]
pub fn action_to_c(action: &Action, keep: &mut Keep) -> ghostty_action_s {
let mut v = zeroed_union();
match action {
Action::CloseTab(m) => v.close_tab_mode = *m as c_int,
Action::NewSplit(d) => v.new_split = *d as c_int,
Action::ToggleFullscreen(f) => v.toggle_fullscreen = *f as c_int,
Action::MoveTab(m) => v.move_tab.amount = m.amount,
Action::GotoTab(t) => v.goto_tab = t.int(),
Action::GotoSplit(s) => v.goto_split = *s as c_int,
Action::GotoWindow(w) => v.goto_window = *w as c_int,
Action::ResizeSplit(r) => {
v.resize_split.amount = r.amount;
v.resize_split.direction = r.direction as c_int;
}
Action::SizeLimit(s) => {
v.size_limit.min_width = s.min_width;
v.size_limit.min_height = s.min_height;
v.size_limit.max_width = s.max_width;
v.size_limit.max_height = s.max_height;
}
Action::InitialSize(s) => {
v.initial_size.width = s.width;
v.initial_size.height = s.height;
}
Action::CellSize(s) => {
v.cell_size.width = s.width;
v.cell_size.height = s.height;
}
Action::Scrollbar(s) => {
v.scrollbar.total = s.total as u64;
v.scrollbar.offset = s.offset as u64;
v.scrollbar.len = s.len as u64;
}
Action::Inspector(i) => v.inspector = *i as c_int,
Action::DesktopNotification(n) => {
v.desktop_notification.title = keep.cstr(&n.title);
v.desktop_notification.body = keep.cstr(&n.body);
}
Action::SetTitle(t) => v.set_title.title = keep.cstr(&t.title),
Action::SetTabTitle(t) => v.set_tab_title.title = keep.cstr(&t.title),
Action::PromptTitle(p) => v.prompt_title = *p as c_int,
Action::Pwd(p) => v.pwd.pwd = keep.cstr(&p.pwd),
Action::MouseShape(s) => v.mouse_shape = *s as c_int,
Action::MouseVisibility(m) => v.mouse_visibility = *m as c_int,
Action::MouseOverLink(l) => {
v.mouse_over_link.url = keep.cstr(&l.url);
v.mouse_over_link.len = l.url.len();
}
Action::RendererHealth(h) => {
v.renderer_health = match h {
Health::Healthy => 0,
Health::Unhealthy => 1,
};
}
Action::QuitTimer(q) => v.quit_timer = *q as c_int,
Action::FloatWindow(f) => v.float_window = *f as c_int,
Action::SecureInput(s) => v.secure_input = *s as c_int,
Action::KeySequence(KeySequence::Trigger(t)) => {
v.key_sequence.active = true;
v.key_sequence.trigger = trigger_to_c(&t.cval());
}
Action::KeySequence(KeySequence::End) => {
v.key_sequence.active = false;
v.key_sequence.trigger = default_trigger();
}
Action::KeyTable(KeyTable::Activate(name)) => {
v.key_table.tag = 0;
v.key_table.value.activate.name = keep.cstr(name);
v.key_table.value.activate.len = name.len();
}
Action::KeyTable(KeyTable::Deactivate) => v.key_table.tag = 1,
Action::KeyTable(KeyTable::DeactivateAll) => v.key_table.tag = 2,
Action::ColorChange(c) => {
v.color_change.kind = c.kind.int();
v.color_change.r = c.r;
v.color_change.g = c.g;
v.color_change.b = c.b;
}
Action::ReloadConfig(r) => v.reload_config.soft = r.soft,
Action::ConfigChange(c) => {
// The embedder reads (and clones) the config during the
// callback; it is freed again when `keep` drops.
if let Some(old) = keep.config.take() {
// SAFETY: created below by an earlier conversion.
unsafe { crate::config_capi::ghostty_config_free(old) };
}
let handle = CConfig::new((*c.config).clone()).into_handle();
keep.config = Some(handle);
v.config_change.config = handle;
}
Action::OpenUrl(u) => {
v.open_url.kind = u.kind as c_int;
v.open_url.url = keep.cstr(&u.url);
v.open_url.len = u.url.len();
}
Action::ShowChildExited(c) => {
v.child_exited.exit_code = c.exit_code;
v.child_exited.timetime_ms = c.runtime_ms;
}
Action::ProgressReport(p) => {
v.progress_report.state = match p.state {
ProgressState::Remove => 0,
ProgressState::Set => 1,
ProgressState::Error => 2,
ProgressState::Indeterminate => 3,
ProgressState::Pause => 4,
};
v.progress_report.progress = p.progress.map_or(-1, |p| p as i8);
}
Action::CommandFinished(c) => {
v.command_finished = ghostty_action_command_finished_s {
exit_code: c.exit_code.map_or(-1, i16::from),
duration: c.duration.duration,
};
}
Action::StartSearch(s) => v.start_search.needle = keep.cstr(&s.needle),
Action::SearchTotal(s) => {
v.search_total.total = s.total.map_or(-1, |t| t as isize);
}
Action::SearchSelected(s) => {
v.search_selected.selected = s.selected.map_or(-1, |t| t as isize);
}
Action::Readonly(r) => v.readonly = *r as c_int,
_ => {}
}
ghostty_action_s {
tag: action.key() as c_int,
action: v,
}
}
#[cfg(test)]
mod tests {
use ghostty_apprt::action::{
CellSize,
CommandFinished,
GotoTab,
MouseOverLink,
ResizeSplit,
ResizeSplitDirection,
SearchTotal,
SetTitle,
};
use super::*;
#[test]
fn target_cval() {
let t = target_to_c(&Target::App);
assert_eq!(t.tag, 0);
let t = target_to_c(&Target::Surface(0x1000));
assert_eq!(t.tag, 1);
// SAFETY: tag says surface.
assert_eq!(unsafe { t.target.surface } as usize, 0x1000);
}
#[test]
fn simple_payloads() {
let mut keep = Keep::default();
let a = action_to_c(
&Action::CellSize(CellSize {
width: 7,
height: 9,
}),
&mut keep,
);
assert_eq!(a.tag, 25);
// SAFETY: tag says cell_size.
assert_eq!(
unsafe { (a.action.cell_size.width, a.action.cell_size.height) },
(7, 9)
);
let a = action_to_c(&Action::GotoTab(GotoTab::Last), &mut keep);
// SAFETY: tag says goto_tab.
assert_eq!(unsafe { a.action.goto_tab }, -3);
let a = action_to_c(
&Action::ResizeSplit(ResizeSplit {
amount: 5,
direction: ResizeSplitDirection::Left,
}),
&mut keep,
);
// SAFETY: tag says resize_split.
assert_eq!(
unsafe {
(
a.action.resize_split.amount,
a.action.resize_split.direction,
)
},
(5, 2)
);
let a = action_to_c(&Action::SearchTotal(SearchTotal { total: None }), &mut keep);
// SAFETY: tag says search_total.
assert_eq!(unsafe { a.action.search_total.total }, -1);
let a = action_to_c(
&Action::CommandFinished(CommandFinished {
exit_code: None,
duration: ghostty_config::types_b::Duration { duration: 5 },
}),
&mut keep,
);
// SAFETY: tag says command_finished.
assert_eq!(
unsafe {
(
a.action.command_finished.exit_code,
a.action.command_finished.duration,
)
},
(-1, 5)
);
}
#[test]
fn strings_stay_valid_while_kept() {
let mut keep = Keep::default();
let a = action_to_c(
&Action::SetTitle(SetTitle {
title: "hello".into(),
}),
&mut keep,
);
// SAFETY: tag says set_title; the pointer points into `keep`.
let s = unsafe { std::ffi::CStr::from_ptr(a.action.set_title.title) };
assert_eq!(s.to_str().unwrap(), "hello");
let a = action_to_c(
&Action::MouseOverLink(MouseOverLink {
url: "https://x".into(),
}),
&mut keep,
);
// SAFETY: tag says mouse_over_link.
assert_eq!(unsafe { a.action.mouse_over_link.len }, 9);
}
#[test]
fn config_change_hands_out_a_config() {
let mut keep = Keep::default();
let cfg = std::sync::Arc::new(ghostty_config::config::Config::default());
let a = action_to_c(
&Action::ConfigChange(ghostty_apprt::action::ConfigChange { config: cfg }),
&mut keep,
);
// SAFETY: tag says config_change.
let h = unsafe { a.action.config_change.config };
let mut out = false;
let key = "maximize";
// SAFETY: valid handle and destination.
assert!(unsafe {
crate::config_capi::ghostty_config_get(
h,
(&raw mut out).cast(),
key.as_ptr().cast(),
key.len(),
)
});
}
}
+98
View File
@@ -0,0 +1,98 @@
//! `ghostty_benchmark_cli` (ZIG/src/benchmark/CApi.zig).
#![allow(unsafe_code)]
use std::ffi::{CStr, c_char};
use ghostty_bench::{args::Args, benches};
/// Split the argument string like `std.process.ArgIteratorGeneral(.{})`:
/// whitespace separated, with double quotes grouping and backslash
/// escapes inside quotes.
fn split_args(s: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut have = false;
let mut in_quote = false;
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'"' => {
in_quote = !in_quote;
have = true;
}
'\\' if in_quote => {
if let Some(n) = chars.next() {
cur.push(n);
}
}
c if c.is_whitespace() && !in_quote => {
if have {
out.push(std::mem::take(&mut cur));
have = false;
}
}
c => {
cur.push(c);
have = true;
}
}
}
if have {
out.push(cur);
}
out
}
/// Run the Ghostty benchmark CLI with the given action and arguments.
///
/// # Safety
/// `action_name` and `args` must be NUL terminated strings.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_benchmark_cli(
action_name: *const c_char,
args: *const c_char,
) -> bool {
if action_name.is_null() || args.is_null() {
return false;
}
// SAFETY: per the function contract.
let (action, args) = unsafe { (CStr::from_ptr(action_name), CStr::from_ptr(args)) };
let action = action.to_string_lossy();
if !benches::ACTIONS.contains(&action.as_ref()) {
log::warn!("unknown action={action}");
return false;
}
let argv = split_args(&args.to_string_lossy());
let parsed = match Args::parse(argv) {
Ok(a) => a,
Err(err) => {
log::warn!("failed to run action={action} err={err}");
return false;
}
};
match benches::main_action(&action, parsed) {
Ok(_) => true,
Err(err) => {
log::warn!("failed to run action={action} err={err}");
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_args_basic() {
assert_eq!(split_args("a b c"), ["a", "b", "c"]);
assert_eq!(split_args("--x=\"a b\" c"), ["--x=a b", "c"]);
assert_eq!(split_args(" ").len(), 0);
}
#[test]
fn unknown_action_returns_false() {
// SAFETY: valid NUL terminated strings.
assert!(!unsafe { ghostty_benchmark_cli(c"nope".as_ptr(), c"".as_ptr()) });
}
}
+727
View File
@@ -0,0 +1,727 @@
//! `ghostty_config_*` (ZIG/src/config/CApi.zig and c_get.zig).
#![allow(unsafe_code)]
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
ffi::{CStr, CString, c_char, c_int, c_short, c_uint, c_void},
rc::Rc,
sync::{Mutex, OnceLock},
};
use ghostty_config::{
c_value::{CValue, QuickTerminalSizeC},
config::Config,
};
use ghostty_input::binding::{Action as BindingAction, TriggerC, TriggerCTag};
use crate::{
main_c::{EMPTY_STRING, string_from_owned},
types::{
ghostty_command_s,
ghostty_config_color_list_s,
ghostty_config_color_s,
ghostty_config_command_list_s,
ghostty_config_palette_s,
ghostty_config_path_s,
ghostty_config_quick_terminal_size_s,
ghostty_config_t,
ghostty_diagnostic_s,
ghostty_input_trigger_key_u,
ghostty_input_trigger_s,
ghostty_quick_terminal_size_s,
ghostty_quick_terminal_size_value_u,
ghostty_string_s,
quick_terminal_size_tag,
trigger_tag,
},
};
/// The C-owned configuration: the config plus the memory that pointers
/// handed out by `ghostty_config_get` / `ghostty_config_get_diagnostic` point
/// into (Zig: the config's arena). Everything stays valid until the config
/// is freed.
pub struct CConfig {
pub config: Config,
cache: RefCell<GetCache>,
diagnostics: RefCell<Vec<CString>>,
}
/// Backing memory of one `ghostty_config_get` result.
#[derive(Default)]
struct Backing {
strings: Vec<CString>,
colors: Box<[ghostty_config_color_s]>,
commands: Box<[ghostty_command_s]>,
}
#[derive(Default)]
struct GetCache {
/// The latest result per key; reused if the value didn't change so
/// repeated gets return the same pointers.
by_key: HashMap<String, (CValue, Rc<Backing>)>,
/// Results that were replaced; kept alive until the config is freed
/// since callers may still hold their pointers.
retired: Vec<Rc<Backing>>,
}
impl CConfig {
pub fn new(config: Config) -> CConfig {
CConfig {
config,
cache: RefCell::default(),
diagnostics: RefCell::default(),
}
}
/// Box the config and return the `ghostty_config_t` handle.
pub fn into_handle(self) -> ghostty_config_t {
Box::into_raw(Box::new(self)).cast::<c_void>()
}
/// The backing memory for `value`, reusing the cached one if equal.
fn backing(&self, key: &str, value: &CValue) -> Rc<Backing> {
let mut cache = self.cache.borrow_mut();
if let Some((old, backing)) = cache.by_key.get(key)
&& old == value
{
return Rc::clone(backing);
}
let backing = Rc::new(build_backing(value));
if let Some((_, old)) = cache
.by_key
.insert(key.to_owned(), (value.clone(), Rc::clone(&backing)))
{
cache.retired.push(old);
}
backing
}
}
/// A C string without interior NULs.
fn cstring(s: &str) -> CString {
CString::new(s.replace('\0', "")).unwrap_or_default()
}
/// Intern a string for the lifetime of the process (enum tag names).
fn intern(s: &str) -> *const c_char {
static POOL: OnceLock<Mutex<HashSet<Box<CStr>>>> = OnceLock::new();
let Ok(mut pool) = POOL.get_or_init(Mutex::default).lock() else {
return c"".as_ptr();
};
let c = cstring(s);
if let Some(existing) = pool.get(c.as_c_str()) {
return existing.as_ptr();
}
let boxed: Box<CStr> = c.into_boxed_c_str();
let ptr = boxed.as_ptr();
pool.insert(boxed);
ptr
}
fn build_backing(value: &CValue) -> Backing {
let mut b = Backing::default();
match value {
CValue::String(s) => b.strings.push(cstring(s)),
CValue::Path { path, .. } => b.strings.push(cstring(path)),
CValue::ColorList(list) => {
b.colors = list
.iter()
.map(|c| ghostty_config_color_s {
r: c[0],
g: c[1],
b: c[2],
})
.collect();
}
CValue::Commands(cmds) => {
for c in cmds {
b.strings.push(cstring(&c.action_key));
b.strings.push(cstring(&c.action));
b.strings.push(cstring(&c.title));
b.strings.push(cstring(&c.description));
}
// The CStrings' heap buffers don't move when the Vec grows.
b.commands = b
.strings
.as_chunks::<4>()
.0
.iter()
.map(|s| ghostty_command_s {
action_key: s[0].as_ptr(),
action: s[1].as_ptr(),
title: s[2].as_ptr(),
description: s[3].as_ptr(),
})
.collect();
}
_ => {}
}
b
}
/// Borrow the config behind a handle.
///
/// # Safety
/// `p` must be null or a handle returned by this library that wasn't freed,
/// and no other reference to it may be live.
pub(crate) unsafe fn cconfig<'a>(p: ghostty_config_t) -> Option<&'a mut CConfig> {
// SAFETY: per the function contract.
unsafe { p.cast::<CConfig>().as_mut() }
}
/// Create a new configuration filled with the initial default values.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_config_new() -> ghostty_config_t {
CConfig::new(Config::default()).into_handle()
}
/// Free a configuration.
///
/// # Safety
/// `ptr` must be null or a handle from this library, not used afterwards.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_free(ptr: ghostty_config_t) {
if ptr.is_null() {
return;
}
// SAFETY: the handle was created by `into_handle` (a leaked Box).
drop(unsafe { Box::from_raw(ptr.cast::<CConfig>()) });
}
/// Deep clone the configuration.
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_clone(ptr: ghostty_config_t) -> ghostty_config_t {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return std::ptr::null_mut();
};
CConfig::new(c.config.clone()).into_handle()
}
/// Load the configuration from the CLI args.
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_load_cli_args(ptr: ghostty_config_t) {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return;
};
if let Err(err) = c.config.load_cli_args() {
log::error!("error loading config err={err}");
}
}
/// Load the configuration from the default file locations. This is usually
/// done first. The default file locations are locations such as the home
/// directory.
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_load_default_files(ptr: ghostty_config_t) {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return;
};
if let Err(err) = c.config.load_default_files() {
log::error!("error loading config err={err}");
}
}
/// Load the configuration from a specific file path. The path must be null
/// terminated.
///
/// # Safety
/// `ptr` must be a valid config handle, `path` a NUL terminated string.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_load_file(ptr: ghostty_config_t, path: *const c_char) {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return;
};
if path.is_null() {
return;
}
// SAFETY: `path` is NUL terminated.
let path = unsafe { CStr::from_ptr(path) }
.to_string_lossy()
.into_owned();
if let Err(err) = c.config.load_file(&path) {
log::error!("error loading config from file path={path} err={err}");
}
}
/// Load the configuration from the user-specified configuration file
/// locations in the previously loaded configuration. This will recursively
/// continue to load up to a built-in limit.
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_load_recursive_files(ptr: ghostty_config_t) {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return;
};
if let Err(err) = c.config.load_recursive_files() {
log::error!("error loading config err={err}");
}
}
/// Finalize the configuration (fill in computed defaults).
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_finalize(ptr: ghostty_config_t) {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return;
};
if let Err(err) = c.config.finalize() {
log::error!("error finalizing config err={err}");
}
}
/// Store `v` at `out` (`T` must be what the C caller expects for the key).
///
/// # Safety
/// `out` must be valid for a write of `T`.
unsafe fn put<T>(out: *mut c_void, v: T) {
// SAFETY: per the function contract; unaligned writes are tolerated.
unsafe { out.cast::<T>().write_unaligned(v) };
}
fn qt_size(v: QuickTerminalSizeC) -> ghostty_quick_terminal_size_s {
match v {
QuickTerminalSizeC::None => ghostty_quick_terminal_size_s {
tag: quick_terminal_size_tag::NONE,
value: ghostty_quick_terminal_size_value_u { pixels: 0 },
},
QuickTerminalSizeC::Percentage(p) => ghostty_quick_terminal_size_s {
tag: quick_terminal_size_tag::PERCENTAGE,
value: ghostty_quick_terminal_size_value_u { percentage: p },
},
QuickTerminalSizeC::Pixels(p) => ghostty_quick_terminal_size_s {
tag: quick_terminal_size_tag::PIXELS,
value: ghostty_quick_terminal_size_value_u { pixels: p },
},
}
}
impl CConfig {
/// `c_get.get`: write the value of `key` to `out`. Returns false if the
/// key is unknown, its type isn't supported by the C API yet or an
/// optional value is unset.
///
/// # Safety
/// `out` must point to storage of the C type of the key (see
/// `ghostty.h` and `c_get.zig`).
pub unsafe fn get(&self, key: &str, out: *mut c_void) -> bool {
let Some(value) = self.config.c_value(key) else {
return false;
};
// SAFETY (all writes below): `out` is valid for the key's C type by
// the function contract.
match &value {
CValue::Unsupported | CValue::Null => return false,
CValue::NullString => unsafe { put(out, std::ptr::null::<c_char>()) },
CValue::String(_) => {
let b = self.backing(key, &value);
unsafe { put(out, b.strings[0].as_ptr()) };
}
CValue::Bool(v) => unsafe { put(out, *v) },
CValue::Uint(v) => unsafe { put::<c_uint>(out, *v) },
CValue::Short(v) => unsafe { put::<c_short>(out, *v) },
CValue::F32(v) => unsafe { put(out, *v) },
CValue::F64(v) => unsafe { put(out, *v) },
CValue::Usize(v) => unsafe { put(out, *v) },
CValue::EnumName(name) => unsafe { put(out, intern(name)) },
CValue::Packed(bits) => unsafe { put::<c_uint>(out, *bits) },
CValue::Color { r, g, b } => unsafe {
put(
out,
ghostty_config_color_s {
r: *r,
g: *g,
b: *b,
},
);
},
CValue::ColorList(list) => {
let b = self.backing(key, &value);
unsafe {
put(
out,
ghostty_config_color_list_s {
colors: b.colors.as_ptr(),
len: list.len(),
},
);
}
}
CValue::Palette(colors) => {
let mut p = ghostty_config_palette_s {
colors: [ghostty_config_color_s { r: 0, g: 0, b: 0 }; 256],
};
for (dst, c) in p.colors.iter_mut().zip(colors.iter()) {
*dst = ghostty_config_color_s {
r: c[0],
g: c[1],
b: c[2],
};
}
unsafe { put(out, p) };
}
CValue::Commands(cmds) => {
let b = self.backing(key, &value);
unsafe {
put(
out,
ghostty_config_command_list_s {
commands: b.commands.as_ptr(),
len: cmds.len(),
},
);
}
}
CValue::QuickTerminalSize { primary, secondary } => unsafe {
put(
out,
ghostty_config_quick_terminal_size_s {
primary: qt_size(*primary),
secondary: qt_size(*secondary),
},
);
},
CValue::Path { optional, .. } => {
let b = self.backing(key, &value);
unsafe {
put(
out,
ghostty_config_path_s {
path: b.strings[0].as_ptr(),
optional: *optional,
},
);
}
}
}
true
}
}
/// Get the value of a config key (see `c_get.zig` for the C types).
///
/// # Safety
/// `ptr` must be a valid config handle, `key_str` must point to `len`
/// bytes and `out` to storage of the key's C type.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_get(
ptr: ghostty_config_t,
out: *mut c_void,
key_str: *const c_char,
len: usize,
) -> bool {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return false;
};
if out.is_null() || key_str.is_null() {
return false;
}
// SAFETY: `key_str` points to `len` bytes.
let key = unsafe { std::slice::from_raw_parts(key_str.cast::<u8>(), len) };
let Ok(key) = std::str::from_utf8(key) else {
return false;
};
// SAFETY: `out` is valid for the key's type per the contract.
unsafe { c.get(key, out) }
}
/// Convert the Rust trigger to the C struct.
pub(crate) fn trigger_to_c(t: &TriggerC) -> ghostty_input_trigger_s {
let (tag, key) = match t.tag {
TriggerCTag::Physical => (
trigger_tag::PHYSICAL,
ghostty_input_trigger_key_u {
physical: t.key as c_int,
},
),
TriggerCTag::Unicode => (
trigger_tag::UNICODE,
ghostty_input_trigger_key_u { unicode: t.key },
),
TriggerCTag::CatchAll => (
trigger_tag::CATCH_ALL,
ghostty_input_trigger_key_u { unicode: 0 },
),
};
ghostty_input_trigger_s {
tag,
key,
mods: c_int::from(t.mods.int()),
}
}
/// The default (unset) trigger (`Trigger.C{}`).
pub(crate) fn default_trigger() -> ghostty_input_trigger_s {
ghostty_input_trigger_s {
tag: trigger_tag::PHYSICAL,
key: ghostty_input_trigger_key_u { physical: 0 },
mods: 0,
}
}
/// Find the trigger bound to the given binding action string.
///
/// # Safety
/// `ptr` must be a valid config handle, `str` must point to `len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_trigger(
ptr: ghostty_config_t,
str: *const c_char,
len: usize,
) -> ghostty_input_trigger_s {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return default_trigger();
};
if str.is_null() {
return default_trigger();
}
// SAFETY: `str` points to `len` bytes.
let s = unsafe { std::slice::from_raw_parts(str.cast::<u8>(), len) };
let Ok(s) = std::str::from_utf8(s) else {
log::error!("error finding trigger err=InvalidFormat");
return default_trigger();
};
match BindingAction::parse(s) {
Ok(action) => {
let trigger = c
.config
.keybind
.set
.get_trigger(&action)
.unwrap_or_default();
trigger_to_c(&trigger.cval())
}
Err(err) => {
log::error!("error finding trigger err={err:?}");
default_trigger()
}
}
}
/// The number of diagnostics.
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_diagnostics_count(ptr: ghostty_config_t) -> u32 {
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return 0;
};
u32::try_from(c.config.diagnostics.items().len()).unwrap_or(u32::MAX)
}
/// The diagnostic at the given index (an empty message if out of range).
///
/// # Safety
/// `ptr` must be a valid config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_config_get_diagnostic(
ptr: ghostty_config_t,
idx: u32,
) -> ghostty_diagnostic_s {
let empty = ghostty_diagnostic_s {
message: c"".as_ptr(),
};
// SAFETY: per the function contract.
let Some(c) = (unsafe { cconfig(ptr) }) else {
return empty;
};
let items = c.config.diagnostics.items();
let idx = idx as usize;
if idx >= items.len() {
return empty;
}
// Precompute (like libghostty's `DiagnosticList.precompute`): the
// messages are formatted once per diagnostic and live as long as the
// config.
let mut msgs = c.diagnostics.borrow_mut();
while msgs.len() < items.len() {
let mut text = String::new();
let _ = items[msgs.len()].format(&mut text);
msgs.push(cstring(&text));
}
ghostty_diagnostic_s {
message: msgs[idx].as_ptr(),
}
}
/// The path of the config file to open in an editor (created if needed).
/// Free with `ghostty_string_free`.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_config_open_path() -> ghostty_string_s {
match ghostty_config::edit::open_path() {
Ok(path) => string_from_owned(&path),
Err(err) => {
log::error!("error opening config in editor err={err}");
EMPTY_STRING
}
}
}
#[cfg(test)]
mod tests {
use std::ffi::c_uint;
use super::*;
fn get<T: Copy>(cfg: ghostty_config_t, key: &str, init: T) -> (bool, T) {
let mut out = init;
// SAFETY: `out` is a valid destination of the key's C type.
let ok = unsafe {
ghostty_config_get(cfg, (&raw mut out).cast(), key.as_ptr().cast(), key.len())
};
(ok, out)
}
fn with_config(f: impl FnOnce(&mut Config)) -> ghostty_config_t {
let h = ghostty_config_new();
// SAFETY: just created.
f(&mut unsafe { cconfig(h) }.unwrap().config);
h
}
#[test]
fn ghostty_config_get_bool() {
let h = with_config(|c| c.maximize = true);
assert_eq!(get(h, "maximize", false), (true, true));
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn ghostty_config_get_enum() {
let h = with_config(|c| c.window_theme = ghostty_config::types_b::WindowTheme::Dark);
let (ok, p) = get::<*const c_char>(h, "window-theme", std::ptr::null());
assert!(ok);
// SAFETY: an interned NUL terminated string.
assert_eq!(unsafe { CStr::from_ptr(p) }.to_str().unwrap(), "dark");
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn ghostty_config_get_optional_null_returns_false() {
let h = with_config(|c| c.unfocused_split_fill = None);
let (ok, _) = get(
h,
"unfocused-split-fill",
ghostty_config_color_s { r: 0, g: 0, b: 0 },
);
assert!(!ok);
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn ghostty_config_get_unknown_key_returns_false() {
let h = ghostty_config_new();
assert!(!get(h, "not-a-real-key", false).0);
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn ghostty_config_get_optional_string_null_returns_true() {
let h = with_config(|c| c.title = None);
let (ok, p) = get::<*const c_char>(h, "title", std::ptr::dangling::<c_char>());
assert!(ok);
assert!(p.is_null());
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn ghostty_config_get_float() {
let h = with_config(|c| c.background_opacity = 0.42);
let (ok, v) = get(h, "background-opacity", 0.0f64);
assert!(ok);
assert!((v - 0.42).abs() < 1e-6);
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn ghostty_config_get_struct_cval_conversion() {
let h = with_config(|c| {
c.background = ghostty_config::types_a::Color {
r: 12,
g: 34,
b: 56,
};
});
let (ok, v) = get(h, "background", ghostty_config_color_s { r: 0, g: 0, b: 0 });
assert!(ok);
assert_eq!((v.r, v.g, v.b), (12, 34, 56));
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn c_get_u8() {
let h = with_config(|c| c.font_size = 24.0);
assert_eq!(get(h, "font-size", 0.0f32), (true, 24.0));
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn c_get_split_preserve_zoom() {
let h = ghostty_config_new();
assert_eq!(get(h, "split-preserve-zoom", 9 as c_uint), (true, 0));
// SAFETY: valid handle.
unsafe {
cconfig(h).unwrap().config.split_preserve_zoom.navigation = true;
}
assert_eq!(get(h, "split-preserve-zoom", 9 as c_uint), (true, 1));
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
#[test]
fn c_get_pointers_are_stable_for_equal_values() {
let h = ghostty_config_new();
let (_, a) = get::<ghostty_config_command_list_s>(
h,
"command-palette-entry",
ghostty_config_command_list_s {
commands: std::ptr::null(),
len: 0,
},
);
let (_, b) = get::<ghostty_config_command_list_s>(
h,
"command-palette-entry",
ghostty_config_command_list_s {
commands: std::ptr::null(),
len: 0,
},
);
assert!(a.len > 0);
assert_eq!(a.commands, b.commands);
// SAFETY: valid handle.
unsafe { ghostty_config_free(h) };
}
}
+2157
View File
@@ -0,0 +1,2157 @@
//! The embedded application runtime (ZIG/src/apprt/embedded.zig): the
//! runtime side of `ghostty-core` implemented over the C callbacks of
//! `ghostty_runtime_config_s`, plus the `ghostty_app_*` / `ghostty_surface_*`
//! exports.
//!
//! Everything here is main-thread only, like the Zig embedded runtime: the
//! host calls the C API from its UI thread. The only cross-thread entry is
//! the `wakeup` callback, invoked by core threads through [`WakerImpl`].
//!
//! Handles: `ghostty_app_t` is a `Box<AppHandle>` and `ghostty_surface_t` a
//! `Box<Surface>`. The address of the surface object doubles as the
//! `SurfaceId` core knows the surface by, and as the `ghostty_surface_t` in
//! `ghostty_target_s` of actions (Zig: `target.cval()` gives `rt_surface`).
#![allow(unsafe_code)]
use std::{
cell::{Cell, OnceCell, RefCell},
collections::HashMap,
ffi::{CStr, CString, c_char, c_int, c_void},
fmt::Write as _,
rc::{Rc, Weak},
sync::Arc,
};
use ghostty_apprt::{
action::{Action, GotoSplit, ResizeSplit, ResizeSplitDirection, SplitDirection, Target},
runtime_api::{CoreApp, CoreSurface, RtApp, RtSurface, SurfaceId, Waker},
structs::{Clipboard, ClipboardRequest, ColorScheme, ContentScale, CursorPos, SurfaceSize},
surface::{NewSurfaceContext, new_config, should_inherit_working_directory},
};
use ghostty_config::{command::Command, config::Config, io::ReadableIO, types_a::WorkingDirectory};
use ghostty_core::{App as CoreAppImpl, Surface as CoreSurfaceImpl, surface::binding::Text};
use ghostty_input::{
Action as KeyAction,
Key,
KeyEvent,
Mods,
MouseButton,
MouseButtonState,
MousePressureStage,
ScrollMods,
binding::Action as BindingAction,
keycodes,
};
use ghostty_terminal::{
point::{Coordinate, Point, Tag as PointTag},
selection::Selection,
terminal::Terminal,
};
use crate::{
action_c::{Keep, action_to_c, target_to_c},
config_capi::cconfig,
main_c::{alloc_c_text, free_c_text},
types::{
ghostty_app_t,
ghostty_binding_flags_e,
ghostty_clipboard_content_s,
ghostty_config_t,
ghostty_input_key_s,
ghostty_point_s,
ghostty_runtime_config_s,
ghostty_selection_s,
ghostty_surface_config_s,
ghostty_surface_size_s,
ghostty_surface_t,
ghostty_text_s,
platform,
},
};
// ---------------------------------------------------------------------
// Waker
/// `App.wakeup`: invoked from any core thread; calls the host's `wakeup_cb`.
struct WakerImpl {
cb: unsafe extern "C" fn(*mut c_void),
userdata: usize,
}
impl Waker for WakerImpl {
fn wake(&self) {
// SAFETY: the embedder promises `wakeup_cb` may be called from any
// thread with the `userdata` it supplied (the documented contract of
// `ghostty_runtime_wakeup_cb`).
unsafe { (self.cb)(self.userdata as *mut c_void) };
}
}
// ---------------------------------------------------------------------
// Runtime app
/// The runtime side of the app (Zig: `embedded.App`), handed to core as
/// `Rc<dyn RtApp>`.
pub struct AppRt {
opts: ghostty_runtime_config_s,
/// The `ghostty_app_t` passed to the action callback.
app_ptr: Cell<usize>,
/// The configuration for the app (a clone, owned by the app).
config: RefCell<Arc<Config>>,
surfaces: RefCell<HashMap<SurfaceId, Weak<SurfaceRt>>>,
keymap: RefCell<ghostty_input::Keymap>,
}
impl AppRt {
fn surface_rt(&self, id: SurfaceId) -> Option<Rc<SurfaceRt>> {
self.surfaces.borrow().get(&id).and_then(Weak::upgrade)
}
/// Special case certain actions before they are sent to the embedder
/// (Zig: `performPreAction`).
fn pre_action(&self, target: &Target<SurfaceId>, action: &Action) {
match (action, target) {
(Action::SetTitle(t), Target::Surface(id)) => {
if let Some(s) = self.surface_rt(*id) {
*s.title.borrow_mut() = Some(t.title.clone());
}
}
// For app updates, we update our core config. The caller owns
// the param so we clone it.
(Action::ConfigChange(c), Target::App) => {
*self.config.borrow_mut() = Arc::clone(&c.config);
}
_ => {}
}
}
/// Reload the keyboard layout (`App.reloadKeymap`).
fn reload_keymap(&self) {
if let Err(err) = self.keymap.borrow_mut().reload() {
log::error!("error reloading keyboard map err={err:?}");
}
}
}
impl RtApp for AppRt {
fn perform_action(&self, target: Target<SurfaceId>, action: Action) -> bool {
self.pre_action(&target, &action);
log::debug!(
"dispatching action target={target:?} action={:?}",
action.key()
);
let Some(cb) = self.opts.action_cb else {
return false;
};
let mut keep = Keep::default();
let t = target_to_c(&target);
let a = action_to_c(&action, &mut keep);
// SAFETY: the embedder supplied the callback; it receives the app
// handle it knows plus plain-data structs whose pointers stay valid
// (`keep`) for the duration of the call.
unsafe { cb(self.app_ptr.get() as ghostty_app_t, t, a) }
}
fn keyboard_layout(&self) -> ghostty_input::KeyboardLayout {
keyboard_layout(&self.keymap.borrow())
}
}
/// `App.keyboardLayout`: we only support keyboard layout detection on macOS.
#[cfg(target_os = "macos")]
fn keyboard_layout(keymap: &ghostty_input::Keymap) -> ghostty_input::KeyboardLayout {
keymap
.source_id()
.and_then(|id| ghostty_input::KeyboardLayout::map_apple_id(&id))
.unwrap_or(ghostty_input::KeyboardLayout::Unknown)
}
#[cfg(not(target_os = "macos"))]
#[allow(clippy::trivially_copy_pass_by_ref)]
fn keyboard_layout(_keymap: &ghostty_input::Keymap) -> ghostty_input::KeyboardLayout {
ghostty_input::KeyboardLayout::Unknown
}
/// The app object behind `ghostty_app_t`.
pub struct AppHandle {
core: Rc<CoreAppImpl>,
rt: Rc<AppRt>,
}
/// Borrow the app behind a handle.
///
/// # Safety
/// `p` must be null or a live handle from `ghostty_app_new`.
unsafe fn app<'a>(p: ghostty_app_t) -> Option<&'a AppHandle> {
// SAFETY: per the function contract.
unsafe { p.cast::<AppHandle>().as_ref() }
}
/// Borrow a C string.
///
/// # Safety
/// `p` must be null or NUL terminated.
unsafe fn cstr<'a>(p: *const c_char) -> Option<&'a CStr> {
if p.is_null() {
None
} else {
// SAFETY: per the function contract.
Some(unsafe { CStr::from_ptr(p) })
}
}
fn mods_from_c(v: c_int) -> Mods {
// `@truncate(@as(c_uint, @bitCast(mods)))` to the u16 backing.
Mods::from_int(v as u32 as u16)
}
/// An owned key event converted from the C struct (Zig: `KeyEvent.core`).
struct OwnedKey {
action: KeyAction,
key: Key,
mods: Mods,
consumed_mods: Mods,
composing: bool,
text: String,
unshifted_codepoint: u32,
}
impl OwnedKey {
/// `None` if the action is invalid.
///
/// # Safety
/// `text` must be null or NUL terminated.
unsafe fn from_c(e: &ghostty_input_key_s) -> Option<OwnedKey> {
let action = match e.action {
0 => KeyAction::Release,
1 => KeyAction::Press,
2 => KeyAction::Repeat,
_ => return None,
};
// SAFETY: per the function contract.
let text = unsafe { cstr(e.text) }
.map(|c| c.to_string_lossy().into_owned())
.unwrap_or_default();
// We want to get the physical unmapped key to process keybinds.
let key = keycodes::entries()
.iter()
.find(|entry| entry.native == e.keycode)
.map_or(Key::Unidentified, |entry| entry.key);
// The unshifted codepoint is a `u21`; anything larger is 0.
let unshifted_codepoint = if e.unshifted_codepoint <= 0x1F_FFFF {
e.unshifted_codepoint
} else {
0
};
Some(OwnedKey {
action,
key,
mods: mods_from_c(e.mods),
consumed_mods: mods_from_c(e.consumed_mods),
composing: e.composing,
text,
unshifted_codepoint,
})
}
fn event(&self) -> KeyEvent<'_> {
KeyEvent {
action: self.action,
key: self.key,
mods: self.mods,
consumed_mods: self.consumed_mods,
composing: self.composing,
utf8: &self.text,
unshifted_codepoint: self.unshifted_codepoint,
}
}
}
// ---------------------------------------------------------------------
// App exports
/// Create a new app.
///
/// # Safety
/// `opts` and `config` must be valid pointers (the config a handle from
/// `ghostty_config_new`/`_clone`).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_new(
opts: *const ghostty_runtime_config_s,
config: ghostty_config_t,
) -> ghostty_app_t {
// SAFETY: per the function contract.
let (Some(opts), Some(config)) = (unsafe { opts.as_ref() }, unsafe { cconfig(config) }) else {
log::error!("error initializing app err=NullArgument");
return std::ptr::null_mut();
};
let Some(wakeup) = opts.wakeup_cb else {
log::error!("error initializing app err=MissingWakeupCallback");
return std::ptr::null_mut();
};
let waker = Arc::new(WakerImpl {
cb: wakeup,
userdata: opts.userdata as usize,
});
let core = match CoreAppImpl::new(waker) {
Ok(c) => Rc::new(c),
Err(err) => {
log::error!("error initializing app err={err}");
return std::ptr::null_mut();
}
};
let keymap = match ghostty_input::Keymap::new() {
Ok(k) => k,
Err(err) => {
log::error!("error initializing app err={err:?}");
return std::ptr::null_mut();
}
};
// We have to clone the config.
let rt = Rc::new(AppRt {
opts: *opts,
app_ptr: Cell::new(0),
config: RefCell::new(Arc::new(config.config.clone())),
surfaces: RefCell::default(),
keymap: RefCell::new(keymap),
});
let ptr = Box::into_raw(Box::new(AppHandle { core, rt }));
// SAFETY: just allocated.
unsafe { &*ptr }.rt.app_ptr.set(ptr as usize);
ptr.cast::<c_void>()
}
/// Tick the event loop. This should be called whenever the "wakeup" callback
/// is invoked for the runtime.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_tick(v: ghostty_app_t) {
// SAFETY: per the function contract.
let Some(a) = (unsafe { app(v) }) else {
return;
};
if let Err(err) = a.core.tick(&*a.rt) {
log::error!("error app tick err={err}");
}
}
/// Return the userdata associated with the app.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_userdata(v: ghostty_app_t) -> *mut c_void {
// SAFETY: per the function contract.
unsafe { app(v) }.map_or(std::ptr::null_mut(), |a| a.rt.opts.userdata)
}
/// Free the app.
///
/// # Safety
/// `v` must be a live app handle with no surfaces left; it is invalid
/// afterwards.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_free(v: ghostty_app_t) {
if v.is_null() {
return;
}
// SAFETY: created by `ghostty_app_new` via `Box::into_raw`.
drop(unsafe { Box::from_raw(v.cast::<AppHandle>()) });
}
/// Update the focused state of the app.
///
/// # Safety
/// `app` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_set_focus(handle: ghostty_app_t, focused: bool) {
// SAFETY: per the function contract.
if let Some(a) = unsafe { app(handle) } {
a.core.focus_event(focused);
}
}
/// Notify the app of a global keypress capture. This will return true if the
/// key was captured by the app, in which case the caller should not process
/// the key.
///
/// # Safety
/// `handle` must be a live app handle; `event.text` null or NUL terminated.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_key(
handle: ghostty_app_t,
event: ghostty_input_key_s,
) -> bool {
// SAFETY: per the function contract.
let Some(a) = (unsafe { app(handle) }) else {
return false;
};
// SAFETY: per the function contract.
let Some(key) = (unsafe { OwnedKey::from_c(&event) }) else {
return false;
};
// Never hold the borrow across the call: actions call into the host.
let config = Arc::clone(&a.rt.config.borrow());
a.core.key_event(&*a.rt, &config, &key.event())
}
/// Returns true if the given key event would trigger a binding if it were
/// sent to the surface right now. The "right now" is important because
/// things like trigger sequences are only valid until the next key event.
///
/// # Safety
/// As [`ghostty_app_key`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_key_is_binding(
handle: ghostty_app_t,
event: ghostty_input_key_s,
) -> bool {
// SAFETY: per the function contract.
let Some(a) = (unsafe { app(handle) }) else {
return false;
};
// SAFETY: per the function contract.
let Some(key) = (unsafe { OwnedKey::from_c(&event) }) else {
log::warn!("error processing key event");
return false;
};
let config = Arc::clone(&a.rt.config.borrow());
a.core.key_event_is_binding(&config, &key.event())
}
/// Notify the app that the keyboard was changed. This causes the keyboard
/// layout to be reloaded from the OS.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_keyboard_changed(v: ghostty_app_t) {
// SAFETY: per the function contract.
if let Some(a) = unsafe { app(v) } {
a.rt.reload_keymap();
}
}
/// Open the configuration.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_open_config(v: ghostty_app_t) {
// SAFETY: per the function contract.
if let Some(a) = unsafe { app(v) } {
a.rt.perform_action(Target::App, Action::OpenConfig);
}
}
/// Update the configuration to the provided config. This will propagate to
/// all surfaces as well.
///
/// # Safety
/// `v` a live app handle, `config` a config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_update_config(v: ghostty_app_t, config: ghostty_config_t) {
// SAFETY: per the function contract.
let (Some(a), Some(c)) = (unsafe { app(v) }, unsafe { cconfig(config) }) else {
return;
};
let cfg = Arc::new(c.config.clone());
if let Err(err) = a.core.update_config(&*a.rt, &cfg) {
log::error!("error updating config err={err}");
}
}
/// Returns true if the app needs to confirm quitting.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_needs_confirm_quit(v: ghostty_app_t) -> bool {
// SAFETY: per the function contract.
unsafe { app(v) }.is_some_and(|a| a.core.needs_confirm_quit())
}
/// Returns true if the app has global keybinds.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_has_global_keybinds(v: ghostty_app_t) -> bool {
use ghostty_input::binding::set::Value;
// SAFETY: per the function contract.
let Some(a) = (unsafe { app(v) }) else {
return false;
};
let config = a.rt.config.borrow();
config
.keybind
.set
.bindings
.values()
.any(|value| match value {
Value::Leader(_) => false,
Value::Leaf(leaf) => leaf.flags.global,
Value::LeafChained(leaf) => leaf.flags.global,
})
}
fn color_scheme_from_c(v: c_int) -> Option<ColorScheme> {
match v {
0 => Some(ColorScheme::Light),
1 => Some(ColorScheme::Dark),
_ => None,
}
}
/// Update the color scheme of the app.
///
/// # Safety
/// `v` must be a live app handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_app_set_color_scheme(v: ghostty_app_t, scheme_raw: c_int) {
// SAFETY: per the function contract.
let Some(a) = (unsafe { app(v) }) else {
return;
};
let Some(scheme) = color_scheme_from_c(scheme_raw) else {
log::warn!("invalid color scheme to ghostty_app_set_color_scheme value={scheme_raw}");
return;
};
a.core.color_scheme_event(&*a.rt, scheme);
}
// ---------------------------------------------------------------------
// Runtime surface
/// The runtime side of a surface (Zig: `embedded.Surface` fields that core
/// reads), handed to core as `Rc<dyn RtSurface>`.
pub struct SurfaceRt {
app: Rc<AppRt>,
id: SurfaceId,
userdata: *mut c_void,
/// The `NSView` / `UIView` of the surface.
view: *mut c_void,
content_scale: Cell<ContentScale>,
size: Cell<SurfaceSize>,
cursor_pos: Cell<CursorPos>,
/// The current title of the surface. The embedded apprt saves this so
/// that `get_title` works without the implementer needing to save it.
title: RefCell<Option<String>>,
}
impl SurfaceRt {
fn target(&self) -> Target<SurfaceId> {
Target::Surface(self.id)
}
}
impl RtSurface for SurfaceRt {
fn close(&self, process_active: bool) {
let Some(cb) = self.app.opts.close_surface_cb else {
log::info!("runtime embedder does not support closing a surface");
return;
};
// SAFETY: embedder supplied callback with its own userdata.
unsafe { cb(self.userdata, process_active) };
}
fn cgroup(&self) -> Option<String> {
None
}
fn get_title(&self) -> Option<String> {
self.title.borrow().clone()
}
fn get_content_scale(&self) -> ContentScale {
self.content_scale.get()
}
fn get_size(&self) -> SurfaceSize {
self.size.get()
}
fn get_cursor_pos(&self) -> CursorPos {
self.cursor_pos.get()
}
fn supports_clipboard(&self, clipboard: Clipboard) -> bool {
match clipboard {
Clipboard::Standard => true,
Clipboard::Selection | Clipboard::Primary => self.app.opts.supports_selection_clipboard,
}
}
fn clipboard_request(&self, clipboard: Clipboard, state: ClipboardRequest) -> bool {
// We need to allocate to get a pointer to store our clipboard
// request so that it is stable until the read_clipboard callback
// and call complete_clipboard_request. This sucks but clipboard
// requests aren't high throughput so it's probably fine.
let Some(cb) = self.app.opts.read_clipboard_cb else {
return false;
};
let state_ptr = Box::into_raw(Box::new(state));
// SAFETY: embedder callback; `state_ptr` stays valid until
// `ghostty_surface_complete_clipboard_request` or we reclaim it.
let started = unsafe {
cb(
self.userdata,
c_int::from(clipboard as u8),
state_ptr.cast::<c_void>(),
)
};
if !started {
// SAFETY: allocated above and not handed over.
drop(unsafe { Box::from_raw(state_ptr) });
return false;
}
true
}
fn set_clipboard(&self, clipboard: Clipboard, contents: &[(String, String)], confirm: bool) {
let Some(cb) = self.app.opts.write_clipboard_cb else {
return;
};
let owned: Vec<(CString, CString)> = contents
.iter()
.map(|(m, d)| {
(
CString::new(m.replace('\0', "")).unwrap_or_default(),
CString::new(d.replace('\0', "")).unwrap_or_default(),
)
})
.collect();
let array: Vec<ghostty_clipboard_content_s> = owned
.iter()
.map(|(m, d)| ghostty_clipboard_content_s {
mime: m.as_ptr(),
data: d.as_ptr(),
})
.collect();
// SAFETY: embedder callback; the array and strings outlive the call.
unsafe {
cb(
self.userdata,
c_int::from(clipboard as u8),
array.as_ptr(),
array.len(),
confirm,
);
}
}
fn default_termio_env(&self) -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
let mut env = ghostty_os::env::get_env_map();
if cfg!(target_vendor = "apple") {
if env.contains_key("__XCODE_BUILT_PRODUCTS_DIR_PATHS") {
for k in [
"__XCODE_BUILT_PRODUCTS_DIR_PATHS",
"__XPC_DYLD_LIBRARY_PATH",
"DYLD_FRAMEWORK_PATH",
"DYLD_INSERT_LIBRARIES",
"DYLD_LIBRARY_PATH",
"LD_LIBRARY_PATH",
"SECURITYSESSIONID",
"XPC_SERVICE_NAME",
] {
env.remove(k);
}
}
// Remove this so that running `ghostty` within Ghostty works.
env.remove("GHOSTTY_MAC_LAUNCH_SOURCE");
// If we were launched from the desktop then we want to remove
// the LANGUAGE env var so that we don't inherit our translation
// settings for Ghostty. If we aren't from the desktop then we
// didn't set our LANGUAGE var so we don't need to remove it.
if launched_from_desktop() {
env.remove("LANGUAGE");
}
}
env.into_iter().collect()
}
fn redraw_inspector(&self) {
// `queueInspectorRender`
self.app
.perform_action(self.target(), Action::RenderInspector);
}
fn platform_view(&self) -> *mut c_void {
self.view
}
#[cfg(feature = "test-platform")]
fn gl_proc_address(&self, name: &str) -> *const c_void {
test_platform::proc_address(name)
}
}
#[cfg(target_os = "macos")]
fn launched_from_desktop() -> bool {
ghostty_os::desktop::launched_from_desktop_lib()
}
#[cfg(not(target_os = "macos"))]
fn launched_from_desktop() -> bool {
ghostty_os::desktop::launched_from_desktop()
}
/// The surface object behind `ghostty_surface_t`.
pub struct Surface {
app: Rc<AppHandle>,
rt: Rc<SurfaceRt>,
core: OnceCell<Rc<CoreSurfaceImpl>>,
inspector: RefCell<Option<Box<crate::inspector::Inspector>>>,
/// Strings handed out by `ghostty_surface_inherited_config`; they stay
/// valid until the surface is freed.
inherited_wd: RefCell<Vec<CString>>,
}
impl Surface {
fn core(&self) -> Option<&Rc<CoreSurfaceImpl>> {
self.core.get()
}
pub(crate) fn inspector_slot(&self) -> &RefCell<Option<Box<crate::inspector::Inspector>>> {
&self.inspector
}
pub(crate) fn core_surface(&self) -> Option<&Rc<CoreSurfaceImpl>> {
self.core()
}
pub(crate) fn rt(&self) -> &Rc<SurfaceRt> {
&self.rt
}
}
impl Drop for Surface {
fn drop(&mut self) {
// Shut down our inspector
if let Some(mut i) = self.inspector.get_mut().take() {
i.deinit(self.core.get().map(Rc::as_ref));
}
// Remove ourselves from the list of known surfaces in the app.
self.app.core.delete_surface(self.rt.id);
self.app.rt.surfaces.borrow_mut().remove(&self.rt.id);
// Clean up our core surface so that all the rendering and IO stop.
drop(self.core.take());
}
}
/// Borrow the surface behind a handle.
///
/// # Safety
/// `p` must be null or a live handle from `ghostty_surface_new`.
pub(crate) unsafe fn surface<'a>(p: ghostty_surface_t) -> Option<&'a Surface> {
// SAFETY: per the function contract.
unsafe { p.cast::<Surface>().as_ref() }
}
/// `ghostty_surface_config_new`.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_surface_config_new() -> ghostty_surface_config_s {
ghostty_surface_config_s {
platform_tag: platform::INVALID,
platform: crate::types::ghostty_platform_u {
macos: crate::types::ghostty_platform_macos_s {
nsview: std::ptr::null_mut(),
},
},
userdata: std::ptr::null_mut(),
scale_factor: 1.0,
font_size: 0.0,
working_directory: std::ptr::null(),
command: std::ptr::null(),
env_vars: std::ptr::null_mut(),
env_var_count: 0,
initial_input: std::ptr::null(),
wait_after_command: false,
context: 0,
}
}
fn context_from_c(v: c_int) -> NewSurfaceContext {
match v {
1 => NewSurfaceContext::Tab,
2 => NewSurfaceContext::Split,
_ => NewSurfaceContext::Window,
}
}
/// Test seam (feature `test-platform`): a fake platform whose OpenGL loader
/// the test installs, so surfaces can be created on Linux.
#[cfg(feature = "test-platform")]
pub mod test_platform {
use std::{cell::RefCell, ffi::c_void, rc::Rc};
/// The `platform_tag` that selects the fake platform.
pub const PLATFORM_TAG: i32 = 1000;
/// A GL function loader.
pub type GlLoader = Rc<dyn Fn(&str) -> *const c_void>;
thread_local! {
static LOADER: RefCell<Option<GlLoader>> = const { RefCell::new(None) };
}
/// Install the GL loader for surfaces created on this thread.
pub fn set_gl_loader(loader: Option<GlLoader>) {
LOADER.with(|l| *l.borrow_mut() = loader);
}
pub(super) fn proc_address(name: &str) -> *const c_void {
LOADER.with(|l| l.borrow().as_ref().map_or(std::ptr::null(), |f| f(name)))
}
}
/// Initialize the platform view (Zig: `Platform.init`). Darwin only.
///
/// # Safety
/// The union member selected by `tag` must be initialized.
unsafe fn platform_view_from_c(
opts: &ghostty_surface_config_s,
) -> Result<*mut c_void, &'static str> {
#[cfg(feature = "test-platform")]
if opts.platform_tag == test_platform::PLATFORM_TAG {
// Any non-null "view" will do; the renderer is OpenGL here.
return Ok(std::ptr::NonNull::<c_void>::dangling().as_ptr());
}
if !cfg!(target_vendor = "apple") {
// The platform union has no members off Darwin.
return Err("UnsupportedPlatform");
}
match opts.platform_tag {
// SAFETY: the tag selects the initialized member.
platform::MACOS => match unsafe { opts.platform.macos.nsview } {
v if v.is_null() => Err("NSViewMustBeSet"),
v => Ok(v),
},
// SAFETY: as above.
platform::IOS => match unsafe { opts.platform.ios.uiview } {
v if v.is_null() => Err("UIViewMustBeSet"),
v => Ok(v),
},
_ => Err("InvalidEnumTag"),
}
}
/// Zig's `std.zig.stringEscape` with non-ASCII code points written as
/// `\u{...}` (the config value is parsed as a Zig string; the bytes of
/// valid UTF-8 round trip either way).
fn string_escape(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for c in input.chars() {
match c {
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
' ' | '!' | '#'..='&' | '\'' | '('..='[' | ']'..='~' => out.push(c),
c if c.is_ascii() => {
let _ = write!(out, "\\x{:02x}", c as u32);
}
c => {
let _ = write!(out, "\\u{{{:x}}}", c as u32);
}
}
}
out
}
/// Apply the surface options to the config (the first half of Zig's
/// `Surface.init`).
///
/// # Safety
/// The pointers in `opts` must satisfy the contract of `ghostty.h`.
unsafe fn apply_surface_options(config: &mut Config, opts: &ghostty_surface_config_s) {
// If we have a working directory from the options then we set it.
// SAFETY: per the function contract.
if let Some(wd) =
unsafe { cstr(opts.working_directory) }.map(|c| c.to_string_lossy().into_owned())
&& !wd.is_empty()
{
match std::fs::metadata(&wd) {
Err(err) => log::warn!("error opening requested working directory dir={wd} err={err}"),
Ok(m) if !m.is_dir() => {
log::warn!("requested working directory is not a directory dir={wd}");
}
Ok(_) => {
let mut wd_val = WorkingDirectory::Path(wd);
wd_val.finalize();
config.working_directory = Some(wd_val);
}
}
}
// If we have a command from the options then we set it.
// SAFETY: per the function contract.
if let Some(cmd) = unsafe { cstr(opts.command) }.map(|c| c.to_string_lossy().into_owned())
&& !cmd.is_empty()
{
config.command = Some(Command::Shell(cmd));
config.wait_after_command = true;
}
// Apply any environment variables that were requested.
if opts.env_var_count > 0 && !opts.env_vars.is_null() {
// SAFETY: the caller supplies `env_var_count` entries.
let vars = unsafe { std::slice::from_raw_parts(opts.env_vars, opts.env_var_count) };
for var in vars {
// SAFETY: entries are NUL terminated strings.
let (Some(k), Some(v)) = (unsafe { cstr(var.key) }, unsafe { cstr(var.value) }) else {
continue;
};
config.env.put(&k.to_string_lossy(), &v.to_string_lossy());
}
}
// If we have an initial input then we set it.
// SAFETY: per the function contract.
if let Some(input) = unsafe { cstr(opts.initial_input) } {
// We need to escape the string because the "raw" field expects a
// Zig string.
config.input.list.clear();
config
.input
.list
.push(ReadableIO::Raw(string_escape(&input.to_string_lossy())));
}
// Wait after command
if opts.wait_after_command {
config.wait_after_command = true;
}
}
/// Create a new surface as part of an app.
///
/// # Safety
/// `app_ptr` a live app handle; `opts` a valid config (see `ghostty.h`).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_new(
app_ptr: ghostty_app_t,
opts: *const ghostty_surface_config_s,
) -> ghostty_surface_t {
// SAFETY: per the function contract.
let (Some(app_ref), Some(opts)) = (unsafe { app(app_ptr) }, unsafe { opts.as_ref() }) else {
log::error!("error initializing surface err=NullArgument");
return std::ptr::null_mut();
};
// SAFETY: per the function contract.
let view = match unsafe { platform_view_from_c(opts) } {
Ok(v) => v,
Err(err) => {
log::error!("error initializing surface err={err}");
return std::ptr::null_mut();
}
};
// Build the runtime surface. Its address is its id.
let scale = opts.scale_factor as f32;
let app_handle = {
// `AppHandle` is owned by the C handle; the surface only needs
// shared access to its parts, which are `Rc`s.
Rc::new(AppHandle {
core: Rc::clone(&app_ref.core),
rt: Rc::clone(&app_ref.rt),
})
};
let mut boxed = Box::new(Surface {
app: app_handle,
rt: Rc::new(SurfaceRt {
app: Rc::clone(&app_ref.rt),
id: 0,
userdata: opts.userdata,
view,
content_scale: Cell::new(ContentScale { x: scale, y: scale }),
size: Cell::new(SurfaceSize {
width: 800,
height: 600,
}),
cursor_pos: Cell::new(CursorPos { x: -1.0, y: -1.0 }),
title: RefCell::new(None),
}),
core: OnceCell::new(),
inspector: RefCell::new(None),
inherited_wd: RefCell::new(Vec::new()),
});
let id = std::ptr::from_mut::<Surface>(&mut *boxed) as usize as SurfaceId;
// The id is only known now (the address of the heap object).
if let Some(rt) = Rc::get_mut(&mut boxed.rt) {
rt.id = id;
}
app_ref
.rt
.surfaces
.borrow_mut()
.insert(id, Rc::downgrade(&boxed.rt));
let ptr = Box::into_raw(boxed);
// Shallow copy the config so that we can modify it.
let mut config = {
let app_config = app_ref.rt.config.borrow();
let prev_pwd = app_ref.core.state().focused_surface().and_then(|s| s.pwd());
new_config(&app_config, context_from_c(opts.context), prev_pwd)
};
// SAFETY: per the function contract.
unsafe { apply_surface_options(&mut config, opts) };
// Initialize our surface right away. We're given a view that is ready
// to use.
// SAFETY: `ptr` is the live allocation made above.
let s = unsafe { &*ptr };
let rt_app: Rc<dyn RtApp> = Rc::clone(&app_ref.rt) as Rc<dyn RtApp>;
let rt_surface: Rc<dyn RtSurface> = Rc::clone(&s.rt) as Rc<dyn RtSurface>;
match app_ref.core.create_surface(id, config, rt_app, rt_surface) {
Ok(core) => {
let _ = s.core.set(Rc::clone(&core));
// If our options requested a specific font-size, set that.
if opts.font_size != 0.0 {
let mut font_size = core.font_size();
font_size.points = opts.font_size;
if let Err(err) = core.set_font_size(font_size) {
log::error!("error initializing surface err={err}");
// SAFETY: allocated by us and not exposed yet.
drop(unsafe { Box::from_raw(ptr) });
return std::ptr::null_mut();
}
}
ptr.cast::<c_void>()
}
Err(err) => {
log::error!("error initializing surface err={err}");
// SAFETY: allocated by us and not exposed yet.
drop(unsafe { Box::from_raw(ptr) });
std::ptr::null_mut()
}
}
}
/// Free a surface.
///
/// # Safety
/// `ptr` must be a live surface handle; it is invalid afterwards.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_free(ptr: ghostty_surface_t) {
if ptr.is_null() {
return;
}
// SAFETY: created by `ghostty_surface_new` via `Box::into_raw`.
drop(unsafe { Box::from_raw(ptr.cast::<Surface>()) });
}
/// Returns the userdata associated with the surface.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_userdata(ptr: ghostty_surface_t) -> *mut c_void {
// SAFETY: per the function contract.
unsafe { surface(ptr) }.map_or(std::ptr::null_mut(), |s| s.rt.userdata)
}
/// Returns the app associated with a surface.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_app(ptr: ghostty_surface_t) -> ghostty_app_t {
// SAFETY: per the function contract.
unsafe { surface(ptr) }.map_or(std::ptr::null_mut(), |s| {
s.rt.app.app_ptr.get() as ghostty_app_t
})
}
/// Returns the config to use for surfaces that inherit from this one.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_inherited_config(
ptr: ghostty_surface_t,
source: c_int,
) -> ghostty_surface_config_s {
let mut result = ghostty_surface_config_new();
result.context = source;
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return result;
};
let Some(core) = s.core() else {
return result;
};
let context = context_from_c(source);
let config = s.rt.app.config.borrow();
// The font size to inherit. If 0, default font size will be used.
if config.window_inherit_font_size {
result.font_size = core.font_size().points;
}
if should_inherit_working_directory(context, &config)
&& let Some(cwd) = core.pwd()
&& let Ok(c) = CString::new(cwd)
{
let mut keep = s.inherited_wd.borrow_mut();
result.working_directory = c.as_ptr();
keep.push(c);
}
result
}
/// Update the configuration to the provided config for only this surface.
///
/// # Safety
/// `ptr` a live surface handle, `config` a config handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_update_config(
ptr: ghostty_surface_t,
config: ghostty_config_t,
) {
// SAFETY: per the function contract.
let (Some(s), Some(c)) = (unsafe { surface(ptr) }, unsafe { cconfig(config) }) else {
return;
};
let Some(core) = s.core() else {
return;
};
if let Err(err) = core.update_config(&Arc::new(c.config.clone())) {
log::error!("error updating config err={err}");
}
}
/// Returns true if the surface needs to confirm quitting.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_needs_confirm_quit(ptr: ghostty_surface_t) -> bool {
// SAFETY: per the function contract.
unsafe { surface(ptr) }
.and_then(Surface::core)
.is_some_and(|c| c.needs_confirm_quit())
}
/// Returns true if the surface process has exited.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_process_exited(ptr: ghostty_surface_t) -> bool {
// SAFETY: per the function contract.
unsafe { surface(ptr) }
.and_then(Surface::core)
.is_some_and(|c| CoreSurface::child_exited(c.as_ref()))
}
/// Returns true if the surface has a selection.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_has_selection(ptr: ghostty_surface_t) -> bool {
// SAFETY: per the function contract.
unsafe { surface(ptr) }
.and_then(Surface::core)
.is_some_and(|c| c.has_selection())
}
/// Fill the C text result from a read.
fn fill_text(
core: &CoreSurfaceImpl,
t: &Terminal,
sel: Selection,
result: &mut ghostty_text_s,
) -> bool {
// Get our text directly from the core surface.
let text: Text = match core.dump_text_locked(t, sel) {
Ok(t) => t,
Err(err) => {
log::warn!("error reading text err={err}");
return false;
}
};
let (tl_px_x, tl_px_y, offset_start, offset_len) =
text.viewport.map_or((-1.0, -1.0, 0, 0), |vp| {
(vp.tl_px_x, vp.tl_px_y, vp.offset_start, vp.offset_len)
});
*result = ghostty_text_s {
tl_px_x,
tl_px_y,
offset_start,
offset_len,
text: alloc_c_text(text.text.as_bytes()),
text_len: text.text.len(),
};
true
}
/// Same as `ghostty_surface_read_text` but reads from the user selection, if
/// any.
///
/// # Safety
/// `ptr` a live surface handle, `result` a valid out pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_read_selection(
ptr: ghostty_surface_t,
result: *mut ghostty_text_s,
) -> bool {
// SAFETY: per the function contract.
let (Some(core), Some(result)) = (unsafe { surface(ptr) }.and_then(Surface::core), unsafe {
result.as_mut()
}) else {
return false;
};
let guard = core.lock_terminal();
let t: &Terminal = &guard;
// If we don't have a selection, do nothing.
let Some(sel) = t.screen().selection else {
return false;
};
fill_text(core, t, sel, result)
}
/// The pin of a C point (Zig: `Point.pin`).
fn point_pin(p: &ghostty_point_s, t: &Terminal) -> Option<ghostty_terminal::page_list::Pin> {
let tag = match p.tag {
0 => PointTag::Active,
1 => PointTag::Viewport,
2 => PointTag::Screen,
3 => PointTag::History,
_ => return None,
};
let pages = &t.screen().pages;
// Clamp our point to the screen bounds.
let clamped_x = p.x.min(u32::from(pages.cols.saturating_sub(1)));
let clamped_y = p.y.min(u32::from(pages.rows.saturating_sub(1)));
match p.coord {
// Exact coordinates require a specific pin.
0 => {
let x = u16::try_from(clamped_x).unwrap_or(u16::MAX);
pages.pin(Point::new(tag, Coordinate { x, y: clamped_y }))
}
1 => pages.get_top_left(tag),
2 => pages.get_bottom_right(tag),
_ => None,
}
}
fn selection_from_c(sel: &ghostty_selection_s, t: &Terminal) -> Option<Selection> {
Some(Selection::new(
point_pin(&sel.top_left, t)?,
point_pin(&sel.bottom_right, t)?,
sel.rectangle,
))
}
/// Read some arbitrary text from the surface.
///
/// This is an expensive operation so it shouldn't be called too often. We
/// recommend that callers cache the result and throttle calls to this
/// function.
///
/// # Safety
/// `ptr` a live surface handle, `result` a valid out pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_read_text(
ptr: ghostty_surface_t,
sel: ghostty_selection_s,
result: *mut ghostty_text_s,
) -> bool {
// SAFETY: per the function contract.
let (Some(core), Some(result)) = (unsafe { surface(ptr) }.and_then(Surface::core), unsafe {
result.as_mut()
}) else {
return false;
};
let guard = core.lock_terminal();
let t: &Terminal = &guard;
let Some(core_sel) = selection_from_c(&sel, t) else {
return false;
};
fill_text(core, t, core_sel, result)
}
/// Free text read from the surface.
///
/// # Safety
/// `text` must be a result of one of the read functions (or null).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_free_text(
_ptr: ghostty_surface_t,
text: *mut ghostty_text_s,
) {
// SAFETY: per the function contract.
let Some(text) = (unsafe { text.as_mut() }) else {
return;
};
// SAFETY: allocated by `alloc_c_text` with `text_len` bytes + NUL.
unsafe { free_c_text(text.text, text.text_len, true) };
text.text = std::ptr::null();
text.text_len = 0;
}
/// Tell the surface that it needs to schedule a render.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_refresh(ptr: ghostty_surface_t) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
&& let Err(err) = core.refresh_callback()
{
log::error!("error in refresh callback err={err}");
}
}
/// Tell the surface that it needs to schedule a render call as soon as
/// possible (NOW if possible).
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_draw(ptr: ghostty_surface_t) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
&& let Err(err) = core.draw()
{
log::error!("error in draw err={err}");
}
}
/// Update the size of a surface. This will trigger resize notifications to
/// the pty and the renderer.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_set_size(ptr: ghostty_surface_t, w: u32, h: u32) {
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return;
};
// Runtimes sometimes generate superfluous resize events even if the
// size did not actually change (SwiftUI). We check that the size
// actually changed from what we last recorded since resizes are
// expensive.
let cur = s.rt.size.get();
if cur.width == w && cur.height == h {
return;
}
let size = SurfaceSize {
width: w,
height: h,
};
s.rt.size.set(size);
if let Some(core) = s.core()
&& let Err(err) = core.size_callback(size)
{
log::error!("error in size callback err={err}");
}
}
/// Return the size information a surface has.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_size(ptr: ghostty_surface_t) -> ghostty_surface_size_s {
let zero = ghostty_surface_size_s {
columns: 0,
rows: 0,
width_px: 0,
height_px: 0,
cell_width_px: 0,
cell_height_px: 0,
};
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return zero;
};
let size = core.size();
let grid = size.grid();
ghostty_surface_size_s {
columns: grid.columns,
rows: grid.rows,
width_px: size.screen.width,
height_px: size.screen.height,
cell_width_px: size.cell.width,
cell_height_px: size.cell.height,
}
}
/// Update the color scheme of the surface.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_set_color_scheme(
ptr: ghostty_surface_t,
scheme_raw: c_int,
) {
let Some(scheme) = color_scheme_from_c(scheme_raw) else {
log::warn!("invalid color scheme to ghostty_surface_set_color_scheme value={scheme_raw}");
return;
};
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
&& let Err(err) = core.color_scheme_callback(scheme)
{
log::error!("error setting color scheme err={err}");
}
}
/// Update the content scale of the surface.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_set_content_scale(ptr: ghostty_surface_t, x: f64, y: f64) {
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return;
};
// We are an embedded API so the caller can send us all sorts of
// garbage. We want to make sure that the float values are valid and we
// don't want to support fractional scaling below 1.
let scaled = |v: f64| if v.is_nan() { 1.0 } else { v.max(1.0) } as f32;
let scale = ContentScale {
x: scaled(x),
y: scaled(y),
};
s.rt.content_scale.set(scale);
if let Some(core) = s.core()
&& let Err(err) = core.content_scale_callback(scale)
{
log::error!("error in content scale callback err={err}");
}
}
/// Update the focused state of a surface.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_set_focus(ptr: ghostty_surface_t, focused: bool) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
&& let Err(err) = core.focus_callback(focused)
{
log::error!("error in focus callback err={err}");
}
}
/// Update the occlusion state of a surface.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_set_occlusion(ptr: ghostty_surface_t, visible: bool) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
&& let Err(err) = core.occlusion_callback(visible)
{
log::error!("error in occlusion callback err={err}");
}
}
/// Filter the mods if necessary. This handles settings such as
/// `macos-option-as-alt`. The filtered mods should be used for key
/// translation but should NOT be sent back via the `_key` function -- the
/// original mods should be used for that.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_key_translation_mods(
ptr: ghostty_surface_t,
mods_raw: c_int,
) -> c_int {
let mods = mods_from_c(mods_raw);
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return mods_raw;
};
let Some(core) = s.core() else {
return mods_raw;
};
let option_as_alt = core
.config()
.macos_option_as_alt
.unwrap_or_else(|| s.rt.app.keyboard_layout().detect_option_as_alt());
c_int::from(mods.translation(option_as_alt).int())
}
/// Send this for raw keypresses (i.e. the keyDown event on macOS). This will
/// handle the keymap translation and send the appropriate key and char
/// events.
///
/// # Safety
/// `ptr` a live surface handle; `event.text` null or NUL terminated.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_key(
ptr: ghostty_surface_t,
event: ghostty_input_key_s,
) -> bool {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return false;
};
// SAFETY: per the function contract.
let Some(key) = (unsafe { OwnedKey::from_c(&event) }) else {
return false;
};
match CoreSurface::key_callback(core.as_ref(), key.event()) {
Ok(ghostty_apprt::runtime_api::InputEffect::Closed) => true,
Ok(ghostty_apprt::runtime_api::InputEffect::Ignored) => false,
Ok(ghostty_apprt::runtime_api::InputEffect::Consumed) => true,
Err(err) => {
log::warn!("error processing key event err={err}");
false
}
}
}
/// Returns true if the given key event would trigger a binding if it were
/// sent to the surface right now. The "right now" is important because
/// things like trigger sequences are only valid until the next key event.
///
/// # Safety
/// As [`ghostty_surface_key`]; `c_flags` null or a valid out pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_key_is_binding(
ptr: ghostty_surface_t,
event: ghostty_input_key_s,
c_flags: *mut ghostty_binding_flags_e,
) -> bool {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return false;
};
// SAFETY: per the function contract.
let Some(key) = (unsafe { OwnedKey::from_c(&event) }) else {
log::warn!("error processing key event");
return false;
};
let Some(flags) = core.key_event_is_binding(&key.event()) else {
return false;
};
if !c_flags.is_null() {
// Zig writes the `u8` `Flags.C` through the (enum-typed) pointer.
// SAFETY: the pointer is valid for at least one byte.
unsafe { c_flags.cast::<u8>().write(flags.cval()) };
}
true
}
/// Send raw text to the terminal. This is treated like a paste so this isn't
/// useful for sending escape sequences. For that, individual key input
/// should be used.
///
/// # Safety
/// `ptr` a live surface handle, `text` points to `len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_text(
ptr: ghostty_surface_t,
text: *const c_char,
len: usize,
) {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return;
};
let bytes: &[u8] = if text.is_null() {
&[]
} else {
// SAFETY: `text` points to `len` readable bytes.
unsafe { std::slice::from_raw_parts(text.cast::<u8>(), len) }
};
if let Err(err) = core.text_callback(&String::from_utf8_lossy(bytes)) {
log::error!("error in key callback err={err}");
}
}
/// Set the preedit text for the surface. This is used for IME composition.
/// If the length is 0, then the preedit text is cleared.
///
/// # Safety
/// `ptr` a live surface handle, `text` points to `len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_preedit(
ptr: ghostty_surface_t,
text: *const c_char,
len: usize,
) {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return;
};
let preedit = if len == 0 || text.is_null() {
None
} else {
// SAFETY: `text` points to `len` readable bytes.
let bytes = unsafe { std::slice::from_raw_parts(text.cast::<u8>(), len) };
Some(String::from_utf8_lossy(bytes).into_owned())
};
if let Err(err) = core.preedit_callback(preedit.as_deref()) {
log::error!("error in preedit callback err={err}");
}
}
/// Returns true if the surface currently has mouse capturing enabled.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_mouse_captured(ptr: ghostty_surface_t) -> bool {
// SAFETY: per the function contract.
unsafe { surface(ptr) }
.and_then(Surface::core)
.is_some_and(|c| c.mouse_captured())
}
/// Send a mouse button event.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_mouse_button(
ptr: ghostty_surface_t,
action: c_int,
button: c_int,
mods: c_int,
) -> bool {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return false;
};
let state = match action {
0 => MouseButtonState::Release,
1 => MouseButtonState::Press,
_ => return false,
};
let Some(button) = mouse_button_from_c(button) else {
return false;
};
match core.mouse_button_callback(state, button, mods_from_c(mods)) {
Ok(v) => v,
Err(err) => {
log::error!("error in mouse button callback err={err}");
false
}
}
}
pub(crate) fn mouse_button_from_c(v: c_int) -> Option<MouseButton> {
Some(match v {
0 => MouseButton::Unknown,
1 => MouseButton::Left,
2 => MouseButton::Right,
3 => MouseButton::Middle,
4 => MouseButton::Four,
5 => MouseButton::Five,
6 => MouseButton::Six,
7 => MouseButton::Seven,
8 => MouseButton::Eight,
9 => MouseButton::Nine,
10 => MouseButton::Ten,
11 => MouseButton::Eleven,
_ => return None,
})
}
/// Update the mouse position within the view.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_mouse_pos(
ptr: ghostty_surface_t,
x: f64,
y: f64,
mods: c_int,
) {
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return;
};
// The cursor position from the host directly is in screen coordinates
// but all our interface works in pixels.
let scale = s.rt.content_scale.get();
let pos = CursorPos {
x: x as f32 * scale.x,
y: y as f32 * scale.y,
};
// There are cases where the platform reports a mouse motion event
// without the cursor actually moving. For example, on macOS, updating
// the window title can trigger a phantom mouse-move event at the same
// coordinates. This can cause the mouse to incorrectly unhide when
// mouse-hide-while-typing is enabled (commonly seen with TUI apps like
// Zellij that frequently update the title). To prevent incorrect
// behavior, we only continue with callback logic if the cursor has
// actually moved.
let prev = s.rt.cursor_pos.get();
if (prev.x - pos.x).abs() < 1.0 && (prev.y - pos.y).abs() < 1.0 {
return;
}
s.rt.cursor_pos.set(pos);
if let Some(core) = s.core()
&& let Err(err) = core.cursor_pos_callback(pos, Some(mods_from_c(mods)))
{
log::error!("error in cursor pos callback err={err}");
}
}
/// Send a scroll event.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_mouse_scroll(
ptr: ghostty_surface_t,
x: f64,
y: f64,
scroll_mods: c_int,
) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
&& let Err(err) = core.scroll_callback(x, y, ScrollMods::from_int(scroll_mods as u32 as u8))
{
log::error!("error in scroll callback err={err}");
}
}
/// Send a mouse pressure event.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_mouse_pressure(
ptr: ghostty_surface_t,
stage_raw: u32,
pressure: f64,
) {
let stage = match stage_raw {
0 => MousePressureStage::None,
1 => MousePressureStage::Normal,
2 => MousePressureStage::Deep,
_ => {
log::warn!("invalid mouse pressure stage value={stage_raw}");
return;
}
};
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core) {
CoreSurface::mouse_pressure_callback(core.as_ref(), stage, pressure);
}
}
/// The position of the IME candidate window.
///
/// # Safety
/// `ptr` a live surface handle; the out pointers valid.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_ime_point(
ptr: ghostty_surface_t,
x: *mut f64,
y: *mut f64,
width: *mut f64,
height: *mut f64,
) {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return;
};
let pos = core.ime_point();
// SAFETY: the out pointers are valid per the contract.
unsafe {
if let Some(x) = x.as_mut() {
*x = pos.x;
}
if let Some(y) = y.as_mut() {
*y = pos.y;
}
if let Some(w) = width.as_mut() {
*w = pos.width;
}
if let Some(h) = height.as_mut() {
*h = pos.height;
}
}
}
/// Request that the surface become closed. This will go through the normal
/// trigger process that a close surface input binding would.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_request_close(ptr: ghostty_surface_t) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core) {
core.close();
}
}
/// Request that the surface split in the given direction.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_split(ptr: ghostty_surface_t, direction: c_int) {
let direction = match direction {
0 => SplitDirection::Right,
1 => SplitDirection::Down,
2 => SplitDirection::Left,
3 => SplitDirection::Up,
_ => return,
};
// SAFETY: per the function contract.
if let Some(s) = unsafe { surface(ptr) } {
s.rt.app
.perform_action(s.rt.target(), Action::NewSplit(direction));
}
}
/// Focus on the next split (if any).
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_split_focus(ptr: ghostty_surface_t, direction: c_int) {
let direction = match direction {
0 => GotoSplit::Previous,
1 => GotoSplit::Next,
2 => GotoSplit::Up,
3 => GotoSplit::Left,
4 => GotoSplit::Down,
5 => GotoSplit::Right,
_ => return,
};
// SAFETY: per the function contract.
if let Some(s) = unsafe { surface(ptr) } {
s.rt.app
.perform_action(s.rt.target(), Action::GotoSplit(direction));
}
}
/// Resize the current split by moving the split divider in the given
/// direction. `direction` specifies which direction the split divider will
/// move relative to the focused split. `amount` is a fractional value
/// between 0 and 1 that specifies by how much the divider will move.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_split_resize(
ptr: ghostty_surface_t,
direction: c_int,
amount: u16,
) {
let direction = match direction {
0 => ResizeSplitDirection::Up,
1 => ResizeSplitDirection::Down,
2 => ResizeSplitDirection::Left,
3 => ResizeSplitDirection::Right,
_ => return,
};
// SAFETY: per the function contract.
if let Some(s) = unsafe { surface(ptr) } {
s.rt.app.perform_action(
s.rt.target(),
Action::ResizeSplit(ResizeSplit { amount, direction }),
);
}
}
/// Equalize the size of all splits in the current window.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_split_equalize(ptr: ghostty_surface_t) {
// SAFETY: per the function contract.
if let Some(s) = unsafe { surface(ptr) } {
s.rt.app
.perform_action(s.rt.target(), Action::EqualizeSplits);
}
}
/// Invoke an action on the surface.
///
/// # Safety
/// `ptr` a live surface handle, `action` points to `len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_binding_action(
ptr: ghostty_surface_t,
action: *const c_char,
len: usize,
) -> bool {
// SAFETY: per the function contract.
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
return false;
};
if action.is_null() {
return false;
}
// SAFETY: `action` points to `len` readable bytes.
let bytes = unsafe { std::slice::from_raw_parts(action.cast::<u8>(), len) };
let action_str = String::from_utf8_lossy(bytes);
let action = match BindingAction::parse(&action_str) {
Ok(a) => a,
Err(err) => {
log::error!("error parsing binding action action={action_str} err={err:?}");
return false;
}
};
match core.perform_binding_action(&action) {
Ok(v) => v,
Err(err) => {
log::error!("error performing binding action action={action} err={err}");
false
}
}
}
/// Complete a clipboard read request started via the read callback. This can
/// only be called once for a given request. Once it is called with a request
/// the request pointer will be invalidated.
///
/// # Safety
/// `ptr` a live surface handle; `text` NUL terminated; `state` a pointer
/// handed to the `read_clipboard` / `confirm_read_clipboard` callbacks.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_complete_clipboard_request(
ptr: ghostty_surface_t,
text: *const c_char,
state: *mut c_void,
confirmed: bool,
) {
// SAFETY: per the function contract.
let (Some(s), Some(str)) = (unsafe { surface(ptr) }, unsafe { cstr(text) }) else {
return;
};
if state.is_null() {
return;
}
let Some(core) = s.core() else {
return;
};
let str = str.to_string_lossy();
// SAFETY: the pointer is a `Box<ClipboardRequest>` leaked by
// `clipboard_request`; it stays alive when confirmation is requested.
let req = unsafe { *state.cast::<ClipboardRequest>() };
// Attempt to complete the request, but we may request confirmation.
match core.complete_clipboard_request(req, &str, confirmed) {
Err(
ghostty_apprt::runtime_api::ClipboardError::UnsafePaste
| ghostty_apprt::runtime_api::ClipboardError::UnauthorizedPaste,
) => {
if let Some(cb) = s.rt.app.opts.confirm_read_clipboard_cb {
let ty = match req.request_type() {
ghostty_apprt::structs::ClipboardRequestType::Paste => 0,
ghostty_apprt::structs::ClipboardRequestType::Osc52Read => 1,
ghostty_apprt::structs::ClipboardRequestType::Osc52Write => 2,
};
let c = CString::new(str.replace('\0', "")).unwrap_or_default();
// SAFETY: embedder callback; `state` is preserved for the
// confirmation route.
unsafe { cb(s.rt.userdata, c.as_ptr(), state, ty) };
}
return;
}
Err(err) => log::error!("error completing clipboard request err={err}"),
Ok(()) => {}
}
// We don't defer this because the clipboard confirmation route
// preserves the clipboard request.
// SAFETY: allocated by `clipboard_request`, consumed exactly once here.
drop(unsafe { Box::from_raw(state.cast::<ClipboardRequest>()) });
}
/// Sets the window background blur on macOS to the desired value. I do this
/// as an extern function because I don't know how to call these functions
/// in Swift.
///
/// This uses an undocumented, non-public API because this is what every
/// terminal appears to use, including Terminal.app.
///
/// # Safety
/// `app` a live app handle; `window` an `NSWindow` (macOS).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_set_window_background_blur(
app_ptr: ghostty_app_t,
window: *mut c_void,
) {
// This is only supported on macOS
#[cfg(target_os = "macos")]
{
use objc2::{msg_send, runtime::AnyObject};
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
fn CGSSetWindowBackgroundBlurRadius(
connection: *mut c_void,
window: usize,
radius: c_int,
) -> i32;
fn CGSDefaultConnectionForThread() -> *mut c_void;
}
// SAFETY: per the function contract.
let Some(a) = (unsafe { app(app_ptr) }) else {
return;
};
if window.is_null() {
return;
}
let config = a.rt.config.borrow();
// Do nothing if we don't have background transparency enabled
if config.background_opacity >= 1.0 {
return;
}
let radius = match config.c_value("background-blur") {
Some(ghostty_config::c_value::CValue::Short(v)) => c_int::from(v),
_ => 0,
};
// SAFETY: `window` is an NSWindow per the contract; the CGS
// functions are the private window server API Terminal.app uses.
unsafe {
let number: isize = msg_send![window.cast::<AnyObject>(), windowNumber];
CGSSetWindowBackgroundBlurRadius(
CGSDefaultConnectionForThread(),
number as usize,
radius,
);
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = (app_ptr, window);
}
}
// ---------------------------------------------------------------------
// Darwin only exports
#[cfg(target_vendor = "apple")]
#[allow(clippy::wildcard_imports)]
mod darwin {
use super::*;
use crate::types::ghostty_text_s;
/// Set the display the surface is on (for the display link).
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_set_display_id(
ptr: ghostty_surface_t,
display_id: u32,
) {
// SAFETY: per the function contract.
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core) {
core.set_macos_display_id(display_id);
}
}
/// This returns a CTFontRef that should be used for quicklook
/// highlighted text. This is always the primary font in use regardless
/// of the selected text.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_quicklook_font(ptr: ghostty_surface_t) -> *mut c_void {
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return std::ptr::null_mut();
};
let Some(core) = s.core() else {
return std::ptr::null_mut();
};
// We need to unscale the content scale. We apply the content scale
// to our font stack because we are rendering at 1x but callers of
// this should be using scaled or apply scale themselves.
let scale = s.rt.get_content_scale();
core.quicklook_font(scale.y).unwrap_or(std::ptr::null_mut())
}
/// This returns the selected word for quicklook. This will populate the
/// buffer with the word under the cursor and the selection info so that
/// quicklook can be rendered.
///
/// This does not modify the selection active on the surface (if any).
///
/// # Safety
/// `ptr` a live surface handle, `result` a valid out pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_quicklook_word(
ptr: ghostty_surface_t,
result: *mut ghostty_text_s,
) -> bool {
// SAFETY: per the function contract.
let (Some(s), Some(result)) = (unsafe { surface(ptr) }, unsafe { result.as_mut() }) else {
return false;
};
let Some(core) = s.core() else {
return false;
};
let guard = core.lock_terminal();
let t: &Terminal = &guard;
// Get our word selection
let pos = s.rt.get_cursor_pos();
let vp = core.pos_to_viewport(f64::from(pos.x), f64::from(pos.y));
let Some(pin) = t
.screen()
.pages
.pin(Point::Viewport(Coordinate { x: vp.x, y: vp.y }))
else {
return false;
};
let Some(sel) = t
.screen()
.select_word(pin, &core.cfg().selection_word_chars)
else {
return false;
};
// Read the selection
fill_text(core, t, sel, result)
}
}
+477
View File
@@ -0,0 +1,477 @@
//! The inspector C API (`ghostty_surface_inspector`, `ghostty_inspector_*`;
//! the `Inspector` struct of ZIG/src/apprt/embedded.zig).
//!
//! The embedded inspector owns a Dear ImGui context, feeds it the host's
//! input events and, on Apple platforms, draws it with Metal into the
//! command buffer the host provides (`ghostty_inspector_metal_*`).
#![allow(unsafe_code)]
#[cfg(target_vendor = "apple")]
use std::time::Instant;
use std::{
ffi::{CStr, c_char, c_int, c_void},
rc::Rc,
};
use ghostty_apprt::runtime_api::RtSurface;
use ghostty_core::Surface as CoreSurfaceImpl;
use ghostty_input::{Action as KeyAction, Key, MouseButtonState, ScrollMods};
use ghostty_inspector::imgui::{Context, Key as ImguiKey, MouseButton as ImguiMouseButton};
use crate::{
embedded::{SurfaceRt, mouse_button_from_c, surface},
types::{ghostty_inspector_t, ghostty_surface_t},
};
/// The inspector state (Zig: `embedded.Inspector`). 1:1 with a surface.
pub struct Inspector {
surface: Rc<SurfaceRt>,
core: Rc<CoreSurfaceImpl>,
ctx: Context,
#[cfg(target_vendor = "apple")]
backend: Option<crate::inspector_metal::MetalBackend>,
content_scale: f64,
/// The scale the ImGui style currently has (a `Style` cannot be rebuilt
/// from Rust like Zig's `ImGuiStyle_ImGuiStyle`, so it is scaled by the
/// change of the factor).
style_scale: f32,
/// Our previous instant used to calculate delta time for animations.
#[cfg(target_vendor = "apple")]
instant: Option<Instant>,
}
impl Inspector {
fn init(surface: Rc<SurfaceRt>, core: Rc<CoreSurfaceImpl>) -> Option<Inspector> {
let mut ctx = match Context::try_create() {
Ok(c) => c,
Err(err) => {
log::error!("error initializing inspector err={err}");
return None;
}
};
if let Err(err) = ctx.set_platform_name(Some("ghostty_embedded")) {
log::warn!("unable to set the imgui platform name err={err}");
}
// Setup our core inspector
let scale = f64::from(RtSurface::get_content_scale(surface.as_ref()).x);
ghostty_inspector::setup(&mut ctx, scale);
if let Err(err) = core.activate_inspector() {
log::error!("failed to activate inspector err={err}");
}
Some(Inspector {
surface,
core,
ctx,
#[cfg(target_vendor = "apple")]
backend: None,
content_scale: 1.0,
style_scale: 1.0,
#[cfg(target_vendor = "apple")]
instant: None,
})
}
/// Stop collecting data and release the backend (Zig: `deinit`).
pub(crate) fn deinit(&mut self, _core: Option<&CoreSurfaceImpl>) {
self.core.deactivate_inspector();
#[cfg(target_vendor = "apple")]
if let Some(b) = self.backend.take() {
b.shutdown();
}
}
/// Queue a render for the next frame.
fn queue_render(&self) {
RtSurface::redraw_inspector(self.surface.as_ref());
}
fn update_content_scale(&mut self, x: f64) {
// Cache our scale because we use it for cursor position
// calculations.
self.content_scale = x;
// Scale the style appropriately.
let scale = x as f32;
let last = std::mem::replace(&mut self.style_scale, scale);
if (scale - last).abs() > f32::EPSILON && last > 0.0 {
self.ctx.style_mut().scale_all_sizes(scale / last);
}
}
fn update_size(&mut self, width: u32, height: u32) {
self.ctx
.io_mut()
.set_display_size([width as f32, height as f32]);
}
fn mouse_button(&mut self, action: MouseButtonState, button: ghostty_input::MouseButton) {
self.queue_render();
let imgui_button = match button {
ghostty_input::MouseButton::Left => ImguiMouseButton::Left,
ghostty_input::MouseButton::Middle => ImguiMouseButton::Middle,
ghostty_input::MouseButton::Right => ImguiMouseButton::Right,
_ => return, // unsupported
};
self.ctx
.io_mut()
.add_mouse_button_event(imgui_button, action == MouseButtonState::Press);
}
fn scroll(&mut self, xoff: f64, yoff: f64, mods: ScrollMods) {
self.queue_render();
// For precision scrolling (trackpads), the values are in pixels
// which scroll way too fast. Scale them down to approximate
// discrete wheel notches. imgui expects 1.0 to scroll ~5 lines of
// text.
let scale = if mods.precision { 0.1 } else { 1.0 };
self.ctx
.io_mut()
.add_mouse_wheel_event([(xoff * scale) as f32, (yoff * scale) as f32]);
}
fn cursor_pos(&mut self, x: f64, y: f64) {
self.queue_render();
self.ctx.io_mut().add_mouse_pos_event([
(x * self.content_scale) as f32,
(y * self.content_scale) as f32,
]);
}
fn focus(&mut self, focused: bool) {
self.queue_render();
self.ctx.io_mut().add_focus_event(focused);
}
fn text(&mut self, text: &str) {
self.queue_render();
self.ctx.io_mut().add_input_characters_utf8(text);
}
fn key(&mut self, action: KeyAction, key: Key, mods: ghostty_input::Mods) {
self.queue_render();
let io = self.ctx.io_mut();
// Update all our modifiers
io.add_key_event(ImguiKey::LeftShift, mods.shift);
io.add_key_event(ImguiKey::LeftCtrl, mods.ctrl);
io.add_key_event(ImguiKey::LeftAlt, mods.alt);
io.add_key_event(ImguiKey::LeftSuper, mods.super_);
// Send our keypress
if let Some(imgui_key) = ghostty_inspector::keys::imgui_key(key) {
io.add_key_event(
imgui_key,
matches!(action, KeyAction::Press | KeyAction::Repeat),
);
}
}
/// Update the delta time (Zig: `newFrame`).
#[cfg(target_vendor = "apple")]
pub(crate) fn new_frame(&mut self) {
let now = Instant::now();
let delta = self.instant.map_or(1.0 / 60.0, |prev| {
now.duration_since(prev).as_secs_f32().max(0.00001)
});
self.instant = Some(now);
self.ctx.io_mut().set_delta_time(delta);
}
/// Build one UI frame, calling the core inspector (`inspector.render`).
/// `end` finishes the frame without producing draw data.
#[cfg(target_vendor = "apple")]
pub(crate) fn build_frame(&mut self) -> &mut ghostty_inspector::imgui::Ui {
self.new_frame();
let core = Rc::clone(&self.core);
let ui = self.ctx.frame();
core.render_inspector(ui);
ui
}
#[cfg(target_vendor = "apple")]
pub(crate) fn context(&mut self) -> &mut Context {
&mut self.ctx
}
#[cfg(target_vendor = "apple")]
pub(crate) fn take_backend(&mut self) -> Option<crate::inspector_metal::MetalBackend> {
self.backend.take()
}
#[cfg(target_vendor = "apple")]
pub(crate) fn set_backend(&mut self, backend: crate::inspector_metal::MetalBackend) {
self.backend = Some(backend);
}
}
/// Borrow the inspector behind a handle.
///
/// # Safety
/// `p` must be null or a handle from `ghostty_surface_inspector` whose
/// surface is still alive and whose inspector wasn't freed.
unsafe fn inspector<'a>(p: ghostty_inspector_t) -> Option<&'a mut Inspector> {
// SAFETY: per the function contract.
unsafe { p.cast::<Inspector>().as_mut() }
}
/// Initialize the inspector instance. A surface can only have one inspector
/// at any given time, so this will return the previous inspector if it was
/// already initialized.
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_surface_inspector(ptr: ghostty_surface_t) -> ghostty_inspector_t {
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return std::ptr::null_mut();
};
let slot = s.inspector_slot();
if let Some(i) = slot.borrow_mut().as_mut() {
return std::ptr::from_mut::<Inspector>(&mut **i).cast::<c_void>();
}
let Some(core) = s.core_surface() else {
return std::ptr::null_mut();
};
let Some(i) = Inspector::init(Rc::clone(s.rt()), Rc::clone(core)) else {
return std::ptr::null_mut();
};
let mut boxed = Box::new(i);
let handle = std::ptr::from_mut::<Inspector>(&mut *boxed).cast::<c_void>();
*slot.borrow_mut() = Some(boxed);
handle
}
/// Free the inspector of a surface (note: takes the surface).
///
/// # Safety
/// `ptr` must be a live surface handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_free(ptr: ghostty_surface_t) {
// SAFETY: per the function contract.
let Some(s) = (unsafe { surface(ptr) }) else {
return;
};
let taken = s.inspector_slot().borrow_mut().take();
if let Some(mut i) = taken {
i.deinit(None);
}
}
/// Set the size of the inspector view in pixels.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_set_size(ptr: ghostty_inspector_t, w: u32, h: u32) {
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.update_size(w, h);
}
}
/// Set the content scale of the inspector view.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_set_content_scale(
ptr: ghostty_inspector_t,
x: f64,
_y: f64,
) {
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.update_content_scale(x);
}
}
/// Send a mouse button event to the inspector.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_mouse_button(
ptr: ghostty_inspector_t,
action: c_int,
button: c_int,
_mods: c_int,
) {
let state = match action {
0 => MouseButtonState::Release,
1 => MouseButtonState::Press,
_ => return,
};
let Some(button) = mouse_button_from_c(button) else {
return;
};
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.mouse_button(state, button);
}
}
/// Send a mouse position to the inspector.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_mouse_pos(ptr: ghostty_inspector_t, x: f64, y: f64) {
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.cursor_pos(x, y);
}
}
/// Send a scroll event to the inspector.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_mouse_scroll(
ptr: ghostty_inspector_t,
x: f64,
y: f64,
scroll_mods: c_int,
) {
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.scroll(x, y, ScrollMods::from_int(scroll_mods as u32 as u8));
}
}
/// Send a key event to the inspector.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_key(
ptr: ghostty_inspector_t,
action: c_int,
key: c_int,
c_mods: c_int,
) {
let action = match action {
0 => KeyAction::Release,
1 => KeyAction::Press,
2 => KeyAction::Repeat,
_ => return,
};
let Some(key) = usize::try_from(key)
.ok()
.and_then(|k| Key::ALL.get(k))
.copied()
else {
return;
};
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.key(
action,
key,
ghostty_input::Mods::from_int(c_mods as u32 as u16),
);
}
}
/// Send text input to the inspector.
///
/// # Safety
/// `ptr` a live inspector handle; `text` NUL terminated.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_text(ptr: ghostty_inspector_t, text: *const c_char) {
if text.is_null() {
return;
}
// SAFETY: per the function contract.
let text = unsafe { CStr::from_ptr(text) }.to_string_lossy();
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.text(&text);
}
}
/// Set the focus state of the inspector.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_set_focus(ptr: ghostty_inspector_t, focused: bool) {
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) } {
i.focus(focused);
}
}
#[cfg(target_vendor = "apple")]
#[allow(clippy::wildcard_imports)]
mod metal_exports {
use super::*;
/// Initialize the inspector for a Metal backend. Takes ownership of one
/// reference to `device`.
///
/// # Safety
/// `ptr` a live inspector handle; `device` a retained `MTLDevice`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_metal_init(
ptr: ghostty_inspector_t,
device: *mut c_void,
) -> bool {
// SAFETY: per the function contract.
let Some(i) = (unsafe { inspector(ptr) }) else {
return false;
};
if let Some(b) = i.backend.take() {
b.shutdown();
}
// SAFETY: `device` is a retained MTLDevice whose reference we own.
match unsafe { crate::inspector_metal::MetalBackend::init(device, i.context()) } {
Ok(b) => {
i.backend = Some(b);
log::debug!("initialized metal backend");
true
}
Err(err) => {
log::warn!("failed to initialize metal backend err={err}");
false
}
}
}
/// Render the inspector into the host's command buffer. Takes ownership
/// of one reference to each of `command_buffer` and `descriptor`.
///
/// # Safety
/// `ptr` a live inspector handle; the others retained Metal objects.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_metal_render(
ptr: ghostty_inspector_t,
command_buffer: *mut c_void,
descriptor: *mut c_void,
) {
// SAFETY: per the function contract.
let Some(i) = (unsafe { inspector(ptr) }) else {
return;
};
// SAFETY: retained objects whose reference we own.
unsafe { crate::inspector_metal::render(i, command_buffer, descriptor) };
}
/// Shut the Metal backend down.
///
/// # Safety
/// `ptr` must be a live inspector handle.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_inspector_metal_shutdown(ptr: ghostty_inspector_t) {
// SAFETY: per the function contract.
if let Some(i) = unsafe { inspector(ptr) }
&& let Some(b) = i.backend.take()
{
b.shutdown();
}
}
}
+633
View File
@@ -0,0 +1,633 @@
//! The Metal backend of the embedded inspector
//! (`ghostty_inspector_metal_{init,render,shutdown}`).
//!
//! Upstream links Dear ImGui's `imgui_impl_metal.mm`; this is the same
//! renderer written against `objc2-metal` and `dear-imgui-rs`'s managed
//! texture protocol: the draw lists are uploaded to Metal buffers and drawn
//! with the `imgui_impl_metal` shaders into the render pass the host
//! provides.
#![allow(unsafe_code)]
use std::{collections::HashMap, ffi::c_void, ptr::NonNull};
use ghostty_inspector::imgui::{
BackendFlags,
TextureFormat,
TextureId,
render::{
DrawCmd,
DrawIdx,
DrawVert,
PendingFrame,
SnapshotTextureId,
SynchronousRendererConsumer,
TextureFeedback,
TextureOp,
TextureRequest,
},
};
use objc2::{rc::Retained, runtime::ProtocolObject};
use objc2_foundation::NSString;
use objc2_metal::{
MTLBlendFactor,
MTLBlendOperation,
MTLCommandBuffer,
MTLCommandEncoder as _,
MTLCompareFunction,
MTLCullMode,
MTLDepthStencilDescriptor,
MTLDepthStencilState,
MTLDevice,
MTLIndexType,
MTLLibrary,
MTLOrigin,
MTLPixelFormat,
MTLPrimitiveType,
MTLRegion,
MTLRenderCommandEncoder,
MTLRenderPassDescriptor,
MTLRenderPipelineDescriptor,
MTLRenderPipelineState,
MTLResourceOptions,
MTLScissorRect,
MTLSize,
MTLTexture,
MTLTextureDescriptor,
MTLVertexDescriptor,
MTLVertexFormat,
MTLVertexStepFunction,
MTLViewport,
};
use crate::inspector::Inspector;
/// The shaders of `imgui_impl_metal.mm`.
const SHADERS: &str = r"
#include <metal_stdlib>
using namespace metal;
struct Uniforms {
float4x4 projectionMatrix;
};
struct VertexIn {
float2 position [[attribute(0)]];
float2 texCoords [[attribute(1)]];
uchar4 color [[attribute(2)]];
};
struct VertexOut {
float4 position [[position]];
float2 texCoords;
float4 color;
};
vertex VertexOut vertex_main(VertexIn in [[stage_in]],
constant Uniforms &uniforms [[buffer(1)]]) {
VertexOut out;
out.position = uniforms.projectionMatrix * float4(in.position, 0, 1);
out.texCoords = in.texCoords;
out.color = float4(in.color) / float4(255.0);
return out;
}
fragment half4 fragment_main(VertexOut in [[stage_in]],
texture2d<half, access::sample> texture [[texture(0)]]) {
constexpr sampler linearSampler(coord::normalized,
min_filter::linear,
mag_filter::linear,
mip_filter::linear);
half4 texColor = texture.sample(linearSampler, in.texCoords);
return half4(in.color) * texColor;
}
";
type Device = Retained<ProtocolObject<dyn MTLDevice>>;
/// A pipeline state for one render target configuration.
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
struct PipelineKey {
color_format: usize,
sample_count: usize,
depth_format: usize,
stencil_format: usize,
}
/// The Metal draw backend of one inspector.
pub struct MetalBackend {
device: Device,
library: Retained<ProtocolObject<dyn MTLLibrary>>,
depth_state: Retained<ProtocolObject<dyn MTLDepthStencilState>>,
pipelines: HashMap<PipelineKey, Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
consumer: SynchronousRendererConsumer,
/// Managed textures by Dear ImGui's identity.
textures: HashMap<SnapshotTextureId, (u64, Retained<ProtocolObject<dyn MTLTexture>>)>,
next_texture_id: u64,
}
impl MetalBackend {
/// Takes ownership of one reference to `device` (Zig: `defer
/// device.release()`).
///
/// # Safety
/// `device` must be a retained `MTLDevice`.
pub unsafe fn init(
device: *mut c_void,
ctx: &mut ghostty_inspector::imgui::Context,
) -> Result<MetalBackend, String> {
// SAFETY: per the function contract we own one reference.
let device: Device =
unsafe { Retained::from_raw(device.cast::<ProtocolObject<dyn MTLDevice>>()) }
.ok_or_else(|| "null Metal device".to_owned())?;
let library = device
.newLibraryWithSource_options_error(&NSString::from_str(SHADERS), None)
.map_err(|e| e.localizedDescription().to_string())?;
let depth_desc = MTLDepthStencilDescriptor::new();
depth_desc.setDepthCompareFunction(MTLCompareFunction::Always);
depth_desc.setDepthWriteEnabled(false);
let depth_state = device
.newDepthStencilStateWithDescriptor(&depth_desc)
.ok_or_else(|| "failed to create the depth stencil state".to_owned())?;
// Publish the renderer capabilities and the managed texture
// consumer.
let consumer = ctx
.create_synchronous_renderer_consumer()
.map_err(|e| e.to_string())?;
let reset = ctx
.prepare_renderer_texture_reset(&consumer)
.map_err(|e| e.to_string())?;
reset.commit();
if let Err(err) = ctx.set_renderer_name(Some("ghostty_embedded_metal")) {
log::warn!("unable to set the imgui renderer name err={err}");
}
let io = ctx.io_mut();
io.set_backend_flags(
io.backend_flags()
| BackendFlags::RENDERER_HAS_VTX_OFFSET
| BackendFlags::RENDERER_HAS_TEXTURES,
);
Ok(MetalBackend {
device,
library,
depth_state,
pipelines: HashMap::new(),
consumer,
textures: HashMap::new(),
next_texture_id: 1,
})
}
/// Release the GPU objects.
pub fn shutdown(self) {
drop(self);
}
fn pipeline(
&mut self,
key: PipelineKey,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
if let Some(p) = self.pipelines.get(&key) {
return Ok(p.clone());
}
let desc = MTLRenderPipelineDescriptor::new();
let vertex = self
.library
.newFunctionWithName(&NSString::from_str("vertex_main"))
.ok_or("missing vertex_main")?;
let fragment = self
.library
.newFunctionWithName(&NSString::from_str("fragment_main"))
.ok_or("missing fragment_main")?;
desc.setVertexFunction(Some(&vertex));
desc.setFragmentFunction(Some(&fragment));
// ImDrawVert: float2 pos, float2 uv, uchar4 col.
let vd = MTLVertexDescriptor::new();
let stride = size_of::<DrawVert>();
// SAFETY: attribute/layout indexes are valid; plain numbers.
unsafe {
let attrs = vd.attributes();
let a0 = attrs.objectAtIndexedSubscript(0);
a0.setFormat(MTLVertexFormat::Float2);
a0.setOffset(std::mem::offset_of!(DrawVert, pos));
a0.setBufferIndex(0);
let a1 = attrs.objectAtIndexedSubscript(1);
a1.setFormat(MTLVertexFormat::Float2);
a1.setOffset(std::mem::offset_of!(DrawVert, uv));
a1.setBufferIndex(0);
let a2 = attrs.objectAtIndexedSubscript(2);
a2.setFormat(MTLVertexFormat::UChar4);
a2.setOffset(std::mem::offset_of!(DrawVert, col));
a2.setBufferIndex(0);
let layout = vd.layouts().objectAtIndexedSubscript(0);
layout.setStepRate(1);
layout.setStepFunction(MTLVertexStepFunction::PerVertex);
layout.setStride(stride);
}
desc.setVertexDescriptor(Some(&vd));
desc.setRasterSampleCount(key.sample_count);
// SAFETY: color attachment 0 exists.
let color = unsafe { desc.colorAttachments().objectAtIndexedSubscript(0) };
color.setPixelFormat(MTLPixelFormat(key.color_format));
color.setBlendingEnabled(true);
color.setRgbBlendOperation(MTLBlendOperation::Add);
color.setAlphaBlendOperation(MTLBlendOperation::Add);
color.setSourceRGBBlendFactor(MTLBlendFactor::SourceAlpha);
color.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
color.setSourceAlphaBlendFactor(MTLBlendFactor::One);
color.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
desc.setDepthAttachmentPixelFormat(MTLPixelFormat(key.depth_format));
desc.setStencilAttachmentPixelFormat(MTLPixelFormat(key.stencil_format));
let state = self
.device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| e.localizedDescription().to_string())?;
self.pipelines.insert(key, state.clone());
Ok(state)
}
/// Create (or recreate) the texture of `key` from a full upload.
fn create_texture(
&mut self,
key: SnapshotTextureId,
format: TextureFormat,
width: u32,
height: u32,
row_pitch: usize,
pixels: &[u8],
) -> Result<TextureId, String> {
let rgba = to_rgba(format, width as usize, height as usize, row_pitch, pixels)?;
let desc = MTLTextureDescriptor::new();
desc.setPixelFormat(MTLPixelFormat::RGBA8Unorm);
// SAFETY: plain numbers validated by texture creation.
unsafe {
desc.setWidth(width as usize);
desc.setHeight(height as usize);
}
let texture = self
.device
.newTextureWithDescriptor(&desc)
.ok_or("failed to create the texture")?;
replace_region(&texture, 0, 0, width as usize, height as usize, &rgba)?;
let id = if let Some((id, _)) = self.textures.get(&key) {
*id
} else {
let id = self.next_texture_id;
self.next_texture_id += 1;
id
};
self.textures.insert(key, (id, texture));
Ok(TextureId::new(id))
}
fn process_request(&mut self, request: &TextureRequest) -> Result<TextureFeedback, String> {
let key = request.texture();
match request.operation() {
TextureOp::Create {
format,
width,
height,
row_pitch,
pixels,
} => {
let id = self.create_texture(key, *format, *width, *height, *row_pitch, pixels)?;
request.uploaded(id).map_err(|e| e.to_string())
}
TextureOp::Update {
format,
width: _,
height: _,
rects,
} => {
let Some((id, texture)) = self.textures.get(&key) else {
// The texture was never created or was destroyed.
return Ok(request.superseded());
};
for r in rects {
let (w, h) = (usize::from(r.rect.w), usize::from(r.rect.h));
let rgba = to_rgba(*format, w, h, r.row_pitch, &r.data)?;
replace_region(
texture,
usize::from(r.rect.x),
usize::from(r.rect.y),
w,
h,
&rgba,
)?;
}
request
.uploaded(TextureId::new(*id))
.map_err(|e| e.to_string())
}
TextureOp::Destroy => {
self.textures.remove(&key);
request.destroyed().map_err(|e| e.to_string())
}
}
}
/// Reconcile the managed textures of the frame and draw it into
/// `encoder` (`ImGui_ImplMetal_RenderDrawData`).
fn render(
&mut self,
frame: PendingFrame<'_>,
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
desc: &MTLRenderPassDescriptor,
) -> Result<(), String> {
let mut feedback = Vec::with_capacity(frame.texture_requests().len());
for request in frame.texture_requests() {
feedback.push(self.process_request(request)?);
}
let frame = frame
.reconcile_texture_feedback(feedback)
.map_err(|e| e.to_string())?;
let draw_data = frame.draw_data();
let [fb_w, fb_h] = {
let size = draw_data.display_size();
let scale = draw_data.framebuffer_scale();
[size[0] * scale[0], size[1] * scale[1]]
};
// Avoid rendering when minimized.
if fb_w <= 0.0 || fb_h <= 0.0 || draw_data.draw_lists_count() == 0 {
return Ok(());
}
// The pipeline matches the render target the host gave us.
// SAFETY: attachment 0 exists.
let color_tex = unsafe { desc.colorAttachments().objectAtIndexedSubscript(0) }.texture();
let depth_tex = desc.depthAttachment().texture();
let stencil_tex = desc.stencilAttachment().texture();
let key = PipelineKey {
color_format: color_tex
.as_ref()
.map_or(MTLPixelFormat::BGRA8Unorm.0, |t| t.pixelFormat().0),
sample_count: color_tex.as_ref().map_or(1, |t| t.sampleCount()),
depth_format: depth_tex
.as_ref()
.map_or(MTLPixelFormat::Invalid.0, |t| t.pixelFormat().0),
stencil_format: stencil_tex
.as_ref()
.map_or(MTLPixelFormat::Invalid.0, |t| t.pixelFormat().0),
};
let pipeline = self.pipeline(key)?;
let encoder = command_buffer
.renderCommandEncoderWithDescriptor(desc)
.ok_or("failed to create the render command encoder")?;
// Render state.
encoder.setCullMode(MTLCullMode::None);
encoder.setDepthStencilState(Some(&self.depth_state));
encoder.setViewport(MTLViewport {
originX: 0.0,
originY: 0.0,
width: f64::from(fb_w),
height: f64::from(fb_h),
znear: 0.0,
zfar: 1.0,
});
encoder.setRenderPipelineState(&pipeline);
// The orthographic projection of the display.
let [dx, dy] = draw_data.display_pos();
let [dw, dh] = draw_data.display_size();
let (l, r, t, b) = (dx, dx + dw, dy, dy + dh);
#[rustfmt::skip]
let projection: [[f32; 4]; 4] = [
[2.0 / (r - l), 0.0, 0.0, 0.0],
[0.0, 2.0 / (t - b), 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[(r + l) / (l - r), (t + b) / (b - t), 0.0, 1.0],
];
// SAFETY: the pointer/length describe `projection`, which outlives
// the call (Metal copies the bytes).
unsafe {
encoder.setVertexBytes_length_atIndex(
NonNull::from(&projection).cast::<c_void>(),
size_of::<[[f32; 4]; 4]>(),
1,
);
}
let clip_off = draw_data.display_pos();
let clip_scale = draw_data.framebuffer_scale();
for list in draw_data.draw_lists() {
let vertices = list.vtx_buffer();
let indices = list.idx_buffer();
if vertices.is_empty() || indices.is_empty() {
continue;
}
let vbuf =
self.new_buffer(vertices.as_ptr().cast(), std::mem::size_of_val(vertices))?;
let ibuf = self.new_buffer(indices.as_ptr().cast(), std::mem::size_of_val(indices))?;
// SAFETY: slot 0 is the vertex stream.
unsafe { encoder.setVertexBuffer_offset_atIndex(Some(&vbuf), 0, 0) };
for cmd in list.commands() {
match cmd {
DrawCmd::Elements { count, cmd_params } => {
// Project the scissor rectangle into framebuffer
// space and clamp it to the framebuffer.
let cr = cmd_params.clip_rect;
let min_x = ((cr[0] - clip_off[0]) * clip_scale[0]).max(0.0);
let min_y = ((cr[1] - clip_off[1]) * clip_scale[1]).max(0.0);
let max_x = ((cr[2] - clip_off[0]) * clip_scale[0]).min(fb_w);
let max_y = ((cr[3] - clip_off[1]) * clip_scale[1]).min(fb_h);
if max_x <= min_x || max_y <= min_y {
continue;
}
encoder.setScissorRect(MTLScissorRect {
x: min_x as usize,
y: min_y as usize,
width: (max_x - min_x) as usize,
height: (max_y - min_y) as usize,
});
// Bind the texture of the command.
let id = cmd_params.texture_id.id();
let Some((_, tex)) = self.textures.values().find(|(tid, _)| *tid == id)
else {
continue;
};
// SAFETY: slot 0 of the fragment stage.
unsafe { encoder.setFragmentTexture_atIndex(Some(tex), 0) };
// The vertex offset applies through the buffer
// offset (`RENDERER_HAS_VTX_OFFSET`).
// SAFETY: the offset stays inside the buffer.
unsafe {
encoder.setVertexBufferOffset_atIndex(
cmd_params.vtx_offset * size_of::<DrawVert>(),
0,
);
encoder.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset(
MTLPrimitiveType::Triangle,
count,
if size_of::<DrawIdx>() == 2 {
MTLIndexType::UInt16
} else {
MTLIndexType::UInt32
},
&ibuf,
cmd_params.idx_offset * size_of::<DrawIdx>(),
);
}
}
// Nothing to reset: state is set per command.
DrawCmd::ResetRenderState
| DrawCmd::SetSamplerLinear
| DrawCmd::SetSamplerNearest => {}
DrawCmd::RawCallback(_) => {
log::warn!("ignoring a raw imgui draw callback");
}
}
}
}
encoder.endEncoding();
Ok(())
}
fn new_buffer(
&self,
ptr: *const c_void,
len: usize,
) -> Result<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>, String> {
let ptr = NonNull::new(ptr.cast_mut()).ok_or("null buffer")?;
// SAFETY: `ptr` points to `len` readable bytes (a slice of the draw
// list); Metal copies them.
unsafe {
self.device.newBufferWithBytes_length_options(
ptr,
len,
MTLResourceOptions::StorageModeShared,
)
}
.ok_or_else(|| "failed to create a buffer".to_owned())
}
}
/// The pixels as tightly packed RGBA8 (alpha-only textures become white with
/// that alpha).
fn to_rgba(
format: TextureFormat,
width: usize,
height: usize,
row_pitch: usize,
data: &[u8],
) -> Result<Vec<u8>, String> {
let bpp = match format {
TextureFormat::RGBA32 => 4,
TextureFormat::Alpha8 => 1,
};
let pitch = if row_pitch == 0 {
width * bpp
} else {
row_pitch
};
if height > 0 && data.len() < pitch * (height - 1) + width * bpp {
return Err("texture upload is too short".to_owned());
}
let mut out = Vec::with_capacity(width * height * 4);
for y in 0..height {
let row = &data[y * pitch..y * pitch + width * bpp];
match format {
TextureFormat::RGBA32 => out.extend_from_slice(row),
TextureFormat::Alpha8 => {
for a in row {
out.extend_from_slice(&[255, 255, 255, *a]);
}
}
}
}
Ok(out)
}
fn replace_region(
texture: &ProtocolObject<dyn MTLTexture>,
x: usize,
y: usize,
width: usize,
height: usize,
rgba: &[u8],
) -> Result<(), String> {
let ptr = NonNull::new(rgba.as_ptr().cast_mut().cast::<c_void>()).ok_or("empty texture")?;
let region = MTLRegion {
origin: MTLOrigin { x, y, z: 0 },
size: MTLSize {
width,
height,
depth: 1,
},
};
// SAFETY: `rgba` has `width * 4 * height` bytes (built by `to_rgba`).
unsafe {
texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(region, 0, ptr, width * 4);
}
Ok(())
}
/// Render the inspector (Zig: `Inspector.renderMetal`). Takes ownership of
/// one reference each to `command_buffer` and `descriptor`.
///
/// # Safety
/// Both must be retained Metal objects.
pub unsafe fn render(
inspector: &mut Inspector,
command_buffer: *mut c_void,
descriptor: *mut c_void,
) {
// SAFETY: per the function contract we own one reference to each; they
// are released when these `Retained`s drop.
let command_buffer: Option<Retained<ProtocolObject<dyn MTLCommandBuffer>>> =
unsafe { Retained::from_raw(command_buffer.cast()) };
// SAFETY: as above.
let descriptor: Option<Retained<MTLRenderPassDescriptor>> =
unsafe { Retained::from_raw(descriptor.cast()) };
let (Some(command_buffer), Some(descriptor)) = (command_buffer, descriptor) else {
return;
};
if let Err(err) = render_frames(inspector, &command_buffer, &descriptor) {
log::error!("error rendering inspector err={err}");
}
}
fn render_frames(
inspector: &mut Inspector,
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
descriptor: &MTLRenderPassDescriptor,
) -> Result<(), String> {
let mut backend = inspector
.take_backend()
.ok_or("the Metal backend isn't initialized")?;
let result = draw_frame(&mut backend, inspector, command_buffer, descriptor);
inspector.set_backend(backend);
result
}
fn draw_frame(
backend: &mut MetalBackend,
inspector: &mut Inspector,
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
descriptor: &MTLRenderPassDescriptor,
) -> Result<(), String> {
// Setup our imgui frame. We need to render multiple frames to ensure
// ImGui completes all its state processing. I don't know how to fix
// this.
inspector.build_frame();
inspector.context().end_frame();
inspector.build_frame();
let pending = inspector.context().render(&backend.consumer);
backend.render(pending, command_buffer, descriptor)
}
+29
View File
@@ -0,0 +1,29 @@
//! libghostty: the embedded C API of Ghostty (`include/ghostty.h`), ported
//! from `ZIG/src/main_c.zig`, `ZIG/src/apprt/embedded.zig`,
//! `ZIG/src/config/CApi.zig` + `c_get.zig`, the C parts of
//! `ZIG/src/apprt/action.zig` and `ZIG/src/benchmark/CApi.zig`.
//!
//! The header is shipped unchanged in `include/`. The exported symbols and
//! the struct / enum layouts of [`types`] match it exactly (verified by
//! `tests/abi.rs`).
//!
//! Platform gating follows Zig: on Apple targets surfaces render with the
//! Metal renderer into the host's `NSView` / `UIView`; elsewhere the
//! platform independent API (config, app, CLI, key translation, ...) works,
//! but `ghostty_surface_new` fails with `UnsupportedPlatform` (Zig's
//! `Platform.init`).
//!
//! `unsafe` is denied crate wide and allowed (with `SAFETY:` comments) only
//! in the FFI modules.
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::undocumented_unsafe_blocks))]
pub mod action_c;
pub mod benchmark;
pub mod config_capi;
pub mod embedded;
pub mod inspector;
#[cfg(target_vendor = "apple")]
pub mod inspector_metal;
pub mod main_c;
pub mod types;
+257
View File
@@ -0,0 +1,257 @@
//! `ghostty_init`, `ghostty_cli_try_action`, `ghostty_info`,
//! `ghostty_translate` and `ghostty_string_free` (ZIG/src/main_c.zig).
#![allow(unsafe_code)]
use std::{
collections::HashMap,
ffi::{CStr, CString, c_char, c_int},
sync::{Mutex, OnceLock},
};
use ghostty_cli::build_info::{self, BuildInfo};
use ghostty_core::global::{self, InitOptions};
use crate::types::{build_mode, ghostty_info_s, ghostty_string_s};
/// The CLI arguments of the process, as given to `ghostty_init` (Zig:
/// `std.os.argv`).
static ARGS: OnceLock<Vec<String>> = OnceLock::new();
/// Build the `ghostty_string_s` for an owned string (Zig:
/// `String.fromSlice` of an allocated `[:0]const u8`). Free with
/// `ghostty_string_free`.
pub(crate) fn string_from_owned(s: &str) -> ghostty_string_s {
let mut bytes = Vec::with_capacity(s.len() + 1);
bytes.extend_from_slice(s.as_bytes());
bytes.push(0);
let boxed = bytes.into_boxed_slice();
let len = boxed.len() - 1;
let ptr = Box::into_raw(boxed).cast::<c_char>();
ghostty_string_s {
ptr,
len,
sentinel: true,
}
}
/// The empty string (`String.empty`).
pub(crate) const EMPTY_STRING: ghostty_string_s = ghostty_string_s {
ptr: std::ptr::null(),
len: 0,
sentinel: false,
};
/// Free text allocated with [`alloc_c_text`]: `text_len` bytes plus the
/// terminating NUL.
///
/// # Safety
/// `ptr` must come from [`alloc_c_text`] (or `string_from_owned`) with the
/// same `len` and not have been freed.
pub(crate) unsafe fn free_c_text(ptr: *const c_char, len: usize, sentinel: bool) {
if ptr.is_null() {
return;
}
let total = len + usize::from(sentinel);
// SAFETY: per the function contract the allocation is a boxed slice of
// exactly `total` bytes.
drop(unsafe {
Box::from_raw(std::ptr::slice_from_raw_parts_mut(
ptr.cast_mut().cast::<u8>(),
total,
))
});
}
/// Allocate a NUL terminated copy of `bytes`; returns the pointer (free with
/// [`free_c_text`] with `sentinel = true`).
pub(crate) fn alloc_c_text(bytes: &[u8]) -> *const c_char {
let mut v = Vec::with_capacity(bytes.len() + 1);
v.extend_from_slice(bytes);
v.push(0);
Box::into_raw(v.into_boxed_slice()).cast::<c_char>()
}
/// Initialize ghostty global state.
///
/// # Safety
/// `argv` must be null or point to `argc` valid NUL terminated strings.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_init(argc: usize, argv: *mut *mut c_char) -> c_int {
let mut args = Vec::with_capacity(argc);
if !argv.is_null() {
for i in 0..argc {
// SAFETY: the caller guarantees `argv[0..argc]` are valid.
let p = unsafe { *argv.add(i) };
if p.is_null() {
break;
}
// SAFETY: each argv entry is a NUL terminated string.
let c = unsafe { CStr::from_ptr(p) };
args.push(c.to_string_lossy().into_owned());
}
}
// We first try to parse any action that we may be executing.
let action = match ghostty_cli::ghostty::detect(&args) {
Ok(a) => a,
Err(err) => {
log::error!("failed to initialize ghostty error={err:?}");
return 1;
}
};
let _ = ARGS.set(args);
global::init_lib(InitOptions {
action: action.is_some(),
resources_dir: None,
});
// `+version` and friends report the library build.
build_info::set(lib_build_info());
0
}
/// The build info of the library (Zig: `build_config` for the lib artifact:
/// no app runtime, CoreText + Metal on Apple targets).
fn lib_build_info() -> BuildInfo {
let mut info = BuildInfo {
app_runtime: ".none".to_owned(),
gtk_build: None,
gtk_runtime: None,
adw_build: None,
adw_runtime: None,
x11: false,
wayland: false,
..BuildInfo::default()
};
if cfg!(target_vendor = "apple") {
".coretext".clone_into(&mut info.font_backend);
"renderer.generic.Renderer(renderer.Metal)".clone_into(&mut info.renderer);
}
info
}
/// Runs an action if it is specified. If there is no action this returns.
/// If there is an action then this doesn't return.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_cli_try_action() {
let Some(args) = ARGS.get() else {
return;
};
// No action: nothing to do.
if !matches!(ghostty_cli::ghostty::detect(args), Ok(Some(_))) {
return;
}
log::info!("executing CLI action");
let code = ghostty_cli::run_from_args(args).unwrap_or(0);
std::process::exit(i32::from(code));
}
/// Return metadata about Ghostty, such as version, build mode, etc.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_info() -> ghostty_info_s {
static VERSION: OnceLock<CString> = OnceLock::new();
let info = build_info::get();
let version = VERSION.get_or_init(|| CString::new(info.version_string).unwrap_or_default());
let mode = match info.build_mode.trim_start_matches('.') {
"Debug" => build_mode::DEBUG,
"ReleaseSafe" => build_mode::RELEASE_SAFE,
"ReleaseFast" => build_mode::RELEASE_FAST,
"ReleaseSmall" => build_mode::RELEASE_SMALL,
_ if cfg!(debug_assertions) => build_mode::DEBUG,
_ => build_mode::RELEASE_SAFE,
};
ghostty_info_s {
build_mode: mode,
version: version.as_ptr(),
version_len: version.as_bytes().len(),
}
}
/// Translate a string maintained by libghostty into the current application
/// language. This will return the same string (same pointer) if no
/// translation is found, so the pointer must be stable through the function
/// call.
///
/// # Safety
/// `msgid` must be a valid NUL terminated string.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_translate(msgid: *const c_char) -> *const c_char {
static CACHE: OnceLock<Mutex<HashMap<String, CString>>> = OnceLock::new();
if msgid.is_null() {
return msgid;
}
// SAFETY: the caller guarantees `msgid` is NUL terminated.
let id = unsafe { CStr::from_ptr(msgid) };
let Ok(id_str) = id.to_str() else {
return msgid;
};
let translated = ghostty_os::i18n::tr(id_str);
if translated == id_str {
return msgid;
}
// Translations are interned so the returned pointer stays valid.
let Ok(mut cache) = CACHE.get_or_init(Mutex::default).lock() else {
return msgid;
};
if let Some(c) = cache.get(&translated) {
return c.as_ptr();
}
let Ok(c) = CString::new(translated.clone()) else {
return msgid;
};
let ptr = c.as_ptr();
cache.insert(translated, c);
ptr
}
/// Free a string allocated by Ghostty.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_string_free(str: ghostty_string_s) {
// SAFETY: strings handed out by this library are allocated by
// `string_from_owned` / `alloc_c_text` with `len` (+1 if sentinel) bytes.
unsafe { free_c_text(str.ptr, str.len, str.sentinel) };
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ghostty_string_s_empty_string() {
let empty = EMPTY_STRING;
ghostty_string_free(empty);
assert_eq!(empty.len, 0);
assert!(!empty.sentinel);
}
#[test]
fn ghostty_string_s_c_string() {
let s = string_from_owned("hello");
assert_eq!(s.len, 5);
assert!(s.sentinel);
// SAFETY: just allocated with a NUL terminator.
let c = unsafe { CStr::from_ptr(s.ptr) };
assert_eq!(c.to_str().unwrap(), "hello");
ghostty_string_free(s);
}
#[test]
fn info_version() {
let info = ghostty_info();
assert!(info.version_len > 0);
// SAFETY: the version pointer is a static NUL terminated string.
let v = unsafe { CStr::from_ptr(info.version) };
assert_eq!(v.to_bytes().len(), info.version_len);
}
#[test]
fn translate_returns_same_pointer_without_translation() {
let id = c"this message has no translation";
// SAFETY: valid NUL terminated string.
let out = unsafe { ghostty_translate(id.as_ptr()) };
assert_eq!(out, id.as_ptr());
}
}
+579
View File
@@ -0,0 +1,579 @@
//! The C types of `include/ghostty.h`, laid out exactly as the header (and
//! the Zig `extern` structs) define them.
//!
//! Enums are `c_int` everywhere (the header uses `int`-sized enums and the C
//! caller may pass any value); the enumerator values live in submodules as
//! `i32` constants named after the header's enumerators.
#![allow(non_camel_case_types, clippy::struct_field_names)]
use std::ffi::{c_char, c_int, c_void};
// ---------------------------------------------------------------------
// Opaque handles
pub type ghostty_app_t = *mut c_void;
pub type ghostty_config_t = *mut c_void;
pub type ghostty_surface_t = *mut c_void;
pub type ghostty_inspector_t = *mut c_void;
// ---------------------------------------------------------------------
// Scalars and enums (all `int`)
pub type ghostty_platform_e = c_int;
pub type ghostty_clipboard_e = c_int;
pub type ghostty_clipboard_request_e = c_int;
pub type ghostty_input_mouse_state_e = c_int;
pub type ghostty_input_mouse_button_e = c_int;
pub type ghostty_input_mouse_momentum_e = c_int;
pub type ghostty_color_scheme_e = c_int;
pub type ghostty_input_scroll_mods_t = c_int;
pub type ghostty_input_mods_e = c_int;
pub type ghostty_binding_flags_e = c_int;
pub type ghostty_input_action_e = c_int;
pub type ghostty_input_key_e = c_int;
pub type ghostty_input_trigger_tag_e = c_int;
pub type ghostty_build_mode_e = c_int;
pub type ghostty_point_tag_e = c_int;
pub type ghostty_point_coord_e = c_int;
pub type ghostty_surface_context_e = c_int;
pub type ghostty_target_tag_e = c_int;
pub type ghostty_action_tag_e = c_int;
/// `GHOSTTY_SUCCESS`.
pub const GHOSTTY_SUCCESS: c_int = 0;
pub mod platform {
pub const INVALID: i32 = 0;
pub const MACOS: i32 = 1;
pub const IOS: i32 = 2;
}
pub mod clipboard {
pub const STANDARD: i32 = 0;
pub const SELECTION: i32 = 1;
}
pub mod trigger_tag {
pub const PHYSICAL: i32 = 0;
pub const UNICODE: i32 = 1;
pub const CATCH_ALL: i32 = 2;
}
pub mod build_mode {
pub const DEBUG: i32 = 0;
pub const RELEASE_SAFE: i32 = 1;
pub const RELEASE_FAST: i32 = 2;
pub const RELEASE_SMALL: i32 = 3;
}
pub mod target_tag {
pub const APP: i32 = 0;
pub const SURFACE: i32 = 1;
}
// ---------------------------------------------------------------------
// Structs
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_clipboard_content_s {
pub mime: *const c_char,
pub data: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_input_key_s {
pub action: ghostty_input_action_e,
pub mods: ghostty_input_mods_e,
pub consumed_mods: ghostty_input_mods_e,
pub keycode: u32,
pub text: *const c_char,
pub unshifted_codepoint: u32,
pub composing: bool,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_input_trigger_key_u {
pub translated: ghostty_input_key_e,
pub physical: ghostty_input_key_e,
pub unicode: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_input_trigger_s {
pub tag: ghostty_input_trigger_tag_e,
pub key: ghostty_input_trigger_key_u,
pub mods: ghostty_input_mods_e,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_command_s {
pub action_key: *const c_char,
pub action: *const c_char,
pub title: *const c_char,
pub description: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_info_s {
pub build_mode: ghostty_build_mode_e,
pub version: *const c_char,
pub version_len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_diagnostic_s {
pub message: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_string_s {
pub ptr: *const c_char,
pub len: usize,
pub sentinel: bool,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_text_s {
pub tl_px_x: f64,
pub tl_px_y: f64,
pub offset_start: u32,
pub offset_len: u32,
pub text: *const c_char,
pub text_len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_point_s {
pub tag: ghostty_point_tag_e,
pub coord: ghostty_point_coord_e,
pub x: u32,
pub y: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_selection_s {
pub top_left: ghostty_point_s,
pub bottom_right: ghostty_point_s,
pub rectangle: bool,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_env_var_s {
pub key: *const c_char,
pub value: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_platform_macos_s {
pub nsview: *mut c_void,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_platform_ios_s {
pub uiview: *mut c_void,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_platform_u {
pub macos: ghostty_platform_macos_s,
pub ios: ghostty_platform_ios_s,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_surface_config_s {
pub platform_tag: ghostty_platform_e,
pub platform: ghostty_platform_u,
pub userdata: *mut c_void,
pub scale_factor: f64,
pub font_size: f32,
pub working_directory: *const c_char,
pub command: *const c_char,
pub env_vars: *mut ghostty_env_var_s,
pub env_var_count: usize,
pub initial_input: *const c_char,
pub wait_after_command: bool,
pub context: ghostty_surface_context_e,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_surface_size_s {
pub columns: u16,
pub rows: u16,
pub width_px: u32,
pub height_px: u32,
pub cell_width_px: u32,
pub cell_height_px: u32,
}
// ---- config types
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_path_s {
pub path: *const c_char,
pub optional: bool,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_color_s {
pub r: u8,
pub g: u8,
pub b: u8,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_color_list_s {
pub colors: *const ghostty_config_color_s,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_command_list_s {
pub commands: *const ghostty_command_s,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_palette_s {
pub colors: [ghostty_config_color_s; 256],
}
pub mod quick_terminal_size_tag {
pub const NONE: i32 = 0;
pub const PERCENTAGE: i32 = 1;
pub const PIXELS: i32 = 2;
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_quick_terminal_size_value_u {
pub percentage: f32,
pub pixels: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_quick_terminal_size_s {
pub tag: c_int,
pub value: ghostty_quick_terminal_size_value_u,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_quick_terminal_size_s {
pub primary: ghostty_quick_terminal_size_s,
pub secondary: ghostty_quick_terminal_size_s,
}
// ---- targets and actions
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_target_u {
pub surface: ghostty_surface_t,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_target_s {
pub tag: ghostty_target_tag_e,
pub target: ghostty_target_u,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_resize_split_s {
pub amount: u16,
pub direction: c_int,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_move_tab_s {
pub amount: isize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_desktop_notification_s {
pub title: *const c_char,
pub body: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_set_title_s {
pub title: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_pwd_s {
pub pwd: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_mouse_over_link_s {
pub url: *const c_char,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_size_limit_s {
pub min_width: u32,
pub min_height: u32,
pub max_width: u32,
pub max_height: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_initial_size_s {
pub width: u32,
pub height: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_cell_size_s {
pub width: u32,
pub height: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_key_sequence_s {
pub active: bool,
pub trigger: ghostty_input_trigger_s,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_key_table_activate_s {
pub name: *const c_char,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_action_key_table_u {
pub activate: ghostty_action_key_table_activate_s,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_key_table_s {
pub tag: c_int,
pub value: ghostty_action_key_table_u,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_color_change_s {
pub kind: c_int,
pub r: u8,
pub g: u8,
pub b: u8,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_config_change_s {
pub config: ghostty_config_t,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_reload_config_s {
pub soft: bool,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_open_url_s {
pub kind: c_int,
pub url: *const c_char,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_surface_message_childexited_s {
pub exit_code: u32,
pub timetime_ms: u64,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_progress_report_s {
pub state: c_int,
pub progress: i8,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_command_finished_s {
pub exit_code: i16,
pub duration: u64,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_start_search_s {
pub needle: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_search_total_s {
pub total: isize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_search_selected_s {
pub selected: isize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_scrollbar_s {
pub total: u64,
pub offset: u64,
pub len: u64,
}
/// `ghostty_action_u`. Fields that are plain enums are `c_int`.
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_action_u {
pub new_split: c_int,
pub toggle_fullscreen: c_int,
pub move_tab: ghostty_action_move_tab_s,
pub goto_tab: c_int,
pub goto_split: c_int,
pub goto_window: c_int,
pub resize_split: ghostty_action_resize_split_s,
pub size_limit: ghostty_action_size_limit_s,
pub initial_size: ghostty_action_initial_size_s,
pub cell_size: ghostty_action_cell_size_s,
pub scrollbar: ghostty_action_scrollbar_s,
pub inspector: c_int,
pub desktop_notification: ghostty_action_desktop_notification_s,
pub set_title: ghostty_action_set_title_s,
pub set_tab_title: ghostty_action_set_title_s,
pub prompt_title: c_int,
pub pwd: ghostty_action_pwd_s,
pub mouse_shape: c_int,
pub mouse_visibility: c_int,
pub mouse_over_link: ghostty_action_mouse_over_link_s,
pub renderer_health: c_int,
pub quit_timer: c_int,
pub float_window: c_int,
pub secure_input: c_int,
pub key_sequence: ghostty_action_key_sequence_s,
pub key_table: ghostty_action_key_table_s,
pub color_change: ghostty_action_color_change_s,
pub reload_config: ghostty_action_reload_config_s,
pub config_change: ghostty_action_config_change_s,
pub open_url: ghostty_action_open_url_s,
pub close_tab_mode: c_int,
pub child_exited: ghostty_surface_message_childexited_s,
pub progress_report: ghostty_action_progress_report_s,
pub command_finished: ghostty_action_command_finished_s,
pub start_search: ghostty_action_start_search_s,
pub search_total: ghostty_action_search_total_s,
pub search_selected: ghostty_action_search_selected_s,
pub readonly: c_int,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_action_s {
pub tag: ghostty_action_tag_e,
pub action: ghostty_action_u,
}
// ---- runtime callbacks
pub type ghostty_runtime_wakeup_cb = Option<unsafe extern "C" fn(*mut c_void)>;
pub type ghostty_runtime_read_clipboard_cb =
Option<unsafe extern "C" fn(*mut c_void, ghostty_clipboard_e, *mut c_void) -> bool>;
pub type ghostty_runtime_confirm_read_clipboard_cb = Option<
unsafe extern "C" fn(*mut c_void, *const c_char, *mut c_void, ghostty_clipboard_request_e),
>;
pub type ghostty_runtime_write_clipboard_cb = Option<
unsafe extern "C" fn(
*mut c_void,
ghostty_clipboard_e,
*const ghostty_clipboard_content_s,
usize,
bool,
),
>;
pub type ghostty_runtime_close_surface_cb = Option<unsafe extern "C" fn(*mut c_void, bool)>;
pub type ghostty_runtime_action_cb =
Option<unsafe extern "C" fn(ghostty_app_t, ghostty_target_s, ghostty_action_s) -> bool>;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_runtime_config_s {
pub userdata: *mut c_void,
pub supports_selection_clipboard: bool,
pub wakeup_cb: ghostty_runtime_wakeup_cb,
pub action_cb: ghostty_runtime_action_cb,
pub read_clipboard_cb: ghostty_runtime_read_clipboard_cb,
pub confirm_read_clipboard_cb: ghostty_runtime_confirm_read_clipboard_cb,
pub write_clipboard_cb: ghostty_runtime_write_clipboard_cb,
pub close_surface_cb: ghostty_runtime_close_surface_cb,
}
// ---- ipc (declared by the header; the embedded runtime never uses them)
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_ipc_target_u {
pub klass: *mut c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct chostty_ipc_target_s {
pub tag: c_int,
pub target: ghostty_ipc_target_u,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_ipc_action_new_window_s {
pub arguments: *mut *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union ghostty_ipc_action_u {
pub new_window: ghostty_ipc_action_new_window_s,
}