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