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
@@ -310,6 +310,12 @@ pub trait RtSurface {
|
||||
let _ = name;
|
||||
std::ptr::null()
|
||||
}
|
||||
/// The native view the renderer attaches to (embedded runtime: the
|
||||
/// `NSView` / `UIView` the host provided; Metal renderer only). Null if
|
||||
/// the runtime has no such view.
|
||||
fn platform_view(&self) -> *mut std::ffi::c_void {
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
/// Callback to create a transient systemd scope for the child process
|
||||
/// (Zig: `apprt/gtk/cgroup.zig` + `post_fork`). Called on the main
|
||||
/// thread when the surface is created; the returned closure is called
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
//! The C representation of config values (Zig: `config/c_get.zig` together
|
||||
//! with the `cval` functions of the config types).
|
||||
//!
|
||||
//! `ghostty_config_get` writes a value of a key-dependent C type into a
|
||||
//! caller supplied pointer. This crate cannot use `unsafe`, so the typed
|
||||
//! value is described by [`CValue`] and the C API crate (`ghostty-lib`) does
|
||||
//! the actual write.
|
||||
|
||||
use ghostty_input::command::CommandC;
|
||||
|
||||
/// A size of the quick terminal along one axis in C form
|
||||
/// (`ghostty_quick_terminal_size_s`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum QuickTerminalSizeC {
|
||||
None,
|
||||
Percentage(f32),
|
||||
Pixels(u32),
|
||||
}
|
||||
|
||||
/// The value of a config field in the form the C API exposes it.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CValue {
|
||||
/// The type of this field is not supported by the C API (Zig `getValue`
|
||||
/// returns false).
|
||||
Unsupported,
|
||||
/// An optional field without a value (returns false).
|
||||
Null,
|
||||
/// An optional string without a value: `ghostty_config_get` returns true
|
||||
/// and stores a null pointer.
|
||||
NullString,
|
||||
/// A string (`?[:0]const u8`): stored as a NUL terminated `const char*`.
|
||||
String(String),
|
||||
/// A boolean (`bool`).
|
||||
Bool(bool),
|
||||
/// `u8` / `u32`, stored as `c_uint`.
|
||||
Uint(u32),
|
||||
/// `i16`, stored as `c_short`.
|
||||
Short(i16),
|
||||
/// `f32`.
|
||||
F32(f32),
|
||||
/// `f64`.
|
||||
F64(f64),
|
||||
/// `usize` (`Duration.cval`).
|
||||
Usize(usize),
|
||||
/// An enum: the tag name, stored as a NUL terminated `const char*`.
|
||||
EnumName(&'static str),
|
||||
/// A packed struct of at most 32 bits: its bits stored as `c_uint`.
|
||||
Packed(u32),
|
||||
/// `ghostty_config_color_s`.
|
||||
Color { r: u8, g: u8, b: u8 },
|
||||
/// `ghostty_config_color_list_s`.
|
||||
ColorList(Vec<[u8; 3]>),
|
||||
/// `ghostty_config_palette_s`.
|
||||
Palette(Box<[[u8; 3]; 256]>),
|
||||
/// `ghostty_config_command_list_s`.
|
||||
Commands(Vec<CommandC>),
|
||||
/// `ghostty_config_quick_terminal_size_s`.
|
||||
QuickTerminalSize {
|
||||
primary: QuickTerminalSizeC,
|
||||
secondary: QuickTerminalSizeC,
|
||||
},
|
||||
/// `ghostty_config_path_s`.
|
||||
Path { path: String, optional: bool },
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
use std::{fmt, io::BufRead};
|
||||
|
||||
use crate::{
|
||||
c_value::CValue,
|
||||
comma_splitter::CommaSplitter,
|
||||
diagnostics::{Diagnostic, DiagnosticList, Location},
|
||||
formatter::EntryFormatter,
|
||||
@@ -113,6 +114,28 @@ pub trait ConfigField: Clone + PartialEq + Default + fmt::Debug {
|
||||
|
||||
/// Write this value in the `+show-config` format.
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result;
|
||||
|
||||
/// The value in the form the C API exposes (Zig: `c_get.getValue`).
|
||||
/// Types the C API doesn't support yet return [`CValue::Unsupported`].
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::Unsupported
|
||||
}
|
||||
|
||||
/// [`ConfigField::c_value`] when the value is wrapped in an `Option`
|
||||
/// (only `String` differs: `?[:0]const u8` is supported, `[]const u8`
|
||||
/// is not).
|
||||
fn c_value_optional(&self) -> CValue {
|
||||
self.c_value()
|
||||
}
|
||||
|
||||
/// The C value of an `Option<Self>` that is `None`.
|
||||
fn c_value_none() -> CValue {
|
||||
if matches!(Self::default().c_value(), CValue::Unsupported) {
|
||||
CValue::Unsupported
|
||||
} else {
|
||||
CValue::Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The destination of CLI parsing.
|
||||
@@ -456,6 +479,10 @@ pub fn parse_int_base0(s: &str) -> Option<i128> {
|
||||
impl ConfigField for bool {
|
||||
const KIND: FieldKind = FieldKind::Bool;
|
||||
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::Bool(*self)
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
*self = parse_bool(value.unwrap_or("t"))?;
|
||||
Ok(())
|
||||
@@ -467,8 +494,13 @@ impl ConfigField for bool {
|
||||
}
|
||||
|
||||
macro_rules! impl_int_field {
|
||||
($($t:ty),*) => {$(
|
||||
($($t:ty => $cv:expr),*) => {$(
|
||||
impl ConfigField for $t {
|
||||
fn c_value(&self) -> CValue {
|
||||
let f: fn($t) -> CValue = $cv;
|
||||
f(*self)
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
let n = parse_int_base0(v).ok_or(ArgError::InvalidValue)?;
|
||||
@@ -482,11 +514,27 @@ macro_rules! impl_int_field {
|
||||
}
|
||||
)*};
|
||||
}
|
||||
impl_int_field!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
|
||||
impl_int_field!(
|
||||
u8 => |v| CValue::Uint(u32::from(v)),
|
||||
u16 => |_| CValue::Unsupported,
|
||||
u32 => CValue::Uint,
|
||||
u64 => |_| CValue::Unsupported,
|
||||
usize => |_| CValue::Unsupported,
|
||||
i8 => |_| CValue::Unsupported,
|
||||
i16 => CValue::Short,
|
||||
i32 => |_| CValue::Unsupported,
|
||||
i64 => |_| CValue::Unsupported,
|
||||
isize => |_| CValue::Unsupported
|
||||
);
|
||||
|
||||
macro_rules! impl_float_field {
|
||||
($($t:ty),*) => {$(
|
||||
($($t:ty => $cv:expr),*) => {$(
|
||||
impl ConfigField for $t {
|
||||
fn c_value(&self) -> CValue {
|
||||
let f: fn($t) -> CValue = $cv;
|
||||
f(*self)
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
*self = v.parse::<$t>().map_err(|_| ArgError::InvalidValue)?;
|
||||
@@ -499,9 +547,17 @@ macro_rules! impl_float_field {
|
||||
}
|
||||
)*};
|
||||
}
|
||||
impl_float_field!(f32, f64);
|
||||
impl_float_field!(f32 => CValue::F32, f64 => CValue::F64);
|
||||
|
||||
impl ConfigField for String {
|
||||
fn c_value_optional(&self) -> CValue {
|
||||
CValue::String(self.clone())
|
||||
}
|
||||
|
||||
fn c_value_none() -> CValue {
|
||||
CValue::NullString
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
*self = value.ok_or(ArgError::ValueRequired)?.to_string();
|
||||
Ok(())
|
||||
@@ -519,6 +575,13 @@ impl<T: ConfigField> ConfigField for Option<T> {
|
||||
_ => FieldKind::OptionalOther,
|
||||
};
|
||||
|
||||
fn c_value(&self) -> CValue {
|
||||
match self {
|
||||
Some(v) => v.c_value_optional(),
|
||||
None => T::c_value_none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
// For optional fields, we just treat it as the child type.
|
||||
if let Some(v) = self {
|
||||
|
||||
@@ -3810,6 +3810,16 @@ impl Default for Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// The value of the config key in the form the C API exposes it (Zig:
|
||||
/// `c_get.get`). `None` if there is no such key.
|
||||
#[must_use]
|
||||
pub fn c_value(&self, key: &str) -> Option<crate::c_value::CValue> {
|
||||
<Config as crate::macros::ConfigStruct>::FIELDS
|
||||
.iter()
|
||||
.find(|f| f.name == key)
|
||||
.map(|f| (f.c_value)(self))
|
||||
}
|
||||
|
||||
/// Build the default configuration (Zig: `Config.default`).
|
||||
#[must_use]
|
||||
pub fn new_default() -> Config {
|
||||
@@ -5412,4 +5422,85 @@ mod tests {
|
||||
assert!(out.contains("\nfont-size = 12\n"));
|
||||
assert!(out.contains("\nbackground = #282c34\n"));
|
||||
}
|
||||
|
||||
/// The keys `ghostty_config_get` supports, derived from the Zig field
|
||||
/// types and `c_get.getValue` (see `testdata/c_get_supported.txt`).
|
||||
#[test]
|
||||
fn c_value_supported_keys_match_zig() {
|
||||
use crate::{c_value::CValue, macros::ConfigStruct};
|
||||
let expected: std::collections::BTreeSet<&str> =
|
||||
include_str!("testdata/c_get_supported.txt")
|
||||
.lines()
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect();
|
||||
let cfg = Config::default();
|
||||
let mut actual = std::collections::BTreeSet::new();
|
||||
for f in <Config as ConfigStruct>::FIELDS {
|
||||
if !matches!((f.c_value)(&cfg), CValue::Unsupported) {
|
||||
actual.insert(f.name);
|
||||
}
|
||||
}
|
||||
let missing: Vec<_> = expected.difference(&actual).collect();
|
||||
let extra: Vec<_> = actual.difference(&expected).collect();
|
||||
assert!(
|
||||
missing.is_empty() && extra.is_empty(),
|
||||
"missing={missing:?} extra={extra:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_value_basic_types() {
|
||||
use crate::c_value::CValue;
|
||||
let mut cfg = Config::default();
|
||||
cfg.maximize = true;
|
||||
assert_eq!(cfg.c_value("maximize"), Some(CValue::Bool(true)));
|
||||
cfg.window_theme = WindowTheme::Dark;
|
||||
assert_eq!(cfg.c_value("window-theme"), Some(CValue::EnumName("dark")));
|
||||
cfg.title = None;
|
||||
assert_eq!(cfg.c_value("title"), Some(CValue::NullString));
|
||||
cfg.unfocused_split_fill = None;
|
||||
assert_eq!(cfg.c_value("unfocused-split-fill"), Some(CValue::Null));
|
||||
cfg.background_opacity = 0.42;
|
||||
assert_eq!(cfg.c_value("background-opacity"), Some(CValue::F64(0.42)));
|
||||
cfg.background = crate::types_a::Color {
|
||||
r: 12,
|
||||
g: 34,
|
||||
b: 56,
|
||||
};
|
||||
assert_eq!(
|
||||
cfg.c_value("background"),
|
||||
Some(CValue::Color {
|
||||
r: 12,
|
||||
g: 34,
|
||||
b: 56
|
||||
})
|
||||
);
|
||||
assert_eq!(cfg.c_value("not-a-real-key"), None);
|
||||
assert_eq!(cfg.c_value("font-family"), Some(CValue::Unsupported));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_value_background_blur() {
|
||||
use crate::{c_value::CValue, types_b::BackgroundBlur};
|
||||
let mut cfg = Config::default();
|
||||
for (v, expect) in [
|
||||
(BackgroundBlur::False, 0),
|
||||
(BackgroundBlur::True, 20),
|
||||
(BackgroundBlur::Radius(42), 42),
|
||||
(BackgroundBlur::MacosGlassRegular, -1),
|
||||
(BackgroundBlur::MacosGlassClear, -2),
|
||||
] {
|
||||
cfg.background_blur = v;
|
||||
assert_eq!(cfg.c_value("background-blur"), Some(CValue::Short(expect)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_value_split_preserve_zoom() {
|
||||
use crate::c_value::CValue;
|
||||
let mut cfg = Config::default();
|
||||
assert_eq!(cfg.c_value("split-preserve-zoom"), Some(CValue::Packed(0)));
|
||||
cfg.split_preserve_zoom.navigation = true;
|
||||
assert_eq!(cfg.c_value("split-preserve-zoom"), Some(CValue::Packed(1)));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use std::fmt;
|
||||
use ghostty_input::{KeyRemapSet, OptionAsAlt};
|
||||
|
||||
use crate::{
|
||||
c_value::CValue,
|
||||
cli_args::{ArgError, ConfigField},
|
||||
formatter::EntryFormatter,
|
||||
};
|
||||
@@ -30,6 +31,15 @@ impl ConfigField for KeyRemapSet {
|
||||
impl ConfigField for OptionAsAlt {
|
||||
const VALID_VALUES: Option<&'static [&'static str]> = None;
|
||||
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::EnumName(match self {
|
||||
OptionAsAlt::False => "false",
|
||||
OptionAsAlt::True => "true",
|
||||
OptionAsAlt::Left => "left",
|
||||
OptionAsAlt::Right => "right",
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
*self = match v {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
)]
|
||||
|
||||
// Generic CLI parsing infrastructure.
|
||||
pub mod c_value;
|
||||
pub mod cli_args;
|
||||
pub mod comma_splitter;
|
||||
pub mod diagnostics;
|
||||
|
||||
@@ -37,6 +37,8 @@ pub struct FieldInfo<T> {
|
||||
pub eq: fn(&T, &T) -> bool,
|
||||
/// Format this field as in `+show-config`.
|
||||
pub format: fn(&T, &str, &mut dyn fmt::Write) -> fmt::Result,
|
||||
/// The value in the form the C API exposes it.
|
||||
pub c_value: fn(&T) -> crate::c_value::CValue,
|
||||
}
|
||||
|
||||
impl<T> FieldInfo<T> {
|
||||
@@ -147,6 +149,7 @@ macro_rules! config_struct {
|
||||
let mut f = $crate::formatter::EntryFormatter::new(name, w);
|
||||
$crate::cli_args::ConfigField::format_entry(&a.$field, &mut f)
|
||||
},
|
||||
c_value: |a: &$name| $crate::cli_args::ConfigField::c_value(&a.$field),
|
||||
},
|
||||
)*
|
||||
];
|
||||
@@ -221,6 +224,10 @@ macro_rules! config_enum {
|
||||
::core::result::Result::Ok(())
|
||||
}
|
||||
|
||||
fn c_value(&self) -> $crate::c_value::CValue {
|
||||
$crate::c_value::CValue::EnumName(self.as_str())
|
||||
}
|
||||
|
||||
fn format_entry(
|
||||
&self,
|
||||
f: &mut $crate::formatter::EntryFormatter<'_>,
|
||||
@@ -274,6 +281,20 @@ macro_rules! config_packed {
|
||||
const KIND: $crate::cli_args::FieldKind =
|
||||
$crate::cli_args::FieldKind::Packed(Self::NAMES);
|
||||
|
||||
fn c_value(&self) -> $crate::c_value::CValue {
|
||||
// Packed bools: the first field is bit 0.
|
||||
let mut bits = 0u32;
|
||||
let mut _i = 0u32;
|
||||
$(
|
||||
if self.$field && _i < 32 { bits |= 1 << _i; }
|
||||
_i += 1;
|
||||
)*
|
||||
if _i > 32 {
|
||||
return $crate::c_value::CValue::Unsupported;
|
||||
}
|
||||
$crate::c_value::CValue::Packed(bits)
|
||||
}
|
||||
|
||||
fn parse_cli(
|
||||
&mut self,
|
||||
value: ::core::option::Option<&str>,
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
c_value::CValue,
|
||||
cli_args::{ArgError, ConfigField},
|
||||
diagnostics::{Diagnostic, DiagnosticList},
|
||||
formatter::EntryFormatter,
|
||||
@@ -171,6 +172,19 @@ impl Path {
|
||||
}
|
||||
|
||||
impl ConfigField for Path {
|
||||
fn c_value(&self) -> CValue {
|
||||
match self {
|
||||
Path::Optional(p) => CValue::Path {
|
||||
path: p.clone(),
|
||||
optional: true,
|
||||
},
|
||||
Path::Required(p) => CValue::Path {
|
||||
path: p.clone(),
|
||||
optional: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse CLI option. The value will be overwritten.
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
let Some(item) = Path::parse(input)? else {
|
||||
|
||||
@@ -46,6 +46,14 @@ impl RepeatableStringMap {
|
||||
.map(|(_, v)| v.as_str())
|
||||
}
|
||||
|
||||
/// Insert or replace an entry (Zig: `map.put`).
|
||||
pub fn put(&mut self, key: &str, value: &str) {
|
||||
match self.map.iter_mut().find(|(k, _)| k == key) {
|
||||
Some((_, v)) => value.clone_into(v),
|
||||
None => self.map.push((key.to_owned(), value.to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over the entries in the map (insertion order).
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||
self.map.iter().map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
language
|
||||
font-synthetic-style
|
||||
font-size
|
||||
font-thicken
|
||||
font-thicken-strength
|
||||
font-shaping-break
|
||||
alpha-blending
|
||||
grapheme-width-method
|
||||
freetype-load-flags
|
||||
background
|
||||
foreground
|
||||
background-image
|
||||
background-image-opacity
|
||||
background-image-position
|
||||
background-image-fit
|
||||
background-image-repeat
|
||||
selection-clear-on-typing
|
||||
selection-clear-on-copy
|
||||
minimum-contrast
|
||||
palette
|
||||
palette-generate
|
||||
palette-harmonious
|
||||
cursor-opacity
|
||||
cursor-style
|
||||
cursor-style-blink
|
||||
cursor-click-to-move
|
||||
mouse-hide-while-typing
|
||||
scroll-to-bottom
|
||||
mouse-shift-capture
|
||||
mouse-reporting
|
||||
background-opacity
|
||||
background-opacity-cells
|
||||
background-blur
|
||||
unfocused-split-opacity
|
||||
unfocused-split-fill
|
||||
split-divider-color
|
||||
split-preserve-zoom
|
||||
notify-on-command-finish
|
||||
notify-on-command-finish-action
|
||||
notify-on-command-finish-after
|
||||
wait-after-command
|
||||
abnormal-command-exit-runtime
|
||||
scrollbar
|
||||
link-url
|
||||
link-previews
|
||||
maximize
|
||||
fullscreen
|
||||
title
|
||||
class
|
||||
x11-instance-name
|
||||
window-padding-balance
|
||||
window-padding-color
|
||||
window-vsync
|
||||
window-inherit-working-directory
|
||||
tab-inherit-working-directory
|
||||
split-inherit-working-directory
|
||||
window-inherit-font-size
|
||||
window-decoration
|
||||
window-title-font-family
|
||||
window-subtitle
|
||||
window-theme
|
||||
window-colorspace
|
||||
window-height
|
||||
window-width
|
||||
window-position-x
|
||||
window-position-y
|
||||
window-save-state
|
||||
window-step-resize
|
||||
window-new-tab-position
|
||||
window-show-tab-bar
|
||||
window-titlebar-background
|
||||
window-titlebar-foreground
|
||||
resize-overlay
|
||||
resize-overlay-position
|
||||
resize-overlay-duration
|
||||
focus-follows-mouse
|
||||
clipboard-read
|
||||
clipboard-write
|
||||
clipboard-trim-trailing-spaces
|
||||
clipboard-paste-protection
|
||||
clipboard-paste-bracketed-safe
|
||||
title-report
|
||||
image-storage-limit
|
||||
copy-on-select
|
||||
right-click-action
|
||||
click-repeat-interval
|
||||
config-default-files
|
||||
confirm-close-surface
|
||||
quit-after-last-window-closed
|
||||
quit-after-last-window-closed-delay
|
||||
initial-window
|
||||
undo-timeout
|
||||
quick-terminal-position
|
||||
quick-terminal-size
|
||||
gtk-quick-terminal-layer
|
||||
quick-terminal-screen
|
||||
quick-terminal-animation-duration
|
||||
quick-terminal-autohide
|
||||
quick-terminal-space-behavior
|
||||
quick-terminal-keyboard-interactivity
|
||||
shell-integration
|
||||
shell-integration-features
|
||||
command-palette-entry
|
||||
osc-color-report-format
|
||||
vt-kam-allowed
|
||||
custom-shader-animation
|
||||
bell-features
|
||||
bell-audio-path
|
||||
bell-audio-volume
|
||||
app-notifications
|
||||
macos-non-native-fullscreen
|
||||
macos-window-buttons
|
||||
macos-titlebar-style
|
||||
macos-titlebar-proxy-icon
|
||||
macos-dock-drop-behavior
|
||||
macos-option-as-alt
|
||||
macos-window-shadow
|
||||
macos-hidden
|
||||
macos-auto-secure-input
|
||||
macos-secure-input-indication
|
||||
macos-applescript
|
||||
macos-icon
|
||||
macos-custom-icon
|
||||
macos-icon-frame
|
||||
macos-icon-ghost-color
|
||||
macos-icon-screen-color
|
||||
macos-shortcuts
|
||||
linux-cgroup
|
||||
linux-cgroup-hard-fail
|
||||
gtk-opengl-debug
|
||||
gtk-single-instance
|
||||
gtk-titlebar
|
||||
gtk-tabs-location
|
||||
gtk-titlebar-hide-when-maximized
|
||||
gtk-toolbar-style
|
||||
gtk-titlebar-style
|
||||
gtk-wide-tabs
|
||||
desktop-notifications
|
||||
progress-style
|
||||
faint-opacity
|
||||
async-backend
|
||||
auto-update
|
||||
auto-update-channel
|
||||
@@ -7,6 +7,7 @@ use std::fmt;
|
||||
use ghostty_vt::color::{DEFAULT as DEFAULT_PALETTE, Palette as TermPalette, PaletteMask, Rgb};
|
||||
|
||||
use crate::{
|
||||
c_value::CValue,
|
||||
cli_args::{ArgError, ConfigField, parse_int_base0, trim_ws},
|
||||
clipboard_codepoint_map::{ClipboardCodepointMap, Entry as ClipboardEntry, Replacement},
|
||||
config_enum,
|
||||
@@ -35,6 +36,15 @@ impl ConfigField for CursorStyle {
|
||||
const VALID_VALUES: Option<&'static [&'static str]> =
|
||||
Some(&["bar", "block", "underline", "block_hollow"]);
|
||||
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::EnumName(match self {
|
||||
CursorStyle::Bar => "bar",
|
||||
CursorStyle::Block => "block",
|
||||
CursorStyle::Underline => "underline",
|
||||
CursorStyle::BlockHollow => "block_hollow",
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
*self = match v {
|
||||
@@ -290,6 +300,14 @@ impl Color {
|
||||
}
|
||||
|
||||
impl ConfigField for Color {
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::Color {
|
||||
r: self.r,
|
||||
g: self.g,
|
||||
b: self.b,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
*self = Color::from_cli(input)?;
|
||||
Ok(())
|
||||
@@ -394,6 +412,10 @@ pub struct ColorList {
|
||||
}
|
||||
|
||||
impl ConfigField for ColorList {
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::ColorList(self.colors.iter().map(|c| [c.r, c.g, c.b]).collect())
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
let input = input.ok_or(ArgError::ValueRequired)?;
|
||||
if input.is_empty() {
|
||||
@@ -454,6 +476,14 @@ impl Default for Palette {
|
||||
}
|
||||
|
||||
impl ConfigField for Palette {
|
||||
fn c_value(&self) -> CValue {
|
||||
let mut out = Box::new([[0u8; 3]; 256]);
|
||||
for (dst, c) in out.iter_mut().zip(self.value.iter()) {
|
||||
*dst = [c.r, c.g, c.b];
|
||||
}
|
||||
CValue::Palette(out)
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
let value = input.ok_or(ArgError::ValueRequired)?;
|
||||
let eql_idx = value.find('=').ok_or(ArgError::InvalidValue)?;
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::fmt;
|
||||
use ghostty_input::binding::Action;
|
||||
|
||||
use crate::{
|
||||
c_value::{CValue, QuickTerminalSizeC},
|
||||
cli_args::{ArgError, ConfigField, parse_auto_struct_with, parse_bool, trim_ws},
|
||||
config_enum,
|
||||
config_packed,
|
||||
@@ -152,6 +153,15 @@ impl RepeatableCommand {
|
||||
}
|
||||
|
||||
impl ConfigField for RepeatableCommand {
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::Commands(
|
||||
self.value
|
||||
.iter()
|
||||
.map(ghostty_input::command::Command::cval)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
// Unset or empty input clears the list
|
||||
let input = input.unwrap_or("");
|
||||
@@ -604,6 +614,18 @@ impl QuickTerminalSize {
|
||||
}
|
||||
|
||||
impl ConfigField for QuickTerminalSize {
|
||||
fn c_value(&self) -> CValue {
|
||||
let conv = |v: Option<QuickTerminalSizeValue>| match v {
|
||||
None => QuickTerminalSizeC::None,
|
||||
Some(QuickTerminalSizeValue::Percentage(p)) => QuickTerminalSizeC::Percentage(p),
|
||||
Some(QuickTerminalSizeValue::Pixels(p)) => QuickTerminalSizeC::Pixels(p),
|
||||
};
|
||||
CValue::QuickTerminalSize {
|
||||
primary: conv(self.primary),
|
||||
secondary: conv(self.secondary),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
let input = input.ok_or(ArgError::ValueRequired)?;
|
||||
let mut it = input.split(',');
|
||||
@@ -786,6 +808,17 @@ impl BackgroundBlur {
|
||||
}
|
||||
|
||||
impl ConfigField for BackgroundBlur {
|
||||
fn c_value(&self) -> CValue {
|
||||
// Sentinel values for the macOS glass styles, like Zig's `cval`.
|
||||
CValue::Short(match self {
|
||||
BackgroundBlur::False => 0,
|
||||
BackgroundBlur::True => 20,
|
||||
BackgroundBlur::Radius(v) => i16::from(*v),
|
||||
BackgroundBlur::MacosGlassRegular => -1,
|
||||
BackgroundBlur::MacosGlassClear => -2,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
let Some(input) = input else {
|
||||
// Emulate behavior for bools
|
||||
@@ -882,6 +915,10 @@ impl WindowDecoration {
|
||||
impl ConfigField for WindowDecoration {
|
||||
const VALID_VALUES: Option<&'static [&'static str]> = Some(Self::NAMES);
|
||||
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::EnumName(self.as_str())
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
*self = Self::parse(input)?;
|
||||
Ok(())
|
||||
@@ -1127,6 +1164,10 @@ impl fmt::Display for Duration {
|
||||
}
|
||||
|
||||
impl ConfigField for Duration {
|
||||
fn c_value(&self) -> CValue {
|
||||
CValue::Usize(self.as_milliseconds() as usize)
|
||||
}
|
||||
|
||||
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
||||
*self = Duration::parse(input)?;
|
||||
Ok(())
|
||||
@@ -1267,6 +1308,47 @@ mod tests {
|
||||
assert_eq!(list.value.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeatable_command_cval() {
|
||||
let mut list = RepeatableCommand::default();
|
||||
list.parse_cli(Some("title:Foo,action:ignore")).unwrap();
|
||||
list.parse_cli(Some("title:Bar,description:bobr,action:text:ale bydle"))
|
||||
.unwrap();
|
||||
assert_eq!(list.value.len(), 2);
|
||||
|
||||
let CValue::Commands(cv) = list.c_value() else {
|
||||
panic!("expected commands");
|
||||
};
|
||||
assert_eq!(cv.len(), 2);
|
||||
|
||||
// First entry
|
||||
assert_eq!(cv[0].title, "Foo");
|
||||
assert_eq!(cv[0].action_key, "ignore");
|
||||
assert_eq!(cv[0].action, "ignore");
|
||||
|
||||
// Second entry
|
||||
assert_eq!(cv[1].title, "Bar");
|
||||
assert_eq!(cv[1].description, "bobr");
|
||||
assert_eq!(cv[1].action_key, "text");
|
||||
assert_eq!(cv[1].action, "text:ale bydle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeatable_command_cval_cleared() {
|
||||
let mut list = RepeatableCommand::default();
|
||||
list.parse_cli(Some("title:Foo,action:ignore")).unwrap();
|
||||
let CValue::Commands(cv) = list.c_value() else {
|
||||
panic!("expected commands");
|
||||
};
|
||||
assert_eq!(cv.len(), 1);
|
||||
|
||||
list.parse_cli(Some("")).unwrap();
|
||||
let CValue::Commands(cv) = list.c_value() else {
|
||||
panic!("expected commands");
|
||||
};
|
||||
assert_eq!(cv.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeatable_command_format_config_empty() {
|
||||
let list = RepeatableCommand::default();
|
||||
|
||||
@@ -30,3 +30,4 @@ workspace = true
|
||||
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies]
|
||||
oslog = { version = "0.2.0", default-features = false }
|
||||
objc2-core-foundation = "0.3.2"
|
||||
@@ -488,6 +488,27 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a surface and add it to the app (`CoreApp::new_surface` with
|
||||
/// the concrete surface type, for runtimes that need more than the
|
||||
/// [`CoreSurface`] trait, e.g. the embedded C API).
|
||||
pub fn create_surface(
|
||||
&self,
|
||||
id: SurfaceId,
|
||||
config: Config,
|
||||
rt_app: Rc<dyn RtApp>,
|
||||
rt_surface: Rc<dyn RtSurface>,
|
||||
) -> Result<Rc<Surface>, CoreError> {
|
||||
let surface = Surface::new(
|
||||
id,
|
||||
Rc::clone(&self.state),
|
||||
Arc::new(config),
|
||||
Rc::clone(&rt_app),
|
||||
rt_surface,
|
||||
)?;
|
||||
self.add_surface(id, &surface, &rt_app);
|
||||
Ok(surface)
|
||||
}
|
||||
|
||||
/// Add an initialized surface. Since we have non-zero surfaces, the quit
|
||||
/// timer is cancelled.
|
||||
pub(crate) fn add_surface(&self, id: SurfaceId, surface: &Rc<Surface>, rt: &Rc<dyn RtApp>) {
|
||||
@@ -589,15 +610,7 @@ impl CoreApp for App {
|
||||
rt_app: Rc<dyn RtApp>,
|
||||
rt_surface: Rc<dyn RtSurface>,
|
||||
) -> Result<Rc<dyn CoreSurface>, CoreError> {
|
||||
let surface = Surface::new(
|
||||
id,
|
||||
Rc::clone(&self.state),
|
||||
Arc::new(config),
|
||||
Rc::clone(&rt_app),
|
||||
rt_surface,
|
||||
)?;
|
||||
self.add_surface(id, &surface, &rt_app);
|
||||
Ok(surface)
|
||||
Ok(self.create_surface(id, config, rt_app, rt_surface)?)
|
||||
}
|
||||
|
||||
fn delete_surface(&self, id: SurfaceId) {
|
||||
|
||||
@@ -38,6 +38,13 @@ impl Logging {
|
||||
/// invalid value (including an empty part) yields the default.
|
||||
#[must_use]
|
||||
pub fn parse(v: &str) -> Logging {
|
||||
Logging::parse_with(v, Logging::default())
|
||||
}
|
||||
|
||||
/// [`Logging::parse`] with an explicit default (lib mode defaults to no
|
||||
/// stderr logging).
|
||||
#[must_use]
|
||||
pub fn parse_with(v: &str, default: Logging) -> Logging {
|
||||
match v {
|
||||
"1" | "t" | "T" | "true" => {
|
||||
return Logging {
|
||||
@@ -53,7 +60,7 @@ impl Logging {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let mut out = Logging::default();
|
||||
let mut out = default;
|
||||
for part in v.split(',') {
|
||||
let part = part.trim_matches([' ', '\t']);
|
||||
let (name, value) = match part.strip_prefix("no-") {
|
||||
@@ -63,7 +70,7 @@ impl Logging {
|
||||
match name {
|
||||
"stderr" => out.stderr = value,
|
||||
"macos" => out.macos = value,
|
||||
_ => return Logging::default(),
|
||||
_ => return default,
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -134,8 +141,25 @@ pub fn state() -> &'static GlobalState {
|
||||
/// Initialize the global state: logging, rlimits, resources dir and i18n.
|
||||
/// Call once, early (Zig: `GlobalState.init`).
|
||||
pub fn init(opts: InitOptions) -> &'static GlobalState {
|
||||
init_with(opts, Logging::default())
|
||||
}
|
||||
|
||||
/// [`init`] for the embedded library (`ghostty_init`): Zig's
|
||||
/// `GlobalState.Logging.stderr` defaults to `build_config.app_runtime !=
|
||||
/// .none`, i.e. off for the lib artifact.
|
||||
pub fn init_lib(opts: InitOptions) -> &'static GlobalState {
|
||||
init_with(
|
||||
opts,
|
||||
Logging {
|
||||
stderr: false,
|
||||
..Logging::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn init_with(opts: InitOptions, default_logging: Logging) -> &'static GlobalState {
|
||||
STATE.get_or_init(|| {
|
||||
let mut logging = Logging::default();
|
||||
let mut logging = default_logging;
|
||||
|
||||
// If we have an action executing, we disable logging by default
|
||||
// since we write to stderr we don't want logs messing up our
|
||||
@@ -146,7 +170,7 @@ pub fn init(opts: InitOptions) -> &'static GlobalState {
|
||||
|
||||
// Env vars are useful for logging too because they are easy to set.
|
||||
if let Ok(v) = std::env::var("GHOSTTY_LOG") {
|
||||
logging = Logging::parse(&v);
|
||||
logging = Logging::parse_with(&v, default_logging);
|
||||
}
|
||||
|
||||
init_logger(logging);
|
||||
|
||||
@@ -89,14 +89,13 @@ use ghostty_font::{
|
||||
};
|
||||
use ghostty_input::{Mods, MouseButton, MouseButtonState, ScrollMods};
|
||||
use ghostty_renderer::{
|
||||
Api as GraphicsApi,
|
||||
Health,
|
||||
// The generic renderer over the OpenGL API (the GTK runtime's graphics
|
||||
// API). `ghostty_renderer::Renderer` is the platform default, which is
|
||||
// Metal on Apple targets; the embedded (macOS) runtime provides its own
|
||||
// Metal-based surface setup (TODO(integration): L1).
|
||||
generic::{Host as RendererHost, Renderer},
|
||||
// The renderer over the graphics API selected for the target (Zig:
|
||||
// `renderer.zig`): Metal on Apple targets, OpenGL elsewhere.
|
||||
Renderer,
|
||||
generic::Host as RendererHost,
|
||||
message::{FontGridChange, Message as RendererMessage},
|
||||
opengl::OpenGL,
|
||||
options::Options as RendererOptions,
|
||||
size::{CellSize, Coordinate, Padding, ScreenSize, Size},
|
||||
state::{Preedit, PreeditCodepoint, State as RenderStateData},
|
||||
@@ -197,6 +196,31 @@ impl RendererHost for HostImpl {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the graphics API for a new surface (Zig: `Renderer.surfaceInit`).
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
fn init_graphics_api(
|
||||
config: &Config,
|
||||
rt_surface: &dyn RtSurface,
|
||||
) -> Result<GraphicsApi, CoreError> {
|
||||
let mut api = GraphicsApi::init(config.alpha_blending);
|
||||
api.prepare_context(|name| rt_surface.gl_proc_address(name))
|
||||
.map_err(|e| format!("failed to prepare the OpenGL context: {e}"))?;
|
||||
Ok(api)
|
||||
}
|
||||
|
||||
/// Create the graphics API for a new surface (Zig: `Renderer.surfaceInit`):
|
||||
/// the Metal layer is attached to the NSView/UIView the host provided.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
fn init_graphics_api(
|
||||
config: &Config,
|
||||
rt_surface: &dyn RtSurface,
|
||||
) -> Result<GraphicsApi, CoreError> {
|
||||
let view = rt_surface.platform_view();
|
||||
let scale = f64::from(rt_surface.get_content_scale().x);
|
||||
GraphicsApi::init_with_view(config.alpha_blending, view, scale)
|
||||
.map_err(|e| format!("failed to initialize the Metal renderer: {e}").into())
|
||||
}
|
||||
|
||||
/// Font state of the surface.
|
||||
pub struct FontState {
|
||||
/// The key of the font grid we hold a reference on.
|
||||
@@ -333,13 +357,8 @@ impl Surface {
|
||||
|
||||
// Initialize our renderer with our initialized surface (Zig:
|
||||
// `Renderer.surfaceInit`): load the GL functions for the current
|
||||
// context.
|
||||
let mut api = OpenGL::init(config.alpha_blending);
|
||||
{
|
||||
let rt = Rc::clone(&rt_surface);
|
||||
api.prepare_context(|name| rt.gl_proc_address(name))
|
||||
.map_err(|e| format!("failed to prepare the OpenGL context: {e}"))?;
|
||||
}
|
||||
// context (OpenGL) or attach the Metal layer to the host's view.
|
||||
let api = init_graphics_api(&config, rt_surface.as_ref())?;
|
||||
|
||||
// Determine our DPI configurations so we can properly configure
|
||||
// font points to pixels and handle other high-DPI scaling factors.
|
||||
@@ -708,6 +727,34 @@ impl Surface {
|
||||
self.renderer_handle.send(msg);
|
||||
}
|
||||
|
||||
/// Tell the renderer which display the surface is on (the embedded
|
||||
/// `ghostty_surface_set_display_id`).
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub fn set_macos_display_id(&self, id: u32) {
|
||||
self.send_renderer(RendererMessage::MacosDisplayId(id));
|
||||
}
|
||||
|
||||
/// A retained `CTFontRef` for the primary font, for macOS quicklook
|
||||
/// (the embedded `ghostty_surface_quicklook_font`). `scale` is the
|
||||
/// content scale. The caller owns the reference.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[must_use]
|
||||
pub fn quicklook_font(&self, scale: f32) -> Option<*mut std::ffi::c_void> {
|
||||
let grid = self.renderer.font_grid();
|
||||
let mut state = grid.write();
|
||||
let face = state
|
||||
.resolver
|
||||
.collection
|
||||
.get_face(ghostty_font::collection::Index::default())
|
||||
.ok()?;
|
||||
let font = face.quicklook_font(f64::from(scale));
|
||||
Some(
|
||||
objc2_core_foundation::CFRetained::into_raw(font)
|
||||
.as_ptr()
|
||||
.cast(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Current sizes.
|
||||
#[must_use]
|
||||
pub fn size(&self) -> Size {
|
||||
@@ -1810,6 +1857,14 @@ impl Surface {
|
||||
|
||||
/// Write the last presented frame as a PNG. Requires the GL context to be
|
||||
/// current.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub fn write_screenshot_png(&self, _path: &std::path::Path) -> Result<(), CoreError> {
|
||||
Err("screenshots are not supported by the Metal renderer".into())
|
||||
}
|
||||
|
||||
/// Write the last presented frame as a PNG. Requires the GL context to be
|
||||
/// current.
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
pub fn write_screenshot_png(&self, path: &std::path::Path) -> Result<(), CoreError> {
|
||||
let Some((w, h, px)) = self.renderer.last_frame_pixels() else {
|
||||
return Err("no frame has been presented yet".into());
|
||||
@@ -1832,11 +1887,13 @@ impl Surface {
|
||||
/// The pixels of the last presented frame (`width`, `height`, RGBA8,
|
||||
/// bottom-up). Test/debug hook for headless integration tests; requires
|
||||
/// the GL context to be current on the calling thread.
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
pub fn frame_pixels(&self) -> Option<(usize, usize, Vec<u8>)> {
|
||||
self.renderer.last_frame_pixels()
|
||||
}
|
||||
|
||||
/// The GLArea was (re)realized; the GL context is current.
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
pub fn renderer_realized(&self) -> Result<(), CoreError> {
|
||||
let rt = Rc::clone(&self.rt_surface);
|
||||
self.renderer
|
||||
@@ -1844,6 +1901,13 @@ impl Surface {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The display was (re)realized (Metal has nothing to reload).
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub fn renderer_realized(&self) -> Result<(), CoreError> {
|
||||
self.renderer.display_realized_with(|_| {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The GLArea is being unrealized; the GL context is current.
|
||||
pub fn renderer_unrealized(&self) {
|
||||
self.renderer.display_unrealized();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
//! Keybinding actions end to end (default and configured bindings).
|
||||
|
||||
// These drive the real surface over a headless OpenGL context; the Apple targets render
|
||||
// with Metal.
|
||||
#![cfg(not(target_vendor = "apple"))]
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
//! IME: preedit state and rendering, commit, composing key events.
|
||||
|
||||
// These drive the real surface over a headless OpenGL context; the Apple targets render
|
||||
// with Metal.
|
||||
#![cfg(not(target_vendor = "apple"))]
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
//! recording (with the pty encoding and triggered bindings) and VT event
|
||||
//! recording while data is read from the pty.
|
||||
|
||||
// These drive the real surface over a headless OpenGL context; the Apple targets render
|
||||
// with Metal.
|
||||
#![cfg(not(target_vendor = "apple"))]
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::assert_is_empty)]
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Keyboard input end to end: key events -> core Surface -> encoder -> pty ->
|
||||
//! a real `cat` child that records the exact bytes it received.
|
||||
|
||||
// These drive the real surface over a headless OpenGL context; the Apple targets render
|
||||
// with Metal.
|
||||
#![cfg(not(target_vendor = "apple"))]
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Mouse end to end: selection, copy-on-select, multi-click, mouse reporting
|
||||
//! formats, alternate scroll, shift bypass and links.
|
||||
|
||||
// These drive the real surface over a headless OpenGL context; the Apple targets render
|
||||
// with Metal.
|
||||
#![cfg(not(target_vendor = "apple"))]
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Terminal <-> runtime events: resize, focus, title, pwd, bell, OSC 52,
|
||||
//! notifications, progress, child exit.
|
||||
|
||||
// These drive the real surface over a headless OpenGL context; the Apple targets render
|
||||
// with Metal.
|
||||
#![cfg(not(target_vendor = "apple"))]
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -697,6 +697,20 @@ impl Face {
|
||||
Ok(util::font_table_bytes(&self.font, tag))
|
||||
}
|
||||
|
||||
/// A copy of this face's font at `size / scale` points: the font to use
|
||||
/// for macOS quicklook (`ghostty_surface_quicklook_font`). The content
|
||||
/// scale is applied to our font stack because we render at 1x, but the
|
||||
/// callers of the quicklook font apply the scale themselves.
|
||||
pub fn quicklook_font(&self, scale: f64) -> CFRetained<CTFont> {
|
||||
// SAFETY: plain CoreText getter; null matrix and no descriptor are
|
||||
// allowed for the copy.
|
||||
unsafe {
|
||||
let size = self.font.size();
|
||||
self.font
|
||||
.copy_with_attributes(size / scale, ptr::null(), None)
|
||||
}
|
||||
}
|
||||
|
||||
/// The underlying CTFont (for shapers).
|
||||
pub(crate) fn ct_font(&self) -> &CTFont {
|
||||
&self.font
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
[package]
|
||||
name = "ghostty-lib"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "libghostty: the embedded C API (include/ghostty.h) used by the macOS/iOS app, ported from apprt/embedded.zig and main_c.zig"
|
||||
|
||||
# Produces `libghostty.{so,a,dylib}`, the names the upstream build ships.
|
||||
[lib]
|
||||
name = "ghostty"
|
||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
|
||||
[features]
|
||||
# Test seam (off by default): accepts a fake platform for `ghostty_surface_new`
|
||||
# on non-Apple targets whose OpenGL functions are supplied by the test (via a
|
||||
# headless EGL context), so the embedded surface flow can be tested on Linux.
|
||||
# Zig rejects every platform off Darwin; production builds are unaffected.
|
||||
test-platform = []
|
||||
|
||||
[dependencies]
|
||||
ghostty-apprt.workspace = true
|
||||
ghostty-bench.workspace = true
|
||||
ghostty-cli.workspace = true
|
||||
ghostty-config.workspace = true
|
||||
ghostty-core.workspace = true
|
||||
ghostty-input.workspace = true
|
||||
ghostty-inspector.workspace = true
|
||||
ghostty-os.workspace = true
|
||||
ghostty-renderer.workspace = true
|
||||
ghostty-terminal.workspace = true
|
||||
ghostty-vt.workspace = true
|
||||
log = "0.4.34"
|
||||
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies]
|
||||
block2 = "0.6.2"
|
||||
dispatch2 = "0.3.1"
|
||||
objc2 = "0.6.5"
|
||||
objc2-core-foundation = "0.3.2"
|
||||
objc2-foundation = "0.3.2"
|
||||
objc2-metal = { version = "0.3.2", features = ["block2"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
# The workspace forbids `unsafe`; this crate is the C ABI boundary, so it
|
||||
# denies it instead and allows it (with SAFETY comments) in the FFI modules.
|
||||
# Everything else is copied from `[workspace.lints]`.
|
||||
[lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_lossless = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
too_many_lines = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
similar_names = "allow"
|
||||
struct_excessive_bools = "allow"
|
||||
fn_params_excessive_bools = "allow"
|
||||
match_same_arms = "allow"
|
||||
unreadable_literal = "allow"
|
||||
many_single_char_names = "allow"
|
||||
doc_markdown = "allow"
|
||||
must_use_candidate = "allow"
|
||||
return_self_not_must_use = "allow"
|
||||
used_underscore_binding = "allow"
|
||||
large_stack_arrays = "allow"
|
||||
inline_always = "allow"
|
||||
# The exported functions are `unsafe extern "C"` mirroring the header contracts.
|
||||
missing_safety_doc = "allow"
|
||||
# `&mut x` coerced to `*mut T` is the idiomatic way to pass out-params in FFI tests.
|
||||
borrow_as_ptr = "allow"
|
||||
# Allocations are made with the layout of the pointee, so the cast is aligned.
|
||||
cast_ptr_alignment = "allow"
|
||||
|
||||
[[test]]
|
||||
name = "surface"
|
||||
required-features = ["test-platform"]
|
||||
@@ -0,0 +1,1178 @@
|
||||
// Ghostty embedding API. The documentation for the embedding API is
|
||||
// only within the Zig source files that define the implementations. This
|
||||
// isn't meant to be a general purpose embedding API (yet) so there hasn't
|
||||
// been documentation or example work beyond that.
|
||||
//
|
||||
// The only consumer of this API is the macOS app, but the API is built to
|
||||
// be more general purpose.
|
||||
#ifndef GHOSTTY_H
|
||||
#define GHOSTTY_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Macros
|
||||
|
||||
#define GHOSTTY_SUCCESS 0
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Types
|
||||
|
||||
// Opaque types
|
||||
typedef void* ghostty_app_t;
|
||||
typedef void* ghostty_config_t;
|
||||
typedef void* ghostty_surface_t;
|
||||
typedef void* ghostty_inspector_t;
|
||||
|
||||
// All the types below are fully defined and must be kept in sync with
|
||||
// their Zig counterparts. Any changes to these types MUST have an associated
|
||||
// Zig change.
|
||||
typedef enum {
|
||||
GHOSTTY_PLATFORM_INVALID,
|
||||
GHOSTTY_PLATFORM_MACOS,
|
||||
GHOSTTY_PLATFORM_IOS,
|
||||
} ghostty_platform_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_CLIPBOARD_STANDARD,
|
||||
GHOSTTY_CLIPBOARD_SELECTION,
|
||||
} ghostty_clipboard_e;
|
||||
|
||||
typedef struct {
|
||||
const char *mime;
|
||||
const char *data;
|
||||
} ghostty_clipboard_content_s;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_CLIPBOARD_REQUEST_PASTE,
|
||||
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ,
|
||||
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE,
|
||||
} ghostty_clipboard_request_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_RELEASE,
|
||||
GHOSTTY_MOUSE_PRESS,
|
||||
} ghostty_input_mouse_state_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_UNKNOWN,
|
||||
GHOSTTY_MOUSE_LEFT,
|
||||
GHOSTTY_MOUSE_RIGHT,
|
||||
GHOSTTY_MOUSE_MIDDLE,
|
||||
GHOSTTY_MOUSE_FOUR,
|
||||
GHOSTTY_MOUSE_FIVE,
|
||||
GHOSTTY_MOUSE_SIX,
|
||||
GHOSTTY_MOUSE_SEVEN,
|
||||
GHOSTTY_MOUSE_EIGHT,
|
||||
GHOSTTY_MOUSE_NINE,
|
||||
GHOSTTY_MOUSE_TEN,
|
||||
GHOSTTY_MOUSE_ELEVEN,
|
||||
} ghostty_input_mouse_button_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_MOMENTUM_NONE,
|
||||
GHOSTTY_MOUSE_MOMENTUM_BEGAN,
|
||||
GHOSTTY_MOUSE_MOMENTUM_STATIONARY,
|
||||
GHOSTTY_MOUSE_MOMENTUM_CHANGED,
|
||||
GHOSTTY_MOUSE_MOMENTUM_ENDED,
|
||||
GHOSTTY_MOUSE_MOMENTUM_CANCELLED,
|
||||
GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN,
|
||||
} ghostty_input_mouse_momentum_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_COLOR_SCHEME_LIGHT = 0,
|
||||
GHOSTTY_COLOR_SCHEME_DARK = 1,
|
||||
} ghostty_color_scheme_e;
|
||||
|
||||
// This is a packed struct (see src/input/mouse.zig) but the C standard
|
||||
// afaik doesn't let us reliably define packed structs so we build it up
|
||||
// from scratch.
|
||||
typedef int ghostty_input_scroll_mods_t;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_MODS_NONE = 0,
|
||||
GHOSTTY_MODS_SHIFT = 1 << 0,
|
||||
GHOSTTY_MODS_CTRL = 1 << 1,
|
||||
GHOSTTY_MODS_ALT = 1 << 2,
|
||||
GHOSTTY_MODS_SUPER = 1 << 3,
|
||||
GHOSTTY_MODS_CAPS = 1 << 4,
|
||||
GHOSTTY_MODS_NUM = 1 << 5,
|
||||
GHOSTTY_MODS_SHIFT_RIGHT = 1 << 6,
|
||||
GHOSTTY_MODS_CTRL_RIGHT = 1 << 7,
|
||||
GHOSTTY_MODS_ALT_RIGHT = 1 << 8,
|
||||
GHOSTTY_MODS_SUPER_RIGHT = 1 << 9,
|
||||
} ghostty_input_mods_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_BINDING_FLAGS_CONSUMED = 1 << 0,
|
||||
GHOSTTY_BINDING_FLAGS_ALL = 1 << 1,
|
||||
GHOSTTY_BINDING_FLAGS_GLOBAL = 1 << 2,
|
||||
GHOSTTY_BINDING_FLAGS_PERFORMABLE = 1 << 3,
|
||||
} ghostty_binding_flags_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_RELEASE,
|
||||
GHOSTTY_ACTION_PRESS,
|
||||
GHOSTTY_ACTION_REPEAT,
|
||||
} ghostty_input_action_e;
|
||||
|
||||
// Based on: https://www.w3.org/TR/uievents-code/
|
||||
typedef enum {
|
||||
GHOSTTY_KEY_UNIDENTIFIED,
|
||||
|
||||
// "Writing System Keys" § 3.1.1
|
||||
GHOSTTY_KEY_BACKQUOTE,
|
||||
GHOSTTY_KEY_BACKSLASH,
|
||||
GHOSTTY_KEY_BRACKET_LEFT,
|
||||
GHOSTTY_KEY_BRACKET_RIGHT,
|
||||
GHOSTTY_KEY_COMMA,
|
||||
GHOSTTY_KEY_DIGIT_0,
|
||||
GHOSTTY_KEY_DIGIT_1,
|
||||
GHOSTTY_KEY_DIGIT_2,
|
||||
GHOSTTY_KEY_DIGIT_3,
|
||||
GHOSTTY_KEY_DIGIT_4,
|
||||
GHOSTTY_KEY_DIGIT_5,
|
||||
GHOSTTY_KEY_DIGIT_6,
|
||||
GHOSTTY_KEY_DIGIT_7,
|
||||
GHOSTTY_KEY_DIGIT_8,
|
||||
GHOSTTY_KEY_DIGIT_9,
|
||||
GHOSTTY_KEY_EQUAL,
|
||||
GHOSTTY_KEY_INTL_BACKSLASH,
|
||||
GHOSTTY_KEY_INTL_RO,
|
||||
GHOSTTY_KEY_INTL_YEN,
|
||||
GHOSTTY_KEY_A,
|
||||
GHOSTTY_KEY_B,
|
||||
GHOSTTY_KEY_C,
|
||||
GHOSTTY_KEY_D,
|
||||
GHOSTTY_KEY_E,
|
||||
GHOSTTY_KEY_F,
|
||||
GHOSTTY_KEY_G,
|
||||
GHOSTTY_KEY_H,
|
||||
GHOSTTY_KEY_I,
|
||||
GHOSTTY_KEY_J,
|
||||
GHOSTTY_KEY_K,
|
||||
GHOSTTY_KEY_L,
|
||||
GHOSTTY_KEY_M,
|
||||
GHOSTTY_KEY_N,
|
||||
GHOSTTY_KEY_O,
|
||||
GHOSTTY_KEY_P,
|
||||
GHOSTTY_KEY_Q,
|
||||
GHOSTTY_KEY_R,
|
||||
GHOSTTY_KEY_S,
|
||||
GHOSTTY_KEY_T,
|
||||
GHOSTTY_KEY_U,
|
||||
GHOSTTY_KEY_V,
|
||||
GHOSTTY_KEY_W,
|
||||
GHOSTTY_KEY_X,
|
||||
GHOSTTY_KEY_Y,
|
||||
GHOSTTY_KEY_Z,
|
||||
GHOSTTY_KEY_MINUS,
|
||||
GHOSTTY_KEY_PERIOD,
|
||||
GHOSTTY_KEY_QUOTE,
|
||||
GHOSTTY_KEY_SEMICOLON,
|
||||
GHOSTTY_KEY_SLASH,
|
||||
|
||||
// "Functional Keys" § 3.1.2
|
||||
GHOSTTY_KEY_ALT_LEFT,
|
||||
GHOSTTY_KEY_ALT_RIGHT,
|
||||
GHOSTTY_KEY_BACKSPACE,
|
||||
GHOSTTY_KEY_CAPS_LOCK,
|
||||
GHOSTTY_KEY_CONTEXT_MENU,
|
||||
GHOSTTY_KEY_CONTROL_LEFT,
|
||||
GHOSTTY_KEY_CONTROL_RIGHT,
|
||||
GHOSTTY_KEY_ENTER,
|
||||
GHOSTTY_KEY_META_LEFT,
|
||||
GHOSTTY_KEY_META_RIGHT,
|
||||
GHOSTTY_KEY_SHIFT_LEFT,
|
||||
GHOSTTY_KEY_SHIFT_RIGHT,
|
||||
GHOSTTY_KEY_SPACE,
|
||||
GHOSTTY_KEY_TAB,
|
||||
GHOSTTY_KEY_CONVERT,
|
||||
GHOSTTY_KEY_KANA_MODE,
|
||||
GHOSTTY_KEY_NON_CONVERT,
|
||||
|
||||
// "Control Pad Section" § 3.2
|
||||
GHOSTTY_KEY_DELETE,
|
||||
GHOSTTY_KEY_END,
|
||||
GHOSTTY_KEY_HELP,
|
||||
GHOSTTY_KEY_HOME,
|
||||
GHOSTTY_KEY_INSERT,
|
||||
GHOSTTY_KEY_PAGE_DOWN,
|
||||
GHOSTTY_KEY_PAGE_UP,
|
||||
|
||||
// "Arrow Pad Section" § 3.3
|
||||
GHOSTTY_KEY_ARROW_DOWN,
|
||||
GHOSTTY_KEY_ARROW_LEFT,
|
||||
GHOSTTY_KEY_ARROW_RIGHT,
|
||||
GHOSTTY_KEY_ARROW_UP,
|
||||
|
||||
// "Numpad Section" § 3.4
|
||||
GHOSTTY_KEY_NUM_LOCK,
|
||||
GHOSTTY_KEY_NUMPAD_0,
|
||||
GHOSTTY_KEY_NUMPAD_1,
|
||||
GHOSTTY_KEY_NUMPAD_2,
|
||||
GHOSTTY_KEY_NUMPAD_3,
|
||||
GHOSTTY_KEY_NUMPAD_4,
|
||||
GHOSTTY_KEY_NUMPAD_5,
|
||||
GHOSTTY_KEY_NUMPAD_6,
|
||||
GHOSTTY_KEY_NUMPAD_7,
|
||||
GHOSTTY_KEY_NUMPAD_8,
|
||||
GHOSTTY_KEY_NUMPAD_9,
|
||||
GHOSTTY_KEY_NUMPAD_ADD,
|
||||
GHOSTTY_KEY_NUMPAD_BACKSPACE,
|
||||
GHOSTTY_KEY_NUMPAD_CLEAR,
|
||||
GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY,
|
||||
GHOSTTY_KEY_NUMPAD_COMMA,
|
||||
GHOSTTY_KEY_NUMPAD_DECIMAL,
|
||||
GHOSTTY_KEY_NUMPAD_DIVIDE,
|
||||
GHOSTTY_KEY_NUMPAD_ENTER,
|
||||
GHOSTTY_KEY_NUMPAD_EQUAL,
|
||||
GHOSTTY_KEY_NUMPAD_MEMORY_ADD,
|
||||
GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR,
|
||||
GHOSTTY_KEY_NUMPAD_MEMORY_RECALL,
|
||||
GHOSTTY_KEY_NUMPAD_MEMORY_STORE,
|
||||
GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT,
|
||||
GHOSTTY_KEY_NUMPAD_MULTIPLY,
|
||||
GHOSTTY_KEY_NUMPAD_PAREN_LEFT,
|
||||
GHOSTTY_KEY_NUMPAD_PAREN_RIGHT,
|
||||
GHOSTTY_KEY_NUMPAD_SUBTRACT,
|
||||
GHOSTTY_KEY_NUMPAD_SEPARATOR,
|
||||
GHOSTTY_KEY_NUMPAD_UP,
|
||||
GHOSTTY_KEY_NUMPAD_DOWN,
|
||||
GHOSTTY_KEY_NUMPAD_RIGHT,
|
||||
GHOSTTY_KEY_NUMPAD_LEFT,
|
||||
GHOSTTY_KEY_NUMPAD_BEGIN,
|
||||
GHOSTTY_KEY_NUMPAD_HOME,
|
||||
GHOSTTY_KEY_NUMPAD_END,
|
||||
GHOSTTY_KEY_NUMPAD_INSERT,
|
||||
GHOSTTY_KEY_NUMPAD_DELETE,
|
||||
GHOSTTY_KEY_NUMPAD_PAGE_UP,
|
||||
GHOSTTY_KEY_NUMPAD_PAGE_DOWN,
|
||||
|
||||
// "Function Section" § 3.5
|
||||
GHOSTTY_KEY_ESCAPE,
|
||||
GHOSTTY_KEY_F1,
|
||||
GHOSTTY_KEY_F2,
|
||||
GHOSTTY_KEY_F3,
|
||||
GHOSTTY_KEY_F4,
|
||||
GHOSTTY_KEY_F5,
|
||||
GHOSTTY_KEY_F6,
|
||||
GHOSTTY_KEY_F7,
|
||||
GHOSTTY_KEY_F8,
|
||||
GHOSTTY_KEY_F9,
|
||||
GHOSTTY_KEY_F10,
|
||||
GHOSTTY_KEY_F11,
|
||||
GHOSTTY_KEY_F12,
|
||||
GHOSTTY_KEY_F13,
|
||||
GHOSTTY_KEY_F14,
|
||||
GHOSTTY_KEY_F15,
|
||||
GHOSTTY_KEY_F16,
|
||||
GHOSTTY_KEY_F17,
|
||||
GHOSTTY_KEY_F18,
|
||||
GHOSTTY_KEY_F19,
|
||||
GHOSTTY_KEY_F20,
|
||||
GHOSTTY_KEY_F21,
|
||||
GHOSTTY_KEY_F22,
|
||||
GHOSTTY_KEY_F23,
|
||||
GHOSTTY_KEY_F24,
|
||||
GHOSTTY_KEY_F25,
|
||||
GHOSTTY_KEY_FN,
|
||||
GHOSTTY_KEY_FN_LOCK,
|
||||
GHOSTTY_KEY_PRINT_SCREEN,
|
||||
GHOSTTY_KEY_SCROLL_LOCK,
|
||||
GHOSTTY_KEY_PAUSE,
|
||||
|
||||
// "Media Keys" § 3.6
|
||||
GHOSTTY_KEY_BROWSER_BACK,
|
||||
GHOSTTY_KEY_BROWSER_FAVORITES,
|
||||
GHOSTTY_KEY_BROWSER_FORWARD,
|
||||
GHOSTTY_KEY_BROWSER_HOME,
|
||||
GHOSTTY_KEY_BROWSER_REFRESH,
|
||||
GHOSTTY_KEY_BROWSER_SEARCH,
|
||||
GHOSTTY_KEY_BROWSER_STOP,
|
||||
GHOSTTY_KEY_EJECT,
|
||||
GHOSTTY_KEY_LAUNCH_APP_1,
|
||||
GHOSTTY_KEY_LAUNCH_APP_2,
|
||||
GHOSTTY_KEY_LAUNCH_MAIL,
|
||||
GHOSTTY_KEY_MEDIA_PLAY_PAUSE,
|
||||
GHOSTTY_KEY_MEDIA_SELECT,
|
||||
GHOSTTY_KEY_MEDIA_STOP,
|
||||
GHOSTTY_KEY_MEDIA_TRACK_NEXT,
|
||||
GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS,
|
||||
GHOSTTY_KEY_POWER,
|
||||
GHOSTTY_KEY_SLEEP,
|
||||
GHOSTTY_KEY_AUDIO_VOLUME_DOWN,
|
||||
GHOSTTY_KEY_AUDIO_VOLUME_MUTE,
|
||||
GHOSTTY_KEY_AUDIO_VOLUME_UP,
|
||||
GHOSTTY_KEY_WAKE_UP,
|
||||
|
||||
// "Legacy, Non-standard, and Special Keys" § 3.7
|
||||
GHOSTTY_KEY_COPY,
|
||||
GHOSTTY_KEY_CUT,
|
||||
GHOSTTY_KEY_PASTE,
|
||||
} ghostty_input_key_e;
|
||||
|
||||
typedef struct {
|
||||
ghostty_input_action_e action;
|
||||
ghostty_input_mods_e mods;
|
||||
ghostty_input_mods_e consumed_mods;
|
||||
uint32_t keycode;
|
||||
const char* text;
|
||||
uint32_t unshifted_codepoint;
|
||||
bool composing;
|
||||
} ghostty_input_key_s;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_TRIGGER_PHYSICAL,
|
||||
GHOSTTY_TRIGGER_UNICODE,
|
||||
GHOSTTY_TRIGGER_CATCH_ALL,
|
||||
} ghostty_input_trigger_tag_e;
|
||||
|
||||
typedef union {
|
||||
ghostty_input_key_e translated;
|
||||
ghostty_input_key_e physical;
|
||||
uint32_t unicode;
|
||||
// catch_all has no payload
|
||||
} ghostty_input_trigger_key_u;
|
||||
|
||||
typedef struct {
|
||||
ghostty_input_trigger_tag_e tag;
|
||||
ghostty_input_trigger_key_u key;
|
||||
ghostty_input_mods_e mods;
|
||||
} ghostty_input_trigger_s;
|
||||
|
||||
typedef struct {
|
||||
const char* action_key;
|
||||
const char* action;
|
||||
const char* title;
|
||||
const char* description;
|
||||
} ghostty_command_s;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_BUILD_MODE_DEBUG,
|
||||
GHOSTTY_BUILD_MODE_RELEASE_SAFE,
|
||||
GHOSTTY_BUILD_MODE_RELEASE_FAST,
|
||||
GHOSTTY_BUILD_MODE_RELEASE_SMALL,
|
||||
} ghostty_build_mode_e;
|
||||
|
||||
typedef struct {
|
||||
ghostty_build_mode_e build_mode;
|
||||
const char* version;
|
||||
uintptr_t version_len;
|
||||
} ghostty_info_s;
|
||||
|
||||
typedef struct {
|
||||
const char* message;
|
||||
} ghostty_diagnostic_s;
|
||||
|
||||
typedef struct {
|
||||
const char* ptr;
|
||||
uintptr_t len;
|
||||
bool sentinel;
|
||||
} ghostty_string_s;
|
||||
|
||||
typedef struct {
|
||||
double tl_px_x;
|
||||
double tl_px_y;
|
||||
uint32_t offset_start;
|
||||
uint32_t offset_len;
|
||||
const char* text;
|
||||
uintptr_t text_len;
|
||||
} ghostty_text_s;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_POINT_ACTIVE,
|
||||
GHOSTTY_POINT_VIEWPORT,
|
||||
GHOSTTY_POINT_SCREEN,
|
||||
GHOSTTY_POINT_SURFACE,
|
||||
} ghostty_point_tag_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_POINT_COORD_EXACT,
|
||||
GHOSTTY_POINT_COORD_TOP_LEFT,
|
||||
GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
|
||||
} ghostty_point_coord_e;
|
||||
|
||||
typedef struct {
|
||||
ghostty_point_tag_e tag;
|
||||
ghostty_point_coord_e coord;
|
||||
uint32_t x;
|
||||
uint32_t y;
|
||||
} ghostty_point_s;
|
||||
|
||||
typedef struct {
|
||||
ghostty_point_s top_left;
|
||||
ghostty_point_s bottom_right;
|
||||
bool rectangle;
|
||||
} ghostty_selection_s;
|
||||
|
||||
typedef struct {
|
||||
const char* key;
|
||||
const char* value;
|
||||
} ghostty_env_var_s;
|
||||
|
||||
typedef struct {
|
||||
void* nsview;
|
||||
} ghostty_platform_macos_s;
|
||||
|
||||
typedef struct {
|
||||
void* uiview;
|
||||
} ghostty_platform_ios_s;
|
||||
|
||||
typedef union {
|
||||
ghostty_platform_macos_s macos;
|
||||
ghostty_platform_ios_s ios;
|
||||
} ghostty_platform_u;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_SURFACE_CONTEXT_WINDOW = 0,
|
||||
GHOSTTY_SURFACE_CONTEXT_TAB = 1,
|
||||
GHOSTTY_SURFACE_CONTEXT_SPLIT = 2,
|
||||
} ghostty_surface_context_e;
|
||||
|
||||
typedef struct {
|
||||
ghostty_platform_e platform_tag;
|
||||
ghostty_platform_u platform;
|
||||
void* userdata;
|
||||
double scale_factor;
|
||||
float font_size;
|
||||
const char* working_directory;
|
||||
const char* command;
|
||||
ghostty_env_var_s* env_vars;
|
||||
size_t env_var_count;
|
||||
const char* initial_input;
|
||||
bool wait_after_command;
|
||||
ghostty_surface_context_e context;
|
||||
} ghostty_surface_config_s;
|
||||
|
||||
typedef struct {
|
||||
uint16_t columns;
|
||||
uint16_t rows;
|
||||
uint32_t width_px;
|
||||
uint32_t height_px;
|
||||
uint32_t cell_width_px;
|
||||
uint32_t cell_height_px;
|
||||
} ghostty_surface_size_s;
|
||||
|
||||
// Config types
|
||||
|
||||
// config.Path
|
||||
typedef struct {
|
||||
const char* path;
|
||||
bool optional;
|
||||
} ghostty_config_path_s;
|
||||
|
||||
// config.Color
|
||||
typedef struct {
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
} ghostty_config_color_s;
|
||||
|
||||
// config.ColorList
|
||||
typedef struct {
|
||||
const ghostty_config_color_s* colors;
|
||||
size_t len;
|
||||
} ghostty_config_color_list_s;
|
||||
|
||||
// config.RepeatableCommand
|
||||
typedef struct {
|
||||
const ghostty_command_s* commands;
|
||||
size_t len;
|
||||
} ghostty_config_command_list_s;
|
||||
|
||||
// config.Palette
|
||||
typedef struct {
|
||||
ghostty_config_color_s colors[256];
|
||||
} ghostty_config_palette_s;
|
||||
|
||||
// config.QuickTerminalSize
|
||||
typedef enum {
|
||||
GHOSTTY_QUICK_TERMINAL_SIZE_NONE,
|
||||
GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE,
|
||||
GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS,
|
||||
} ghostty_quick_terminal_size_tag_e;
|
||||
|
||||
typedef union {
|
||||
float percentage;
|
||||
uint32_t pixels;
|
||||
} ghostty_quick_terminal_size_value_u;
|
||||
|
||||
typedef struct {
|
||||
ghostty_quick_terminal_size_tag_e tag;
|
||||
ghostty_quick_terminal_size_value_u value;
|
||||
} ghostty_quick_terminal_size_s;
|
||||
|
||||
typedef struct {
|
||||
ghostty_quick_terminal_size_s primary;
|
||||
ghostty_quick_terminal_size_s secondary;
|
||||
} ghostty_config_quick_terminal_size_s;
|
||||
|
||||
// config.Fullscreen
|
||||
typedef enum {
|
||||
GHOSTTY_CONFIG_FULLSCREEN_FALSE,
|
||||
GHOSTTY_CONFIG_FULLSCREEN_TRUE,
|
||||
GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE,
|
||||
GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_VISIBLE_MENU,
|
||||
GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_PADDED_NOTCH,
|
||||
} ghostty_config_fullscreen_e;
|
||||
|
||||
// apprt.Target.Key
|
||||
typedef enum {
|
||||
GHOSTTY_TARGET_APP,
|
||||
GHOSTTY_TARGET_SURFACE,
|
||||
} ghostty_target_tag_e;
|
||||
|
||||
typedef union {
|
||||
ghostty_surface_t surface;
|
||||
} ghostty_target_u;
|
||||
|
||||
typedef struct {
|
||||
ghostty_target_tag_e tag;
|
||||
ghostty_target_u target;
|
||||
} ghostty_target_s;
|
||||
|
||||
// apprt.action.SplitDirection
|
||||
typedef enum {
|
||||
GHOSTTY_SPLIT_DIRECTION_RIGHT,
|
||||
GHOSTTY_SPLIT_DIRECTION_DOWN,
|
||||
GHOSTTY_SPLIT_DIRECTION_LEFT,
|
||||
GHOSTTY_SPLIT_DIRECTION_UP,
|
||||
} ghostty_action_split_direction_e;
|
||||
|
||||
// apprt.action.GotoSplit
|
||||
typedef enum {
|
||||
GHOSTTY_GOTO_SPLIT_PREVIOUS,
|
||||
GHOSTTY_GOTO_SPLIT_NEXT,
|
||||
GHOSTTY_GOTO_SPLIT_UP,
|
||||
GHOSTTY_GOTO_SPLIT_LEFT,
|
||||
GHOSTTY_GOTO_SPLIT_DOWN,
|
||||
GHOSTTY_GOTO_SPLIT_RIGHT,
|
||||
} ghostty_action_goto_split_e;
|
||||
|
||||
// apprt.action.GotoWindow
|
||||
typedef enum {
|
||||
GHOSTTY_GOTO_WINDOW_PREVIOUS,
|
||||
GHOSTTY_GOTO_WINDOW_NEXT,
|
||||
} ghostty_action_goto_window_e;
|
||||
|
||||
// apprt.action.ResizeSplit.Direction
|
||||
typedef enum {
|
||||
GHOSTTY_RESIZE_SPLIT_UP,
|
||||
GHOSTTY_RESIZE_SPLIT_DOWN,
|
||||
GHOSTTY_RESIZE_SPLIT_LEFT,
|
||||
GHOSTTY_RESIZE_SPLIT_RIGHT,
|
||||
} ghostty_action_resize_split_direction_e;
|
||||
|
||||
// apprt.action.ResizeSplit
|
||||
typedef struct {
|
||||
uint16_t amount;
|
||||
ghostty_action_resize_split_direction_e direction;
|
||||
} ghostty_action_resize_split_s;
|
||||
|
||||
// apprt.action.MoveTab
|
||||
typedef struct {
|
||||
ssize_t amount;
|
||||
} ghostty_action_move_tab_s;
|
||||
|
||||
// apprt.action.GotoTab
|
||||
typedef enum {
|
||||
GHOSTTY_GOTO_TAB_PREVIOUS = -1,
|
||||
GHOSTTY_GOTO_TAB_NEXT = -2,
|
||||
GHOSTTY_GOTO_TAB_LAST = -3,
|
||||
} ghostty_action_goto_tab_e;
|
||||
|
||||
// apprt.action.Fullscreen
|
||||
typedef enum {
|
||||
GHOSTTY_FULLSCREEN_NATIVE,
|
||||
GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE,
|
||||
GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_VISIBLE_MENU,
|
||||
GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_PADDED_NOTCH,
|
||||
} ghostty_action_fullscreen_e;
|
||||
|
||||
// apprt.action.FloatWindow
|
||||
typedef enum {
|
||||
GHOSTTY_FLOAT_WINDOW_ON,
|
||||
GHOSTTY_FLOAT_WINDOW_OFF,
|
||||
GHOSTTY_FLOAT_WINDOW_TOGGLE,
|
||||
} ghostty_action_float_window_e;
|
||||
|
||||
// apprt.action.SecureInput
|
||||
typedef enum {
|
||||
GHOSTTY_SECURE_INPUT_ON,
|
||||
GHOSTTY_SECURE_INPUT_OFF,
|
||||
GHOSTTY_SECURE_INPUT_TOGGLE,
|
||||
} ghostty_action_secure_input_e;
|
||||
|
||||
// apprt.action.Inspector
|
||||
typedef enum {
|
||||
GHOSTTY_INSPECTOR_TOGGLE,
|
||||
GHOSTTY_INSPECTOR_SHOW,
|
||||
GHOSTTY_INSPECTOR_HIDE,
|
||||
} ghostty_action_inspector_e;
|
||||
|
||||
// apprt.action.QuitTimer
|
||||
typedef enum {
|
||||
GHOSTTY_QUIT_TIMER_START,
|
||||
GHOSTTY_QUIT_TIMER_STOP,
|
||||
} ghostty_action_quit_timer_e;
|
||||
|
||||
// apprt.action.Readonly
|
||||
typedef enum {
|
||||
GHOSTTY_READONLY_OFF,
|
||||
GHOSTTY_READONLY_ON,
|
||||
} ghostty_action_readonly_e;
|
||||
|
||||
// apprt.action.DesktopNotification.C
|
||||
typedef struct {
|
||||
const char* title;
|
||||
const char* body;
|
||||
} ghostty_action_desktop_notification_s;
|
||||
|
||||
// apprt.action.SetTitle.C
|
||||
typedef struct {
|
||||
const char* title;
|
||||
} ghostty_action_set_title_s;
|
||||
|
||||
// apprt.action.PromptTitle
|
||||
typedef enum {
|
||||
GHOSTTY_PROMPT_TITLE_SURFACE,
|
||||
GHOSTTY_PROMPT_TITLE_TAB,
|
||||
} ghostty_action_prompt_title_e;
|
||||
|
||||
// apprt.action.Pwd.C
|
||||
typedef struct {
|
||||
const char* pwd;
|
||||
} ghostty_action_pwd_s;
|
||||
|
||||
// terminal.MouseShape
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_SHAPE_DEFAULT,
|
||||
GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU,
|
||||
GHOSTTY_MOUSE_SHAPE_HELP,
|
||||
GHOSTTY_MOUSE_SHAPE_POINTER,
|
||||
GHOSTTY_MOUSE_SHAPE_PROGRESS,
|
||||
GHOSTTY_MOUSE_SHAPE_WAIT,
|
||||
GHOSTTY_MOUSE_SHAPE_CELL,
|
||||
GHOSTTY_MOUSE_SHAPE_CROSSHAIR,
|
||||
GHOSTTY_MOUSE_SHAPE_TEXT,
|
||||
GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT,
|
||||
GHOSTTY_MOUSE_SHAPE_ALIAS,
|
||||
GHOSTTY_MOUSE_SHAPE_COPY,
|
||||
GHOSTTY_MOUSE_SHAPE_MOVE,
|
||||
GHOSTTY_MOUSE_SHAPE_NO_DROP,
|
||||
GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED,
|
||||
GHOSTTY_MOUSE_SHAPE_GRAB,
|
||||
GHOSTTY_MOUSE_SHAPE_GRABBING,
|
||||
GHOSTTY_MOUSE_SHAPE_ALL_SCROLL,
|
||||
GHOSTTY_MOUSE_SHAPE_COL_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_ROW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_N_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_E_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_S_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_W_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NE_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_SE_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_SW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_EW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NS_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NESW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_ZOOM_IN,
|
||||
GHOSTTY_MOUSE_SHAPE_ZOOM_OUT,
|
||||
} ghostty_action_mouse_shape_e;
|
||||
|
||||
// apprt.action.MouseVisibility
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_VISIBLE,
|
||||
GHOSTTY_MOUSE_HIDDEN,
|
||||
} ghostty_action_mouse_visibility_e;
|
||||
|
||||
// apprt.action.MouseOverLink
|
||||
typedef struct {
|
||||
const char* url;
|
||||
size_t len;
|
||||
} ghostty_action_mouse_over_link_s;
|
||||
|
||||
// apprt.action.SizeLimit
|
||||
typedef struct {
|
||||
uint32_t min_width;
|
||||
uint32_t min_height;
|
||||
uint32_t max_width;
|
||||
uint32_t max_height;
|
||||
} ghostty_action_size_limit_s;
|
||||
|
||||
// apprt.action.InitialSize
|
||||
typedef struct {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
} ghostty_action_initial_size_s;
|
||||
|
||||
// apprt.action.CellSize
|
||||
typedef struct {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
} ghostty_action_cell_size_s;
|
||||
|
||||
// renderer.Health
|
||||
typedef enum {
|
||||
GHOSTTY_RENDERER_HEALTH_HEALTHY,
|
||||
GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
|
||||
} ghostty_action_renderer_health_e;
|
||||
|
||||
// apprt.action.KeySequence
|
||||
typedef struct {
|
||||
bool active;
|
||||
ghostty_input_trigger_s trigger;
|
||||
} ghostty_action_key_sequence_s;
|
||||
|
||||
// apprt.action.KeyTable.Tag
|
||||
typedef enum {
|
||||
GHOSTTY_KEY_TABLE_ACTIVATE,
|
||||
GHOSTTY_KEY_TABLE_DEACTIVATE,
|
||||
GHOSTTY_KEY_TABLE_DEACTIVATE_ALL,
|
||||
} ghostty_action_key_table_tag_e;
|
||||
|
||||
// apprt.action.KeyTable.CValue
|
||||
typedef union {
|
||||
struct {
|
||||
const char *name;
|
||||
size_t len;
|
||||
} activate;
|
||||
} ghostty_action_key_table_u;
|
||||
|
||||
// apprt.action.KeyTable.C
|
||||
typedef struct {
|
||||
ghostty_action_key_table_tag_e tag;
|
||||
ghostty_action_key_table_u value;
|
||||
} ghostty_action_key_table_s;
|
||||
|
||||
// apprt.action.ColorKind
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1,
|
||||
GHOSTTY_ACTION_COLOR_KIND_BACKGROUND = -2,
|
||||
GHOSTTY_ACTION_COLOR_KIND_CURSOR = -3,
|
||||
} ghostty_action_color_kind_e;
|
||||
|
||||
// apprt.action.ColorChange
|
||||
typedef struct {
|
||||
ghostty_action_color_kind_e kind;
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
} ghostty_action_color_change_s;
|
||||
|
||||
// apprt.action.ConfigChange
|
||||
typedef struct {
|
||||
ghostty_config_t config;
|
||||
} ghostty_action_config_change_s;
|
||||
|
||||
// apprt.action.ReloadConfig
|
||||
typedef struct {
|
||||
bool soft;
|
||||
} ghostty_action_reload_config_s;
|
||||
|
||||
// apprt.action.OpenUrlKind
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN,
|
||||
GHOSTTY_ACTION_OPEN_URL_KIND_TEXT,
|
||||
GHOSTTY_ACTION_OPEN_URL_KIND_HTML,
|
||||
} ghostty_action_open_url_kind_e;
|
||||
|
||||
// apprt.action.OpenUrl.C
|
||||
typedef struct {
|
||||
ghostty_action_open_url_kind_e kind;
|
||||
const char* url;
|
||||
uintptr_t len;
|
||||
} ghostty_action_open_url_s;
|
||||
|
||||
// apprt.action.CloseTabMode
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
|
||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
|
||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT,
|
||||
} ghostty_action_close_tab_mode_e;
|
||||
|
||||
// apprt.surface.Message.ChildExited
|
||||
typedef struct {
|
||||
uint32_t exit_code;
|
||||
uint64_t timetime_ms;
|
||||
} ghostty_surface_message_childexited_s;
|
||||
|
||||
// terminal.osc.Command.ProgressReport.State
|
||||
typedef enum {
|
||||
GHOSTTY_PROGRESS_STATE_REMOVE,
|
||||
GHOSTTY_PROGRESS_STATE_SET,
|
||||
GHOSTTY_PROGRESS_STATE_ERROR,
|
||||
GHOSTTY_PROGRESS_STATE_INDETERMINATE,
|
||||
GHOSTTY_PROGRESS_STATE_PAUSE,
|
||||
} ghostty_action_progress_report_state_e;
|
||||
|
||||
// terminal.osc.Command.ProgressReport.C
|
||||
typedef struct {
|
||||
ghostty_action_progress_report_state_e state;
|
||||
// -1 if no progress was reported, otherwise 0-100 indicating percent
|
||||
// completeness.
|
||||
int8_t progress;
|
||||
} ghostty_action_progress_report_s;
|
||||
|
||||
// apprt.action.CommandFinished.C
|
||||
typedef struct {
|
||||
// -1 if no exit code was reported, otherwise 0-255
|
||||
int16_t exit_code;
|
||||
// number of nanoseconds that command was running for
|
||||
uint64_t duration;
|
||||
} ghostty_action_command_finished_s;
|
||||
|
||||
// apprt.action.StartSearch.C
|
||||
typedef struct {
|
||||
const char* needle;
|
||||
} ghostty_action_start_search_s;
|
||||
|
||||
// apprt.action.SearchTotal
|
||||
typedef struct {
|
||||
ssize_t total;
|
||||
} ghostty_action_search_total_s;
|
||||
|
||||
// apprt.action.SearchSelected
|
||||
typedef struct {
|
||||
ssize_t selected;
|
||||
} ghostty_action_search_selected_s;
|
||||
|
||||
// terminal.Scrollbar
|
||||
typedef struct {
|
||||
uint64_t total;
|
||||
uint64_t offset;
|
||||
uint64_t len;
|
||||
} ghostty_action_scrollbar_s;
|
||||
|
||||
// apprt.Action.Key
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_QUIT,
|
||||
GHOSTTY_ACTION_NEW_WINDOW,
|
||||
GHOSTTY_ACTION_NEW_TAB,
|
||||
GHOSTTY_ACTION_CLOSE_TAB,
|
||||
GHOSTTY_ACTION_NEW_SPLIT,
|
||||
GHOSTTY_ACTION_CLOSE_ALL_WINDOWS,
|
||||
GHOSTTY_ACTION_TOGGLE_MAXIMIZE,
|
||||
GHOSTTY_ACTION_TOGGLE_FULLSCREEN,
|
||||
GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW,
|
||||
GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
|
||||
GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
|
||||
GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE,
|
||||
GHOSTTY_ACTION_TOGGLE_VISIBILITY,
|
||||
GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY,
|
||||
GHOSTTY_ACTION_MOVE_TAB,
|
||||
GHOSTTY_ACTION_GOTO_TAB,
|
||||
GHOSTTY_ACTION_GOTO_SPLIT,
|
||||
GHOSTTY_ACTION_GOTO_WINDOW,
|
||||
GHOSTTY_ACTION_RESIZE_SPLIT,
|
||||
GHOSTTY_ACTION_EQUALIZE_SPLITS,
|
||||
GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM,
|
||||
GHOSTTY_ACTION_PRESENT_TERMINAL,
|
||||
GHOSTTY_ACTION_SIZE_LIMIT,
|
||||
GHOSTTY_ACTION_RESET_WINDOW_SIZE,
|
||||
GHOSTTY_ACTION_INITIAL_SIZE,
|
||||
GHOSTTY_ACTION_CELL_SIZE,
|
||||
GHOSTTY_ACTION_SCROLLBAR,
|
||||
GHOSTTY_ACTION_RENDER,
|
||||
GHOSTTY_ACTION_INSPECTOR,
|
||||
GHOSTTY_ACTION_SHOW_GTK_INSPECTOR,
|
||||
GHOSTTY_ACTION_RENDER_INSPECTOR,
|
||||
GHOSTTY_ACTION_DESKTOP_NOTIFICATION,
|
||||
GHOSTTY_ACTION_SET_TITLE,
|
||||
GHOSTTY_ACTION_SET_TAB_TITLE,
|
||||
GHOSTTY_ACTION_PROMPT_TITLE,
|
||||
GHOSTTY_ACTION_PWD,
|
||||
GHOSTTY_ACTION_MOUSE_SHAPE,
|
||||
GHOSTTY_ACTION_MOUSE_VISIBILITY,
|
||||
GHOSTTY_ACTION_MOUSE_OVER_LINK,
|
||||
GHOSTTY_ACTION_RENDERER_HEALTH,
|
||||
GHOSTTY_ACTION_OPEN_CONFIG,
|
||||
GHOSTTY_ACTION_QUIT_TIMER,
|
||||
GHOSTTY_ACTION_FLOAT_WINDOW,
|
||||
GHOSTTY_ACTION_SECURE_INPUT,
|
||||
GHOSTTY_ACTION_KEY_SEQUENCE,
|
||||
GHOSTTY_ACTION_KEY_TABLE,
|
||||
GHOSTTY_ACTION_COLOR_CHANGE,
|
||||
GHOSTTY_ACTION_RELOAD_CONFIG,
|
||||
GHOSTTY_ACTION_CONFIG_CHANGE,
|
||||
GHOSTTY_ACTION_CLOSE_WINDOW,
|
||||
GHOSTTY_ACTION_RING_BELL,
|
||||
GHOSTTY_ACTION_UNDO,
|
||||
GHOSTTY_ACTION_REDO,
|
||||
GHOSTTY_ACTION_CHECK_FOR_UPDATES,
|
||||
GHOSTTY_ACTION_OPEN_URL,
|
||||
GHOSTTY_ACTION_SHOW_CHILD_EXITED,
|
||||
GHOSTTY_ACTION_PROGRESS_REPORT,
|
||||
GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
|
||||
GHOSTTY_ACTION_COMMAND_FINISHED,
|
||||
GHOSTTY_ACTION_START_SEARCH,
|
||||
GHOSTTY_ACTION_END_SEARCH,
|
||||
GHOSTTY_ACTION_SEARCH_TOTAL,
|
||||
GHOSTTY_ACTION_SEARCH_SELECTED,
|
||||
GHOSTTY_ACTION_READONLY,
|
||||
GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD,
|
||||
} ghostty_action_tag_e;
|
||||
|
||||
typedef union {
|
||||
ghostty_action_split_direction_e new_split;
|
||||
ghostty_action_fullscreen_e toggle_fullscreen;
|
||||
ghostty_action_move_tab_s move_tab;
|
||||
ghostty_action_goto_tab_e goto_tab;
|
||||
ghostty_action_goto_split_e goto_split;
|
||||
ghostty_action_goto_window_e goto_window;
|
||||
ghostty_action_resize_split_s resize_split;
|
||||
ghostty_action_size_limit_s size_limit;
|
||||
ghostty_action_initial_size_s initial_size;
|
||||
ghostty_action_cell_size_s cell_size;
|
||||
ghostty_action_scrollbar_s scrollbar;
|
||||
ghostty_action_inspector_e inspector;
|
||||
ghostty_action_desktop_notification_s desktop_notification;
|
||||
ghostty_action_set_title_s set_title;
|
||||
ghostty_action_set_title_s set_tab_title;
|
||||
ghostty_action_prompt_title_e prompt_title;
|
||||
ghostty_action_pwd_s pwd;
|
||||
ghostty_action_mouse_shape_e mouse_shape;
|
||||
ghostty_action_mouse_visibility_e mouse_visibility;
|
||||
ghostty_action_mouse_over_link_s mouse_over_link;
|
||||
ghostty_action_renderer_health_e renderer_health;
|
||||
ghostty_action_quit_timer_e quit_timer;
|
||||
ghostty_action_float_window_e float_window;
|
||||
ghostty_action_secure_input_e secure_input;
|
||||
ghostty_action_key_sequence_s key_sequence;
|
||||
ghostty_action_key_table_s key_table;
|
||||
ghostty_action_color_change_s color_change;
|
||||
ghostty_action_reload_config_s reload_config;
|
||||
ghostty_action_config_change_s config_change;
|
||||
ghostty_action_open_url_s open_url;
|
||||
ghostty_action_close_tab_mode_e close_tab_mode;
|
||||
ghostty_surface_message_childexited_s child_exited;
|
||||
ghostty_action_progress_report_s progress_report;
|
||||
ghostty_action_command_finished_s command_finished;
|
||||
ghostty_action_start_search_s start_search;
|
||||
ghostty_action_search_total_s search_total;
|
||||
ghostty_action_search_selected_s search_selected;
|
||||
ghostty_action_readonly_e readonly;
|
||||
} ghostty_action_u;
|
||||
|
||||
typedef struct {
|
||||
ghostty_action_tag_e tag;
|
||||
ghostty_action_u action;
|
||||
} ghostty_action_s;
|
||||
|
||||
typedef void (*ghostty_runtime_wakeup_cb)(void*);
|
||||
typedef bool (*ghostty_runtime_read_clipboard_cb)(void*,
|
||||
ghostty_clipboard_e,
|
||||
void*);
|
||||
typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
|
||||
void*,
|
||||
const char*,
|
||||
void*,
|
||||
ghostty_clipboard_request_e);
|
||||
typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
|
||||
ghostty_clipboard_e,
|
||||
const ghostty_clipboard_content_s*,
|
||||
size_t,
|
||||
bool);
|
||||
typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
|
||||
typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,
|
||||
ghostty_target_s,
|
||||
ghostty_action_s);
|
||||
|
||||
typedef struct {
|
||||
void* userdata;
|
||||
bool supports_selection_clipboard;
|
||||
ghostty_runtime_wakeup_cb wakeup_cb;
|
||||
ghostty_runtime_action_cb action_cb;
|
||||
ghostty_runtime_read_clipboard_cb read_clipboard_cb;
|
||||
ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb;
|
||||
ghostty_runtime_write_clipboard_cb write_clipboard_cb;
|
||||
ghostty_runtime_close_surface_cb close_surface_cb;
|
||||
} ghostty_runtime_config_s;
|
||||
|
||||
// apprt.ipc.Target.Key
|
||||
typedef enum {
|
||||
GHOSTTY_IPC_TARGET_CLASS,
|
||||
GHOSTTY_IPC_TARGET_DETECT,
|
||||
} ghostty_ipc_target_tag_e;
|
||||
|
||||
typedef union {
|
||||
char *klass;
|
||||
} ghostty_ipc_target_u;
|
||||
|
||||
typedef struct {
|
||||
ghostty_ipc_target_tag_e tag;
|
||||
ghostty_ipc_target_u target;
|
||||
} chostty_ipc_target_s;
|
||||
|
||||
// apprt.ipc.Action.NewWindow
|
||||
typedef struct {
|
||||
// This should be a null terminated list of strings.
|
||||
const char **arguments;
|
||||
} ghostty_ipc_action_new_window_s;
|
||||
|
||||
typedef union {
|
||||
ghostty_ipc_action_new_window_s new_window;
|
||||
} ghostty_ipc_action_u;
|
||||
|
||||
// apprt.ipc.Action.Key
|
||||
typedef enum {
|
||||
GHOSTTY_IPC_ACTION_NEW_WINDOW,
|
||||
} ghostty_ipc_action_tag_e;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Published API
|
||||
|
||||
int ghostty_init(uintptr_t, char**);
|
||||
void ghostty_cli_try_action(void);
|
||||
ghostty_info_s ghostty_info(void);
|
||||
const char* ghostty_translate(const char*);
|
||||
void ghostty_string_free(ghostty_string_s);
|
||||
|
||||
ghostty_config_t ghostty_config_new();
|
||||
void ghostty_config_free(ghostty_config_t);
|
||||
ghostty_config_t ghostty_config_clone(ghostty_config_t);
|
||||
void ghostty_config_load_cli_args(ghostty_config_t);
|
||||
void ghostty_config_load_file(ghostty_config_t, const char*);
|
||||
void ghostty_config_load_default_files(ghostty_config_t);
|
||||
void ghostty_config_load_recursive_files(ghostty_config_t);
|
||||
void ghostty_config_finalize(ghostty_config_t);
|
||||
bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t);
|
||||
ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
|
||||
const char*,
|
||||
uintptr_t);
|
||||
uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
|
||||
ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
|
||||
ghostty_string_s ghostty_config_open_path(void);
|
||||
|
||||
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
|
||||
ghostty_config_t);
|
||||
void ghostty_app_free(ghostty_app_t);
|
||||
void ghostty_app_tick(ghostty_app_t);
|
||||
void* ghostty_app_userdata(ghostty_app_t);
|
||||
void ghostty_app_set_focus(ghostty_app_t, bool);
|
||||
bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
|
||||
bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s);
|
||||
void ghostty_app_keyboard_changed(ghostty_app_t);
|
||||
void ghostty_app_open_config(ghostty_app_t);
|
||||
void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
|
||||
bool ghostty_app_needs_confirm_quit(ghostty_app_t);
|
||||
bool ghostty_app_has_global_keybinds(ghostty_app_t);
|
||||
void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e);
|
||||
|
||||
ghostty_surface_config_s ghostty_surface_config_new();
|
||||
|
||||
ghostty_surface_t ghostty_surface_new(ghostty_app_t,
|
||||
const ghostty_surface_config_s*);
|
||||
void ghostty_surface_free(ghostty_surface_t);
|
||||
void* ghostty_surface_userdata(ghostty_surface_t);
|
||||
ghostty_app_t ghostty_surface_app(ghostty_surface_t);
|
||||
ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e);
|
||||
void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
|
||||
bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
|
||||
bool ghostty_surface_process_exited(ghostty_surface_t);
|
||||
void ghostty_surface_refresh(ghostty_surface_t);
|
||||
void ghostty_surface_draw(ghostty_surface_t);
|
||||
void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
|
||||
void ghostty_surface_set_focus(ghostty_surface_t, bool);
|
||||
void ghostty_surface_set_occlusion(ghostty_surface_t, bool);
|
||||
void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
|
||||
ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t);
|
||||
void ghostty_surface_set_color_scheme(ghostty_surface_t,
|
||||
ghostty_color_scheme_e);
|
||||
ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,
|
||||
ghostty_input_mods_e);
|
||||
bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
|
||||
bool ghostty_surface_key_is_binding(ghostty_surface_t,
|
||||
ghostty_input_key_s,
|
||||
ghostty_binding_flags_e*);
|
||||
void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
|
||||
void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t);
|
||||
bool ghostty_surface_mouse_captured(ghostty_surface_t);
|
||||
bool ghostty_surface_mouse_button(ghostty_surface_t,
|
||||
ghostty_input_mouse_state_e,
|
||||
ghostty_input_mouse_button_e,
|
||||
ghostty_input_mods_e);
|
||||
void ghostty_surface_mouse_pos(ghostty_surface_t,
|
||||
double,
|
||||
double,
|
||||
ghostty_input_mods_e);
|
||||
void ghostty_surface_mouse_scroll(ghostty_surface_t,
|
||||
double,
|
||||
double,
|
||||
ghostty_input_scroll_mods_t);
|
||||
void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
|
||||
void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*);
|
||||
void ghostty_surface_request_close(ghostty_surface_t);
|
||||
void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
|
||||
void ghostty_surface_split_focus(ghostty_surface_t,
|
||||
ghostty_action_goto_split_e);
|
||||
void ghostty_surface_split_resize(ghostty_surface_t,
|
||||
ghostty_action_resize_split_direction_e,
|
||||
uint16_t);
|
||||
void ghostty_surface_split_equalize(ghostty_surface_t);
|
||||
bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t);
|
||||
void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
|
||||
const char*,
|
||||
void*,
|
||||
bool);
|
||||
bool ghostty_surface_has_selection(ghostty_surface_t);
|
||||
bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*);
|
||||
bool ghostty_surface_read_text(ghostty_surface_t,
|
||||
ghostty_selection_s,
|
||||
ghostty_text_s*);
|
||||
void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*);
|
||||
|
||||
#ifdef __APPLE__
|
||||
void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t);
|
||||
void* ghostty_surface_quicklook_font(ghostty_surface_t);
|
||||
bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*);
|
||||
#endif
|
||||
|
||||
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
|
||||
void ghostty_inspector_free(ghostty_surface_t);
|
||||
void ghostty_inspector_set_focus(ghostty_inspector_t, bool);
|
||||
void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double);
|
||||
void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t);
|
||||
void ghostty_inspector_mouse_button(ghostty_inspector_t,
|
||||
ghostty_input_mouse_state_e,
|
||||
ghostty_input_mouse_button_e,
|
||||
ghostty_input_mods_e);
|
||||
void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double);
|
||||
void ghostty_inspector_mouse_scroll(ghostty_inspector_t,
|
||||
double,
|
||||
double,
|
||||
ghostty_input_scroll_mods_t);
|
||||
void ghostty_inspector_key(ghostty_inspector_t,
|
||||
ghostty_input_action_e,
|
||||
ghostty_input_key_e,
|
||||
ghostty_input_mods_e);
|
||||
void ghostty_inspector_text(ghostty_inspector_t, const char*);
|
||||
|
||||
#ifdef __APPLE__
|
||||
bool ghostty_inspector_metal_init(ghostty_inspector_t, void*);
|
||||
void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*);
|
||||
bool ghostty_inspector_metal_shutdown(ghostty_inspector_t);
|
||||
#endif
|
||||
|
||||
// APIs I'd like to get rid of eventually but are still needed for now.
|
||||
// Don't use these unless you know what you're doing.
|
||||
void ghostty_set_window_background_blur(ghostty_app_t, void*);
|
||||
|
||||
// Benchmark API, if available.
|
||||
bool ghostty_benchmark_cli(const char*, const char*);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* GHOSTTY_H */
|
||||
@@ -0,0 +1,7 @@
|
||||
// This makes Ghostty available to the XCode build for the macOS app.
|
||||
// We append "Kit" to it not to be cute, but because targets have to have
|
||||
// unique names and we use Ghostty for other things.
|
||||
module GhosttyKit {
|
||||
umbrella header "ghostty.h"
|
||||
export *
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
//! `apprt.Target.cval` and `apprt.Action.cval`: the conversion of core
|
||||
//! actions to the `ghostty_target_s` / `ghostty_action_s` C structs
|
||||
//! (ZIG/src/apprt/action.zig).
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ffi::{CString, c_char, c_int};
|
||||
|
||||
use ghostty_apprt::{
|
||||
action::{Action, KeySequence, KeyTable, Target},
|
||||
runtime_api::SurfaceId,
|
||||
};
|
||||
use ghostty_renderer::Health;
|
||||
use ghostty_vt::osc::ProgressState;
|
||||
|
||||
use crate::{
|
||||
config_capi::{CConfig, default_trigger, trigger_to_c},
|
||||
types::{
|
||||
ghostty_action_command_finished_s,
|
||||
ghostty_action_s,
|
||||
ghostty_action_u,
|
||||
ghostty_config_t,
|
||||
ghostty_target_s,
|
||||
ghostty_target_u,
|
||||
target_tag,
|
||||
},
|
||||
};
|
||||
|
||||
/// Memory that the pointers inside a converted action point to. It must
|
||||
/// outlive the callback invocation.
|
||||
#[derive(Default)]
|
||||
pub struct Keep {
|
||||
strings: Vec<CString>,
|
||||
config: Option<ghostty_config_t>,
|
||||
}
|
||||
|
||||
impl Keep {
|
||||
/// A NUL terminated copy of `s` (interior NULs are dropped).
|
||||
fn cstr(&mut self, s: &str) -> *const c_char {
|
||||
let c = CString::new(s.replace('\0', "")).unwrap_or_default();
|
||||
let ptr = c.as_ptr();
|
||||
// The heap buffer of a CString doesn't move when the Vec grows.
|
||||
self.strings.push(c);
|
||||
ptr
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Keep {
|
||||
fn drop(&mut self) {
|
||||
if let Some(h) = self.config.take() {
|
||||
// SAFETY: created by `CConfig::into_handle` in `action_to_c`
|
||||
// and not handed out beyond the callback.
|
||||
unsafe { crate::config_capi::ghostty_config_free(h) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `Target.cval`. The surface handle is the id of the surface (the address
|
||||
/// of the embedded runtime's surface object, see `embedded.rs`).
|
||||
#[must_use]
|
||||
pub fn target_to_c(target: &Target<SurfaceId>) -> ghostty_target_s {
|
||||
match target {
|
||||
Target::App => ghostty_target_s {
|
||||
tag: target_tag::APP,
|
||||
target: ghostty_target_u {
|
||||
surface: std::ptr::null_mut(),
|
||||
},
|
||||
},
|
||||
Target::Surface(id) => ghostty_target_s {
|
||||
tag: target_tag::SURFACE,
|
||||
target: ghostty_target_u {
|
||||
surface: *id as usize as *mut std::ffi::c_void,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn zeroed_union() -> ghostty_action_u {
|
||||
// SAFETY: every field of the union is plain old data for which the
|
||||
// all-zero bit pattern is valid (integers, floats, null pointers, bool).
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
|
||||
/// `Action.cval`.
|
||||
#[must_use]
|
||||
pub fn action_to_c(action: &Action, keep: &mut Keep) -> ghostty_action_s {
|
||||
let mut v = zeroed_union();
|
||||
match action {
|
||||
Action::CloseTab(m) => v.close_tab_mode = *m as c_int,
|
||||
Action::NewSplit(d) => v.new_split = *d as c_int,
|
||||
Action::ToggleFullscreen(f) => v.toggle_fullscreen = *f as c_int,
|
||||
Action::MoveTab(m) => v.move_tab.amount = m.amount,
|
||||
Action::GotoTab(t) => v.goto_tab = t.int(),
|
||||
Action::GotoSplit(s) => v.goto_split = *s as c_int,
|
||||
Action::GotoWindow(w) => v.goto_window = *w as c_int,
|
||||
Action::ResizeSplit(r) => {
|
||||
v.resize_split.amount = r.amount;
|
||||
v.resize_split.direction = r.direction as c_int;
|
||||
}
|
||||
Action::SizeLimit(s) => {
|
||||
v.size_limit.min_width = s.min_width;
|
||||
v.size_limit.min_height = s.min_height;
|
||||
v.size_limit.max_width = s.max_width;
|
||||
v.size_limit.max_height = s.max_height;
|
||||
}
|
||||
Action::InitialSize(s) => {
|
||||
v.initial_size.width = s.width;
|
||||
v.initial_size.height = s.height;
|
||||
}
|
||||
Action::CellSize(s) => {
|
||||
v.cell_size.width = s.width;
|
||||
v.cell_size.height = s.height;
|
||||
}
|
||||
Action::Scrollbar(s) => {
|
||||
v.scrollbar.total = s.total as u64;
|
||||
v.scrollbar.offset = s.offset as u64;
|
||||
v.scrollbar.len = s.len as u64;
|
||||
}
|
||||
Action::Inspector(i) => v.inspector = *i as c_int,
|
||||
Action::DesktopNotification(n) => {
|
||||
v.desktop_notification.title = keep.cstr(&n.title);
|
||||
v.desktop_notification.body = keep.cstr(&n.body);
|
||||
}
|
||||
Action::SetTitle(t) => v.set_title.title = keep.cstr(&t.title),
|
||||
Action::SetTabTitle(t) => v.set_tab_title.title = keep.cstr(&t.title),
|
||||
Action::PromptTitle(p) => v.prompt_title = *p as c_int,
|
||||
Action::Pwd(p) => v.pwd.pwd = keep.cstr(&p.pwd),
|
||||
Action::MouseShape(s) => v.mouse_shape = *s as c_int,
|
||||
Action::MouseVisibility(m) => v.mouse_visibility = *m as c_int,
|
||||
Action::MouseOverLink(l) => {
|
||||
v.mouse_over_link.url = keep.cstr(&l.url);
|
||||
v.mouse_over_link.len = l.url.len();
|
||||
}
|
||||
Action::RendererHealth(h) => {
|
||||
v.renderer_health = match h {
|
||||
Health::Healthy => 0,
|
||||
Health::Unhealthy => 1,
|
||||
};
|
||||
}
|
||||
Action::QuitTimer(q) => v.quit_timer = *q as c_int,
|
||||
Action::FloatWindow(f) => v.float_window = *f as c_int,
|
||||
Action::SecureInput(s) => v.secure_input = *s as c_int,
|
||||
Action::KeySequence(KeySequence::Trigger(t)) => {
|
||||
v.key_sequence.active = true;
|
||||
v.key_sequence.trigger = trigger_to_c(&t.cval());
|
||||
}
|
||||
Action::KeySequence(KeySequence::End) => {
|
||||
v.key_sequence.active = false;
|
||||
v.key_sequence.trigger = default_trigger();
|
||||
}
|
||||
Action::KeyTable(KeyTable::Activate(name)) => {
|
||||
v.key_table.tag = 0;
|
||||
v.key_table.value.activate.name = keep.cstr(name);
|
||||
v.key_table.value.activate.len = name.len();
|
||||
}
|
||||
Action::KeyTable(KeyTable::Deactivate) => v.key_table.tag = 1,
|
||||
Action::KeyTable(KeyTable::DeactivateAll) => v.key_table.tag = 2,
|
||||
Action::ColorChange(c) => {
|
||||
v.color_change.kind = c.kind.int();
|
||||
v.color_change.r = c.r;
|
||||
v.color_change.g = c.g;
|
||||
v.color_change.b = c.b;
|
||||
}
|
||||
Action::ReloadConfig(r) => v.reload_config.soft = r.soft,
|
||||
Action::ConfigChange(c) => {
|
||||
// The embedder reads (and clones) the config during the
|
||||
// callback; it is freed again when `keep` drops.
|
||||
if let Some(old) = keep.config.take() {
|
||||
// SAFETY: created below by an earlier conversion.
|
||||
unsafe { crate::config_capi::ghostty_config_free(old) };
|
||||
}
|
||||
let handle = CConfig::new((*c.config).clone()).into_handle();
|
||||
keep.config = Some(handle);
|
||||
v.config_change.config = handle;
|
||||
}
|
||||
Action::OpenUrl(u) => {
|
||||
v.open_url.kind = u.kind as c_int;
|
||||
v.open_url.url = keep.cstr(&u.url);
|
||||
v.open_url.len = u.url.len();
|
||||
}
|
||||
Action::ShowChildExited(c) => {
|
||||
v.child_exited.exit_code = c.exit_code;
|
||||
v.child_exited.timetime_ms = c.runtime_ms;
|
||||
}
|
||||
Action::ProgressReport(p) => {
|
||||
v.progress_report.state = match p.state {
|
||||
ProgressState::Remove => 0,
|
||||
ProgressState::Set => 1,
|
||||
ProgressState::Error => 2,
|
||||
ProgressState::Indeterminate => 3,
|
||||
ProgressState::Pause => 4,
|
||||
};
|
||||
v.progress_report.progress = p.progress.map_or(-1, |p| p as i8);
|
||||
}
|
||||
Action::CommandFinished(c) => {
|
||||
v.command_finished = ghostty_action_command_finished_s {
|
||||
exit_code: c.exit_code.map_or(-1, i16::from),
|
||||
duration: c.duration.duration,
|
||||
};
|
||||
}
|
||||
Action::StartSearch(s) => v.start_search.needle = keep.cstr(&s.needle),
|
||||
Action::SearchTotal(s) => {
|
||||
v.search_total.total = s.total.map_or(-1, |t| t as isize);
|
||||
}
|
||||
Action::SearchSelected(s) => {
|
||||
v.search_selected.selected = s.selected.map_or(-1, |t| t as isize);
|
||||
}
|
||||
Action::Readonly(r) => v.readonly = *r as c_int,
|
||||
_ => {}
|
||||
}
|
||||
ghostty_action_s {
|
||||
tag: action.key() as c_int,
|
||||
action: v,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ghostty_apprt::action::{
|
||||
CellSize,
|
||||
CommandFinished,
|
||||
GotoTab,
|
||||
MouseOverLink,
|
||||
ResizeSplit,
|
||||
ResizeSplitDirection,
|
||||
SearchTotal,
|
||||
SetTitle,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn target_cval() {
|
||||
let t = target_to_c(&Target::App);
|
||||
assert_eq!(t.tag, 0);
|
||||
let t = target_to_c(&Target::Surface(0x1000));
|
||||
assert_eq!(t.tag, 1);
|
||||
// SAFETY: tag says surface.
|
||||
assert_eq!(unsafe { t.target.surface } as usize, 0x1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_payloads() {
|
||||
let mut keep = Keep::default();
|
||||
let a = action_to_c(
|
||||
&Action::CellSize(CellSize {
|
||||
width: 7,
|
||||
height: 9,
|
||||
}),
|
||||
&mut keep,
|
||||
);
|
||||
assert_eq!(a.tag, 25);
|
||||
// SAFETY: tag says cell_size.
|
||||
assert_eq!(
|
||||
unsafe { (a.action.cell_size.width, a.action.cell_size.height) },
|
||||
(7, 9)
|
||||
);
|
||||
|
||||
let a = action_to_c(&Action::GotoTab(GotoTab::Last), &mut keep);
|
||||
// SAFETY: tag says goto_tab.
|
||||
assert_eq!(unsafe { a.action.goto_tab }, -3);
|
||||
|
||||
let a = action_to_c(
|
||||
&Action::ResizeSplit(ResizeSplit {
|
||||
amount: 5,
|
||||
direction: ResizeSplitDirection::Left,
|
||||
}),
|
||||
&mut keep,
|
||||
);
|
||||
// SAFETY: tag says resize_split.
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
(
|
||||
a.action.resize_split.amount,
|
||||
a.action.resize_split.direction,
|
||||
)
|
||||
},
|
||||
(5, 2)
|
||||
);
|
||||
|
||||
let a = action_to_c(&Action::SearchTotal(SearchTotal { total: None }), &mut keep);
|
||||
// SAFETY: tag says search_total.
|
||||
assert_eq!(unsafe { a.action.search_total.total }, -1);
|
||||
|
||||
let a = action_to_c(
|
||||
&Action::CommandFinished(CommandFinished {
|
||||
exit_code: None,
|
||||
duration: ghostty_config::types_b::Duration { duration: 5 },
|
||||
}),
|
||||
&mut keep,
|
||||
);
|
||||
// SAFETY: tag says command_finished.
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
(
|
||||
a.action.command_finished.exit_code,
|
||||
a.action.command_finished.duration,
|
||||
)
|
||||
},
|
||||
(-1, 5)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strings_stay_valid_while_kept() {
|
||||
let mut keep = Keep::default();
|
||||
let a = action_to_c(
|
||||
&Action::SetTitle(SetTitle {
|
||||
title: "hello".into(),
|
||||
}),
|
||||
&mut keep,
|
||||
);
|
||||
// SAFETY: tag says set_title; the pointer points into `keep`.
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(a.action.set_title.title) };
|
||||
assert_eq!(s.to_str().unwrap(), "hello");
|
||||
|
||||
let a = action_to_c(
|
||||
&Action::MouseOverLink(MouseOverLink {
|
||||
url: "https://x".into(),
|
||||
}),
|
||||
&mut keep,
|
||||
);
|
||||
// SAFETY: tag says mouse_over_link.
|
||||
assert_eq!(unsafe { a.action.mouse_over_link.len }, 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_change_hands_out_a_config() {
|
||||
let mut keep = Keep::default();
|
||||
let cfg = std::sync::Arc::new(ghostty_config::config::Config::default());
|
||||
let a = action_to_c(
|
||||
&Action::ConfigChange(ghostty_apprt::action::ConfigChange { config: cfg }),
|
||||
&mut keep,
|
||||
);
|
||||
// SAFETY: tag says config_change.
|
||||
let h = unsafe { a.action.config_change.config };
|
||||
let mut out = false;
|
||||
let key = "maximize";
|
||||
// SAFETY: valid handle and destination.
|
||||
assert!(unsafe {
|
||||
crate::config_capi::ghostty_config_get(
|
||||
h,
|
||||
(&raw mut out).cast(),
|
||||
key.as_ptr().cast(),
|
||||
key.len(),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//! `ghostty_benchmark_cli` (ZIG/src/benchmark/CApi.zig).
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ffi::{CStr, c_char};
|
||||
|
||||
use ghostty_bench::{args::Args, benches};
|
||||
|
||||
/// Split the argument string like `std.process.ArgIteratorGeneral(.{})`:
|
||||
/// whitespace separated, with double quotes grouping and backslash
|
||||
/// escapes inside quotes.
|
||||
fn split_args(s: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut cur = String::new();
|
||||
let mut have = false;
|
||||
let mut in_quote = false;
|
||||
let mut chars = s.chars().peekable();
|
||||
while let Some(c) = chars.next() {
|
||||
match c {
|
||||
'"' => {
|
||||
in_quote = !in_quote;
|
||||
have = true;
|
||||
}
|
||||
'\\' if in_quote => {
|
||||
if let Some(n) = chars.next() {
|
||||
cur.push(n);
|
||||
}
|
||||
}
|
||||
c if c.is_whitespace() && !in_quote => {
|
||||
if have {
|
||||
out.push(std::mem::take(&mut cur));
|
||||
have = false;
|
||||
}
|
||||
}
|
||||
c => {
|
||||
cur.push(c);
|
||||
have = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if have {
|
||||
out.push(cur);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Run the Ghostty benchmark CLI with the given action and arguments.
|
||||
///
|
||||
/// # Safety
|
||||
/// `action_name` and `args` must be NUL terminated strings.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_benchmark_cli(
|
||||
action_name: *const c_char,
|
||||
args: *const c_char,
|
||||
) -> bool {
|
||||
if action_name.is_null() || args.is_null() {
|
||||
return false;
|
||||
}
|
||||
// SAFETY: per the function contract.
|
||||
let (action, args) = unsafe { (CStr::from_ptr(action_name), CStr::from_ptr(args)) };
|
||||
let action = action.to_string_lossy();
|
||||
if !benches::ACTIONS.contains(&action.as_ref()) {
|
||||
log::warn!("unknown action={action}");
|
||||
return false;
|
||||
}
|
||||
let argv = split_args(&args.to_string_lossy());
|
||||
let parsed = match Args::parse(argv) {
|
||||
Ok(a) => a,
|
||||
Err(err) => {
|
||||
log::warn!("failed to run action={action} err={err}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
match benches::main_action(&action, parsed) {
|
||||
Ok(_) => true,
|
||||
Err(err) => {
|
||||
log::warn!("failed to run action={action} err={err}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn split_args_basic() {
|
||||
assert_eq!(split_args("a b c"), ["a", "b", "c"]);
|
||||
assert_eq!(split_args("--x=\"a b\" c"), ["--x=a b", "c"]);
|
||||
assert_eq!(split_args(" ").len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_action_returns_false() {
|
||||
// SAFETY: valid NUL terminated strings.
|
||||
assert!(!unsafe { ghostty_benchmark_cli(c"nope".as_ptr(), c"".as_ptr()) });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,727 @@
|
||||
//! `ghostty_config_*` (ZIG/src/config/CApi.zig and c_get.zig).
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::{CStr, CString, c_char, c_int, c_short, c_uint, c_void},
|
||||
rc::Rc,
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use ghostty_config::{
|
||||
c_value::{CValue, QuickTerminalSizeC},
|
||||
config::Config,
|
||||
};
|
||||
use ghostty_input::binding::{Action as BindingAction, TriggerC, TriggerCTag};
|
||||
|
||||
use crate::{
|
||||
main_c::{EMPTY_STRING, string_from_owned},
|
||||
types::{
|
||||
ghostty_command_s,
|
||||
ghostty_config_color_list_s,
|
||||
ghostty_config_color_s,
|
||||
ghostty_config_command_list_s,
|
||||
ghostty_config_palette_s,
|
||||
ghostty_config_path_s,
|
||||
ghostty_config_quick_terminal_size_s,
|
||||
ghostty_config_t,
|
||||
ghostty_diagnostic_s,
|
||||
ghostty_input_trigger_key_u,
|
||||
ghostty_input_trigger_s,
|
||||
ghostty_quick_terminal_size_s,
|
||||
ghostty_quick_terminal_size_value_u,
|
||||
ghostty_string_s,
|
||||
quick_terminal_size_tag,
|
||||
trigger_tag,
|
||||
},
|
||||
};
|
||||
|
||||
/// The C-owned configuration: the config plus the memory that pointers
|
||||
/// handed out by `ghostty_config_get` / `ghostty_config_get_diagnostic` point
|
||||
/// into (Zig: the config's arena). Everything stays valid until the config
|
||||
/// is freed.
|
||||
pub struct CConfig {
|
||||
pub config: Config,
|
||||
cache: RefCell<GetCache>,
|
||||
diagnostics: RefCell<Vec<CString>>,
|
||||
}
|
||||
|
||||
/// Backing memory of one `ghostty_config_get` result.
|
||||
#[derive(Default)]
|
||||
struct Backing {
|
||||
strings: Vec<CString>,
|
||||
colors: Box<[ghostty_config_color_s]>,
|
||||
commands: Box<[ghostty_command_s]>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GetCache {
|
||||
/// The latest result per key; reused if the value didn't change so
|
||||
/// repeated gets return the same pointers.
|
||||
by_key: HashMap<String, (CValue, Rc<Backing>)>,
|
||||
/// Results that were replaced; kept alive until the config is freed
|
||||
/// since callers may still hold their pointers.
|
||||
retired: Vec<Rc<Backing>>,
|
||||
}
|
||||
|
||||
impl CConfig {
|
||||
pub fn new(config: Config) -> CConfig {
|
||||
CConfig {
|
||||
config,
|
||||
cache: RefCell::default(),
|
||||
diagnostics: RefCell::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Box the config and return the `ghostty_config_t` handle.
|
||||
pub fn into_handle(self) -> ghostty_config_t {
|
||||
Box::into_raw(Box::new(self)).cast::<c_void>()
|
||||
}
|
||||
|
||||
/// The backing memory for `value`, reusing the cached one if equal.
|
||||
fn backing(&self, key: &str, value: &CValue) -> Rc<Backing> {
|
||||
let mut cache = self.cache.borrow_mut();
|
||||
if let Some((old, backing)) = cache.by_key.get(key)
|
||||
&& old == value
|
||||
{
|
||||
return Rc::clone(backing);
|
||||
}
|
||||
let backing = Rc::new(build_backing(value));
|
||||
if let Some((_, old)) = cache
|
||||
.by_key
|
||||
.insert(key.to_owned(), (value.clone(), Rc::clone(&backing)))
|
||||
{
|
||||
cache.retired.push(old);
|
||||
}
|
||||
backing
|
||||
}
|
||||
}
|
||||
|
||||
/// A C string without interior NULs.
|
||||
fn cstring(s: &str) -> CString {
|
||||
CString::new(s.replace('\0', "")).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Intern a string for the lifetime of the process (enum tag names).
|
||||
fn intern(s: &str) -> *const c_char {
|
||||
static POOL: OnceLock<Mutex<HashSet<Box<CStr>>>> = OnceLock::new();
|
||||
let Ok(mut pool) = POOL.get_or_init(Mutex::default).lock() else {
|
||||
return c"".as_ptr();
|
||||
};
|
||||
let c = cstring(s);
|
||||
if let Some(existing) = pool.get(c.as_c_str()) {
|
||||
return existing.as_ptr();
|
||||
}
|
||||
let boxed: Box<CStr> = c.into_boxed_c_str();
|
||||
let ptr = boxed.as_ptr();
|
||||
pool.insert(boxed);
|
||||
ptr
|
||||
}
|
||||
|
||||
fn build_backing(value: &CValue) -> Backing {
|
||||
let mut b = Backing::default();
|
||||
match value {
|
||||
CValue::String(s) => b.strings.push(cstring(s)),
|
||||
CValue::Path { path, .. } => b.strings.push(cstring(path)),
|
||||
CValue::ColorList(list) => {
|
||||
b.colors = list
|
||||
.iter()
|
||||
.map(|c| ghostty_config_color_s {
|
||||
r: c[0],
|
||||
g: c[1],
|
||||
b: c[2],
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
CValue::Commands(cmds) => {
|
||||
for c in cmds {
|
||||
b.strings.push(cstring(&c.action_key));
|
||||
b.strings.push(cstring(&c.action));
|
||||
b.strings.push(cstring(&c.title));
|
||||
b.strings.push(cstring(&c.description));
|
||||
}
|
||||
// The CStrings' heap buffers don't move when the Vec grows.
|
||||
b.commands = b
|
||||
.strings
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|s| ghostty_command_s {
|
||||
action_key: s[0].as_ptr(),
|
||||
action: s[1].as_ptr(),
|
||||
title: s[2].as_ptr(),
|
||||
description: s[3].as_ptr(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
/// Borrow the config behind a handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// `p` must be null or a handle returned by this library that wasn't freed,
|
||||
/// and no other reference to it may be live.
|
||||
pub(crate) unsafe fn cconfig<'a>(p: ghostty_config_t) -> Option<&'a mut CConfig> {
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { p.cast::<CConfig>().as_mut() }
|
||||
}
|
||||
|
||||
/// Create a new configuration filled with the initial default values.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn ghostty_config_new() -> ghostty_config_t {
|
||||
CConfig::new(Config::default()).into_handle()
|
||||
}
|
||||
|
||||
/// Free a configuration.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be null or a handle from this library, not used afterwards.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_config_free(ptr: ghostty_config_t) {
|
||||
if ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: the handle was created by `into_handle` (a leaked Box).
|
||||
drop(unsafe { Box::from_raw(ptr.cast::<CConfig>()) });
|
||||
}
|
||||
|
||||
/// Deep clone the configuration.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a valid config handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_config_clone(ptr: ghostty_config_t) -> ghostty_config_t {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(c) = (unsafe { cconfig(ptr) }) else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
CConfig::new(c.config.clone()).into_handle()
|
||||
}
|
||||
|
||||
/// Load the configuration from the CLI args.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a valid config handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_config_load_cli_args(ptr: ghostty_config_t) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(c) = (unsafe { cconfig(ptr) }) else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = c.config.load_cli_args() {
|
||||
log::error!("error loading config err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the configuration from the default file locations. This is usually
|
||||
/// done first. The default file locations are locations such as the home
|
||||
/// directory.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a valid config handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_config_load_default_files(ptr: ghostty_config_t) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(c) = (unsafe { cconfig(ptr) }) else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = c.config.load_default_files() {
|
||||
log::error!("error loading config err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the configuration from a specific file path. The path must be null
|
||||
/// terminated.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a valid config handle, `path` a NUL terminated string.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_config_load_file(ptr: ghostty_config_t, path: *const c_char) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(c) = (unsafe { cconfig(ptr) }) else {
|
||||
return;
|
||||
};
|
||||
if path.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: `path` is NUL terminated.
|
||||
let path = unsafe { CStr::from_ptr(path) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if let Err(err) = c.config.load_file(&path) {
|
||||
log::error!("error loading config from file path={path} err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the configuration from the user-specified configuration file
|
||||
/// locations in the previously loaded configuration. This will recursively
|
||||
/// continue to load up to a built-in limit.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a valid config handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_config_load_recursive_files(ptr: ghostty_config_t) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(c) = (unsafe { cconfig(ptr) }) else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = c.config.load_recursive_files() {
|
||||
log::error!("error loading config err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Finalize the configuration (fill in computed defaults).
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a valid config handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_config_finalize(ptr: ghostty_config_t) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(c) = (unsafe { cconfig(ptr) }) else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = c.config.finalize() {
|
||||
log::error!("error finalizing config err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Store `v` at `out` (`T` must be what the C caller expects for the key).
|
||||
///
|
||||
/// # Safety
|
||||
/// `out` must be valid for a write of `T`.
|
||||
unsafe fn put<T>(out: *mut c_void, v: T) {
|
||||
// SAFETY: per the function contract; unaligned writes are tolerated.
|
||||
unsafe { out.cast::<T>().write_unaligned(v) };
|
||||
}
|
||||
|
||||
fn qt_size(v: QuickTerminalSizeC) -> ghostty_quick_terminal_size_s {
|
||||
match v {
|
||||
QuickTerminalSizeC::None => ghostty_quick_terminal_size_s {
|
||||
tag: quick_terminal_size_tag::NONE,
|
||||
value: ghostty_quick_terminal_size_value_u { pixels: 0 },
|
||||
},
|
||||
QuickTerminalSizeC::Percentage(p) => ghostty_quick_terminal_size_s {
|
||||
tag: quick_terminal_size_tag::PERCENTAGE,
|
||||
value: ghostty_quick_terminal_size_value_u { percentage: p },
|
||||
},
|
||||
QuickTerminalSizeC::Pixels(p) => ghostty_quick_terminal_size_s {
|
||||
tag: quick_terminal_size_tag::PIXELS,
|
||||
value: ghostty_quick_terminal_size_value_u { pixels: p },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl CConfig {
|
||||
/// `c_get.get`: write the value of `key` to `out`. Returns false if the
|
||||
/// key is unknown, its type isn't supported by the C API yet or an
|
||||
/// optional value is unset.
|
||||
///
|
||||
/// # Safety
|
||||
/// `out` must point to storage of the C type of the key (see
|
||||
/// `ghostty.h` and `c_get.zig`).
|
||||
pub unsafe fn get(&self, key: &str, out: *mut c_void) -> bool {
|
||||
let Some(value) = self.config.c_value(key) else {
|
||||
return false;
|
||||
};
|
||||
// SAFETY (all writes below): `out` is valid for the key's C type by
|
||||
// the function contract.
|
||||
match &value {
|
||||
CValue::Unsupported | CValue::Null => return false,
|
||||
CValue::NullString => unsafe { put(out, std::ptr::null::<c_char>()) },
|
||||
CValue::String(_) => {
|
||||
let b = self.backing(key, &value);
|
||||
unsafe { put(out, b.strings[0].as_ptr()) };
|
||||
}
|
||||
CValue::Bool(v) => unsafe { put(out, *v) },
|
||||
CValue::Uint(v) => unsafe { put::<c_uint>(out, *v) },
|
||||
CValue::Short(v) => unsafe { put::<c_short>(out, *v) },
|
||||
CValue::F32(v) => unsafe { put(out, *v) },
|
||||
CValue::F64(v) => unsafe { put(out, *v) },
|
||||
CValue::Usize(v) => unsafe { put(out, *v) },
|
||||
CValue::EnumName(name) => unsafe { put(out, intern(name)) },
|
||||
CValue::Packed(bits) => unsafe { put::<c_uint>(out, *bits) },
|
||||
CValue::Color { r, g, b } => unsafe {
|
||||
put(
|
||||
out,
|
||||
ghostty_config_color_s {
|
||||
r: *r,
|
||||
g: *g,
|
||||
b: *b,
|
||||
},
|
||||
);
|
||||
},
|
||||
CValue::ColorList(list) => {
|
||||
let b = self.backing(key, &value);
|
||||
unsafe {
|
||||
put(
|
||||
out,
|
||||
ghostty_config_color_list_s {
|
||||
colors: b.colors.as_ptr(),
|
||||
len: list.len(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
CValue::Palette(colors) => {
|
||||
let mut p = ghostty_config_palette_s {
|
||||
colors: [ghostty_config_color_s { r: 0, g: 0, b: 0 }; 256],
|
||||
};
|
||||
for (dst, c) in p.colors.iter_mut().zip(colors.iter()) {
|
||||
*dst = ghostty_config_color_s {
|
||||
r: c[0],
|
||||
g: c[1],
|
||||
b: c[2],
|
||||
};
|
||||
}
|
||||
unsafe { put(out, p) };
|
||||
}
|
||||
CValue::Commands(cmds) => {
|
||||
let b = self.backing(key, &value);
|
||||
unsafe {
|
||||
put(
|
||||
out,
|
||||
ghostty_config_command_list_s {
|
||||
commands: b.commands.as_ptr(),
|
||||
len: cmds.len(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
CValue::QuickTerminalSize { primary, secondary } => unsafe {
|
||||
put(
|
||||
out,
|
||||
ghostty_config_quick_terminal_size_s {
|
||||
primary: qt_size(*primary),
|
||||
secondary: qt_size(*secondary),
|
||||
},
|
||||
);
|
||||
},
|
||||
CValue::Path { optional, .. } => {
|
||||
let b = self.backing(key, &value);
|
||||
unsafe {
|
||||
put(
|
||||
out,
|
||||
ghostty_config_path_s {
|
||||
path: b.strings[0].as_ptr(),
|
||||
optional: *optional,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the value of a config key (see `c_get.zig` for the C types).
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a valid config handle, `key_str` must point to `len`
|
||||
/// bytes and `out` to storage of the key's C type.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_config_get(
|
||||
ptr: ghostty_config_t,
|
||||
out: *mut c_void,
|
||||
key_str: *const c_char,
|
||||
len: usize,
|
||||
) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(c) = (unsafe { cconfig(ptr) }) else {
|
||||
return false;
|
||||
};
|
||||
if out.is_null() || key_str.is_null() {
|
||||
return false;
|
||||
}
|
||||
// SAFETY: `key_str` points to `len` bytes.
|
||||
let key = unsafe { std::slice::from_raw_parts(key_str.cast::<u8>(), len) };
|
||||
let Ok(key) = std::str::from_utf8(key) else {
|
||||
return false;
|
||||
};
|
||||
// SAFETY: `out` is valid for the key's type per the contract.
|
||||
unsafe { c.get(key, out) }
|
||||
}
|
||||
|
||||
/// Convert the Rust trigger to the C struct.
|
||||
pub(crate) fn trigger_to_c(t: &TriggerC) -> ghostty_input_trigger_s {
|
||||
let (tag, key) = match t.tag {
|
||||
TriggerCTag::Physical => (
|
||||
trigger_tag::PHYSICAL,
|
||||
ghostty_input_trigger_key_u {
|
||||
physical: t.key as c_int,
|
||||
},
|
||||
),
|
||||
TriggerCTag::Unicode => (
|
||||
trigger_tag::UNICODE,
|
||||
ghostty_input_trigger_key_u { unicode: t.key },
|
||||
),
|
||||
TriggerCTag::CatchAll => (
|
||||
trigger_tag::CATCH_ALL,
|
||||
ghostty_input_trigger_key_u { unicode: 0 },
|
||||
),
|
||||
};
|
||||
ghostty_input_trigger_s {
|
||||
tag,
|
||||
key,
|
||||
mods: c_int::from(t.mods.int()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The default (unset) trigger (`Trigger.C{}`).
|
||||
pub(crate) fn default_trigger() -> ghostty_input_trigger_s {
|
||||
ghostty_input_trigger_s {
|
||||
tag: trigger_tag::PHYSICAL,
|
||||
key: ghostty_input_trigger_key_u { physical: 0 },
|
||||
mods: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the trigger bound to the given binding action string.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a valid config handle, `str` must point to `len` bytes.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_config_trigger(
|
||||
ptr: ghostty_config_t,
|
||||
str: *const c_char,
|
||||
len: usize,
|
||||
) -> ghostty_input_trigger_s {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(c) = (unsafe { cconfig(ptr) }) else {
|
||||
return default_trigger();
|
||||
};
|
||||
if str.is_null() {
|
||||
return default_trigger();
|
||||
}
|
||||
// SAFETY: `str` points to `len` bytes.
|
||||
let s = unsafe { std::slice::from_raw_parts(str.cast::<u8>(), len) };
|
||||
let Ok(s) = std::str::from_utf8(s) else {
|
||||
log::error!("error finding trigger err=InvalidFormat");
|
||||
return default_trigger();
|
||||
};
|
||||
match BindingAction::parse(s) {
|
||||
Ok(action) => {
|
||||
let trigger = c
|
||||
.config
|
||||
.keybind
|
||||
.set
|
||||
.get_trigger(&action)
|
||||
.unwrap_or_default();
|
||||
trigger_to_c(&trigger.cval())
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("error finding trigger err={err:?}");
|
||||
default_trigger()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of diagnostics.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a valid config handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_config_diagnostics_count(ptr: ghostty_config_t) -> u32 {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(c) = (unsafe { cconfig(ptr) }) else {
|
||||
return 0;
|
||||
};
|
||||
u32::try_from(c.config.diagnostics.items().len()).unwrap_or(u32::MAX)
|
||||
}
|
||||
|
||||
/// The diagnostic at the given index (an empty message if out of range).
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a valid config handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_config_get_diagnostic(
|
||||
ptr: ghostty_config_t,
|
||||
idx: u32,
|
||||
) -> ghostty_diagnostic_s {
|
||||
let empty = ghostty_diagnostic_s {
|
||||
message: c"".as_ptr(),
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
let Some(c) = (unsafe { cconfig(ptr) }) else {
|
||||
return empty;
|
||||
};
|
||||
let items = c.config.diagnostics.items();
|
||||
let idx = idx as usize;
|
||||
if idx >= items.len() {
|
||||
return empty;
|
||||
}
|
||||
|
||||
// Precompute (like libghostty's `DiagnosticList.precompute`): the
|
||||
// messages are formatted once per diagnostic and live as long as the
|
||||
// config.
|
||||
let mut msgs = c.diagnostics.borrow_mut();
|
||||
while msgs.len() < items.len() {
|
||||
let mut text = String::new();
|
||||
let _ = items[msgs.len()].format(&mut text);
|
||||
msgs.push(cstring(&text));
|
||||
}
|
||||
ghostty_diagnostic_s {
|
||||
message: msgs[idx].as_ptr(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The path of the config file to open in an editor (created if needed).
|
||||
/// Free with `ghostty_string_free`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn ghostty_config_open_path() -> ghostty_string_s {
|
||||
match ghostty_config::edit::open_path() {
|
||||
Ok(path) => string_from_owned(&path),
|
||||
Err(err) => {
|
||||
log::error!("error opening config in editor err={err}");
|
||||
EMPTY_STRING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ffi::c_uint;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn get<T: Copy>(cfg: ghostty_config_t, key: &str, init: T) -> (bool, T) {
|
||||
let mut out = init;
|
||||
// SAFETY: `out` is a valid destination of the key's C type.
|
||||
let ok = unsafe {
|
||||
ghostty_config_get(cfg, (&raw mut out).cast(), key.as_ptr().cast(), key.len())
|
||||
};
|
||||
(ok, out)
|
||||
}
|
||||
|
||||
fn with_config(f: impl FnOnce(&mut Config)) -> ghostty_config_t {
|
||||
let h = ghostty_config_new();
|
||||
// SAFETY: just created.
|
||||
f(&mut unsafe { cconfig(h) }.unwrap().config);
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ghostty_config_get_bool() {
|
||||
let h = with_config(|c| c.maximize = true);
|
||||
assert_eq!(get(h, "maximize", false), (true, true));
|
||||
// SAFETY: valid handle.
|
||||
unsafe { ghostty_config_free(h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ghostty_config_get_enum() {
|
||||
let h = with_config(|c| c.window_theme = ghostty_config::types_b::WindowTheme::Dark);
|
||||
let (ok, p) = get::<*const c_char>(h, "window-theme", std::ptr::null());
|
||||
assert!(ok);
|
||||
// SAFETY: an interned NUL terminated string.
|
||||
assert_eq!(unsafe { CStr::from_ptr(p) }.to_str().unwrap(), "dark");
|
||||
// SAFETY: valid handle.
|
||||
unsafe { ghostty_config_free(h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ghostty_config_get_optional_null_returns_false() {
|
||||
let h = with_config(|c| c.unfocused_split_fill = None);
|
||||
let (ok, _) = get(
|
||||
h,
|
||||
"unfocused-split-fill",
|
||||
ghostty_config_color_s { r: 0, g: 0, b: 0 },
|
||||
);
|
||||
assert!(!ok);
|
||||
// SAFETY: valid handle.
|
||||
unsafe { ghostty_config_free(h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ghostty_config_get_unknown_key_returns_false() {
|
||||
let h = ghostty_config_new();
|
||||
assert!(!get(h, "not-a-real-key", false).0);
|
||||
// SAFETY: valid handle.
|
||||
unsafe { ghostty_config_free(h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ghostty_config_get_optional_string_null_returns_true() {
|
||||
let h = with_config(|c| c.title = None);
|
||||
let (ok, p) = get::<*const c_char>(h, "title", std::ptr::dangling::<c_char>());
|
||||
assert!(ok);
|
||||
assert!(p.is_null());
|
||||
// SAFETY: valid handle.
|
||||
unsafe { ghostty_config_free(h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ghostty_config_get_float() {
|
||||
let h = with_config(|c| c.background_opacity = 0.42);
|
||||
let (ok, v) = get(h, "background-opacity", 0.0f64);
|
||||
assert!(ok);
|
||||
assert!((v - 0.42).abs() < 1e-6);
|
||||
// SAFETY: valid handle.
|
||||
unsafe { ghostty_config_free(h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ghostty_config_get_struct_cval_conversion() {
|
||||
let h = with_config(|c| {
|
||||
c.background = ghostty_config::types_a::Color {
|
||||
r: 12,
|
||||
g: 34,
|
||||
b: 56,
|
||||
};
|
||||
});
|
||||
let (ok, v) = get(h, "background", ghostty_config_color_s { r: 0, g: 0, b: 0 });
|
||||
assert!(ok);
|
||||
assert_eq!((v.r, v.g, v.b), (12, 34, 56));
|
||||
// SAFETY: valid handle.
|
||||
unsafe { ghostty_config_free(h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_get_u8() {
|
||||
let h = with_config(|c| c.font_size = 24.0);
|
||||
assert_eq!(get(h, "font-size", 0.0f32), (true, 24.0));
|
||||
// SAFETY: valid handle.
|
||||
unsafe { ghostty_config_free(h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_get_split_preserve_zoom() {
|
||||
let h = ghostty_config_new();
|
||||
assert_eq!(get(h, "split-preserve-zoom", 9 as c_uint), (true, 0));
|
||||
// SAFETY: valid handle.
|
||||
unsafe {
|
||||
cconfig(h).unwrap().config.split_preserve_zoom.navigation = true;
|
||||
}
|
||||
assert_eq!(get(h, "split-preserve-zoom", 9 as c_uint), (true, 1));
|
||||
// SAFETY: valid handle.
|
||||
unsafe { ghostty_config_free(h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_get_pointers_are_stable_for_equal_values() {
|
||||
let h = ghostty_config_new();
|
||||
let (_, a) = get::<ghostty_config_command_list_s>(
|
||||
h,
|
||||
"command-palette-entry",
|
||||
ghostty_config_command_list_s {
|
||||
commands: std::ptr::null(),
|
||||
len: 0,
|
||||
},
|
||||
);
|
||||
let (_, b) = get::<ghostty_config_command_list_s>(
|
||||
h,
|
||||
"command-palette-entry",
|
||||
ghostty_config_command_list_s {
|
||||
commands: std::ptr::null(),
|
||||
len: 0,
|
||||
},
|
||||
);
|
||||
assert!(a.len > 0);
|
||||
assert_eq!(a.commands, b.commands);
|
||||
// SAFETY: valid handle.
|
||||
unsafe { ghostty_config_free(h) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2157 @@
|
||||
//! The embedded application runtime (ZIG/src/apprt/embedded.zig): the
|
||||
//! runtime side of `ghostty-core` implemented over the C callbacks of
|
||||
//! `ghostty_runtime_config_s`, plus the `ghostty_app_*` / `ghostty_surface_*`
|
||||
//! exports.
|
||||
//!
|
||||
//! Everything here is main-thread only, like the Zig embedded runtime: the
|
||||
//! host calls the C API from its UI thread. The only cross-thread entry is
|
||||
//! the `wakeup` callback, invoked by core threads through [`WakerImpl`].
|
||||
//!
|
||||
//! Handles: `ghostty_app_t` is a `Box<AppHandle>` and `ghostty_surface_t` a
|
||||
//! `Box<Surface>`. The address of the surface object doubles as the
|
||||
//! `SurfaceId` core knows the surface by, and as the `ghostty_surface_t` in
|
||||
//! `ghostty_target_s` of actions (Zig: `target.cval()` gives `rt_surface`).
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::{
|
||||
cell::{Cell, OnceCell, RefCell},
|
||||
collections::HashMap,
|
||||
ffi::{CStr, CString, c_char, c_int, c_void},
|
||||
fmt::Write as _,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use ghostty_apprt::{
|
||||
action::{Action, GotoSplit, ResizeSplit, ResizeSplitDirection, SplitDirection, Target},
|
||||
runtime_api::{CoreApp, CoreSurface, RtApp, RtSurface, SurfaceId, Waker},
|
||||
structs::{Clipboard, ClipboardRequest, ColorScheme, ContentScale, CursorPos, SurfaceSize},
|
||||
surface::{NewSurfaceContext, new_config, should_inherit_working_directory},
|
||||
};
|
||||
use ghostty_config::{command::Command, config::Config, io::ReadableIO, types_a::WorkingDirectory};
|
||||
use ghostty_core::{App as CoreAppImpl, Surface as CoreSurfaceImpl, surface::binding::Text};
|
||||
use ghostty_input::{
|
||||
Action as KeyAction,
|
||||
Key,
|
||||
KeyEvent,
|
||||
Mods,
|
||||
MouseButton,
|
||||
MouseButtonState,
|
||||
MousePressureStage,
|
||||
ScrollMods,
|
||||
binding::Action as BindingAction,
|
||||
keycodes,
|
||||
};
|
||||
use ghostty_terminal::{
|
||||
point::{Coordinate, Point, Tag as PointTag},
|
||||
selection::Selection,
|
||||
terminal::Terminal,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
action_c::{Keep, action_to_c, target_to_c},
|
||||
config_capi::cconfig,
|
||||
main_c::{alloc_c_text, free_c_text},
|
||||
types::{
|
||||
ghostty_app_t,
|
||||
ghostty_binding_flags_e,
|
||||
ghostty_clipboard_content_s,
|
||||
ghostty_config_t,
|
||||
ghostty_input_key_s,
|
||||
ghostty_point_s,
|
||||
ghostty_runtime_config_s,
|
||||
ghostty_selection_s,
|
||||
ghostty_surface_config_s,
|
||||
ghostty_surface_size_s,
|
||||
ghostty_surface_t,
|
||||
ghostty_text_s,
|
||||
platform,
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Waker
|
||||
|
||||
/// `App.wakeup`: invoked from any core thread; calls the host's `wakeup_cb`.
|
||||
struct WakerImpl {
|
||||
cb: unsafe extern "C" fn(*mut c_void),
|
||||
userdata: usize,
|
||||
}
|
||||
|
||||
impl Waker for WakerImpl {
|
||||
fn wake(&self) {
|
||||
// SAFETY: the embedder promises `wakeup_cb` may be called from any
|
||||
// thread with the `userdata` it supplied (the documented contract of
|
||||
// `ghostty_runtime_wakeup_cb`).
|
||||
unsafe { (self.cb)(self.userdata as *mut c_void) };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Runtime app
|
||||
|
||||
/// The runtime side of the app (Zig: `embedded.App`), handed to core as
|
||||
/// `Rc<dyn RtApp>`.
|
||||
pub struct AppRt {
|
||||
opts: ghostty_runtime_config_s,
|
||||
/// The `ghostty_app_t` passed to the action callback.
|
||||
app_ptr: Cell<usize>,
|
||||
/// The configuration for the app (a clone, owned by the app).
|
||||
config: RefCell<Arc<Config>>,
|
||||
surfaces: RefCell<HashMap<SurfaceId, Weak<SurfaceRt>>>,
|
||||
keymap: RefCell<ghostty_input::Keymap>,
|
||||
}
|
||||
|
||||
impl AppRt {
|
||||
fn surface_rt(&self, id: SurfaceId) -> Option<Rc<SurfaceRt>> {
|
||||
self.surfaces.borrow().get(&id).and_then(Weak::upgrade)
|
||||
}
|
||||
|
||||
/// Special case certain actions before they are sent to the embedder
|
||||
/// (Zig: `performPreAction`).
|
||||
fn pre_action(&self, target: &Target<SurfaceId>, action: &Action) {
|
||||
match (action, target) {
|
||||
(Action::SetTitle(t), Target::Surface(id)) => {
|
||||
if let Some(s) = self.surface_rt(*id) {
|
||||
*s.title.borrow_mut() = Some(t.title.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// For app updates, we update our core config. The caller owns
|
||||
// the param so we clone it.
|
||||
(Action::ConfigChange(c), Target::App) => {
|
||||
*self.config.borrow_mut() = Arc::clone(&c.config);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reload the keyboard layout (`App.reloadKeymap`).
|
||||
fn reload_keymap(&self) {
|
||||
if let Err(err) = self.keymap.borrow_mut().reload() {
|
||||
log::error!("error reloading keyboard map err={err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RtApp for AppRt {
|
||||
fn perform_action(&self, target: Target<SurfaceId>, action: Action) -> bool {
|
||||
self.pre_action(&target, &action);
|
||||
|
||||
log::debug!(
|
||||
"dispatching action target={target:?} action={:?}",
|
||||
action.key()
|
||||
);
|
||||
let Some(cb) = self.opts.action_cb else {
|
||||
return false;
|
||||
};
|
||||
let mut keep = Keep::default();
|
||||
let t = target_to_c(&target);
|
||||
let a = action_to_c(&action, &mut keep);
|
||||
// SAFETY: the embedder supplied the callback; it receives the app
|
||||
// handle it knows plus plain-data structs whose pointers stay valid
|
||||
// (`keep`) for the duration of the call.
|
||||
unsafe { cb(self.app_ptr.get() as ghostty_app_t, t, a) }
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> ghostty_input::KeyboardLayout {
|
||||
keyboard_layout(&self.keymap.borrow())
|
||||
}
|
||||
}
|
||||
|
||||
/// `App.keyboardLayout`: we only support keyboard layout detection on macOS.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn keyboard_layout(keymap: &ghostty_input::Keymap) -> ghostty_input::KeyboardLayout {
|
||||
keymap
|
||||
.source_id()
|
||||
.and_then(|id| ghostty_input::KeyboardLayout::map_apple_id(&id))
|
||||
.unwrap_or(ghostty_input::KeyboardLayout::Unknown)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn keyboard_layout(_keymap: &ghostty_input::Keymap) -> ghostty_input::KeyboardLayout {
|
||||
ghostty_input::KeyboardLayout::Unknown
|
||||
}
|
||||
|
||||
/// The app object behind `ghostty_app_t`.
|
||||
pub struct AppHandle {
|
||||
core: Rc<CoreAppImpl>,
|
||||
rt: Rc<AppRt>,
|
||||
}
|
||||
|
||||
/// Borrow the app behind a handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// `p` must be null or a live handle from `ghostty_app_new`.
|
||||
unsafe fn app<'a>(p: ghostty_app_t) -> Option<&'a AppHandle> {
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { p.cast::<AppHandle>().as_ref() }
|
||||
}
|
||||
|
||||
/// Borrow a C string.
|
||||
///
|
||||
/// # Safety
|
||||
/// `p` must be null or NUL terminated.
|
||||
unsafe fn cstr<'a>(p: *const c_char) -> Option<&'a CStr> {
|
||||
if p.is_null() {
|
||||
None
|
||||
} else {
|
||||
// SAFETY: per the function contract.
|
||||
Some(unsafe { CStr::from_ptr(p) })
|
||||
}
|
||||
}
|
||||
|
||||
fn mods_from_c(v: c_int) -> Mods {
|
||||
// `@truncate(@as(c_uint, @bitCast(mods)))` to the u16 backing.
|
||||
Mods::from_int(v as u32 as u16)
|
||||
}
|
||||
|
||||
/// An owned key event converted from the C struct (Zig: `KeyEvent.core`).
|
||||
struct OwnedKey {
|
||||
action: KeyAction,
|
||||
key: Key,
|
||||
mods: Mods,
|
||||
consumed_mods: Mods,
|
||||
composing: bool,
|
||||
text: String,
|
||||
unshifted_codepoint: u32,
|
||||
}
|
||||
|
||||
impl OwnedKey {
|
||||
/// `None` if the action is invalid.
|
||||
///
|
||||
/// # Safety
|
||||
/// `text` must be null or NUL terminated.
|
||||
unsafe fn from_c(e: &ghostty_input_key_s) -> Option<OwnedKey> {
|
||||
let action = match e.action {
|
||||
0 => KeyAction::Release,
|
||||
1 => KeyAction::Press,
|
||||
2 => KeyAction::Repeat,
|
||||
_ => return None,
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
let text = unsafe { cstr(e.text) }
|
||||
.map(|c| c.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
|
||||
// We want to get the physical unmapped key to process keybinds.
|
||||
let key = keycodes::entries()
|
||||
.iter()
|
||||
.find(|entry| entry.native == e.keycode)
|
||||
.map_or(Key::Unidentified, |entry| entry.key);
|
||||
|
||||
// The unshifted codepoint is a `u21`; anything larger is 0.
|
||||
let unshifted_codepoint = if e.unshifted_codepoint <= 0x1F_FFFF {
|
||||
e.unshifted_codepoint
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Some(OwnedKey {
|
||||
action,
|
||||
key,
|
||||
mods: mods_from_c(e.mods),
|
||||
consumed_mods: mods_from_c(e.consumed_mods),
|
||||
composing: e.composing,
|
||||
text,
|
||||
unshifted_codepoint,
|
||||
})
|
||||
}
|
||||
|
||||
fn event(&self) -> KeyEvent<'_> {
|
||||
KeyEvent {
|
||||
action: self.action,
|
||||
key: self.key,
|
||||
mods: self.mods,
|
||||
consumed_mods: self.consumed_mods,
|
||||
composing: self.composing,
|
||||
utf8: &self.text,
|
||||
unshifted_codepoint: self.unshifted_codepoint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// App exports
|
||||
|
||||
/// Create a new app.
|
||||
///
|
||||
/// # Safety
|
||||
/// `opts` and `config` must be valid pointers (the config a handle from
|
||||
/// `ghostty_config_new`/`_clone`).
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_new(
|
||||
opts: *const ghostty_runtime_config_s,
|
||||
config: ghostty_config_t,
|
||||
) -> ghostty_app_t {
|
||||
// SAFETY: per the function contract.
|
||||
let (Some(opts), Some(config)) = (unsafe { opts.as_ref() }, unsafe { cconfig(config) }) else {
|
||||
log::error!("error initializing app err=NullArgument");
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
let Some(wakeup) = opts.wakeup_cb else {
|
||||
log::error!("error initializing app err=MissingWakeupCallback");
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
|
||||
let waker = Arc::new(WakerImpl {
|
||||
cb: wakeup,
|
||||
userdata: opts.userdata as usize,
|
||||
});
|
||||
let core = match CoreAppImpl::new(waker) {
|
||||
Ok(c) => Rc::new(c),
|
||||
Err(err) => {
|
||||
log::error!("error initializing app err={err}");
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let keymap = match ghostty_input::Keymap::new() {
|
||||
Ok(k) => k,
|
||||
Err(err) => {
|
||||
log::error!("error initializing app err={err:?}");
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
};
|
||||
|
||||
// We have to clone the config.
|
||||
let rt = Rc::new(AppRt {
|
||||
opts: *opts,
|
||||
app_ptr: Cell::new(0),
|
||||
config: RefCell::new(Arc::new(config.config.clone())),
|
||||
surfaces: RefCell::default(),
|
||||
keymap: RefCell::new(keymap),
|
||||
});
|
||||
let ptr = Box::into_raw(Box::new(AppHandle { core, rt }));
|
||||
// SAFETY: just allocated.
|
||||
unsafe { &*ptr }.rt.app_ptr.set(ptr as usize);
|
||||
ptr.cast::<c_void>()
|
||||
}
|
||||
|
||||
/// Tick the event loop. This should be called whenever the "wakeup" callback
|
||||
/// is invoked for the runtime.
|
||||
///
|
||||
/// # Safety
|
||||
/// `v` must be a live app handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_tick(v: ghostty_app_t) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(a) = (unsafe { app(v) }) else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = a.core.tick(&*a.rt) {
|
||||
log::error!("error app tick err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the userdata associated with the app.
|
||||
///
|
||||
/// # Safety
|
||||
/// `v` must be a live app handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_userdata(v: ghostty_app_t) -> *mut c_void {
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { app(v) }.map_or(std::ptr::null_mut(), |a| a.rt.opts.userdata)
|
||||
}
|
||||
|
||||
/// Free the app.
|
||||
///
|
||||
/// # Safety
|
||||
/// `v` must be a live app handle with no surfaces left; it is invalid
|
||||
/// afterwards.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_free(v: ghostty_app_t) {
|
||||
if v.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: created by `ghostty_app_new` via `Box::into_raw`.
|
||||
drop(unsafe { Box::from_raw(v.cast::<AppHandle>()) });
|
||||
}
|
||||
|
||||
/// Update the focused state of the app.
|
||||
///
|
||||
/// # Safety
|
||||
/// `app` must be a live app handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_set_focus(handle: ghostty_app_t, focused: bool) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(a) = unsafe { app(handle) } {
|
||||
a.core.focus_event(focused);
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify the app of a global keypress capture. This will return true if the
|
||||
/// key was captured by the app, in which case the caller should not process
|
||||
/// the key.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be a live app handle; `event.text` null or NUL terminated.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_key(
|
||||
handle: ghostty_app_t,
|
||||
event: ghostty_input_key_s,
|
||||
) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(a) = (unsafe { app(handle) }) else {
|
||||
return false;
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
let Some(key) = (unsafe { OwnedKey::from_c(&event) }) else {
|
||||
return false;
|
||||
};
|
||||
// Never hold the borrow across the call: actions call into the host.
|
||||
let config = Arc::clone(&a.rt.config.borrow());
|
||||
a.core.key_event(&*a.rt, &config, &key.event())
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a binding if it were
|
||||
/// sent to the surface right now. The "right now" is important because
|
||||
/// things like trigger sequences are only valid until the next key event.
|
||||
///
|
||||
/// # Safety
|
||||
/// As [`ghostty_app_key`].
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_key_is_binding(
|
||||
handle: ghostty_app_t,
|
||||
event: ghostty_input_key_s,
|
||||
) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(a) = (unsafe { app(handle) }) else {
|
||||
return false;
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
let Some(key) = (unsafe { OwnedKey::from_c(&event) }) else {
|
||||
log::warn!("error processing key event");
|
||||
return false;
|
||||
};
|
||||
let config = Arc::clone(&a.rt.config.borrow());
|
||||
a.core.key_event_is_binding(&config, &key.event())
|
||||
}
|
||||
|
||||
/// Notify the app that the keyboard was changed. This causes the keyboard
|
||||
/// layout to be reloaded from the OS.
|
||||
///
|
||||
/// # Safety
|
||||
/// `v` must be a live app handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_keyboard_changed(v: ghostty_app_t) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(a) = unsafe { app(v) } {
|
||||
a.rt.reload_keymap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the configuration.
|
||||
///
|
||||
/// # Safety
|
||||
/// `v` must be a live app handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_open_config(v: ghostty_app_t) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(a) = unsafe { app(v) } {
|
||||
a.rt.perform_action(Target::App, Action::OpenConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the configuration to the provided config. This will propagate to
|
||||
/// all surfaces as well.
|
||||
///
|
||||
/// # Safety
|
||||
/// `v` a live app handle, `config` a config handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_update_config(v: ghostty_app_t, config: ghostty_config_t) {
|
||||
// SAFETY: per the function contract.
|
||||
let (Some(a), Some(c)) = (unsafe { app(v) }, unsafe { cconfig(config) }) else {
|
||||
return;
|
||||
};
|
||||
let cfg = Arc::new(c.config.clone());
|
||||
if let Err(err) = a.core.update_config(&*a.rt, &cfg) {
|
||||
log::error!("error updating config err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the app needs to confirm quitting.
|
||||
///
|
||||
/// # Safety
|
||||
/// `v` must be a live app handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_needs_confirm_quit(v: ghostty_app_t) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { app(v) }.is_some_and(|a| a.core.needs_confirm_quit())
|
||||
}
|
||||
|
||||
/// Returns true if the app has global keybinds.
|
||||
///
|
||||
/// # Safety
|
||||
/// `v` must be a live app handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_has_global_keybinds(v: ghostty_app_t) -> bool {
|
||||
use ghostty_input::binding::set::Value;
|
||||
// SAFETY: per the function contract.
|
||||
let Some(a) = (unsafe { app(v) }) else {
|
||||
return false;
|
||||
};
|
||||
let config = a.rt.config.borrow();
|
||||
config
|
||||
.keybind
|
||||
.set
|
||||
.bindings
|
||||
.values()
|
||||
.any(|value| match value {
|
||||
Value::Leader(_) => false,
|
||||
Value::Leaf(leaf) => leaf.flags.global,
|
||||
Value::LeafChained(leaf) => leaf.flags.global,
|
||||
})
|
||||
}
|
||||
|
||||
fn color_scheme_from_c(v: c_int) -> Option<ColorScheme> {
|
||||
match v {
|
||||
0 => Some(ColorScheme::Light),
|
||||
1 => Some(ColorScheme::Dark),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the color scheme of the app.
|
||||
///
|
||||
/// # Safety
|
||||
/// `v` must be a live app handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_app_set_color_scheme(v: ghostty_app_t, scheme_raw: c_int) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(a) = (unsafe { app(v) }) else {
|
||||
return;
|
||||
};
|
||||
let Some(scheme) = color_scheme_from_c(scheme_raw) else {
|
||||
log::warn!("invalid color scheme to ghostty_app_set_color_scheme value={scheme_raw}");
|
||||
return;
|
||||
};
|
||||
a.core.color_scheme_event(&*a.rt, scheme);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Runtime surface
|
||||
|
||||
/// The runtime side of a surface (Zig: `embedded.Surface` fields that core
|
||||
/// reads), handed to core as `Rc<dyn RtSurface>`.
|
||||
pub struct SurfaceRt {
|
||||
app: Rc<AppRt>,
|
||||
id: SurfaceId,
|
||||
userdata: *mut c_void,
|
||||
/// The `NSView` / `UIView` of the surface.
|
||||
view: *mut c_void,
|
||||
content_scale: Cell<ContentScale>,
|
||||
size: Cell<SurfaceSize>,
|
||||
cursor_pos: Cell<CursorPos>,
|
||||
/// The current title of the surface. The embedded apprt saves this so
|
||||
/// that `get_title` works without the implementer needing to save it.
|
||||
title: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
impl SurfaceRt {
|
||||
fn target(&self) -> Target<SurfaceId> {
|
||||
Target::Surface(self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl RtSurface for SurfaceRt {
|
||||
fn close(&self, process_active: bool) {
|
||||
let Some(cb) = self.app.opts.close_surface_cb else {
|
||||
log::info!("runtime embedder does not support closing a surface");
|
||||
return;
|
||||
};
|
||||
// SAFETY: embedder supplied callback with its own userdata.
|
||||
unsafe { cb(self.userdata, process_active) };
|
||||
}
|
||||
|
||||
fn cgroup(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_title(&self) -> Option<String> {
|
||||
self.title.borrow().clone()
|
||||
}
|
||||
|
||||
fn get_content_scale(&self) -> ContentScale {
|
||||
self.content_scale.get()
|
||||
}
|
||||
|
||||
fn get_size(&self) -> SurfaceSize {
|
||||
self.size.get()
|
||||
}
|
||||
|
||||
fn get_cursor_pos(&self) -> CursorPos {
|
||||
self.cursor_pos.get()
|
||||
}
|
||||
|
||||
fn supports_clipboard(&self, clipboard: Clipboard) -> bool {
|
||||
match clipboard {
|
||||
Clipboard::Standard => true,
|
||||
Clipboard::Selection | Clipboard::Primary => self.app.opts.supports_selection_clipboard,
|
||||
}
|
||||
}
|
||||
|
||||
fn clipboard_request(&self, clipboard: Clipboard, state: ClipboardRequest) -> bool {
|
||||
// We need to allocate to get a pointer to store our clipboard
|
||||
// request so that it is stable until the read_clipboard callback
|
||||
// and call complete_clipboard_request. This sucks but clipboard
|
||||
// requests aren't high throughput so it's probably fine.
|
||||
let Some(cb) = self.app.opts.read_clipboard_cb else {
|
||||
return false;
|
||||
};
|
||||
let state_ptr = Box::into_raw(Box::new(state));
|
||||
// SAFETY: embedder callback; `state_ptr` stays valid until
|
||||
// `ghostty_surface_complete_clipboard_request` or we reclaim it.
|
||||
let started = unsafe {
|
||||
cb(
|
||||
self.userdata,
|
||||
c_int::from(clipboard as u8),
|
||||
state_ptr.cast::<c_void>(),
|
||||
)
|
||||
};
|
||||
if !started {
|
||||
// SAFETY: allocated above and not handed over.
|
||||
drop(unsafe { Box::from_raw(state_ptr) });
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn set_clipboard(&self, clipboard: Clipboard, contents: &[(String, String)], confirm: bool) {
|
||||
let Some(cb) = self.app.opts.write_clipboard_cb else {
|
||||
return;
|
||||
};
|
||||
let owned: Vec<(CString, CString)> = contents
|
||||
.iter()
|
||||
.map(|(m, d)| {
|
||||
(
|
||||
CString::new(m.replace('\0', "")).unwrap_or_default(),
|
||||
CString::new(d.replace('\0', "")).unwrap_or_default(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let array: Vec<ghostty_clipboard_content_s> = owned
|
||||
.iter()
|
||||
.map(|(m, d)| ghostty_clipboard_content_s {
|
||||
mime: m.as_ptr(),
|
||||
data: d.as_ptr(),
|
||||
})
|
||||
.collect();
|
||||
// SAFETY: embedder callback; the array and strings outlive the call.
|
||||
unsafe {
|
||||
cb(
|
||||
self.userdata,
|
||||
c_int::from(clipboard as u8),
|
||||
array.as_ptr(),
|
||||
array.len(),
|
||||
confirm,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn default_termio_env(&self) -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
|
||||
let mut env = ghostty_os::env::get_env_map();
|
||||
|
||||
if cfg!(target_vendor = "apple") {
|
||||
if env.contains_key("__XCODE_BUILT_PRODUCTS_DIR_PATHS") {
|
||||
for k in [
|
||||
"__XCODE_BUILT_PRODUCTS_DIR_PATHS",
|
||||
"__XPC_DYLD_LIBRARY_PATH",
|
||||
"DYLD_FRAMEWORK_PATH",
|
||||
"DYLD_INSERT_LIBRARIES",
|
||||
"DYLD_LIBRARY_PATH",
|
||||
"LD_LIBRARY_PATH",
|
||||
"SECURITYSESSIONID",
|
||||
"XPC_SERVICE_NAME",
|
||||
] {
|
||||
env.remove(k);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove this so that running `ghostty` within Ghostty works.
|
||||
env.remove("GHOSTTY_MAC_LAUNCH_SOURCE");
|
||||
|
||||
// If we were launched from the desktop then we want to remove
|
||||
// the LANGUAGE env var so that we don't inherit our translation
|
||||
// settings for Ghostty. If we aren't from the desktop then we
|
||||
// didn't set our LANGUAGE var so we don't need to remove it.
|
||||
if launched_from_desktop() {
|
||||
env.remove("LANGUAGE");
|
||||
}
|
||||
}
|
||||
|
||||
env.into_iter().collect()
|
||||
}
|
||||
|
||||
fn redraw_inspector(&self) {
|
||||
// `queueInspectorRender`
|
||||
self.app
|
||||
.perform_action(self.target(), Action::RenderInspector);
|
||||
}
|
||||
|
||||
fn platform_view(&self) -> *mut c_void {
|
||||
self.view
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-platform")]
|
||||
fn gl_proc_address(&self, name: &str) -> *const c_void {
|
||||
test_platform::proc_address(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn launched_from_desktop() -> bool {
|
||||
ghostty_os::desktop::launched_from_desktop_lib()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn launched_from_desktop() -> bool {
|
||||
ghostty_os::desktop::launched_from_desktop()
|
||||
}
|
||||
|
||||
/// The surface object behind `ghostty_surface_t`.
|
||||
pub struct Surface {
|
||||
app: Rc<AppHandle>,
|
||||
rt: Rc<SurfaceRt>,
|
||||
core: OnceCell<Rc<CoreSurfaceImpl>>,
|
||||
inspector: RefCell<Option<Box<crate::inspector::Inspector>>>,
|
||||
/// Strings handed out by `ghostty_surface_inherited_config`; they stay
|
||||
/// valid until the surface is freed.
|
||||
inherited_wd: RefCell<Vec<CString>>,
|
||||
}
|
||||
|
||||
impl Surface {
|
||||
fn core(&self) -> Option<&Rc<CoreSurfaceImpl>> {
|
||||
self.core.get()
|
||||
}
|
||||
|
||||
pub(crate) fn inspector_slot(&self) -> &RefCell<Option<Box<crate::inspector::Inspector>>> {
|
||||
&self.inspector
|
||||
}
|
||||
|
||||
pub(crate) fn core_surface(&self) -> Option<&Rc<CoreSurfaceImpl>> {
|
||||
self.core()
|
||||
}
|
||||
|
||||
pub(crate) fn rt(&self) -> &Rc<SurfaceRt> {
|
||||
&self.rt
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Surface {
|
||||
fn drop(&mut self) {
|
||||
// Shut down our inspector
|
||||
if let Some(mut i) = self.inspector.get_mut().take() {
|
||||
i.deinit(self.core.get().map(Rc::as_ref));
|
||||
}
|
||||
|
||||
// Remove ourselves from the list of known surfaces in the app.
|
||||
self.app.core.delete_surface(self.rt.id);
|
||||
self.app.rt.surfaces.borrow_mut().remove(&self.rt.id);
|
||||
|
||||
// Clean up our core surface so that all the rendering and IO stop.
|
||||
drop(self.core.take());
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrow the surface behind a handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// `p` must be null or a live handle from `ghostty_surface_new`.
|
||||
pub(crate) unsafe fn surface<'a>(p: ghostty_surface_t) -> Option<&'a Surface> {
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { p.cast::<Surface>().as_ref() }
|
||||
}
|
||||
|
||||
/// `ghostty_surface_config_new`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn ghostty_surface_config_new() -> ghostty_surface_config_s {
|
||||
ghostty_surface_config_s {
|
||||
platform_tag: platform::INVALID,
|
||||
platform: crate::types::ghostty_platform_u {
|
||||
macos: crate::types::ghostty_platform_macos_s {
|
||||
nsview: std::ptr::null_mut(),
|
||||
},
|
||||
},
|
||||
userdata: std::ptr::null_mut(),
|
||||
scale_factor: 1.0,
|
||||
font_size: 0.0,
|
||||
working_directory: std::ptr::null(),
|
||||
command: std::ptr::null(),
|
||||
env_vars: std::ptr::null_mut(),
|
||||
env_var_count: 0,
|
||||
initial_input: std::ptr::null(),
|
||||
wait_after_command: false,
|
||||
context: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn context_from_c(v: c_int) -> NewSurfaceContext {
|
||||
match v {
|
||||
1 => NewSurfaceContext::Tab,
|
||||
2 => NewSurfaceContext::Split,
|
||||
_ => NewSurfaceContext::Window,
|
||||
}
|
||||
}
|
||||
|
||||
/// Test seam (feature `test-platform`): a fake platform whose OpenGL loader
|
||||
/// the test installs, so surfaces can be created on Linux.
|
||||
#[cfg(feature = "test-platform")]
|
||||
pub mod test_platform {
|
||||
use std::{cell::RefCell, ffi::c_void, rc::Rc};
|
||||
|
||||
/// The `platform_tag` that selects the fake platform.
|
||||
pub const PLATFORM_TAG: i32 = 1000;
|
||||
|
||||
/// A GL function loader.
|
||||
pub type GlLoader = Rc<dyn Fn(&str) -> *const c_void>;
|
||||
|
||||
thread_local! {
|
||||
static LOADER: RefCell<Option<GlLoader>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Install the GL loader for surfaces created on this thread.
|
||||
pub fn set_gl_loader(loader: Option<GlLoader>) {
|
||||
LOADER.with(|l| *l.borrow_mut() = loader);
|
||||
}
|
||||
|
||||
pub(super) fn proc_address(name: &str) -> *const c_void {
|
||||
LOADER.with(|l| l.borrow().as_ref().map_or(std::ptr::null(), |f| f(name)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the platform view (Zig: `Platform.init`). Darwin only.
|
||||
///
|
||||
/// # Safety
|
||||
/// The union member selected by `tag` must be initialized.
|
||||
unsafe fn platform_view_from_c(
|
||||
opts: &ghostty_surface_config_s,
|
||||
) -> Result<*mut c_void, &'static str> {
|
||||
#[cfg(feature = "test-platform")]
|
||||
if opts.platform_tag == test_platform::PLATFORM_TAG {
|
||||
// Any non-null "view" will do; the renderer is OpenGL here.
|
||||
return Ok(std::ptr::NonNull::<c_void>::dangling().as_ptr());
|
||||
}
|
||||
if !cfg!(target_vendor = "apple") {
|
||||
// The platform union has no members off Darwin.
|
||||
return Err("UnsupportedPlatform");
|
||||
}
|
||||
match opts.platform_tag {
|
||||
// SAFETY: the tag selects the initialized member.
|
||||
platform::MACOS => match unsafe { opts.platform.macos.nsview } {
|
||||
v if v.is_null() => Err("NSViewMustBeSet"),
|
||||
v => Ok(v),
|
||||
},
|
||||
// SAFETY: as above.
|
||||
platform::IOS => match unsafe { opts.platform.ios.uiview } {
|
||||
v if v.is_null() => Err("UIViewMustBeSet"),
|
||||
v => Ok(v),
|
||||
},
|
||||
_ => Err("InvalidEnumTag"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Zig's `std.zig.stringEscape` with non-ASCII code points written as
|
||||
/// `\u{...}` (the config value is parsed as a Zig string; the bytes of
|
||||
/// valid UTF-8 round trip either way).
|
||||
fn string_escape(input: &str) -> String {
|
||||
let mut out = String::with_capacity(input.len());
|
||||
for c in input.chars() {
|
||||
match c {
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'"' => out.push_str("\\\""),
|
||||
' ' | '!' | '#'..='&' | '\'' | '('..='[' | ']'..='~' => out.push(c),
|
||||
c if c.is_ascii() => {
|
||||
let _ = write!(out, "\\x{:02x}", c as u32);
|
||||
}
|
||||
c => {
|
||||
let _ = write!(out, "\\u{{{:x}}}", c as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Apply the surface options to the config (the first half of Zig's
|
||||
/// `Surface.init`).
|
||||
///
|
||||
/// # Safety
|
||||
/// The pointers in `opts` must satisfy the contract of `ghostty.h`.
|
||||
unsafe fn apply_surface_options(config: &mut Config, opts: &ghostty_surface_config_s) {
|
||||
// If we have a working directory from the options then we set it.
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(wd) =
|
||||
unsafe { cstr(opts.working_directory) }.map(|c| c.to_string_lossy().into_owned())
|
||||
&& !wd.is_empty()
|
||||
{
|
||||
match std::fs::metadata(&wd) {
|
||||
Err(err) => log::warn!("error opening requested working directory dir={wd} err={err}"),
|
||||
Ok(m) if !m.is_dir() => {
|
||||
log::warn!("requested working directory is not a directory dir={wd}");
|
||||
}
|
||||
Ok(_) => {
|
||||
let mut wd_val = WorkingDirectory::Path(wd);
|
||||
wd_val.finalize();
|
||||
config.working_directory = Some(wd_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a command from the options then we set it.
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(cmd) = unsafe { cstr(opts.command) }.map(|c| c.to_string_lossy().into_owned())
|
||||
&& !cmd.is_empty()
|
||||
{
|
||||
config.command = Some(Command::Shell(cmd));
|
||||
config.wait_after_command = true;
|
||||
}
|
||||
|
||||
// Apply any environment variables that were requested.
|
||||
if opts.env_var_count > 0 && !opts.env_vars.is_null() {
|
||||
// SAFETY: the caller supplies `env_var_count` entries.
|
||||
let vars = unsafe { std::slice::from_raw_parts(opts.env_vars, opts.env_var_count) };
|
||||
for var in vars {
|
||||
// SAFETY: entries are NUL terminated strings.
|
||||
let (Some(k), Some(v)) = (unsafe { cstr(var.key) }, unsafe { cstr(var.value) }) else {
|
||||
continue;
|
||||
};
|
||||
config.env.put(&k.to_string_lossy(), &v.to_string_lossy());
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an initial input then we set it.
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(input) = unsafe { cstr(opts.initial_input) } {
|
||||
// We need to escape the string because the "raw" field expects a
|
||||
// Zig string.
|
||||
config.input.list.clear();
|
||||
config
|
||||
.input
|
||||
.list
|
||||
.push(ReadableIO::Raw(string_escape(&input.to_string_lossy())));
|
||||
}
|
||||
|
||||
// Wait after command
|
||||
if opts.wait_after_command {
|
||||
config.wait_after_command = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new surface as part of an app.
|
||||
///
|
||||
/// # Safety
|
||||
/// `app_ptr` a live app handle; `opts` a valid config (see `ghostty.h`).
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_new(
|
||||
app_ptr: ghostty_app_t,
|
||||
opts: *const ghostty_surface_config_s,
|
||||
) -> ghostty_surface_t {
|
||||
// SAFETY: per the function contract.
|
||||
let (Some(app_ref), Some(opts)) = (unsafe { app(app_ptr) }, unsafe { opts.as_ref() }) else {
|
||||
log::error!("error initializing surface err=NullArgument");
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
let view = match unsafe { platform_view_from_c(opts) } {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
log::error!("error initializing surface err={err}");
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
};
|
||||
|
||||
// Build the runtime surface. Its address is its id.
|
||||
let scale = opts.scale_factor as f32;
|
||||
let app_handle = {
|
||||
// `AppHandle` is owned by the C handle; the surface only needs
|
||||
// shared access to its parts, which are `Rc`s.
|
||||
Rc::new(AppHandle {
|
||||
core: Rc::clone(&app_ref.core),
|
||||
rt: Rc::clone(&app_ref.rt),
|
||||
})
|
||||
};
|
||||
let mut boxed = Box::new(Surface {
|
||||
app: app_handle,
|
||||
rt: Rc::new(SurfaceRt {
|
||||
app: Rc::clone(&app_ref.rt),
|
||||
id: 0,
|
||||
userdata: opts.userdata,
|
||||
view,
|
||||
content_scale: Cell::new(ContentScale { x: scale, y: scale }),
|
||||
size: Cell::new(SurfaceSize {
|
||||
width: 800,
|
||||
height: 600,
|
||||
}),
|
||||
cursor_pos: Cell::new(CursorPos { x: -1.0, y: -1.0 }),
|
||||
title: RefCell::new(None),
|
||||
}),
|
||||
core: OnceCell::new(),
|
||||
inspector: RefCell::new(None),
|
||||
inherited_wd: RefCell::new(Vec::new()),
|
||||
});
|
||||
let id = std::ptr::from_mut::<Surface>(&mut *boxed) as usize as SurfaceId;
|
||||
// The id is only known now (the address of the heap object).
|
||||
if let Some(rt) = Rc::get_mut(&mut boxed.rt) {
|
||||
rt.id = id;
|
||||
}
|
||||
app_ref
|
||||
.rt
|
||||
.surfaces
|
||||
.borrow_mut()
|
||||
.insert(id, Rc::downgrade(&boxed.rt));
|
||||
let ptr = Box::into_raw(boxed);
|
||||
|
||||
// Shallow copy the config so that we can modify it.
|
||||
let mut config = {
|
||||
let app_config = app_ref.rt.config.borrow();
|
||||
let prev_pwd = app_ref.core.state().focused_surface().and_then(|s| s.pwd());
|
||||
new_config(&app_config, context_from_c(opts.context), prev_pwd)
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { apply_surface_options(&mut config, opts) };
|
||||
|
||||
// Initialize our surface right away. We're given a view that is ready
|
||||
// to use.
|
||||
// SAFETY: `ptr` is the live allocation made above.
|
||||
let s = unsafe { &*ptr };
|
||||
let rt_app: Rc<dyn RtApp> = Rc::clone(&app_ref.rt) as Rc<dyn RtApp>;
|
||||
let rt_surface: Rc<dyn RtSurface> = Rc::clone(&s.rt) as Rc<dyn RtSurface>;
|
||||
match app_ref.core.create_surface(id, config, rt_app, rt_surface) {
|
||||
Ok(core) => {
|
||||
let _ = s.core.set(Rc::clone(&core));
|
||||
// If our options requested a specific font-size, set that.
|
||||
if opts.font_size != 0.0 {
|
||||
let mut font_size = core.font_size();
|
||||
font_size.points = opts.font_size;
|
||||
if let Err(err) = core.set_font_size(font_size) {
|
||||
log::error!("error initializing surface err={err}");
|
||||
// SAFETY: allocated by us and not exposed yet.
|
||||
drop(unsafe { Box::from_raw(ptr) });
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
ptr.cast::<c_void>()
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("error initializing surface err={err}");
|
||||
// SAFETY: allocated by us and not exposed yet.
|
||||
drop(unsafe { Box::from_raw(ptr) });
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Free a surface.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle; it is invalid afterwards.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_free(ptr: ghostty_surface_t) {
|
||||
if ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: created by `ghostty_surface_new` via `Box::into_raw`.
|
||||
drop(unsafe { Box::from_raw(ptr.cast::<Surface>()) });
|
||||
}
|
||||
|
||||
/// Returns the userdata associated with the surface.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_userdata(ptr: ghostty_surface_t) -> *mut c_void {
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { surface(ptr) }.map_or(std::ptr::null_mut(), |s| s.rt.userdata)
|
||||
}
|
||||
|
||||
/// Returns the app associated with a surface.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_app(ptr: ghostty_surface_t) -> ghostty_app_t {
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { surface(ptr) }.map_or(std::ptr::null_mut(), |s| {
|
||||
s.rt.app.app_ptr.get() as ghostty_app_t
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the config to use for surfaces that inherit from this one.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_inherited_config(
|
||||
ptr: ghostty_surface_t,
|
||||
source: c_int,
|
||||
) -> ghostty_surface_config_s {
|
||||
let mut result = ghostty_surface_config_new();
|
||||
result.context = source;
|
||||
// SAFETY: per the function contract.
|
||||
let Some(s) = (unsafe { surface(ptr) }) else {
|
||||
return result;
|
||||
};
|
||||
let Some(core) = s.core() else {
|
||||
return result;
|
||||
};
|
||||
let context = context_from_c(source);
|
||||
let config = s.rt.app.config.borrow();
|
||||
|
||||
// The font size to inherit. If 0, default font size will be used.
|
||||
if config.window_inherit_font_size {
|
||||
result.font_size = core.font_size().points;
|
||||
}
|
||||
|
||||
if should_inherit_working_directory(context, &config)
|
||||
&& let Some(cwd) = core.pwd()
|
||||
&& let Ok(c) = CString::new(cwd)
|
||||
{
|
||||
let mut keep = s.inherited_wd.borrow_mut();
|
||||
result.working_directory = c.as_ptr();
|
||||
keep.push(c);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Update the configuration to the provided config for only this surface.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live surface handle, `config` a config handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_update_config(
|
||||
ptr: ghostty_surface_t,
|
||||
config: ghostty_config_t,
|
||||
) {
|
||||
// SAFETY: per the function contract.
|
||||
let (Some(s), Some(c)) = (unsafe { surface(ptr) }, unsafe { cconfig(config) }) else {
|
||||
return;
|
||||
};
|
||||
let Some(core) = s.core() else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = core.update_config(&Arc::new(c.config.clone())) {
|
||||
log::error!("error updating config err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the surface needs to confirm quitting.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_needs_confirm_quit(ptr: ghostty_surface_t) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { surface(ptr) }
|
||||
.and_then(Surface::core)
|
||||
.is_some_and(|c| c.needs_confirm_quit())
|
||||
}
|
||||
|
||||
/// Returns true if the surface process has exited.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_process_exited(ptr: ghostty_surface_t) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { surface(ptr) }
|
||||
.and_then(Surface::core)
|
||||
.is_some_and(|c| CoreSurface::child_exited(c.as_ref()))
|
||||
}
|
||||
|
||||
/// Returns true if the surface has a selection.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_has_selection(ptr: ghostty_surface_t) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { surface(ptr) }
|
||||
.and_then(Surface::core)
|
||||
.is_some_and(|c| c.has_selection())
|
||||
}
|
||||
|
||||
/// Fill the C text result from a read.
|
||||
fn fill_text(
|
||||
core: &CoreSurfaceImpl,
|
||||
t: &Terminal,
|
||||
sel: Selection,
|
||||
result: &mut ghostty_text_s,
|
||||
) -> bool {
|
||||
// Get our text directly from the core surface.
|
||||
let text: Text = match core.dump_text_locked(t, sel) {
|
||||
Ok(t) => t,
|
||||
Err(err) => {
|
||||
log::warn!("error reading text err={err}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let (tl_px_x, tl_px_y, offset_start, offset_len) =
|
||||
text.viewport.map_or((-1.0, -1.0, 0, 0), |vp| {
|
||||
(vp.tl_px_x, vp.tl_px_y, vp.offset_start, vp.offset_len)
|
||||
});
|
||||
*result = ghostty_text_s {
|
||||
tl_px_x,
|
||||
tl_px_y,
|
||||
offset_start,
|
||||
offset_len,
|
||||
text: alloc_c_text(text.text.as_bytes()),
|
||||
text_len: text.text.len(),
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
/// Same as `ghostty_surface_read_text` but reads from the user selection, if
|
||||
/// any.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live surface handle, `result` a valid out pointer.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_read_selection(
|
||||
ptr: ghostty_surface_t,
|
||||
result: *mut ghostty_text_s,
|
||||
) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
let (Some(core), Some(result)) = (unsafe { surface(ptr) }.and_then(Surface::core), unsafe {
|
||||
result.as_mut()
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
let guard = core.lock_terminal();
|
||||
let t: &Terminal = &guard;
|
||||
|
||||
// If we don't have a selection, do nothing.
|
||||
let Some(sel) = t.screen().selection else {
|
||||
return false;
|
||||
};
|
||||
fill_text(core, t, sel, result)
|
||||
}
|
||||
|
||||
/// The pin of a C point (Zig: `Point.pin`).
|
||||
fn point_pin(p: &ghostty_point_s, t: &Terminal) -> Option<ghostty_terminal::page_list::Pin> {
|
||||
let tag = match p.tag {
|
||||
0 => PointTag::Active,
|
||||
1 => PointTag::Viewport,
|
||||
2 => PointTag::Screen,
|
||||
3 => PointTag::History,
|
||||
_ => return None,
|
||||
};
|
||||
let pages = &t.screen().pages;
|
||||
|
||||
// Clamp our point to the screen bounds.
|
||||
let clamped_x = p.x.min(u32::from(pages.cols.saturating_sub(1)));
|
||||
let clamped_y = p.y.min(u32::from(pages.rows.saturating_sub(1)));
|
||||
|
||||
match p.coord {
|
||||
// Exact coordinates require a specific pin.
|
||||
0 => {
|
||||
let x = u16::try_from(clamped_x).unwrap_or(u16::MAX);
|
||||
pages.pin(Point::new(tag, Coordinate { x, y: clamped_y }))
|
||||
}
|
||||
1 => pages.get_top_left(tag),
|
||||
2 => pages.get_bottom_right(tag),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_from_c(sel: &ghostty_selection_s, t: &Terminal) -> Option<Selection> {
|
||||
Some(Selection::new(
|
||||
point_pin(&sel.top_left, t)?,
|
||||
point_pin(&sel.bottom_right, t)?,
|
||||
sel.rectangle,
|
||||
))
|
||||
}
|
||||
|
||||
/// Read some arbitrary text from the surface.
|
||||
///
|
||||
/// This is an expensive operation so it shouldn't be called too often. We
|
||||
/// recommend that callers cache the result and throttle calls to this
|
||||
/// function.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live surface handle, `result` a valid out pointer.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_read_text(
|
||||
ptr: ghostty_surface_t,
|
||||
sel: ghostty_selection_s,
|
||||
result: *mut ghostty_text_s,
|
||||
) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
let (Some(core), Some(result)) = (unsafe { surface(ptr) }.and_then(Surface::core), unsafe {
|
||||
result.as_mut()
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
let guard = core.lock_terminal();
|
||||
let t: &Terminal = &guard;
|
||||
let Some(core_sel) = selection_from_c(&sel, t) else {
|
||||
return false;
|
||||
};
|
||||
fill_text(core, t, core_sel, result)
|
||||
}
|
||||
|
||||
/// Free text read from the surface.
|
||||
///
|
||||
/// # Safety
|
||||
/// `text` must be a result of one of the read functions (or null).
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_free_text(
|
||||
_ptr: ghostty_surface_t,
|
||||
text: *mut ghostty_text_s,
|
||||
) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(text) = (unsafe { text.as_mut() }) else {
|
||||
return;
|
||||
};
|
||||
// SAFETY: allocated by `alloc_c_text` with `text_len` bytes + NUL.
|
||||
unsafe { free_c_text(text.text, text.text_len, true) };
|
||||
text.text = std::ptr::null();
|
||||
text.text_len = 0;
|
||||
}
|
||||
|
||||
/// Tell the surface that it needs to schedule a render.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_refresh(ptr: ghostty_surface_t) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
|
||||
&& let Err(err) = core.refresh_callback()
|
||||
{
|
||||
log::error!("error in refresh callback err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Tell the surface that it needs to schedule a render call as soon as
|
||||
/// possible (NOW if possible).
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_draw(ptr: ghostty_surface_t) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
|
||||
&& let Err(err) = core.draw()
|
||||
{
|
||||
log::error!("error in draw err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the size of a surface. This will trigger resize notifications to
|
||||
/// the pty and the renderer.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_set_size(ptr: ghostty_surface_t, w: u32, h: u32) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(s) = (unsafe { surface(ptr) }) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Runtimes sometimes generate superfluous resize events even if the
|
||||
// size did not actually change (SwiftUI). We check that the size
|
||||
// actually changed from what we last recorded since resizes are
|
||||
// expensive.
|
||||
let cur = s.rt.size.get();
|
||||
if cur.width == w && cur.height == h {
|
||||
return;
|
||||
}
|
||||
let size = SurfaceSize {
|
||||
width: w,
|
||||
height: h,
|
||||
};
|
||||
s.rt.size.set(size);
|
||||
|
||||
if let Some(core) = s.core()
|
||||
&& let Err(err) = core.size_callback(size)
|
||||
{
|
||||
log::error!("error in size callback err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the size information a surface has.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_size(ptr: ghostty_surface_t) -> ghostty_surface_size_s {
|
||||
let zero = ghostty_surface_size_s {
|
||||
columns: 0,
|
||||
rows: 0,
|
||||
width_px: 0,
|
||||
height_px: 0,
|
||||
cell_width_px: 0,
|
||||
cell_height_px: 0,
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
|
||||
return zero;
|
||||
};
|
||||
let size = core.size();
|
||||
let grid = size.grid();
|
||||
ghostty_surface_size_s {
|
||||
columns: grid.columns,
|
||||
rows: grid.rows,
|
||||
width_px: size.screen.width,
|
||||
height_px: size.screen.height,
|
||||
cell_width_px: size.cell.width,
|
||||
cell_height_px: size.cell.height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the color scheme of the surface.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_set_color_scheme(
|
||||
ptr: ghostty_surface_t,
|
||||
scheme_raw: c_int,
|
||||
) {
|
||||
let Some(scheme) = color_scheme_from_c(scheme_raw) else {
|
||||
log::warn!("invalid color scheme to ghostty_surface_set_color_scheme value={scheme_raw}");
|
||||
return;
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
|
||||
&& let Err(err) = core.color_scheme_callback(scheme)
|
||||
{
|
||||
log::error!("error setting color scheme err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the content scale of the surface.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_set_content_scale(ptr: ghostty_surface_t, x: f64, y: f64) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(s) = (unsafe { surface(ptr) }) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// We are an embedded API so the caller can send us all sorts of
|
||||
// garbage. We want to make sure that the float values are valid and we
|
||||
// don't want to support fractional scaling below 1.
|
||||
let scaled = |v: f64| if v.is_nan() { 1.0 } else { v.max(1.0) } as f32;
|
||||
let scale = ContentScale {
|
||||
x: scaled(x),
|
||||
y: scaled(y),
|
||||
};
|
||||
s.rt.content_scale.set(scale);
|
||||
|
||||
if let Some(core) = s.core()
|
||||
&& let Err(err) = core.content_scale_callback(scale)
|
||||
{
|
||||
log::error!("error in content scale callback err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the focused state of a surface.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_set_focus(ptr: ghostty_surface_t, focused: bool) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
|
||||
&& let Err(err) = core.focus_callback(focused)
|
||||
{
|
||||
log::error!("error in focus callback err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the occlusion state of a surface.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_set_occlusion(ptr: ghostty_surface_t, visible: bool) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
|
||||
&& let Err(err) = core.occlusion_callback(visible)
|
||||
{
|
||||
log::error!("error in occlusion callback err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter the mods if necessary. This handles settings such as
|
||||
/// `macos-option-as-alt`. The filtered mods should be used for key
|
||||
/// translation but should NOT be sent back via the `_key` function -- the
|
||||
/// original mods should be used for that.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_key_translation_mods(
|
||||
ptr: ghostty_surface_t,
|
||||
mods_raw: c_int,
|
||||
) -> c_int {
|
||||
let mods = mods_from_c(mods_raw);
|
||||
// SAFETY: per the function contract.
|
||||
let Some(s) = (unsafe { surface(ptr) }) else {
|
||||
return mods_raw;
|
||||
};
|
||||
let Some(core) = s.core() else {
|
||||
return mods_raw;
|
||||
};
|
||||
let option_as_alt = core
|
||||
.config()
|
||||
.macos_option_as_alt
|
||||
.unwrap_or_else(|| s.rt.app.keyboard_layout().detect_option_as_alt());
|
||||
c_int::from(mods.translation(option_as_alt).int())
|
||||
}
|
||||
|
||||
/// Send this for raw keypresses (i.e. the keyDown event on macOS). This will
|
||||
/// handle the keymap translation and send the appropriate key and char
|
||||
/// events.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live surface handle; `event.text` null or NUL terminated.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_key(
|
||||
ptr: ghostty_surface_t,
|
||||
event: ghostty_input_key_s,
|
||||
) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
|
||||
return false;
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
let Some(key) = (unsafe { OwnedKey::from_c(&event) }) else {
|
||||
return false;
|
||||
};
|
||||
match CoreSurface::key_callback(core.as_ref(), key.event()) {
|
||||
Ok(ghostty_apprt::runtime_api::InputEffect::Closed) => true,
|
||||
Ok(ghostty_apprt::runtime_api::InputEffect::Ignored) => false,
|
||||
Ok(ghostty_apprt::runtime_api::InputEffect::Consumed) => true,
|
||||
Err(err) => {
|
||||
log::warn!("error processing key event err={err}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a binding if it were
|
||||
/// sent to the surface right now. The "right now" is important because
|
||||
/// things like trigger sequences are only valid until the next key event.
|
||||
///
|
||||
/// # Safety
|
||||
/// As [`ghostty_surface_key`]; `c_flags` null or a valid out pointer.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_key_is_binding(
|
||||
ptr: ghostty_surface_t,
|
||||
event: ghostty_input_key_s,
|
||||
c_flags: *mut ghostty_binding_flags_e,
|
||||
) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
|
||||
return false;
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
let Some(key) = (unsafe { OwnedKey::from_c(&event) }) else {
|
||||
log::warn!("error processing key event");
|
||||
return false;
|
||||
};
|
||||
let Some(flags) = core.key_event_is_binding(&key.event()) else {
|
||||
return false;
|
||||
};
|
||||
if !c_flags.is_null() {
|
||||
// Zig writes the `u8` `Flags.C` through the (enum-typed) pointer.
|
||||
// SAFETY: the pointer is valid for at least one byte.
|
||||
unsafe { c_flags.cast::<u8>().write(flags.cval()) };
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Send raw text to the terminal. This is treated like a paste so this isn't
|
||||
/// useful for sending escape sequences. For that, individual key input
|
||||
/// should be used.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live surface handle, `text` points to `len` bytes.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_text(
|
||||
ptr: ghostty_surface_t,
|
||||
text: *const c_char,
|
||||
len: usize,
|
||||
) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
|
||||
return;
|
||||
};
|
||||
let bytes: &[u8] = if text.is_null() {
|
||||
&[]
|
||||
} else {
|
||||
// SAFETY: `text` points to `len` readable bytes.
|
||||
unsafe { std::slice::from_raw_parts(text.cast::<u8>(), len) }
|
||||
};
|
||||
if let Err(err) = core.text_callback(&String::from_utf8_lossy(bytes)) {
|
||||
log::error!("error in key callback err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the preedit text for the surface. This is used for IME composition.
|
||||
/// If the length is 0, then the preedit text is cleared.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live surface handle, `text` points to `len` bytes.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_preedit(
|
||||
ptr: ghostty_surface_t,
|
||||
text: *const c_char,
|
||||
len: usize,
|
||||
) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
|
||||
return;
|
||||
};
|
||||
let preedit = if len == 0 || text.is_null() {
|
||||
None
|
||||
} else {
|
||||
// SAFETY: `text` points to `len` readable bytes.
|
||||
let bytes = unsafe { std::slice::from_raw_parts(text.cast::<u8>(), len) };
|
||||
Some(String::from_utf8_lossy(bytes).into_owned())
|
||||
};
|
||||
if let Err(err) = core.preedit_callback(preedit.as_deref()) {
|
||||
log::error!("error in preedit callback err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the surface currently has mouse capturing enabled.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_mouse_captured(ptr: ghostty_surface_t) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { surface(ptr) }
|
||||
.and_then(Surface::core)
|
||||
.is_some_and(|c| c.mouse_captured())
|
||||
}
|
||||
|
||||
/// Send a mouse button event.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_mouse_button(
|
||||
ptr: ghostty_surface_t,
|
||||
action: c_int,
|
||||
button: c_int,
|
||||
mods: c_int,
|
||||
) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
|
||||
return false;
|
||||
};
|
||||
let state = match action {
|
||||
0 => MouseButtonState::Release,
|
||||
1 => MouseButtonState::Press,
|
||||
_ => return false,
|
||||
};
|
||||
let Some(button) = mouse_button_from_c(button) else {
|
||||
return false;
|
||||
};
|
||||
match core.mouse_button_callback(state, button, mods_from_c(mods)) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
log::error!("error in mouse button callback err={err}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mouse_button_from_c(v: c_int) -> Option<MouseButton> {
|
||||
Some(match v {
|
||||
0 => MouseButton::Unknown,
|
||||
1 => MouseButton::Left,
|
||||
2 => MouseButton::Right,
|
||||
3 => MouseButton::Middle,
|
||||
4 => MouseButton::Four,
|
||||
5 => MouseButton::Five,
|
||||
6 => MouseButton::Six,
|
||||
7 => MouseButton::Seven,
|
||||
8 => MouseButton::Eight,
|
||||
9 => MouseButton::Nine,
|
||||
10 => MouseButton::Ten,
|
||||
11 => MouseButton::Eleven,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update the mouse position within the view.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_mouse_pos(
|
||||
ptr: ghostty_surface_t,
|
||||
x: f64,
|
||||
y: f64,
|
||||
mods: c_int,
|
||||
) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(s) = (unsafe { surface(ptr) }) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// The cursor position from the host directly is in screen coordinates
|
||||
// but all our interface works in pixels.
|
||||
let scale = s.rt.content_scale.get();
|
||||
let pos = CursorPos {
|
||||
x: x as f32 * scale.x,
|
||||
y: y as f32 * scale.y,
|
||||
};
|
||||
|
||||
// There are cases where the platform reports a mouse motion event
|
||||
// without the cursor actually moving. For example, on macOS, updating
|
||||
// the window title can trigger a phantom mouse-move event at the same
|
||||
// coordinates. This can cause the mouse to incorrectly unhide when
|
||||
// mouse-hide-while-typing is enabled (commonly seen with TUI apps like
|
||||
// Zellij that frequently update the title). To prevent incorrect
|
||||
// behavior, we only continue with callback logic if the cursor has
|
||||
// actually moved.
|
||||
let prev = s.rt.cursor_pos.get();
|
||||
if (prev.x - pos.x).abs() < 1.0 && (prev.y - pos.y).abs() < 1.0 {
|
||||
return;
|
||||
}
|
||||
s.rt.cursor_pos.set(pos);
|
||||
|
||||
if let Some(core) = s.core()
|
||||
&& let Err(err) = core.cursor_pos_callback(pos, Some(mods_from_c(mods)))
|
||||
{
|
||||
log::error!("error in cursor pos callback err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a scroll event.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_mouse_scroll(
|
||||
ptr: ghostty_surface_t,
|
||||
x: f64,
|
||||
y: f64,
|
||||
scroll_mods: c_int,
|
||||
) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core)
|
||||
&& let Err(err) = core.scroll_callback(x, y, ScrollMods::from_int(scroll_mods as u32 as u8))
|
||||
{
|
||||
log::error!("error in scroll callback err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a mouse pressure event.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_mouse_pressure(
|
||||
ptr: ghostty_surface_t,
|
||||
stage_raw: u32,
|
||||
pressure: f64,
|
||||
) {
|
||||
let stage = match stage_raw {
|
||||
0 => MousePressureStage::None,
|
||||
1 => MousePressureStage::Normal,
|
||||
2 => MousePressureStage::Deep,
|
||||
_ => {
|
||||
log::warn!("invalid mouse pressure stage value={stage_raw}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core) {
|
||||
CoreSurface::mouse_pressure_callback(core.as_ref(), stage, pressure);
|
||||
}
|
||||
}
|
||||
|
||||
/// The position of the IME candidate window.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live surface handle; the out pointers valid.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_ime_point(
|
||||
ptr: ghostty_surface_t,
|
||||
x: *mut f64,
|
||||
y: *mut f64,
|
||||
width: *mut f64,
|
||||
height: *mut f64,
|
||||
) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
|
||||
return;
|
||||
};
|
||||
let pos = core.ime_point();
|
||||
// SAFETY: the out pointers are valid per the contract.
|
||||
unsafe {
|
||||
if let Some(x) = x.as_mut() {
|
||||
*x = pos.x;
|
||||
}
|
||||
if let Some(y) = y.as_mut() {
|
||||
*y = pos.y;
|
||||
}
|
||||
if let Some(w) = width.as_mut() {
|
||||
*w = pos.width;
|
||||
}
|
||||
if let Some(h) = height.as_mut() {
|
||||
*h = pos.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request that the surface become closed. This will go through the normal
|
||||
/// trigger process that a close surface input binding would.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_request_close(ptr: ghostty_surface_t) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core) {
|
||||
core.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Request that the surface split in the given direction.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_split(ptr: ghostty_surface_t, direction: c_int) {
|
||||
let direction = match direction {
|
||||
0 => SplitDirection::Right,
|
||||
1 => SplitDirection::Down,
|
||||
2 => SplitDirection::Left,
|
||||
3 => SplitDirection::Up,
|
||||
_ => return,
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(s) = unsafe { surface(ptr) } {
|
||||
s.rt.app
|
||||
.perform_action(s.rt.target(), Action::NewSplit(direction));
|
||||
}
|
||||
}
|
||||
|
||||
/// Focus on the next split (if any).
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_split_focus(ptr: ghostty_surface_t, direction: c_int) {
|
||||
let direction = match direction {
|
||||
0 => GotoSplit::Previous,
|
||||
1 => GotoSplit::Next,
|
||||
2 => GotoSplit::Up,
|
||||
3 => GotoSplit::Left,
|
||||
4 => GotoSplit::Down,
|
||||
5 => GotoSplit::Right,
|
||||
_ => return,
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(s) = unsafe { surface(ptr) } {
|
||||
s.rt.app
|
||||
.perform_action(s.rt.target(), Action::GotoSplit(direction));
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize the current split by moving the split divider in the given
|
||||
/// direction. `direction` specifies which direction the split divider will
|
||||
/// move relative to the focused split. `amount` is a fractional value
|
||||
/// between 0 and 1 that specifies by how much the divider will move.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_split_resize(
|
||||
ptr: ghostty_surface_t,
|
||||
direction: c_int,
|
||||
amount: u16,
|
||||
) {
|
||||
let direction = match direction {
|
||||
0 => ResizeSplitDirection::Up,
|
||||
1 => ResizeSplitDirection::Down,
|
||||
2 => ResizeSplitDirection::Left,
|
||||
3 => ResizeSplitDirection::Right,
|
||||
_ => return,
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(s) = unsafe { surface(ptr) } {
|
||||
s.rt.app.perform_action(
|
||||
s.rt.target(),
|
||||
Action::ResizeSplit(ResizeSplit { amount, direction }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Equalize the size of all splits in the current window.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_split_equalize(ptr: ghostty_surface_t) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(s) = unsafe { surface(ptr) } {
|
||||
s.rt.app
|
||||
.perform_action(s.rt.target(), Action::EqualizeSplits);
|
||||
}
|
||||
}
|
||||
|
||||
/// Invoke an action on the surface.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live surface handle, `action` points to `len` bytes.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_binding_action(
|
||||
ptr: ghostty_surface_t,
|
||||
action: *const c_char,
|
||||
len: usize,
|
||||
) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(core) = (unsafe { surface(ptr) }).and_then(Surface::core) else {
|
||||
return false;
|
||||
};
|
||||
if action.is_null() {
|
||||
return false;
|
||||
}
|
||||
// SAFETY: `action` points to `len` readable bytes.
|
||||
let bytes = unsafe { std::slice::from_raw_parts(action.cast::<u8>(), len) };
|
||||
let action_str = String::from_utf8_lossy(bytes);
|
||||
let action = match BindingAction::parse(&action_str) {
|
||||
Ok(a) => a,
|
||||
Err(err) => {
|
||||
log::error!("error parsing binding action action={action_str} err={err:?}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
match core.perform_binding_action(&action) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
log::error!("error performing binding action action={action} err={err}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete a clipboard read request started via the read callback. This can
|
||||
/// only be called once for a given request. Once it is called with a request
|
||||
/// the request pointer will be invalidated.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live surface handle; `text` NUL terminated; `state` a pointer
|
||||
/// handed to the `read_clipboard` / `confirm_read_clipboard` callbacks.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_complete_clipboard_request(
|
||||
ptr: ghostty_surface_t,
|
||||
text: *const c_char,
|
||||
state: *mut c_void,
|
||||
confirmed: bool,
|
||||
) {
|
||||
// SAFETY: per the function contract.
|
||||
let (Some(s), Some(str)) = (unsafe { surface(ptr) }, unsafe { cstr(text) }) else {
|
||||
return;
|
||||
};
|
||||
if state.is_null() {
|
||||
return;
|
||||
}
|
||||
let Some(core) = s.core() else {
|
||||
return;
|
||||
};
|
||||
let str = str.to_string_lossy();
|
||||
// SAFETY: the pointer is a `Box<ClipboardRequest>` leaked by
|
||||
// `clipboard_request`; it stays alive when confirmation is requested.
|
||||
let req = unsafe { *state.cast::<ClipboardRequest>() };
|
||||
|
||||
// Attempt to complete the request, but we may request confirmation.
|
||||
match core.complete_clipboard_request(req, &str, confirmed) {
|
||||
Err(
|
||||
ghostty_apprt::runtime_api::ClipboardError::UnsafePaste
|
||||
| ghostty_apprt::runtime_api::ClipboardError::UnauthorizedPaste,
|
||||
) => {
|
||||
if let Some(cb) = s.rt.app.opts.confirm_read_clipboard_cb {
|
||||
let ty = match req.request_type() {
|
||||
ghostty_apprt::structs::ClipboardRequestType::Paste => 0,
|
||||
ghostty_apprt::structs::ClipboardRequestType::Osc52Read => 1,
|
||||
ghostty_apprt::structs::ClipboardRequestType::Osc52Write => 2,
|
||||
};
|
||||
let c = CString::new(str.replace('\0', "")).unwrap_or_default();
|
||||
// SAFETY: embedder callback; `state` is preserved for the
|
||||
// confirmation route.
|
||||
unsafe { cb(s.rt.userdata, c.as_ptr(), state, ty) };
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(err) => log::error!("error completing clipboard request err={err}"),
|
||||
Ok(()) => {}
|
||||
}
|
||||
|
||||
// We don't defer this because the clipboard confirmation route
|
||||
// preserves the clipboard request.
|
||||
// SAFETY: allocated by `clipboard_request`, consumed exactly once here.
|
||||
drop(unsafe { Box::from_raw(state.cast::<ClipboardRequest>()) });
|
||||
}
|
||||
|
||||
/// Sets the window background blur on macOS to the desired value. I do this
|
||||
/// as an extern function because I don't know how to call these functions
|
||||
/// in Swift.
|
||||
///
|
||||
/// This uses an undocumented, non-public API because this is what every
|
||||
/// terminal appears to use, including Terminal.app.
|
||||
///
|
||||
/// # Safety
|
||||
/// `app` a live app handle; `window` an `NSWindow` (macOS).
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_set_window_background_blur(
|
||||
app_ptr: ghostty_app_t,
|
||||
window: *mut c_void,
|
||||
) {
|
||||
// This is only supported on macOS
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use objc2::{msg_send, runtime::AnyObject};
|
||||
|
||||
#[link(name = "CoreGraphics", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
fn CGSSetWindowBackgroundBlurRadius(
|
||||
connection: *mut c_void,
|
||||
window: usize,
|
||||
radius: c_int,
|
||||
) -> i32;
|
||||
fn CGSDefaultConnectionForThread() -> *mut c_void;
|
||||
}
|
||||
|
||||
// SAFETY: per the function contract.
|
||||
let Some(a) = (unsafe { app(app_ptr) }) else {
|
||||
return;
|
||||
};
|
||||
if window.is_null() {
|
||||
return;
|
||||
}
|
||||
let config = a.rt.config.borrow();
|
||||
|
||||
// Do nothing if we don't have background transparency enabled
|
||||
if config.background_opacity >= 1.0 {
|
||||
return;
|
||||
}
|
||||
let radius = match config.c_value("background-blur") {
|
||||
Some(ghostty_config::c_value::CValue::Short(v)) => c_int::from(v),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
// SAFETY: `window` is an NSWindow per the contract; the CGS
|
||||
// functions are the private window server API Terminal.app uses.
|
||||
unsafe {
|
||||
let number: isize = msg_send![window.cast::<AnyObject>(), windowNumber];
|
||||
CGSSetWindowBackgroundBlurRadius(
|
||||
CGSDefaultConnectionForThread(),
|
||||
number as usize,
|
||||
radius,
|
||||
);
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let _ = (app_ptr, window);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Darwin only exports
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
mod darwin {
|
||||
use super::*;
|
||||
use crate::types::ghostty_text_s;
|
||||
|
||||
/// Set the display the surface is on (for the display link).
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_set_display_id(
|
||||
ptr: ghostty_surface_t,
|
||||
display_id: u32,
|
||||
) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(core) = unsafe { surface(ptr) }.and_then(Surface::core) {
|
||||
core.set_macos_display_id(display_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// This returns a CTFontRef that should be used for quicklook
|
||||
/// highlighted text. This is always the primary font in use regardless
|
||||
/// of the selected text.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_quicklook_font(ptr: ghostty_surface_t) -> *mut c_void {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(s) = (unsafe { surface(ptr) }) else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
let Some(core) = s.core() else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
// We need to unscale the content scale. We apply the content scale
|
||||
// to our font stack because we are rendering at 1x but callers of
|
||||
// this should be using scaled or apply scale themselves.
|
||||
let scale = s.rt.get_content_scale();
|
||||
core.quicklook_font(scale.y).unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
/// This returns the selected word for quicklook. This will populate the
|
||||
/// buffer with the word under the cursor and the selection info so that
|
||||
/// quicklook can be rendered.
|
||||
///
|
||||
/// This does not modify the selection active on the surface (if any).
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live surface handle, `result` a valid out pointer.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_quicklook_word(
|
||||
ptr: ghostty_surface_t,
|
||||
result: *mut ghostty_text_s,
|
||||
) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
let (Some(s), Some(result)) = (unsafe { surface(ptr) }, unsafe { result.as_mut() }) else {
|
||||
return false;
|
||||
};
|
||||
let Some(core) = s.core() else {
|
||||
return false;
|
||||
};
|
||||
let guard = core.lock_terminal();
|
||||
let t: &Terminal = &guard;
|
||||
|
||||
// Get our word selection
|
||||
let pos = s.rt.get_cursor_pos();
|
||||
let vp = core.pos_to_viewport(f64::from(pos.x), f64::from(pos.y));
|
||||
let Some(pin) = t
|
||||
.screen()
|
||||
.pages
|
||||
.pin(Point::Viewport(Coordinate { x: vp.x, y: vp.y }))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(sel) = t
|
||||
.screen()
|
||||
.select_word(pin, &core.cfg().selection_word_chars)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Read the selection
|
||||
fill_text(core, t, sel, result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
//! The inspector C API (`ghostty_surface_inspector`, `ghostty_inspector_*`;
|
||||
//! the `Inspector` struct of ZIG/src/apprt/embedded.zig).
|
||||
//!
|
||||
//! The embedded inspector owns a Dear ImGui context, feeds it the host's
|
||||
//! input events and, on Apple platforms, draws it with Metal into the
|
||||
//! command buffer the host provides (`ghostty_inspector_metal_*`).
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
use std::time::Instant;
|
||||
use std::{
|
||||
ffi::{CStr, c_char, c_int, c_void},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use ghostty_apprt::runtime_api::RtSurface;
|
||||
use ghostty_core::Surface as CoreSurfaceImpl;
|
||||
use ghostty_input::{Action as KeyAction, Key, MouseButtonState, ScrollMods};
|
||||
use ghostty_inspector::imgui::{Context, Key as ImguiKey, MouseButton as ImguiMouseButton};
|
||||
|
||||
use crate::{
|
||||
embedded::{SurfaceRt, mouse_button_from_c, surface},
|
||||
types::{ghostty_inspector_t, ghostty_surface_t},
|
||||
};
|
||||
|
||||
/// The inspector state (Zig: `embedded.Inspector`). 1:1 with a surface.
|
||||
pub struct Inspector {
|
||||
surface: Rc<SurfaceRt>,
|
||||
core: Rc<CoreSurfaceImpl>,
|
||||
ctx: Context,
|
||||
#[cfg(target_vendor = "apple")]
|
||||
backend: Option<crate::inspector_metal::MetalBackend>,
|
||||
content_scale: f64,
|
||||
/// The scale the ImGui style currently has (a `Style` cannot be rebuilt
|
||||
/// from Rust like Zig's `ImGuiStyle_ImGuiStyle`, so it is scaled by the
|
||||
/// change of the factor).
|
||||
style_scale: f32,
|
||||
/// Our previous instant used to calculate delta time for animations.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
instant: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Inspector {
|
||||
fn init(surface: Rc<SurfaceRt>, core: Rc<CoreSurfaceImpl>) -> Option<Inspector> {
|
||||
let mut ctx = match Context::try_create() {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
log::error!("error initializing inspector err={err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if let Err(err) = ctx.set_platform_name(Some("ghostty_embedded")) {
|
||||
log::warn!("unable to set the imgui platform name err={err}");
|
||||
}
|
||||
|
||||
// Setup our core inspector
|
||||
let scale = f64::from(RtSurface::get_content_scale(surface.as_ref()).x);
|
||||
ghostty_inspector::setup(&mut ctx, scale);
|
||||
if let Err(err) = core.activate_inspector() {
|
||||
log::error!("failed to activate inspector err={err}");
|
||||
}
|
||||
|
||||
Some(Inspector {
|
||||
surface,
|
||||
core,
|
||||
ctx,
|
||||
#[cfg(target_vendor = "apple")]
|
||||
backend: None,
|
||||
content_scale: 1.0,
|
||||
style_scale: 1.0,
|
||||
#[cfg(target_vendor = "apple")]
|
||||
instant: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Stop collecting data and release the backend (Zig: `deinit`).
|
||||
pub(crate) fn deinit(&mut self, _core: Option<&CoreSurfaceImpl>) {
|
||||
self.core.deactivate_inspector();
|
||||
#[cfg(target_vendor = "apple")]
|
||||
if let Some(b) = self.backend.take() {
|
||||
b.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue a render for the next frame.
|
||||
fn queue_render(&self) {
|
||||
RtSurface::redraw_inspector(self.surface.as_ref());
|
||||
}
|
||||
|
||||
fn update_content_scale(&mut self, x: f64) {
|
||||
// Cache our scale because we use it for cursor position
|
||||
// calculations.
|
||||
self.content_scale = x;
|
||||
|
||||
// Scale the style appropriately.
|
||||
let scale = x as f32;
|
||||
let last = std::mem::replace(&mut self.style_scale, scale);
|
||||
if (scale - last).abs() > f32::EPSILON && last > 0.0 {
|
||||
self.ctx.style_mut().scale_all_sizes(scale / last);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_size(&mut self, width: u32, height: u32) {
|
||||
self.ctx
|
||||
.io_mut()
|
||||
.set_display_size([width as f32, height as f32]);
|
||||
}
|
||||
|
||||
fn mouse_button(&mut self, action: MouseButtonState, button: ghostty_input::MouseButton) {
|
||||
self.queue_render();
|
||||
let imgui_button = match button {
|
||||
ghostty_input::MouseButton::Left => ImguiMouseButton::Left,
|
||||
ghostty_input::MouseButton::Middle => ImguiMouseButton::Middle,
|
||||
ghostty_input::MouseButton::Right => ImguiMouseButton::Right,
|
||||
_ => return, // unsupported
|
||||
};
|
||||
self.ctx
|
||||
.io_mut()
|
||||
.add_mouse_button_event(imgui_button, action == MouseButtonState::Press);
|
||||
}
|
||||
|
||||
fn scroll(&mut self, xoff: f64, yoff: f64, mods: ScrollMods) {
|
||||
self.queue_render();
|
||||
|
||||
// For precision scrolling (trackpads), the values are in pixels
|
||||
// which scroll way too fast. Scale them down to approximate
|
||||
// discrete wheel notches. imgui expects 1.0 to scroll ~5 lines of
|
||||
// text.
|
||||
let scale = if mods.precision { 0.1 } else { 1.0 };
|
||||
self.ctx
|
||||
.io_mut()
|
||||
.add_mouse_wheel_event([(xoff * scale) as f32, (yoff * scale) as f32]);
|
||||
}
|
||||
|
||||
fn cursor_pos(&mut self, x: f64, y: f64) {
|
||||
self.queue_render();
|
||||
self.ctx.io_mut().add_mouse_pos_event([
|
||||
(x * self.content_scale) as f32,
|
||||
(y * self.content_scale) as f32,
|
||||
]);
|
||||
}
|
||||
|
||||
fn focus(&mut self, focused: bool) {
|
||||
self.queue_render();
|
||||
self.ctx.io_mut().add_focus_event(focused);
|
||||
}
|
||||
|
||||
fn text(&mut self, text: &str) {
|
||||
self.queue_render();
|
||||
self.ctx.io_mut().add_input_characters_utf8(text);
|
||||
}
|
||||
|
||||
fn key(&mut self, action: KeyAction, key: Key, mods: ghostty_input::Mods) {
|
||||
self.queue_render();
|
||||
let io = self.ctx.io_mut();
|
||||
|
||||
// Update all our modifiers
|
||||
io.add_key_event(ImguiKey::LeftShift, mods.shift);
|
||||
io.add_key_event(ImguiKey::LeftCtrl, mods.ctrl);
|
||||
io.add_key_event(ImguiKey::LeftAlt, mods.alt);
|
||||
io.add_key_event(ImguiKey::LeftSuper, mods.super_);
|
||||
|
||||
// Send our keypress
|
||||
if let Some(imgui_key) = ghostty_inspector::keys::imgui_key(key) {
|
||||
io.add_key_event(
|
||||
imgui_key,
|
||||
matches!(action, KeyAction::Press | KeyAction::Repeat),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the delta time (Zig: `newFrame`).
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub(crate) fn new_frame(&mut self) {
|
||||
let now = Instant::now();
|
||||
let delta = self.instant.map_or(1.0 / 60.0, |prev| {
|
||||
now.duration_since(prev).as_secs_f32().max(0.00001)
|
||||
});
|
||||
self.instant = Some(now);
|
||||
self.ctx.io_mut().set_delta_time(delta);
|
||||
}
|
||||
|
||||
/// Build one UI frame, calling the core inspector (`inspector.render`).
|
||||
/// `end` finishes the frame without producing draw data.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub(crate) fn build_frame(&mut self) -> &mut ghostty_inspector::imgui::Ui {
|
||||
self.new_frame();
|
||||
let core = Rc::clone(&self.core);
|
||||
let ui = self.ctx.frame();
|
||||
core.render_inspector(ui);
|
||||
ui
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub(crate) fn context(&mut self) -> &mut Context {
|
||||
&mut self.ctx
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub(crate) fn take_backend(&mut self) -> Option<crate::inspector_metal::MetalBackend> {
|
||||
self.backend.take()
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub(crate) fn set_backend(&mut self, backend: crate::inspector_metal::MetalBackend) {
|
||||
self.backend = Some(backend);
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrow the inspector behind a handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// `p` must be null or a handle from `ghostty_surface_inspector` whose
|
||||
/// surface is still alive and whose inspector wasn't freed.
|
||||
unsafe fn inspector<'a>(p: ghostty_inspector_t) -> Option<&'a mut Inspector> {
|
||||
// SAFETY: per the function contract.
|
||||
unsafe { p.cast::<Inspector>().as_mut() }
|
||||
}
|
||||
|
||||
/// Initialize the inspector instance. A surface can only have one inspector
|
||||
/// at any given time, so this will return the previous inspector if it was
|
||||
/// already initialized.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_surface_inspector(ptr: ghostty_surface_t) -> ghostty_inspector_t {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(s) = (unsafe { surface(ptr) }) else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
let slot = s.inspector_slot();
|
||||
if let Some(i) = slot.borrow_mut().as_mut() {
|
||||
return std::ptr::from_mut::<Inspector>(&mut **i).cast::<c_void>();
|
||||
}
|
||||
let Some(core) = s.core_surface() else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
let Some(i) = Inspector::init(Rc::clone(s.rt()), Rc::clone(core)) else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
let mut boxed = Box::new(i);
|
||||
let handle = std::ptr::from_mut::<Inspector>(&mut *boxed).cast::<c_void>();
|
||||
*slot.borrow_mut() = Some(boxed);
|
||||
handle
|
||||
}
|
||||
|
||||
/// Free the inspector of a surface (note: takes the surface).
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live surface handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_inspector_free(ptr: ghostty_surface_t) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(s) = (unsafe { surface(ptr) }) else {
|
||||
return;
|
||||
};
|
||||
let taken = s.inspector_slot().borrow_mut().take();
|
||||
if let Some(mut i) = taken {
|
||||
i.deinit(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the size of the inspector view in pixels.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live inspector handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_inspector_set_size(ptr: ghostty_inspector_t, w: u32, h: u32) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(i) = unsafe { inspector(ptr) } {
|
||||
i.update_size(w, h);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the content scale of the inspector view.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live inspector handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_inspector_set_content_scale(
|
||||
ptr: ghostty_inspector_t,
|
||||
x: f64,
|
||||
_y: f64,
|
||||
) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(i) = unsafe { inspector(ptr) } {
|
||||
i.update_content_scale(x);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a mouse button event to the inspector.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live inspector handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_inspector_mouse_button(
|
||||
ptr: ghostty_inspector_t,
|
||||
action: c_int,
|
||||
button: c_int,
|
||||
_mods: c_int,
|
||||
) {
|
||||
let state = match action {
|
||||
0 => MouseButtonState::Release,
|
||||
1 => MouseButtonState::Press,
|
||||
_ => return,
|
||||
};
|
||||
let Some(button) = mouse_button_from_c(button) else {
|
||||
return;
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(i) = unsafe { inspector(ptr) } {
|
||||
i.mouse_button(state, button);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a mouse position to the inspector.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live inspector handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_inspector_mouse_pos(ptr: ghostty_inspector_t, x: f64, y: f64) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(i) = unsafe { inspector(ptr) } {
|
||||
i.cursor_pos(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a scroll event to the inspector.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live inspector handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_inspector_mouse_scroll(
|
||||
ptr: ghostty_inspector_t,
|
||||
x: f64,
|
||||
y: f64,
|
||||
scroll_mods: c_int,
|
||||
) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(i) = unsafe { inspector(ptr) } {
|
||||
i.scroll(x, y, ScrollMods::from_int(scroll_mods as u32 as u8));
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a key event to the inspector.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live inspector handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_inspector_key(
|
||||
ptr: ghostty_inspector_t,
|
||||
action: c_int,
|
||||
key: c_int,
|
||||
c_mods: c_int,
|
||||
) {
|
||||
let action = match action {
|
||||
0 => KeyAction::Release,
|
||||
1 => KeyAction::Press,
|
||||
2 => KeyAction::Repeat,
|
||||
_ => return,
|
||||
};
|
||||
let Some(key) = usize::try_from(key)
|
||||
.ok()
|
||||
.and_then(|k| Key::ALL.get(k))
|
||||
.copied()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(i) = unsafe { inspector(ptr) } {
|
||||
i.key(
|
||||
action,
|
||||
key,
|
||||
ghostty_input::Mods::from_int(c_mods as u32 as u16),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send text input to the inspector.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live inspector handle; `text` NUL terminated.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_inspector_text(ptr: ghostty_inspector_t, text: *const c_char) {
|
||||
if text.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: per the function contract.
|
||||
let text = unsafe { CStr::from_ptr(text) }.to_string_lossy();
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(i) = unsafe { inspector(ptr) } {
|
||||
i.text(&text);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the focus state of the inspector.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live inspector handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_inspector_set_focus(ptr: ghostty_inspector_t, focused: bool) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(i) = unsafe { inspector(ptr) } {
|
||||
i.focus(focused);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
mod metal_exports {
|
||||
use super::*;
|
||||
|
||||
/// Initialize the inspector for a Metal backend. Takes ownership of one
|
||||
/// reference to `device`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live inspector handle; `device` a retained `MTLDevice`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_inspector_metal_init(
|
||||
ptr: ghostty_inspector_t,
|
||||
device: *mut c_void,
|
||||
) -> bool {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(i) = (unsafe { inspector(ptr) }) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(b) = i.backend.take() {
|
||||
b.shutdown();
|
||||
}
|
||||
// SAFETY: `device` is a retained MTLDevice whose reference we own.
|
||||
match unsafe { crate::inspector_metal::MetalBackend::init(device, i.context()) } {
|
||||
Ok(b) => {
|
||||
i.backend = Some(b);
|
||||
log::debug!("initialized metal backend");
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to initialize metal backend err={err}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the inspector into the host's command buffer. Takes ownership
|
||||
/// of one reference to each of `command_buffer` and `descriptor`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` a live inspector handle; the others retained Metal objects.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_inspector_metal_render(
|
||||
ptr: ghostty_inspector_t,
|
||||
command_buffer: *mut c_void,
|
||||
descriptor: *mut c_void,
|
||||
) {
|
||||
// SAFETY: per the function contract.
|
||||
let Some(i) = (unsafe { inspector(ptr) }) else {
|
||||
return;
|
||||
};
|
||||
// SAFETY: retained objects whose reference we own.
|
||||
unsafe { crate::inspector_metal::render(i, command_buffer, descriptor) };
|
||||
}
|
||||
|
||||
/// Shut the Metal backend down.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be a live inspector handle.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_inspector_metal_shutdown(ptr: ghostty_inspector_t) {
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(i) = unsafe { inspector(ptr) }
|
||||
&& let Some(b) = i.backend.take()
|
||||
{
|
||||
b.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
//! The Metal backend of the embedded inspector
|
||||
//! (`ghostty_inspector_metal_{init,render,shutdown}`).
|
||||
//!
|
||||
//! Upstream links Dear ImGui's `imgui_impl_metal.mm`; this is the same
|
||||
//! renderer written against `objc2-metal` and `dear-imgui-rs`'s managed
|
||||
//! texture protocol: the draw lists are uploaded to Metal buffers and drawn
|
||||
//! with the `imgui_impl_metal` shaders into the render pass the host
|
||||
//! provides.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::{collections::HashMap, ffi::c_void, ptr::NonNull};
|
||||
|
||||
use ghostty_inspector::imgui::{
|
||||
BackendFlags,
|
||||
TextureFormat,
|
||||
TextureId,
|
||||
render::{
|
||||
DrawCmd,
|
||||
DrawIdx,
|
||||
DrawVert,
|
||||
PendingFrame,
|
||||
SnapshotTextureId,
|
||||
SynchronousRendererConsumer,
|
||||
TextureFeedback,
|
||||
TextureOp,
|
||||
TextureRequest,
|
||||
},
|
||||
};
|
||||
use objc2::{rc::Retained, runtime::ProtocolObject};
|
||||
use objc2_foundation::NSString;
|
||||
use objc2_metal::{
|
||||
MTLBlendFactor,
|
||||
MTLBlendOperation,
|
||||
MTLCommandBuffer,
|
||||
MTLCommandEncoder as _,
|
||||
MTLCompareFunction,
|
||||
MTLCullMode,
|
||||
MTLDepthStencilDescriptor,
|
||||
MTLDepthStencilState,
|
||||
MTLDevice,
|
||||
MTLIndexType,
|
||||
MTLLibrary,
|
||||
MTLOrigin,
|
||||
MTLPixelFormat,
|
||||
MTLPrimitiveType,
|
||||
MTLRegion,
|
||||
MTLRenderCommandEncoder,
|
||||
MTLRenderPassDescriptor,
|
||||
MTLRenderPipelineDescriptor,
|
||||
MTLRenderPipelineState,
|
||||
MTLResourceOptions,
|
||||
MTLScissorRect,
|
||||
MTLSize,
|
||||
MTLTexture,
|
||||
MTLTextureDescriptor,
|
||||
MTLVertexDescriptor,
|
||||
MTLVertexFormat,
|
||||
MTLVertexStepFunction,
|
||||
MTLViewport,
|
||||
};
|
||||
|
||||
use crate::inspector::Inspector;
|
||||
|
||||
/// The shaders of `imgui_impl_metal.mm`.
|
||||
const SHADERS: &str = r"
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct Uniforms {
|
||||
float4x4 projectionMatrix;
|
||||
};
|
||||
|
||||
struct VertexIn {
|
||||
float2 position [[attribute(0)]];
|
||||
float2 texCoords [[attribute(1)]];
|
||||
uchar4 color [[attribute(2)]];
|
||||
};
|
||||
|
||||
struct VertexOut {
|
||||
float4 position [[position]];
|
||||
float2 texCoords;
|
||||
float4 color;
|
||||
};
|
||||
|
||||
vertex VertexOut vertex_main(VertexIn in [[stage_in]],
|
||||
constant Uniforms &uniforms [[buffer(1)]]) {
|
||||
VertexOut out;
|
||||
out.position = uniforms.projectionMatrix * float4(in.position, 0, 1);
|
||||
out.texCoords = in.texCoords;
|
||||
out.color = float4(in.color) / float4(255.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
fragment half4 fragment_main(VertexOut in [[stage_in]],
|
||||
texture2d<half, access::sample> texture [[texture(0)]]) {
|
||||
constexpr sampler linearSampler(coord::normalized,
|
||||
min_filter::linear,
|
||||
mag_filter::linear,
|
||||
mip_filter::linear);
|
||||
half4 texColor = texture.sample(linearSampler, in.texCoords);
|
||||
return half4(in.color) * texColor;
|
||||
}
|
||||
";
|
||||
|
||||
type Device = Retained<ProtocolObject<dyn MTLDevice>>;
|
||||
|
||||
/// A pipeline state for one render target configuration.
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
|
||||
struct PipelineKey {
|
||||
color_format: usize,
|
||||
sample_count: usize,
|
||||
depth_format: usize,
|
||||
stencil_format: usize,
|
||||
}
|
||||
|
||||
/// The Metal draw backend of one inspector.
|
||||
pub struct MetalBackend {
|
||||
device: Device,
|
||||
library: Retained<ProtocolObject<dyn MTLLibrary>>,
|
||||
depth_state: Retained<ProtocolObject<dyn MTLDepthStencilState>>,
|
||||
pipelines: HashMap<PipelineKey, Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
|
||||
consumer: SynchronousRendererConsumer,
|
||||
/// Managed textures by Dear ImGui's identity.
|
||||
textures: HashMap<SnapshotTextureId, (u64, Retained<ProtocolObject<dyn MTLTexture>>)>,
|
||||
next_texture_id: u64,
|
||||
}
|
||||
|
||||
impl MetalBackend {
|
||||
/// Takes ownership of one reference to `device` (Zig: `defer
|
||||
/// device.release()`).
|
||||
///
|
||||
/// # Safety
|
||||
/// `device` must be a retained `MTLDevice`.
|
||||
pub unsafe fn init(
|
||||
device: *mut c_void,
|
||||
ctx: &mut ghostty_inspector::imgui::Context,
|
||||
) -> Result<MetalBackend, String> {
|
||||
// SAFETY: per the function contract we own one reference.
|
||||
let device: Device =
|
||||
unsafe { Retained::from_raw(device.cast::<ProtocolObject<dyn MTLDevice>>()) }
|
||||
.ok_or_else(|| "null Metal device".to_owned())?;
|
||||
|
||||
let library = device
|
||||
.newLibraryWithSource_options_error(&NSString::from_str(SHADERS), None)
|
||||
.map_err(|e| e.localizedDescription().to_string())?;
|
||||
|
||||
let depth_desc = MTLDepthStencilDescriptor::new();
|
||||
depth_desc.setDepthCompareFunction(MTLCompareFunction::Always);
|
||||
depth_desc.setDepthWriteEnabled(false);
|
||||
let depth_state = device
|
||||
.newDepthStencilStateWithDescriptor(&depth_desc)
|
||||
.ok_or_else(|| "failed to create the depth stencil state".to_owned())?;
|
||||
|
||||
// Publish the renderer capabilities and the managed texture
|
||||
// consumer.
|
||||
let consumer = ctx
|
||||
.create_synchronous_renderer_consumer()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let reset = ctx
|
||||
.prepare_renderer_texture_reset(&consumer)
|
||||
.map_err(|e| e.to_string())?;
|
||||
reset.commit();
|
||||
if let Err(err) = ctx.set_renderer_name(Some("ghostty_embedded_metal")) {
|
||||
log::warn!("unable to set the imgui renderer name err={err}");
|
||||
}
|
||||
let io = ctx.io_mut();
|
||||
io.set_backend_flags(
|
||||
io.backend_flags()
|
||||
| BackendFlags::RENDERER_HAS_VTX_OFFSET
|
||||
| BackendFlags::RENDERER_HAS_TEXTURES,
|
||||
);
|
||||
|
||||
Ok(MetalBackend {
|
||||
device,
|
||||
library,
|
||||
depth_state,
|
||||
pipelines: HashMap::new(),
|
||||
consumer,
|
||||
textures: HashMap::new(),
|
||||
next_texture_id: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// Release the GPU objects.
|
||||
pub fn shutdown(self) {
|
||||
drop(self);
|
||||
}
|
||||
|
||||
fn pipeline(
|
||||
&mut self,
|
||||
key: PipelineKey,
|
||||
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
|
||||
if let Some(p) = self.pipelines.get(&key) {
|
||||
return Ok(p.clone());
|
||||
}
|
||||
|
||||
let desc = MTLRenderPipelineDescriptor::new();
|
||||
let vertex = self
|
||||
.library
|
||||
.newFunctionWithName(&NSString::from_str("vertex_main"))
|
||||
.ok_or("missing vertex_main")?;
|
||||
let fragment = self
|
||||
.library
|
||||
.newFunctionWithName(&NSString::from_str("fragment_main"))
|
||||
.ok_or("missing fragment_main")?;
|
||||
desc.setVertexFunction(Some(&vertex));
|
||||
desc.setFragmentFunction(Some(&fragment));
|
||||
|
||||
// ImDrawVert: float2 pos, float2 uv, uchar4 col.
|
||||
let vd = MTLVertexDescriptor::new();
|
||||
let stride = size_of::<DrawVert>();
|
||||
// SAFETY: attribute/layout indexes are valid; plain numbers.
|
||||
unsafe {
|
||||
let attrs = vd.attributes();
|
||||
let a0 = attrs.objectAtIndexedSubscript(0);
|
||||
a0.setFormat(MTLVertexFormat::Float2);
|
||||
a0.setOffset(std::mem::offset_of!(DrawVert, pos));
|
||||
a0.setBufferIndex(0);
|
||||
let a1 = attrs.objectAtIndexedSubscript(1);
|
||||
a1.setFormat(MTLVertexFormat::Float2);
|
||||
a1.setOffset(std::mem::offset_of!(DrawVert, uv));
|
||||
a1.setBufferIndex(0);
|
||||
let a2 = attrs.objectAtIndexedSubscript(2);
|
||||
a2.setFormat(MTLVertexFormat::UChar4);
|
||||
a2.setOffset(std::mem::offset_of!(DrawVert, col));
|
||||
a2.setBufferIndex(0);
|
||||
let layout = vd.layouts().objectAtIndexedSubscript(0);
|
||||
layout.setStepRate(1);
|
||||
layout.setStepFunction(MTLVertexStepFunction::PerVertex);
|
||||
layout.setStride(stride);
|
||||
}
|
||||
desc.setVertexDescriptor(Some(&vd));
|
||||
desc.setRasterSampleCount(key.sample_count);
|
||||
|
||||
// SAFETY: color attachment 0 exists.
|
||||
let color = unsafe { desc.colorAttachments().objectAtIndexedSubscript(0) };
|
||||
color.setPixelFormat(MTLPixelFormat(key.color_format));
|
||||
color.setBlendingEnabled(true);
|
||||
color.setRgbBlendOperation(MTLBlendOperation::Add);
|
||||
color.setAlphaBlendOperation(MTLBlendOperation::Add);
|
||||
color.setSourceRGBBlendFactor(MTLBlendFactor::SourceAlpha);
|
||||
color.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
|
||||
color.setSourceAlphaBlendFactor(MTLBlendFactor::One);
|
||||
color.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
|
||||
desc.setDepthAttachmentPixelFormat(MTLPixelFormat(key.depth_format));
|
||||
desc.setStencilAttachmentPixelFormat(MTLPixelFormat(key.stencil_format));
|
||||
|
||||
let state = self
|
||||
.device
|
||||
.newRenderPipelineStateWithDescriptor_error(&desc)
|
||||
.map_err(|e| e.localizedDescription().to_string())?;
|
||||
self.pipelines.insert(key, state.clone());
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// Create (or recreate) the texture of `key` from a full upload.
|
||||
fn create_texture(
|
||||
&mut self,
|
||||
key: SnapshotTextureId,
|
||||
format: TextureFormat,
|
||||
width: u32,
|
||||
height: u32,
|
||||
row_pitch: usize,
|
||||
pixels: &[u8],
|
||||
) -> Result<TextureId, String> {
|
||||
let rgba = to_rgba(format, width as usize, height as usize, row_pitch, pixels)?;
|
||||
|
||||
let desc = MTLTextureDescriptor::new();
|
||||
desc.setPixelFormat(MTLPixelFormat::RGBA8Unorm);
|
||||
// SAFETY: plain numbers validated by texture creation.
|
||||
unsafe {
|
||||
desc.setWidth(width as usize);
|
||||
desc.setHeight(height as usize);
|
||||
}
|
||||
let texture = self
|
||||
.device
|
||||
.newTextureWithDescriptor(&desc)
|
||||
.ok_or("failed to create the texture")?;
|
||||
replace_region(&texture, 0, 0, width as usize, height as usize, &rgba)?;
|
||||
|
||||
let id = if let Some((id, _)) = self.textures.get(&key) {
|
||||
*id
|
||||
} else {
|
||||
let id = self.next_texture_id;
|
||||
self.next_texture_id += 1;
|
||||
id
|
||||
};
|
||||
self.textures.insert(key, (id, texture));
|
||||
Ok(TextureId::new(id))
|
||||
}
|
||||
|
||||
fn process_request(&mut self, request: &TextureRequest) -> Result<TextureFeedback, String> {
|
||||
let key = request.texture();
|
||||
match request.operation() {
|
||||
TextureOp::Create {
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
row_pitch,
|
||||
pixels,
|
||||
} => {
|
||||
let id = self.create_texture(key, *format, *width, *height, *row_pitch, pixels)?;
|
||||
request.uploaded(id).map_err(|e| e.to_string())
|
||||
}
|
||||
TextureOp::Update {
|
||||
format,
|
||||
width: _,
|
||||
height: _,
|
||||
rects,
|
||||
} => {
|
||||
let Some((id, texture)) = self.textures.get(&key) else {
|
||||
// The texture was never created or was destroyed.
|
||||
return Ok(request.superseded());
|
||||
};
|
||||
for r in rects {
|
||||
let (w, h) = (usize::from(r.rect.w), usize::from(r.rect.h));
|
||||
let rgba = to_rgba(*format, w, h, r.row_pitch, &r.data)?;
|
||||
replace_region(
|
||||
texture,
|
||||
usize::from(r.rect.x),
|
||||
usize::from(r.rect.y),
|
||||
w,
|
||||
h,
|
||||
&rgba,
|
||||
)?;
|
||||
}
|
||||
request
|
||||
.uploaded(TextureId::new(*id))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
TextureOp::Destroy => {
|
||||
self.textures.remove(&key);
|
||||
request.destroyed().map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconcile the managed textures of the frame and draw it into
|
||||
/// `encoder` (`ImGui_ImplMetal_RenderDrawData`).
|
||||
fn render(
|
||||
&mut self,
|
||||
frame: PendingFrame<'_>,
|
||||
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
|
||||
desc: &MTLRenderPassDescriptor,
|
||||
) -> Result<(), String> {
|
||||
let mut feedback = Vec::with_capacity(frame.texture_requests().len());
|
||||
for request in frame.texture_requests() {
|
||||
feedback.push(self.process_request(request)?);
|
||||
}
|
||||
let frame = frame
|
||||
.reconcile_texture_feedback(feedback)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let draw_data = frame.draw_data();
|
||||
|
||||
let [fb_w, fb_h] = {
|
||||
let size = draw_data.display_size();
|
||||
let scale = draw_data.framebuffer_scale();
|
||||
[size[0] * scale[0], size[1] * scale[1]]
|
||||
};
|
||||
// Avoid rendering when minimized.
|
||||
if fb_w <= 0.0 || fb_h <= 0.0 || draw_data.draw_lists_count() == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// The pipeline matches the render target the host gave us.
|
||||
// SAFETY: attachment 0 exists.
|
||||
let color_tex = unsafe { desc.colorAttachments().objectAtIndexedSubscript(0) }.texture();
|
||||
let depth_tex = desc.depthAttachment().texture();
|
||||
let stencil_tex = desc.stencilAttachment().texture();
|
||||
let key = PipelineKey {
|
||||
color_format: color_tex
|
||||
.as_ref()
|
||||
.map_or(MTLPixelFormat::BGRA8Unorm.0, |t| t.pixelFormat().0),
|
||||
sample_count: color_tex.as_ref().map_or(1, |t| t.sampleCount()),
|
||||
depth_format: depth_tex
|
||||
.as_ref()
|
||||
.map_or(MTLPixelFormat::Invalid.0, |t| t.pixelFormat().0),
|
||||
stencil_format: stencil_tex
|
||||
.as_ref()
|
||||
.map_or(MTLPixelFormat::Invalid.0, |t| t.pixelFormat().0),
|
||||
};
|
||||
let pipeline = self.pipeline(key)?;
|
||||
|
||||
let encoder = command_buffer
|
||||
.renderCommandEncoderWithDescriptor(desc)
|
||||
.ok_or("failed to create the render command encoder")?;
|
||||
|
||||
// Render state.
|
||||
encoder.setCullMode(MTLCullMode::None);
|
||||
encoder.setDepthStencilState(Some(&self.depth_state));
|
||||
encoder.setViewport(MTLViewport {
|
||||
originX: 0.0,
|
||||
originY: 0.0,
|
||||
width: f64::from(fb_w),
|
||||
height: f64::from(fb_h),
|
||||
znear: 0.0,
|
||||
zfar: 1.0,
|
||||
});
|
||||
encoder.setRenderPipelineState(&pipeline);
|
||||
|
||||
// The orthographic projection of the display.
|
||||
let [dx, dy] = draw_data.display_pos();
|
||||
let [dw, dh] = draw_data.display_size();
|
||||
let (l, r, t, b) = (dx, dx + dw, dy, dy + dh);
|
||||
#[rustfmt::skip]
|
||||
let projection: [[f32; 4]; 4] = [
|
||||
[2.0 / (r - l), 0.0, 0.0, 0.0],
|
||||
[0.0, 2.0 / (t - b), 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[(r + l) / (l - r), (t + b) / (b - t), 0.0, 1.0],
|
||||
];
|
||||
// SAFETY: the pointer/length describe `projection`, which outlives
|
||||
// the call (Metal copies the bytes).
|
||||
unsafe {
|
||||
encoder.setVertexBytes_length_atIndex(
|
||||
NonNull::from(&projection).cast::<c_void>(),
|
||||
size_of::<[[f32; 4]; 4]>(),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
let clip_off = draw_data.display_pos();
|
||||
let clip_scale = draw_data.framebuffer_scale();
|
||||
for list in draw_data.draw_lists() {
|
||||
let vertices = list.vtx_buffer();
|
||||
let indices = list.idx_buffer();
|
||||
if vertices.is_empty() || indices.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let vbuf =
|
||||
self.new_buffer(vertices.as_ptr().cast(), std::mem::size_of_val(vertices))?;
|
||||
let ibuf = self.new_buffer(indices.as_ptr().cast(), std::mem::size_of_val(indices))?;
|
||||
// SAFETY: slot 0 is the vertex stream.
|
||||
unsafe { encoder.setVertexBuffer_offset_atIndex(Some(&vbuf), 0, 0) };
|
||||
|
||||
for cmd in list.commands() {
|
||||
match cmd {
|
||||
DrawCmd::Elements { count, cmd_params } => {
|
||||
// Project the scissor rectangle into framebuffer
|
||||
// space and clamp it to the framebuffer.
|
||||
let cr = cmd_params.clip_rect;
|
||||
let min_x = ((cr[0] - clip_off[0]) * clip_scale[0]).max(0.0);
|
||||
let min_y = ((cr[1] - clip_off[1]) * clip_scale[1]).max(0.0);
|
||||
let max_x = ((cr[2] - clip_off[0]) * clip_scale[0]).min(fb_w);
|
||||
let max_y = ((cr[3] - clip_off[1]) * clip_scale[1]).min(fb_h);
|
||||
if max_x <= min_x || max_y <= min_y {
|
||||
continue;
|
||||
}
|
||||
encoder.setScissorRect(MTLScissorRect {
|
||||
x: min_x as usize,
|
||||
y: min_y as usize,
|
||||
width: (max_x - min_x) as usize,
|
||||
height: (max_y - min_y) as usize,
|
||||
});
|
||||
|
||||
// Bind the texture of the command.
|
||||
let id = cmd_params.texture_id.id();
|
||||
let Some((_, tex)) = self.textures.values().find(|(tid, _)| *tid == id)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// SAFETY: slot 0 of the fragment stage.
|
||||
unsafe { encoder.setFragmentTexture_atIndex(Some(tex), 0) };
|
||||
|
||||
// The vertex offset applies through the buffer
|
||||
// offset (`RENDERER_HAS_VTX_OFFSET`).
|
||||
// SAFETY: the offset stays inside the buffer.
|
||||
unsafe {
|
||||
encoder.setVertexBufferOffset_atIndex(
|
||||
cmd_params.vtx_offset * size_of::<DrawVert>(),
|
||||
0,
|
||||
);
|
||||
encoder.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset(
|
||||
MTLPrimitiveType::Triangle,
|
||||
count,
|
||||
if size_of::<DrawIdx>() == 2 {
|
||||
MTLIndexType::UInt16
|
||||
} else {
|
||||
MTLIndexType::UInt32
|
||||
},
|
||||
&ibuf,
|
||||
cmd_params.idx_offset * size_of::<DrawIdx>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Nothing to reset: state is set per command.
|
||||
DrawCmd::ResetRenderState
|
||||
| DrawCmd::SetSamplerLinear
|
||||
| DrawCmd::SetSamplerNearest => {}
|
||||
DrawCmd::RawCallback(_) => {
|
||||
log::warn!("ignoring a raw imgui draw callback");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encoder.endEncoding();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_buffer(
|
||||
&self,
|
||||
ptr: *const c_void,
|
||||
len: usize,
|
||||
) -> Result<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>, String> {
|
||||
let ptr = NonNull::new(ptr.cast_mut()).ok_or("null buffer")?;
|
||||
// SAFETY: `ptr` points to `len` readable bytes (a slice of the draw
|
||||
// list); Metal copies them.
|
||||
unsafe {
|
||||
self.device.newBufferWithBytes_length_options(
|
||||
ptr,
|
||||
len,
|
||||
MTLResourceOptions::StorageModeShared,
|
||||
)
|
||||
}
|
||||
.ok_or_else(|| "failed to create a buffer".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
/// The pixels as tightly packed RGBA8 (alpha-only textures become white with
|
||||
/// that alpha).
|
||||
fn to_rgba(
|
||||
format: TextureFormat,
|
||||
width: usize,
|
||||
height: usize,
|
||||
row_pitch: usize,
|
||||
data: &[u8],
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let bpp = match format {
|
||||
TextureFormat::RGBA32 => 4,
|
||||
TextureFormat::Alpha8 => 1,
|
||||
};
|
||||
let pitch = if row_pitch == 0 {
|
||||
width * bpp
|
||||
} else {
|
||||
row_pitch
|
||||
};
|
||||
if height > 0 && data.len() < pitch * (height - 1) + width * bpp {
|
||||
return Err("texture upload is too short".to_owned());
|
||||
}
|
||||
let mut out = Vec::with_capacity(width * height * 4);
|
||||
for y in 0..height {
|
||||
let row = &data[y * pitch..y * pitch + width * bpp];
|
||||
match format {
|
||||
TextureFormat::RGBA32 => out.extend_from_slice(row),
|
||||
TextureFormat::Alpha8 => {
|
||||
for a in row {
|
||||
out.extend_from_slice(&[255, 255, 255, *a]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn replace_region(
|
||||
texture: &ProtocolObject<dyn MTLTexture>,
|
||||
x: usize,
|
||||
y: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
rgba: &[u8],
|
||||
) -> Result<(), String> {
|
||||
let ptr = NonNull::new(rgba.as_ptr().cast_mut().cast::<c_void>()).ok_or("empty texture")?;
|
||||
let region = MTLRegion {
|
||||
origin: MTLOrigin { x, y, z: 0 },
|
||||
size: MTLSize {
|
||||
width,
|
||||
height,
|
||||
depth: 1,
|
||||
},
|
||||
};
|
||||
// SAFETY: `rgba` has `width * 4 * height` bytes (built by `to_rgba`).
|
||||
unsafe {
|
||||
texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(region, 0, ptr, width * 4);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render the inspector (Zig: `Inspector.renderMetal`). Takes ownership of
|
||||
/// one reference each to `command_buffer` and `descriptor`.
|
||||
///
|
||||
/// # Safety
|
||||
/// Both must be retained Metal objects.
|
||||
pub unsafe fn render(
|
||||
inspector: &mut Inspector,
|
||||
command_buffer: *mut c_void,
|
||||
descriptor: *mut c_void,
|
||||
) {
|
||||
// SAFETY: per the function contract we own one reference to each; they
|
||||
// are released when these `Retained`s drop.
|
||||
let command_buffer: Option<Retained<ProtocolObject<dyn MTLCommandBuffer>>> =
|
||||
unsafe { Retained::from_raw(command_buffer.cast()) };
|
||||
// SAFETY: as above.
|
||||
let descriptor: Option<Retained<MTLRenderPassDescriptor>> =
|
||||
unsafe { Retained::from_raw(descriptor.cast()) };
|
||||
let (Some(command_buffer), Some(descriptor)) = (command_buffer, descriptor) else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = render_frames(inspector, &command_buffer, &descriptor) {
|
||||
log::error!("error rendering inspector err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn render_frames(
|
||||
inspector: &mut Inspector,
|
||||
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
|
||||
descriptor: &MTLRenderPassDescriptor,
|
||||
) -> Result<(), String> {
|
||||
let mut backend = inspector
|
||||
.take_backend()
|
||||
.ok_or("the Metal backend isn't initialized")?;
|
||||
let result = draw_frame(&mut backend, inspector, command_buffer, descriptor);
|
||||
inspector.set_backend(backend);
|
||||
result
|
||||
}
|
||||
|
||||
fn draw_frame(
|
||||
backend: &mut MetalBackend,
|
||||
inspector: &mut Inspector,
|
||||
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
|
||||
descriptor: &MTLRenderPassDescriptor,
|
||||
) -> Result<(), String> {
|
||||
// Setup our imgui frame. We need to render multiple frames to ensure
|
||||
// ImGui completes all its state processing. I don't know how to fix
|
||||
// this.
|
||||
inspector.build_frame();
|
||||
inspector.context().end_frame();
|
||||
|
||||
inspector.build_frame();
|
||||
let pending = inspector.context().render(&backend.consumer);
|
||||
backend.render(pending, command_buffer, descriptor)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//! libghostty: the embedded C API of Ghostty (`include/ghostty.h`), ported
|
||||
//! from `ZIG/src/main_c.zig`, `ZIG/src/apprt/embedded.zig`,
|
||||
//! `ZIG/src/config/CApi.zig` + `c_get.zig`, the C parts of
|
||||
//! `ZIG/src/apprt/action.zig` and `ZIG/src/benchmark/CApi.zig`.
|
||||
//!
|
||||
//! The header is shipped unchanged in `include/`. The exported symbols and
|
||||
//! the struct / enum layouts of [`types`] match it exactly (verified by
|
||||
//! `tests/abi.rs`).
|
||||
//!
|
||||
//! Platform gating follows Zig: on Apple targets surfaces render with the
|
||||
//! Metal renderer into the host's `NSView` / `UIView`; elsewhere the
|
||||
//! platform independent API (config, app, CLI, key translation, ...) works,
|
||||
//! but `ghostty_surface_new` fails with `UnsupportedPlatform` (Zig's
|
||||
//! `Platform.init`).
|
||||
//!
|
||||
//! `unsafe` is denied crate wide and allowed (with `SAFETY:` comments) only
|
||||
//! in the FFI modules.
|
||||
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::undocumented_unsafe_blocks))]
|
||||
|
||||
pub mod action_c;
|
||||
pub mod benchmark;
|
||||
pub mod config_capi;
|
||||
pub mod embedded;
|
||||
pub mod inspector;
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub mod inspector_metal;
|
||||
pub mod main_c;
|
||||
pub mod types;
|
||||
@@ -0,0 +1,257 @@
|
||||
//! `ghostty_init`, `ghostty_cli_try_action`, `ghostty_info`,
|
||||
//! `ghostty_translate` and `ghostty_string_free` (ZIG/src/main_c.zig).
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::{CStr, CString, c_char, c_int},
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use ghostty_cli::build_info::{self, BuildInfo};
|
||||
use ghostty_core::global::{self, InitOptions};
|
||||
|
||||
use crate::types::{build_mode, ghostty_info_s, ghostty_string_s};
|
||||
|
||||
/// The CLI arguments of the process, as given to `ghostty_init` (Zig:
|
||||
/// `std.os.argv`).
|
||||
static ARGS: OnceLock<Vec<String>> = OnceLock::new();
|
||||
|
||||
/// Build the `ghostty_string_s` for an owned string (Zig:
|
||||
/// `String.fromSlice` of an allocated `[:0]const u8`). Free with
|
||||
/// `ghostty_string_free`.
|
||||
pub(crate) fn string_from_owned(s: &str) -> ghostty_string_s {
|
||||
let mut bytes = Vec::with_capacity(s.len() + 1);
|
||||
bytes.extend_from_slice(s.as_bytes());
|
||||
bytes.push(0);
|
||||
let boxed = bytes.into_boxed_slice();
|
||||
let len = boxed.len() - 1;
|
||||
let ptr = Box::into_raw(boxed).cast::<c_char>();
|
||||
ghostty_string_s {
|
||||
ptr,
|
||||
len,
|
||||
sentinel: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The empty string (`String.empty`).
|
||||
pub(crate) const EMPTY_STRING: ghostty_string_s = ghostty_string_s {
|
||||
ptr: std::ptr::null(),
|
||||
len: 0,
|
||||
sentinel: false,
|
||||
};
|
||||
|
||||
/// Free text allocated with [`alloc_c_text`]: `text_len` bytes plus the
|
||||
/// terminating NUL.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must come from [`alloc_c_text`] (or `string_from_owned`) with the
|
||||
/// same `len` and not have been freed.
|
||||
pub(crate) unsafe fn free_c_text(ptr: *const c_char, len: usize, sentinel: bool) {
|
||||
if ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
let total = len + usize::from(sentinel);
|
||||
// SAFETY: per the function contract the allocation is a boxed slice of
|
||||
// exactly `total` bytes.
|
||||
drop(unsafe {
|
||||
Box::from_raw(std::ptr::slice_from_raw_parts_mut(
|
||||
ptr.cast_mut().cast::<u8>(),
|
||||
total,
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
/// Allocate a NUL terminated copy of `bytes`; returns the pointer (free with
|
||||
/// [`free_c_text`] with `sentinel = true`).
|
||||
pub(crate) fn alloc_c_text(bytes: &[u8]) -> *const c_char {
|
||||
let mut v = Vec::with_capacity(bytes.len() + 1);
|
||||
v.extend_from_slice(bytes);
|
||||
v.push(0);
|
||||
Box::into_raw(v.into_boxed_slice()).cast::<c_char>()
|
||||
}
|
||||
|
||||
/// Initialize ghostty global state.
|
||||
///
|
||||
/// # Safety
|
||||
/// `argv` must be null or point to `argc` valid NUL terminated strings.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_init(argc: usize, argv: *mut *mut c_char) -> c_int {
|
||||
let mut args = Vec::with_capacity(argc);
|
||||
if !argv.is_null() {
|
||||
for i in 0..argc {
|
||||
// SAFETY: the caller guarantees `argv[0..argc]` are valid.
|
||||
let p = unsafe { *argv.add(i) };
|
||||
if p.is_null() {
|
||||
break;
|
||||
}
|
||||
// SAFETY: each argv entry is a NUL terminated string.
|
||||
let c = unsafe { CStr::from_ptr(p) };
|
||||
args.push(c.to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// We first try to parse any action that we may be executing.
|
||||
let action = match ghostty_cli::ghostty::detect(&args) {
|
||||
Ok(a) => a,
|
||||
Err(err) => {
|
||||
log::error!("failed to initialize ghostty error={err:?}");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
let _ = ARGS.set(args);
|
||||
|
||||
global::init_lib(InitOptions {
|
||||
action: action.is_some(),
|
||||
resources_dir: None,
|
||||
});
|
||||
|
||||
// `+version` and friends report the library build.
|
||||
build_info::set(lib_build_info());
|
||||
0
|
||||
}
|
||||
|
||||
/// The build info of the library (Zig: `build_config` for the lib artifact:
|
||||
/// no app runtime, CoreText + Metal on Apple targets).
|
||||
fn lib_build_info() -> BuildInfo {
|
||||
let mut info = BuildInfo {
|
||||
app_runtime: ".none".to_owned(),
|
||||
gtk_build: None,
|
||||
gtk_runtime: None,
|
||||
adw_build: None,
|
||||
adw_runtime: None,
|
||||
x11: false,
|
||||
wayland: false,
|
||||
..BuildInfo::default()
|
||||
};
|
||||
if cfg!(target_vendor = "apple") {
|
||||
".coretext".clone_into(&mut info.font_backend);
|
||||
"renderer.generic.Renderer(renderer.Metal)".clone_into(&mut info.renderer);
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
/// Runs an action if it is specified. If there is no action this returns.
|
||||
/// If there is an action then this doesn't return.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn ghostty_cli_try_action() {
|
||||
let Some(args) = ARGS.get() else {
|
||||
return;
|
||||
};
|
||||
// No action: nothing to do.
|
||||
if !matches!(ghostty_cli::ghostty::detect(args), Ok(Some(_))) {
|
||||
return;
|
||||
}
|
||||
log::info!("executing CLI action");
|
||||
let code = ghostty_cli::run_from_args(args).unwrap_or(0);
|
||||
std::process::exit(i32::from(code));
|
||||
}
|
||||
|
||||
/// Return metadata about Ghostty, such as version, build mode, etc.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn ghostty_info() -> ghostty_info_s {
|
||||
static VERSION: OnceLock<CString> = OnceLock::new();
|
||||
let info = build_info::get();
|
||||
let version = VERSION.get_or_init(|| CString::new(info.version_string).unwrap_or_default());
|
||||
let mode = match info.build_mode.trim_start_matches('.') {
|
||||
"Debug" => build_mode::DEBUG,
|
||||
"ReleaseSafe" => build_mode::RELEASE_SAFE,
|
||||
"ReleaseFast" => build_mode::RELEASE_FAST,
|
||||
"ReleaseSmall" => build_mode::RELEASE_SMALL,
|
||||
_ if cfg!(debug_assertions) => build_mode::DEBUG,
|
||||
_ => build_mode::RELEASE_SAFE,
|
||||
};
|
||||
ghostty_info_s {
|
||||
build_mode: mode,
|
||||
version: version.as_ptr(),
|
||||
version_len: version.as_bytes().len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Translate a string maintained by libghostty into the current application
|
||||
/// language. This will return the same string (same pointer) if no
|
||||
/// translation is found, so the pointer must be stable through the function
|
||||
/// call.
|
||||
///
|
||||
/// # Safety
|
||||
/// `msgid` must be a valid NUL terminated string.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_translate(msgid: *const c_char) -> *const c_char {
|
||||
static CACHE: OnceLock<Mutex<HashMap<String, CString>>> = OnceLock::new();
|
||||
if msgid.is_null() {
|
||||
return msgid;
|
||||
}
|
||||
|
||||
// SAFETY: the caller guarantees `msgid` is NUL terminated.
|
||||
let id = unsafe { CStr::from_ptr(msgid) };
|
||||
let Ok(id_str) = id.to_str() else {
|
||||
return msgid;
|
||||
};
|
||||
let translated = ghostty_os::i18n::tr(id_str);
|
||||
if translated == id_str {
|
||||
return msgid;
|
||||
}
|
||||
|
||||
// Translations are interned so the returned pointer stays valid.
|
||||
let Ok(mut cache) = CACHE.get_or_init(Mutex::default).lock() else {
|
||||
return msgid;
|
||||
};
|
||||
if let Some(c) = cache.get(&translated) {
|
||||
return c.as_ptr();
|
||||
}
|
||||
let Ok(c) = CString::new(translated.clone()) else {
|
||||
return msgid;
|
||||
};
|
||||
let ptr = c.as_ptr();
|
||||
cache.insert(translated, c);
|
||||
ptr
|
||||
}
|
||||
|
||||
/// Free a string allocated by Ghostty.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn ghostty_string_free(str: ghostty_string_s) {
|
||||
// SAFETY: strings handed out by this library are allocated by
|
||||
// `string_from_owned` / `alloc_c_text` with `len` (+1 if sentinel) bytes.
|
||||
unsafe { free_c_text(str.ptr, str.len, str.sentinel) };
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ghostty_string_s_empty_string() {
|
||||
let empty = EMPTY_STRING;
|
||||
ghostty_string_free(empty);
|
||||
assert_eq!(empty.len, 0);
|
||||
assert!(!empty.sentinel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ghostty_string_s_c_string() {
|
||||
let s = string_from_owned("hello");
|
||||
assert_eq!(s.len, 5);
|
||||
assert!(s.sentinel);
|
||||
// SAFETY: just allocated with a NUL terminator.
|
||||
let c = unsafe { CStr::from_ptr(s.ptr) };
|
||||
assert_eq!(c.to_str().unwrap(), "hello");
|
||||
ghostty_string_free(s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_version() {
|
||||
let info = ghostty_info();
|
||||
assert!(info.version_len > 0);
|
||||
// SAFETY: the version pointer is a static NUL terminated string.
|
||||
let v = unsafe { CStr::from_ptr(info.version) };
|
||||
assert_eq!(v.to_bytes().len(), info.version_len);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_returns_same_pointer_without_translation() {
|
||||
let id = c"this message has no translation";
|
||||
// SAFETY: valid NUL terminated string.
|
||||
let out = unsafe { ghostty_translate(id.as_ptr()) };
|
||||
assert_eq!(out, id.as_ptr());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
//! The C types of `include/ghostty.h`, laid out exactly as the header (and
|
||||
//! the Zig `extern` structs) define them.
|
||||
//!
|
||||
//! Enums are `c_int` everywhere (the header uses `int`-sized enums and the C
|
||||
//! caller may pass any value); the enumerator values live in submodules as
|
||||
//! `i32` constants named after the header's enumerators.
|
||||
#![allow(non_camel_case_types, clippy::struct_field_names)]
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Opaque handles
|
||||
|
||||
pub type ghostty_app_t = *mut c_void;
|
||||
pub type ghostty_config_t = *mut c_void;
|
||||
pub type ghostty_surface_t = *mut c_void;
|
||||
pub type ghostty_inspector_t = *mut c_void;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Scalars and enums (all `int`)
|
||||
|
||||
pub type ghostty_platform_e = c_int;
|
||||
pub type ghostty_clipboard_e = c_int;
|
||||
pub type ghostty_clipboard_request_e = c_int;
|
||||
pub type ghostty_input_mouse_state_e = c_int;
|
||||
pub type ghostty_input_mouse_button_e = c_int;
|
||||
pub type ghostty_input_mouse_momentum_e = c_int;
|
||||
pub type ghostty_color_scheme_e = c_int;
|
||||
pub type ghostty_input_scroll_mods_t = c_int;
|
||||
pub type ghostty_input_mods_e = c_int;
|
||||
pub type ghostty_binding_flags_e = c_int;
|
||||
pub type ghostty_input_action_e = c_int;
|
||||
pub type ghostty_input_key_e = c_int;
|
||||
pub type ghostty_input_trigger_tag_e = c_int;
|
||||
pub type ghostty_build_mode_e = c_int;
|
||||
pub type ghostty_point_tag_e = c_int;
|
||||
pub type ghostty_point_coord_e = c_int;
|
||||
pub type ghostty_surface_context_e = c_int;
|
||||
pub type ghostty_target_tag_e = c_int;
|
||||
pub type ghostty_action_tag_e = c_int;
|
||||
|
||||
/// `GHOSTTY_SUCCESS`.
|
||||
pub const GHOSTTY_SUCCESS: c_int = 0;
|
||||
|
||||
pub mod platform {
|
||||
pub const INVALID: i32 = 0;
|
||||
pub const MACOS: i32 = 1;
|
||||
pub const IOS: i32 = 2;
|
||||
}
|
||||
|
||||
pub mod clipboard {
|
||||
pub const STANDARD: i32 = 0;
|
||||
pub const SELECTION: i32 = 1;
|
||||
}
|
||||
|
||||
pub mod trigger_tag {
|
||||
pub const PHYSICAL: i32 = 0;
|
||||
pub const UNICODE: i32 = 1;
|
||||
pub const CATCH_ALL: i32 = 2;
|
||||
}
|
||||
|
||||
pub mod build_mode {
|
||||
pub const DEBUG: i32 = 0;
|
||||
pub const RELEASE_SAFE: i32 = 1;
|
||||
pub const RELEASE_FAST: i32 = 2;
|
||||
pub const RELEASE_SMALL: i32 = 3;
|
||||
}
|
||||
|
||||
pub mod target_tag {
|
||||
pub const APP: i32 = 0;
|
||||
pub const SURFACE: i32 = 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Structs
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_clipboard_content_s {
|
||||
pub mime: *const c_char,
|
||||
pub data: *const c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_input_key_s {
|
||||
pub action: ghostty_input_action_e,
|
||||
pub mods: ghostty_input_mods_e,
|
||||
pub consumed_mods: ghostty_input_mods_e,
|
||||
pub keycode: u32,
|
||||
pub text: *const c_char,
|
||||
pub unshifted_codepoint: u32,
|
||||
pub composing: bool,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub union ghostty_input_trigger_key_u {
|
||||
pub translated: ghostty_input_key_e,
|
||||
pub physical: ghostty_input_key_e,
|
||||
pub unicode: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_input_trigger_s {
|
||||
pub tag: ghostty_input_trigger_tag_e,
|
||||
pub key: ghostty_input_trigger_key_u,
|
||||
pub mods: ghostty_input_mods_e,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_command_s {
|
||||
pub action_key: *const c_char,
|
||||
pub action: *const c_char,
|
||||
pub title: *const c_char,
|
||||
pub description: *const c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_info_s {
|
||||
pub build_mode: ghostty_build_mode_e,
|
||||
pub version: *const c_char,
|
||||
pub version_len: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_diagnostic_s {
|
||||
pub message: *const c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_string_s {
|
||||
pub ptr: *const c_char,
|
||||
pub len: usize,
|
||||
pub sentinel: bool,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_text_s {
|
||||
pub tl_px_x: f64,
|
||||
pub tl_px_y: f64,
|
||||
pub offset_start: u32,
|
||||
pub offset_len: u32,
|
||||
pub text: *const c_char,
|
||||
pub text_len: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_point_s {
|
||||
pub tag: ghostty_point_tag_e,
|
||||
pub coord: ghostty_point_coord_e,
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_selection_s {
|
||||
pub top_left: ghostty_point_s,
|
||||
pub bottom_right: ghostty_point_s,
|
||||
pub rectangle: bool,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_env_var_s {
|
||||
pub key: *const c_char,
|
||||
pub value: *const c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_platform_macos_s {
|
||||
pub nsview: *mut c_void,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_platform_ios_s {
|
||||
pub uiview: *mut c_void,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub union ghostty_platform_u {
|
||||
pub macos: ghostty_platform_macos_s,
|
||||
pub ios: ghostty_platform_ios_s,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_surface_config_s {
|
||||
pub platform_tag: ghostty_platform_e,
|
||||
pub platform: ghostty_platform_u,
|
||||
pub userdata: *mut c_void,
|
||||
pub scale_factor: f64,
|
||||
pub font_size: f32,
|
||||
pub working_directory: *const c_char,
|
||||
pub command: *const c_char,
|
||||
pub env_vars: *mut ghostty_env_var_s,
|
||||
pub env_var_count: usize,
|
||||
pub initial_input: *const c_char,
|
||||
pub wait_after_command: bool,
|
||||
pub context: ghostty_surface_context_e,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_surface_size_s {
|
||||
pub columns: u16,
|
||||
pub rows: u16,
|
||||
pub width_px: u32,
|
||||
pub height_px: u32,
|
||||
pub cell_width_px: u32,
|
||||
pub cell_height_px: u32,
|
||||
}
|
||||
|
||||
// ---- config types
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_config_path_s {
|
||||
pub path: *const c_char,
|
||||
pub optional: bool,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_config_color_s {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_config_color_list_s {
|
||||
pub colors: *const ghostty_config_color_s,
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_config_command_list_s {
|
||||
pub commands: *const ghostty_command_s,
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_config_palette_s {
|
||||
pub colors: [ghostty_config_color_s; 256],
|
||||
}
|
||||
|
||||
pub mod quick_terminal_size_tag {
|
||||
pub const NONE: i32 = 0;
|
||||
pub const PERCENTAGE: i32 = 1;
|
||||
pub const PIXELS: i32 = 2;
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub union ghostty_quick_terminal_size_value_u {
|
||||
pub percentage: f32,
|
||||
pub pixels: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_quick_terminal_size_s {
|
||||
pub tag: c_int,
|
||||
pub value: ghostty_quick_terminal_size_value_u,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_config_quick_terminal_size_s {
|
||||
pub primary: ghostty_quick_terminal_size_s,
|
||||
pub secondary: ghostty_quick_terminal_size_s,
|
||||
}
|
||||
|
||||
// ---- targets and actions
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub union ghostty_target_u {
|
||||
pub surface: ghostty_surface_t,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_target_s {
|
||||
pub tag: ghostty_target_tag_e,
|
||||
pub target: ghostty_target_u,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_resize_split_s {
|
||||
pub amount: u16,
|
||||
pub direction: c_int,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_move_tab_s {
|
||||
pub amount: isize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_desktop_notification_s {
|
||||
pub title: *const c_char,
|
||||
pub body: *const c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_set_title_s {
|
||||
pub title: *const c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_pwd_s {
|
||||
pub pwd: *const c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_mouse_over_link_s {
|
||||
pub url: *const c_char,
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_size_limit_s {
|
||||
pub min_width: u32,
|
||||
pub min_height: u32,
|
||||
pub max_width: u32,
|
||||
pub max_height: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_initial_size_s {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_cell_size_s {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_key_sequence_s {
|
||||
pub active: bool,
|
||||
pub trigger: ghostty_input_trigger_s,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_key_table_activate_s {
|
||||
pub name: *const c_char,
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub union ghostty_action_key_table_u {
|
||||
pub activate: ghostty_action_key_table_activate_s,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_key_table_s {
|
||||
pub tag: c_int,
|
||||
pub value: ghostty_action_key_table_u,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_color_change_s {
|
||||
pub kind: c_int,
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_config_change_s {
|
||||
pub config: ghostty_config_t,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_reload_config_s {
|
||||
pub soft: bool,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_open_url_s {
|
||||
pub kind: c_int,
|
||||
pub url: *const c_char,
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_surface_message_childexited_s {
|
||||
pub exit_code: u32,
|
||||
pub timetime_ms: u64,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_progress_report_s {
|
||||
pub state: c_int,
|
||||
pub progress: i8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_command_finished_s {
|
||||
pub exit_code: i16,
|
||||
pub duration: u64,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_start_search_s {
|
||||
pub needle: *const c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_search_total_s {
|
||||
pub total: isize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_search_selected_s {
|
||||
pub selected: isize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_scrollbar_s {
|
||||
pub total: u64,
|
||||
pub offset: u64,
|
||||
pub len: u64,
|
||||
}
|
||||
|
||||
/// `ghostty_action_u`. Fields that are plain enums are `c_int`.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub union ghostty_action_u {
|
||||
pub new_split: c_int,
|
||||
pub toggle_fullscreen: c_int,
|
||||
pub move_tab: ghostty_action_move_tab_s,
|
||||
pub goto_tab: c_int,
|
||||
pub goto_split: c_int,
|
||||
pub goto_window: c_int,
|
||||
pub resize_split: ghostty_action_resize_split_s,
|
||||
pub size_limit: ghostty_action_size_limit_s,
|
||||
pub initial_size: ghostty_action_initial_size_s,
|
||||
pub cell_size: ghostty_action_cell_size_s,
|
||||
pub scrollbar: ghostty_action_scrollbar_s,
|
||||
pub inspector: c_int,
|
||||
pub desktop_notification: ghostty_action_desktop_notification_s,
|
||||
pub set_title: ghostty_action_set_title_s,
|
||||
pub set_tab_title: ghostty_action_set_title_s,
|
||||
pub prompt_title: c_int,
|
||||
pub pwd: ghostty_action_pwd_s,
|
||||
pub mouse_shape: c_int,
|
||||
pub mouse_visibility: c_int,
|
||||
pub mouse_over_link: ghostty_action_mouse_over_link_s,
|
||||
pub renderer_health: c_int,
|
||||
pub quit_timer: c_int,
|
||||
pub float_window: c_int,
|
||||
pub secure_input: c_int,
|
||||
pub key_sequence: ghostty_action_key_sequence_s,
|
||||
pub key_table: ghostty_action_key_table_s,
|
||||
pub color_change: ghostty_action_color_change_s,
|
||||
pub reload_config: ghostty_action_reload_config_s,
|
||||
pub config_change: ghostty_action_config_change_s,
|
||||
pub open_url: ghostty_action_open_url_s,
|
||||
pub close_tab_mode: c_int,
|
||||
pub child_exited: ghostty_surface_message_childexited_s,
|
||||
pub progress_report: ghostty_action_progress_report_s,
|
||||
pub command_finished: ghostty_action_command_finished_s,
|
||||
pub start_search: ghostty_action_start_search_s,
|
||||
pub search_total: ghostty_action_search_total_s,
|
||||
pub search_selected: ghostty_action_search_selected_s,
|
||||
pub readonly: c_int,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_action_s {
|
||||
pub tag: ghostty_action_tag_e,
|
||||
pub action: ghostty_action_u,
|
||||
}
|
||||
|
||||
// ---- runtime callbacks
|
||||
|
||||
pub type ghostty_runtime_wakeup_cb = Option<unsafe extern "C" fn(*mut c_void)>;
|
||||
pub type ghostty_runtime_read_clipboard_cb =
|
||||
Option<unsafe extern "C" fn(*mut c_void, ghostty_clipboard_e, *mut c_void) -> bool>;
|
||||
pub type ghostty_runtime_confirm_read_clipboard_cb = Option<
|
||||
unsafe extern "C" fn(*mut c_void, *const c_char, *mut c_void, ghostty_clipboard_request_e),
|
||||
>;
|
||||
pub type ghostty_runtime_write_clipboard_cb = Option<
|
||||
unsafe extern "C" fn(
|
||||
*mut c_void,
|
||||
ghostty_clipboard_e,
|
||||
*const ghostty_clipboard_content_s,
|
||||
usize,
|
||||
bool,
|
||||
),
|
||||
>;
|
||||
pub type ghostty_runtime_close_surface_cb = Option<unsafe extern "C" fn(*mut c_void, bool)>;
|
||||
pub type ghostty_runtime_action_cb =
|
||||
Option<unsafe extern "C" fn(ghostty_app_t, ghostty_target_s, ghostty_action_s) -> bool>;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_runtime_config_s {
|
||||
pub userdata: *mut c_void,
|
||||
pub supports_selection_clipboard: bool,
|
||||
pub wakeup_cb: ghostty_runtime_wakeup_cb,
|
||||
pub action_cb: ghostty_runtime_action_cb,
|
||||
pub read_clipboard_cb: ghostty_runtime_read_clipboard_cb,
|
||||
pub confirm_read_clipboard_cb: ghostty_runtime_confirm_read_clipboard_cb,
|
||||
pub write_clipboard_cb: ghostty_runtime_write_clipboard_cb,
|
||||
pub close_surface_cb: ghostty_runtime_close_surface_cb,
|
||||
}
|
||||
|
||||
// ---- ipc (declared by the header; the embedded runtime never uses them)
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub union ghostty_ipc_target_u {
|
||||
pub klass: *mut c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct chostty_ipc_target_s {
|
||||
pub tag: c_int,
|
||||
pub target: ghostty_ipc_target_u,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ghostty_ipc_action_new_window_s {
|
||||
pub arguments: *mut *const c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub union ghostty_ipc_action_u {
|
||||
pub new_window: ghostty_ipc_action_new_window_s,
|
||||
}
|
||||
@@ -0,0 +1,1385 @@
|
||||
//! ABI verification: a C program generated from the unchanged
|
||||
//! `include/ghostty.h` prints `sizeof`/`alignof`/`offsetof` of every struct
|
||||
//! and union, `sizeof` of every enum and the value of every enumerator, and
|
||||
//! takes the address of every declared function (so linking proves the
|
||||
//! symbol is exported). The values are compared against the Rust
|
||||
//! definitions. A second test compares the `nm` exports with the header.
|
||||
#![cfg(unix)]
|
||||
#![allow(clippy::unwrap_used, clippy::items_after_statements)]
|
||||
|
||||
mod common;
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fmt::Write as _,
|
||||
mem::{align_of, offset_of, size_of},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use ghostty::types::*;
|
||||
use ghostty_apprt::{
|
||||
action::{self, Key as ActionKey},
|
||||
structs::{Clipboard, ClipboardRequestType, ColorScheme},
|
||||
surface::NewSurfaceContext,
|
||||
};
|
||||
use ghostty_input::{
|
||||
Action as KeyAction,
|
||||
Key,
|
||||
Mods,
|
||||
MouseButton,
|
||||
MouseButtonState,
|
||||
binding::Flags,
|
||||
key_mods::{ModSide, Side},
|
||||
mouse::Momentum,
|
||||
};
|
||||
use ghostty_vt::mouse_shape::MouseShape;
|
||||
|
||||
/// `GHOSTTY_FOO_BAR` / `FooBar` / `foo_bar` -> `foobar`.
|
||||
fn norm(s: &str) -> String {
|
||||
s.chars()
|
||||
.filter(|c| *c != '_')
|
||||
.map(|c| c.to_ascii_lowercase())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parsed `typedef enum { ... } Name;` blocks: typedef name -> enumerators.
|
||||
fn header_enums(text: &str) -> BTreeMap<String, Vec<String>> {
|
||||
let mut map = BTreeMap::new();
|
||||
let mut rest = text;
|
||||
while let Some(start) = rest.find("typedef enum") {
|
||||
let after = &rest[start..];
|
||||
let open = after.find('{').unwrap();
|
||||
let close = after.find('}').unwrap();
|
||||
let name_end = after[close..].find(';').unwrap() + close;
|
||||
let name = after[close + 1..name_end].trim().to_string();
|
||||
let names = after[open + 1..close]
|
||||
.split(',')
|
||||
.map(|e| e.split('=').next().unwrap().trim().to_string())
|
||||
.filter(|e| !e.is_empty())
|
||||
.collect();
|
||||
map.insert(name, names);
|
||||
rest = &after[name_end..];
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// Parsed `typedef struct|union { ... } name;` blocks: name -> depth-1 field
|
||||
/// names.
|
||||
fn header_records(text: &str) -> BTreeMap<String, Vec<String>> {
|
||||
let mut map = BTreeMap::new();
|
||||
let b = text.as_bytes();
|
||||
let mut i = 0;
|
||||
while let Some(pos) = text[i..].find("typedef ") {
|
||||
let start = i + pos;
|
||||
let after = &text[start + 8..];
|
||||
let kw = if after.starts_with("struct {") {
|
||||
Some("struct {")
|
||||
} else if after.starts_with("union {") {
|
||||
Some("union {")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
i = start + 8;
|
||||
let Some(kw) = kw else { continue };
|
||||
let open = start + 8 + kw.len() - 1;
|
||||
// Match braces.
|
||||
let mut depth = 0usize;
|
||||
let mut j = open;
|
||||
let mut fields = Vec::new();
|
||||
let mut stmt = String::new();
|
||||
loop {
|
||||
match b[j] {
|
||||
b'{' => {
|
||||
depth += 1;
|
||||
if depth > 1 {
|
||||
stmt.push('{');
|
||||
}
|
||||
}
|
||||
b'}' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
stmt.push('}');
|
||||
}
|
||||
b';' if depth == 1 => {
|
||||
// A field: the last identifier, minus array suffix.
|
||||
let s = stmt.trim();
|
||||
let s = if let Some(p) = s.rfind('}') {
|
||||
&s[p + 1..]
|
||||
} else {
|
||||
s
|
||||
};
|
||||
let s = s.split('[').next().unwrap().trim();
|
||||
let name = s
|
||||
.rsplit(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
|
||||
.next()
|
||||
.unwrap();
|
||||
if !name.is_empty() {
|
||||
fields.push(name.to_string());
|
||||
}
|
||||
stmt.clear();
|
||||
}
|
||||
c => stmt.push(c as char),
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
let end = text[j..].find(';').unwrap() + j;
|
||||
let name = text[j + 1..end].trim().to_string();
|
||||
map.insert(name, fields);
|
||||
i = end;
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// Names and values of a Rust-side enum, keyed by normalized name.
|
||||
fn vals<T: std::fmt::Debug + Copy>(items: &[T], f: impl Fn(T) -> i64) -> BTreeMap<String, i64> {
|
||||
items
|
||||
.iter()
|
||||
.map(|i| (norm(&format!("{i:?}")), f(*i)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn named(items: &[(&str, i64)]) -> BTreeMap<String, i64> {
|
||||
items.iter().map(|(n, v)| (norm(n), *v)).collect()
|
||||
}
|
||||
|
||||
struct Probe {
|
||||
c: String,
|
||||
expected: BTreeMap<String, i64>,
|
||||
records: BTreeMap<String, Vec<String>>,
|
||||
covered_records: BTreeSet<String>,
|
||||
covered_enums: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl Probe {
|
||||
fn emit(&mut self, key: &str, c_expr: &str, expected: i64) {
|
||||
let _ = writeln!(
|
||||
self.c,
|
||||
" printf(\"%s=%lld\\n\", \"{key}\", (long long)({c_expr}));"
|
||||
);
|
||||
self.expected.insert(key.to_string(), expected);
|
||||
}
|
||||
|
||||
/// `fields` are the Rust offsets in the header's field order; the field
|
||||
/// names are taken from the parsed header (so a missing or reordered
|
||||
/// field is caught).
|
||||
fn layout(&mut self, ty: &str, size: usize, align: usize, offsets: &[usize]) {
|
||||
let fields = self
|
||||
.records
|
||||
.get(ty)
|
||||
.unwrap_or_else(|| panic!("no record {ty} in the header"))
|
||||
.clone();
|
||||
assert_eq!(
|
||||
fields.len(),
|
||||
offsets.len(),
|
||||
"{ty}: the header has fields {fields:?}"
|
||||
);
|
||||
self.covered_records.insert(ty.to_string());
|
||||
self.emit(
|
||||
&format!("sizeof({ty})"),
|
||||
&format!("sizeof({ty})"),
|
||||
size as i64,
|
||||
);
|
||||
self.emit(
|
||||
&format!("alignof({ty})"),
|
||||
&format!("_Alignof({ty})"),
|
||||
align as i64,
|
||||
);
|
||||
for (f, off) in fields.iter().zip(offsets) {
|
||||
self.emit(
|
||||
&format!("offsetof({ty},{f})"),
|
||||
&format!("offsetof({ty}, {f})"),
|
||||
*off as i64,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check every enumerator of `typedef` against `rust` (names after
|
||||
/// `prefix`); the C compiler provides the values.
|
||||
fn enumerators(
|
||||
&mut self,
|
||||
enums: &BTreeMap<String, Vec<String>>,
|
||||
typedef: &str,
|
||||
prefix: &str,
|
||||
rust: &BTreeMap<String, i64>,
|
||||
) {
|
||||
let names = enums
|
||||
.get(typedef)
|
||||
.unwrap_or_else(|| panic!("no enum {typedef}"));
|
||||
self.covered_enums.insert(typedef.to_string());
|
||||
// All GHOSTTY enums are `int` sized.
|
||||
self.emit(
|
||||
&format!("sizeof({typedef})"),
|
||||
&format!("sizeof({typedef})"),
|
||||
4,
|
||||
);
|
||||
for n in names {
|
||||
let short = n
|
||||
.strip_prefix(prefix)
|
||||
.unwrap_or_else(|| panic!("{n} lacks {prefix}"));
|
||||
let want = *rust
|
||||
.get(&norm(short))
|
||||
.unwrap_or_else(|| panic!("{typedef}: Rust has no member for {n}"));
|
||||
self.emit(n, n, want);
|
||||
}
|
||||
assert_eq!(names.len(), rust.len(), "{typedef}: member count differs");
|
||||
}
|
||||
}
|
||||
|
||||
/// `layout!(probe, c_name, RustType, field, ...)`
|
||||
macro_rules! layout {
|
||||
($p:expr, $ty:ty, $name:literal, $($f:ident),+ $(,)?) => {
|
||||
$p.layout(
|
||||
$name,
|
||||
size_of::<$ty>(),
|
||||
align_of::<$ty>(),
|
||||
&[$(offset_of!($ty, $f)),+],
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
fn build_probe() -> Probe {
|
||||
let text = common::header_text();
|
||||
let enums = header_enums(&text);
|
||||
let mut p = Probe {
|
||||
c: String::new(),
|
||||
expected: BTreeMap::new(),
|
||||
records: header_records(&text),
|
||||
covered_records: BTreeSet::new(),
|
||||
covered_enums: BTreeSet::new(),
|
||||
};
|
||||
|
||||
// ---- structs and unions ----
|
||||
layout!(
|
||||
p,
|
||||
ghostty_clipboard_content_s,
|
||||
"ghostty_clipboard_content_s",
|
||||
mime,
|
||||
data
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_input_key_s,
|
||||
"ghostty_input_key_s",
|
||||
action,
|
||||
mods,
|
||||
consumed_mods,
|
||||
keycode,
|
||||
text,
|
||||
unshifted_codepoint,
|
||||
composing
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_input_trigger_key_u,
|
||||
"ghostty_input_trigger_key_u",
|
||||
translated,
|
||||
physical,
|
||||
unicode
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_input_trigger_s,
|
||||
"ghostty_input_trigger_s",
|
||||
tag,
|
||||
key,
|
||||
mods
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_command_s,
|
||||
"ghostty_command_s",
|
||||
action_key,
|
||||
action,
|
||||
title,
|
||||
description
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_info_s,
|
||||
"ghostty_info_s",
|
||||
build_mode,
|
||||
version,
|
||||
version_len
|
||||
);
|
||||
layout!(p, ghostty_diagnostic_s, "ghostty_diagnostic_s", message);
|
||||
layout!(p, ghostty_string_s, "ghostty_string_s", ptr, len, sentinel);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_text_s,
|
||||
"ghostty_text_s",
|
||||
tl_px_x,
|
||||
tl_px_y,
|
||||
offset_start,
|
||||
offset_len,
|
||||
text,
|
||||
text_len
|
||||
);
|
||||
layout!(p, ghostty_point_s, "ghostty_point_s", tag, coord, x, y);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_selection_s,
|
||||
"ghostty_selection_s",
|
||||
top_left,
|
||||
bottom_right,
|
||||
rectangle
|
||||
);
|
||||
layout!(p, ghostty_env_var_s, "ghostty_env_var_s", key, value);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_platform_macos_s,
|
||||
"ghostty_platform_macos_s",
|
||||
nsview
|
||||
);
|
||||
layout!(p, ghostty_platform_ios_s, "ghostty_platform_ios_s", uiview);
|
||||
layout!(p, ghostty_platform_u, "ghostty_platform_u", macos, ios);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_surface_config_s,
|
||||
"ghostty_surface_config_s",
|
||||
platform_tag,
|
||||
platform,
|
||||
userdata,
|
||||
scale_factor,
|
||||
font_size,
|
||||
working_directory,
|
||||
command,
|
||||
env_vars,
|
||||
env_var_count,
|
||||
initial_input,
|
||||
wait_after_command,
|
||||
context
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_surface_size_s,
|
||||
"ghostty_surface_size_s",
|
||||
columns,
|
||||
rows,
|
||||
width_px,
|
||||
height_px,
|
||||
cell_width_px,
|
||||
cell_height_px
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_config_path_s,
|
||||
"ghostty_config_path_s",
|
||||
path,
|
||||
optional
|
||||
);
|
||||
layout!(p, ghostty_config_color_s, "ghostty_config_color_s", r, g, b);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_config_color_list_s,
|
||||
"ghostty_config_color_list_s",
|
||||
colors,
|
||||
len
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_config_command_list_s,
|
||||
"ghostty_config_command_list_s",
|
||||
commands,
|
||||
len
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_config_palette_s,
|
||||
"ghostty_config_palette_s",
|
||||
colors
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_quick_terminal_size_value_u,
|
||||
"ghostty_quick_terminal_size_value_u",
|
||||
percentage,
|
||||
pixels
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_quick_terminal_size_s,
|
||||
"ghostty_quick_terminal_size_s",
|
||||
tag,
|
||||
value
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_config_quick_terminal_size_s,
|
||||
"ghostty_config_quick_terminal_size_s",
|
||||
primary,
|
||||
secondary
|
||||
);
|
||||
layout!(p, ghostty_target_u, "ghostty_target_u", surface);
|
||||
layout!(p, ghostty_target_s, "ghostty_target_s", tag, target);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_resize_split_s,
|
||||
"ghostty_action_resize_split_s",
|
||||
amount,
|
||||
direction
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_move_tab_s,
|
||||
"ghostty_action_move_tab_s",
|
||||
amount
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_desktop_notification_s,
|
||||
"ghostty_action_desktop_notification_s",
|
||||
title,
|
||||
body
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_set_title_s,
|
||||
"ghostty_action_set_title_s",
|
||||
title
|
||||
);
|
||||
layout!(p, ghostty_action_pwd_s, "ghostty_action_pwd_s", pwd);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_mouse_over_link_s,
|
||||
"ghostty_action_mouse_over_link_s",
|
||||
url,
|
||||
len
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_size_limit_s,
|
||||
"ghostty_action_size_limit_s",
|
||||
min_width,
|
||||
min_height,
|
||||
max_width,
|
||||
max_height
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_initial_size_s,
|
||||
"ghostty_action_initial_size_s",
|
||||
width,
|
||||
height
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_cell_size_s,
|
||||
"ghostty_action_cell_size_s",
|
||||
width,
|
||||
height
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_key_sequence_s,
|
||||
"ghostty_action_key_sequence_s",
|
||||
active,
|
||||
trigger
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_key_table_u,
|
||||
"ghostty_action_key_table_u",
|
||||
activate
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_key_table_s,
|
||||
"ghostty_action_key_table_s",
|
||||
tag,
|
||||
value
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_color_change_s,
|
||||
"ghostty_action_color_change_s",
|
||||
kind,
|
||||
r,
|
||||
g,
|
||||
b
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_config_change_s,
|
||||
"ghostty_action_config_change_s",
|
||||
config
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_reload_config_s,
|
||||
"ghostty_action_reload_config_s",
|
||||
soft
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_open_url_s,
|
||||
"ghostty_action_open_url_s",
|
||||
kind,
|
||||
url,
|
||||
len
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_surface_message_childexited_s,
|
||||
"ghostty_surface_message_childexited_s",
|
||||
exit_code,
|
||||
timetime_ms
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_progress_report_s,
|
||||
"ghostty_action_progress_report_s",
|
||||
state,
|
||||
progress
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_command_finished_s,
|
||||
"ghostty_action_command_finished_s",
|
||||
exit_code,
|
||||
duration
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_start_search_s,
|
||||
"ghostty_action_start_search_s",
|
||||
needle
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_search_total_s,
|
||||
"ghostty_action_search_total_s",
|
||||
total
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_search_selected_s,
|
||||
"ghostty_action_search_selected_s",
|
||||
selected
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_scrollbar_s,
|
||||
"ghostty_action_scrollbar_s",
|
||||
total,
|
||||
offset,
|
||||
len
|
||||
);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_action_u,
|
||||
"ghostty_action_u",
|
||||
new_split,
|
||||
toggle_fullscreen,
|
||||
move_tab,
|
||||
goto_tab,
|
||||
goto_split,
|
||||
goto_window,
|
||||
resize_split,
|
||||
size_limit,
|
||||
initial_size,
|
||||
cell_size,
|
||||
scrollbar,
|
||||
inspector,
|
||||
desktop_notification,
|
||||
set_title,
|
||||
set_tab_title,
|
||||
prompt_title,
|
||||
pwd,
|
||||
mouse_shape,
|
||||
mouse_visibility,
|
||||
mouse_over_link,
|
||||
renderer_health,
|
||||
quit_timer,
|
||||
float_window,
|
||||
secure_input,
|
||||
key_sequence,
|
||||
key_table,
|
||||
color_change,
|
||||
reload_config,
|
||||
config_change,
|
||||
open_url,
|
||||
close_tab_mode,
|
||||
child_exited,
|
||||
progress_report,
|
||||
command_finished,
|
||||
start_search,
|
||||
search_total,
|
||||
search_selected,
|
||||
readonly
|
||||
);
|
||||
layout!(p, ghostty_action_s, "ghostty_action_s", tag, action);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_runtime_config_s,
|
||||
"ghostty_runtime_config_s",
|
||||
userdata,
|
||||
supports_selection_clipboard,
|
||||
wakeup_cb,
|
||||
action_cb,
|
||||
read_clipboard_cb,
|
||||
confirm_read_clipboard_cb,
|
||||
write_clipboard_cb,
|
||||
close_surface_cb
|
||||
);
|
||||
layout!(p, ghostty_ipc_target_u, "ghostty_ipc_target_u", klass);
|
||||
layout!(p, chostty_ipc_target_s, "chostty_ipc_target_s", tag, target);
|
||||
layout!(
|
||||
p,
|
||||
ghostty_ipc_action_new_window_s,
|
||||
"ghostty_ipc_action_new_window_s",
|
||||
arguments
|
||||
);
|
||||
layout!(p, ghostty_ipc_action_u, "ghostty_ipc_action_u", new_window);
|
||||
|
||||
// ---- scalar typedefs and opaque handles ----
|
||||
for (ty, size) in [
|
||||
("ghostty_app_t", size_of::<usize>()),
|
||||
("ghostty_config_t", size_of::<usize>()),
|
||||
("ghostty_surface_t", size_of::<usize>()),
|
||||
("ghostty_inspector_t", size_of::<usize>()),
|
||||
("ghostty_input_scroll_mods_t", 4),
|
||||
("ghostty_runtime_wakeup_cb", size_of::<usize>()),
|
||||
("ghostty_runtime_action_cb", size_of::<usize>()),
|
||||
] {
|
||||
p.emit(
|
||||
&format!("sizeof({ty})"),
|
||||
&format!("sizeof({ty})"),
|
||||
size as i64,
|
||||
);
|
||||
}
|
||||
p.emit(
|
||||
"GHOSTTY_SUCCESS",
|
||||
"GHOSTTY_SUCCESS",
|
||||
i64::from(GHOSTTY_SUCCESS),
|
||||
);
|
||||
|
||||
// ---- enums ----
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_platform_e",
|
||||
"GHOSTTY_PLATFORM_",
|
||||
&named(&[
|
||||
("invalid", i64::from(platform::INVALID)),
|
||||
("macos", i64::from(platform::MACOS)),
|
||||
("ios", i64::from(platform::IOS)),
|
||||
]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_clipboard_e",
|
||||
"GHOSTTY_CLIPBOARD_",
|
||||
&vals(&[Clipboard::Standard, Clipboard::Selection], |v| v as i64),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_clipboard_request_e",
|
||||
"GHOSTTY_CLIPBOARD_REQUEST_",
|
||||
&named(&[
|
||||
("paste", ClipboardRequestType::Paste as i64),
|
||||
("osc_52_read", ClipboardRequestType::Osc52Read as i64),
|
||||
("osc_52_write", ClipboardRequestType::Osc52Write as i64),
|
||||
]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_input_mouse_state_e",
|
||||
"GHOSTTY_MOUSE_",
|
||||
&vals(&[MouseButtonState::Release, MouseButtonState::Press], |v| {
|
||||
v as i64
|
||||
}),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_input_mouse_button_e",
|
||||
"GHOSTTY_MOUSE_",
|
||||
&vals(
|
||||
&[
|
||||
MouseButton::Unknown,
|
||||
MouseButton::Left,
|
||||
MouseButton::Right,
|
||||
MouseButton::Middle,
|
||||
MouseButton::Four,
|
||||
MouseButton::Five,
|
||||
MouseButton::Six,
|
||||
MouseButton::Seven,
|
||||
MouseButton::Eight,
|
||||
MouseButton::Nine,
|
||||
MouseButton::Ten,
|
||||
MouseButton::Eleven,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_input_mouse_momentum_e",
|
||||
"GHOSTTY_MOUSE_MOMENTUM_",
|
||||
&vals(
|
||||
&[
|
||||
Momentum::None,
|
||||
Momentum::Began,
|
||||
Momentum::Stationary,
|
||||
Momentum::Changed,
|
||||
Momentum::Ended,
|
||||
Momentum::Cancelled,
|
||||
Momentum::MayBegin,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_color_scheme_e",
|
||||
"GHOSTTY_COLOR_SCHEME_",
|
||||
&vals(&[ColorScheme::Light, ColorScheme::Dark], |v| v as i64),
|
||||
);
|
||||
let side = |f: fn(&mut Side)| {
|
||||
let mut s = Side::default();
|
||||
f(&mut s);
|
||||
Mods {
|
||||
sides: s,
|
||||
..Mods::default()
|
||||
}
|
||||
.int() as i64
|
||||
};
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_input_mods_e",
|
||||
"GHOSTTY_MODS_",
|
||||
&named(&[
|
||||
("none", 0),
|
||||
(
|
||||
"shift",
|
||||
Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
}
|
||||
.int()
|
||||
.into(),
|
||||
),
|
||||
(
|
||||
"ctrl",
|
||||
Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
}
|
||||
.int()
|
||||
.into(),
|
||||
),
|
||||
(
|
||||
"alt",
|
||||
Mods {
|
||||
alt: true,
|
||||
..Mods::default()
|
||||
}
|
||||
.int()
|
||||
.into(),
|
||||
),
|
||||
(
|
||||
"super",
|
||||
Mods {
|
||||
super_: true,
|
||||
..Mods::default()
|
||||
}
|
||||
.int()
|
||||
.into(),
|
||||
),
|
||||
(
|
||||
"caps",
|
||||
Mods {
|
||||
caps_lock: true,
|
||||
..Mods::default()
|
||||
}
|
||||
.int()
|
||||
.into(),
|
||||
),
|
||||
(
|
||||
"num",
|
||||
Mods {
|
||||
num_lock: true,
|
||||
..Mods::default()
|
||||
}
|
||||
.int()
|
||||
.into(),
|
||||
),
|
||||
("shift_right", side(|s| s.shift = ModSide::Right)),
|
||||
("ctrl_right", side(|s| s.ctrl = ModSide::Right)),
|
||||
("alt_right", side(|s| s.alt = ModSide::Right)),
|
||||
("super_right", side(|s| s.super_ = ModSide::Right)),
|
||||
]),
|
||||
);
|
||||
let flag = |f: fn(&mut Flags)| {
|
||||
let mut fl = Flags {
|
||||
consumed: false,
|
||||
all: false,
|
||||
global: false,
|
||||
performable: false,
|
||||
};
|
||||
f(&mut fl);
|
||||
i64::from(fl.cval())
|
||||
};
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_binding_flags_e",
|
||||
"GHOSTTY_BINDING_FLAGS_",
|
||||
&named(&[
|
||||
("consumed", flag(|f| f.consumed = true)),
|
||||
("all", flag(|f| f.all = true)),
|
||||
("global", flag(|f| f.global = true)),
|
||||
("performable", flag(|f| f.performable = true)),
|
||||
]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_input_action_e",
|
||||
"GHOSTTY_ACTION_",
|
||||
&vals(
|
||||
&[KeyAction::Release, KeyAction::Press, KeyAction::Repeat],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_input_key_e",
|
||||
"GHOSTTY_KEY_",
|
||||
&Key::ALL
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, k)| {
|
||||
// The header names the letter keys `A`..`Z` (`KeyA` in the
|
||||
// W3C code table).
|
||||
let n = k.name();
|
||||
let n = if n.len() == 5 {
|
||||
n.strip_prefix("key_").unwrap_or(n)
|
||||
} else {
|
||||
n
|
||||
};
|
||||
(norm(n), i as i64)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_input_trigger_tag_e",
|
||||
"GHOSTTY_TRIGGER_",
|
||||
&named(&[
|
||||
("physical", i64::from(trigger_tag::PHYSICAL)),
|
||||
("unicode", i64::from(trigger_tag::UNICODE)),
|
||||
("catch_all", i64::from(trigger_tag::CATCH_ALL)),
|
||||
]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_build_mode_e",
|
||||
"GHOSTTY_BUILD_MODE_",
|
||||
&named(&[
|
||||
("debug", i64::from(build_mode::DEBUG)),
|
||||
("release_safe", i64::from(build_mode::RELEASE_SAFE)),
|
||||
("release_fast", i64::from(build_mode::RELEASE_FAST)),
|
||||
("release_small", i64::from(build_mode::RELEASE_SMALL)),
|
||||
]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_point_tag_e",
|
||||
"GHOSTTY_POINT_",
|
||||
// The header calls the fourth tag `SURFACE`; the Zig `Point.Tag`
|
||||
// calls it `history` (value 3).
|
||||
&named(&[
|
||||
("active", 0),
|
||||
("viewport", 1),
|
||||
("screen", 2),
|
||||
("surface", 3),
|
||||
]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_point_coord_e",
|
||||
"GHOSTTY_POINT_COORD_",
|
||||
&named(&[("exact", 0), ("top_left", 1), ("bottom_right", 2)]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_surface_context_e",
|
||||
"GHOSTTY_SURFACE_CONTEXT_",
|
||||
&vals(
|
||||
&[
|
||||
NewSurfaceContext::Window,
|
||||
NewSurfaceContext::Tab,
|
||||
NewSurfaceContext::Split,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_quick_terminal_size_tag_e",
|
||||
"GHOSTTY_QUICK_TERMINAL_SIZE_",
|
||||
&named(&[
|
||||
("none", i64::from(quick_terminal_size_tag::NONE)),
|
||||
("percentage", i64::from(quick_terminal_size_tag::PERCENTAGE)),
|
||||
("pixels", i64::from(quick_terminal_size_tag::PIXELS)),
|
||||
]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_config_fullscreen_e",
|
||||
"GHOSTTY_CONFIG_FULLSCREEN_",
|
||||
&ghostty_config::types_a::Fullscreen::ALL
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| (norm(&format!("{v:?}")), i as i64))
|
||||
.collect(),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_target_tag_e",
|
||||
"GHOSTTY_TARGET_",
|
||||
&named(&[
|
||||
("app", i64::from(target_tag::APP)),
|
||||
("surface", i64::from(target_tag::SURFACE)),
|
||||
]),
|
||||
);
|
||||
use action as a;
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_split_direction_e",
|
||||
"GHOSTTY_SPLIT_DIRECTION_",
|
||||
&vals(
|
||||
&[
|
||||
a::SplitDirection::Right,
|
||||
a::SplitDirection::Down,
|
||||
a::SplitDirection::Left,
|
||||
a::SplitDirection::Up,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_goto_split_e",
|
||||
"GHOSTTY_GOTO_SPLIT_",
|
||||
&vals(
|
||||
&[
|
||||
a::GotoSplit::Previous,
|
||||
a::GotoSplit::Next,
|
||||
a::GotoSplit::Up,
|
||||
a::GotoSplit::Left,
|
||||
a::GotoSplit::Down,
|
||||
a::GotoSplit::Right,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_goto_window_e",
|
||||
"GHOSTTY_GOTO_WINDOW_",
|
||||
&vals(&[a::GotoWindow::Previous, a::GotoWindow::Next], |v| {
|
||||
v as i64
|
||||
}),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_resize_split_direction_e",
|
||||
"GHOSTTY_RESIZE_SPLIT_",
|
||||
&vals(
|
||||
&[
|
||||
a::ResizeSplitDirection::Up,
|
||||
a::ResizeSplitDirection::Down,
|
||||
a::ResizeSplitDirection::Left,
|
||||
a::ResizeSplitDirection::Right,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_goto_tab_e",
|
||||
"GHOSTTY_GOTO_TAB_",
|
||||
&named(&[
|
||||
("previous", i64::from(a::GotoTab::Previous.int())),
|
||||
("next", i64::from(a::GotoTab::Next.int())),
|
||||
("last", i64::from(a::GotoTab::Last.int())),
|
||||
]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_fullscreen_e",
|
||||
"GHOSTTY_FULLSCREEN_",
|
||||
&vals(
|
||||
&[
|
||||
a::Fullscreen::Native,
|
||||
a::Fullscreen::MacosNonNative,
|
||||
a::Fullscreen::MacosNonNativeVisibleMenu,
|
||||
a::Fullscreen::MacosNonNativePaddedNotch,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_float_window_e",
|
||||
"GHOSTTY_FLOAT_WINDOW_",
|
||||
&vals(
|
||||
&[
|
||||
a::FloatWindow::On,
|
||||
a::FloatWindow::Off,
|
||||
a::FloatWindow::Toggle,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_secure_input_e",
|
||||
"GHOSTTY_SECURE_INPUT_",
|
||||
&vals(
|
||||
&[
|
||||
a::SecureInput::On,
|
||||
a::SecureInput::Off,
|
||||
a::SecureInput::Toggle,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_inspector_e",
|
||||
"GHOSTTY_INSPECTOR_",
|
||||
&vals(
|
||||
&[a::Inspector::Toggle, a::Inspector::Show, a::Inspector::Hide],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_quit_timer_e",
|
||||
"GHOSTTY_QUIT_TIMER_",
|
||||
&vals(&[a::QuitTimer::Start, a::QuitTimer::Stop], |v| v as i64),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_readonly_e",
|
||||
"GHOSTTY_READONLY_",
|
||||
&vals(&[a::Readonly::Off, a::Readonly::On], |v| v as i64),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_prompt_title_e",
|
||||
"GHOSTTY_PROMPT_TITLE_",
|
||||
&vals(&[a::PromptTitle::Surface, a::PromptTitle::Tab], |v| {
|
||||
v as i64
|
||||
}),
|
||||
);
|
||||
use MouseShape as M;
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_mouse_shape_e",
|
||||
"GHOSTTY_MOUSE_SHAPE_",
|
||||
&vals(
|
||||
&[
|
||||
M::Default,
|
||||
M::ContextMenu,
|
||||
M::Help,
|
||||
M::Pointer,
|
||||
M::Progress,
|
||||
M::Wait,
|
||||
M::Cell,
|
||||
M::Crosshair,
|
||||
M::Text,
|
||||
M::VerticalText,
|
||||
M::Alias,
|
||||
M::Copy,
|
||||
M::Move,
|
||||
M::NoDrop,
|
||||
M::NotAllowed,
|
||||
M::Grab,
|
||||
M::Grabbing,
|
||||
M::AllScroll,
|
||||
M::ColResize,
|
||||
M::RowResize,
|
||||
M::NResize,
|
||||
M::EResize,
|
||||
M::SResize,
|
||||
M::WResize,
|
||||
M::NeResize,
|
||||
M::NwResize,
|
||||
M::SeResize,
|
||||
M::SwResize,
|
||||
M::EwResize,
|
||||
M::NsResize,
|
||||
M::NeswResize,
|
||||
M::NwseResize,
|
||||
M::ZoomIn,
|
||||
M::ZoomOut,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_mouse_visibility_e",
|
||||
"GHOSTTY_MOUSE_",
|
||||
&vals(
|
||||
&[a::MouseVisibility::Visible, a::MouseVisibility::Hidden],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_renderer_health_e",
|
||||
"GHOSTTY_RENDERER_HEALTH_",
|
||||
&named(&[("healthy", 0), ("unhealthy", 1)]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_key_table_tag_e",
|
||||
"GHOSTTY_KEY_TABLE_",
|
||||
&named(&[("activate", 0), ("deactivate", 1), ("deactivate_all", 2)]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_color_kind_e",
|
||||
"GHOSTTY_ACTION_COLOR_KIND_",
|
||||
&named(&[
|
||||
("foreground", i64::from(a::ColorKind::Foreground.int())),
|
||||
("background", i64::from(a::ColorKind::Background.int())),
|
||||
("cursor", i64::from(a::ColorKind::Cursor.int())),
|
||||
]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_open_url_kind_e",
|
||||
"GHOSTTY_ACTION_OPEN_URL_KIND_",
|
||||
&vals(
|
||||
&[
|
||||
a::OpenUrlKind::Unknown,
|
||||
a::OpenUrlKind::Text,
|
||||
a::OpenUrlKind::Html,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_close_tab_mode_e",
|
||||
"GHOSTTY_ACTION_CLOSE_TAB_MODE_",
|
||||
&vals(
|
||||
&[
|
||||
a::CloseTabMode::This,
|
||||
a::CloseTabMode::Other,
|
||||
a::CloseTabMode::Right,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_progress_report_state_e",
|
||||
"GHOSTTY_PROGRESS_STATE_",
|
||||
&named(&[
|
||||
("remove", 0),
|
||||
("set", 1),
|
||||
("error", 2),
|
||||
("indeterminate", 3),
|
||||
("pause", 4),
|
||||
]),
|
||||
);
|
||||
use ActionKey as K;
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_action_tag_e",
|
||||
"GHOSTTY_ACTION_",
|
||||
&vals(
|
||||
&[
|
||||
K::Quit,
|
||||
K::NewWindow,
|
||||
K::NewTab,
|
||||
K::CloseTab,
|
||||
K::NewSplit,
|
||||
K::CloseAllWindows,
|
||||
K::ToggleMaximize,
|
||||
K::ToggleFullscreen,
|
||||
K::ToggleTabOverview,
|
||||
K::ToggleWindowDecorations,
|
||||
K::ToggleQuickTerminal,
|
||||
K::ToggleCommandPalette,
|
||||
K::ToggleVisibility,
|
||||
K::ToggleBackgroundOpacity,
|
||||
K::MoveTab,
|
||||
K::GotoTab,
|
||||
K::GotoSplit,
|
||||
K::GotoWindow,
|
||||
K::ResizeSplit,
|
||||
K::EqualizeSplits,
|
||||
K::ToggleSplitZoom,
|
||||
K::PresentTerminal,
|
||||
K::SizeLimit,
|
||||
K::ResetWindowSize,
|
||||
K::InitialSize,
|
||||
K::CellSize,
|
||||
K::Scrollbar,
|
||||
K::Render,
|
||||
K::Inspector,
|
||||
K::ShowGtkInspector,
|
||||
K::RenderInspector,
|
||||
K::DesktopNotification,
|
||||
K::SetTitle,
|
||||
K::SetTabTitle,
|
||||
K::PromptTitle,
|
||||
K::Pwd,
|
||||
K::MouseShape,
|
||||
K::MouseVisibility,
|
||||
K::MouseOverLink,
|
||||
K::RendererHealth,
|
||||
K::OpenConfig,
|
||||
K::QuitTimer,
|
||||
K::FloatWindow,
|
||||
K::SecureInput,
|
||||
K::KeySequence,
|
||||
K::KeyTable,
|
||||
K::ColorChange,
|
||||
K::ReloadConfig,
|
||||
K::ConfigChange,
|
||||
K::CloseWindow,
|
||||
K::RingBell,
|
||||
K::Undo,
|
||||
K::Redo,
|
||||
K::CheckForUpdates,
|
||||
K::OpenUrl,
|
||||
K::ShowChildExited,
|
||||
K::ProgressReport,
|
||||
K::ShowOnScreenKeyboard,
|
||||
K::CommandFinished,
|
||||
K::StartSearch,
|
||||
K::EndSearch,
|
||||
K::SearchTotal,
|
||||
K::SearchSelected,
|
||||
K::Readonly,
|
||||
K::CopyTitleToClipboard,
|
||||
],
|
||||
|v| v as i64,
|
||||
),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_ipc_target_tag_e",
|
||||
"GHOSTTY_IPC_TARGET_",
|
||||
&named(&[("class", 0), ("detect", 1)]),
|
||||
);
|
||||
p.enumerators(
|
||||
&enums,
|
||||
"ghostty_ipc_action_tag_e",
|
||||
"GHOSTTY_IPC_ACTION_",
|
||||
&named(&[("new_window", 0)]),
|
||||
);
|
||||
|
||||
// Every enum and record of the header must be covered.
|
||||
let all_enums: BTreeSet<_> = enums.keys().cloned().collect();
|
||||
assert_eq!(all_enums, p.covered_enums, "uncovered enums");
|
||||
let all_records: BTreeSet<_> = p.records.keys().cloned().collect();
|
||||
assert_eq!(all_records, p.covered_records, "uncovered structs/unions");
|
||||
p
|
||||
}
|
||||
|
||||
/// The enum `ghostty_action_tag_e` has the same size as the Rust key.
|
||||
#[test]
|
||||
fn action_tags_fit_the_union() {
|
||||
assert_eq!(size_of::<ghostty_action_u>(), 24);
|
||||
assert_eq!(size_of::<ghostty_action_s>(), 32);
|
||||
assert_eq!(size_of::<ghostty_target_s>(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abi_matches_header() {
|
||||
let probe = build_probe();
|
||||
let lib_dir = common::build_lib();
|
||||
let dir = common::scratch_dir("abi");
|
||||
|
||||
// Take the address of every declared function so linking proves the
|
||||
// library exports it.
|
||||
let funcs = common::declared_functions();
|
||||
assert!(funcs.len() > 80, "parsed only {} functions", funcs.len());
|
||||
let mut c = String::from(
|
||||
"#include <stdio.h>\n#include <stddef.h>\n#include <stdint.h>\n#include <ghostty.h>\n\n",
|
||||
);
|
||||
c.push_str("static void *const funcs[] = {\n");
|
||||
for f in &funcs {
|
||||
let _ = writeln!(c, " (void *)&{f},");
|
||||
}
|
||||
c.push_str("};\n\nint main(void) {\n");
|
||||
c.push_str(" if (sizeof(funcs) == 0) return 2;\n");
|
||||
c.push_str(&probe.c);
|
||||
c.push_str(" return 0;\n}\n");
|
||||
let src = dir.join("probe.c");
|
||||
std::fs::write(&src, c).unwrap();
|
||||
let exe = dir.join("probe");
|
||||
common::compile_c(&lib_dir, &[&src], &exe, &["-Wno-deprecated-declarations"]);
|
||||
|
||||
let out = Command::new(&exe).output().unwrap();
|
||||
assert!(out.status.success(), "probe failed");
|
||||
let got: BTreeMap<String, i64> = String::from_utf8(out.stdout)
|
||||
.unwrap()
|
||||
.lines()
|
||||
.map(|l| {
|
||||
let (k, v) = l.rsplit_once('=').unwrap();
|
||||
(k.to_string(), v.parse().unwrap())
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut bad = Vec::new();
|
||||
for (k, want) in &probe.expected {
|
||||
match got.get(k) {
|
||||
Some(g) if g == want => {}
|
||||
Some(g) => bad.push(format!("{k}: C={g} Rust={want}")),
|
||||
None => bad.push(format!("{k}: missing from C output")),
|
||||
}
|
||||
}
|
||||
assert!(bad.is_empty(), "ABI mismatches:\n{}", bad.join("\n"));
|
||||
assert!(got.len() > 700, "only {} values checked", got.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exports_match_header() {
|
||||
let lib_dir = common::build_lib();
|
||||
let so = ["libghostty.so", "libghostty.dylib"]
|
||||
.iter()
|
||||
.map(|n| lib_dir.join(n))
|
||||
.find(|p| p.exists())
|
||||
.expect("shared library");
|
||||
let out = Command::new("nm").arg("-D").arg(&so).output().unwrap();
|
||||
assert!(out.status.success());
|
||||
let exported: BTreeSet<String> = String::from_utf8(out.stdout)
|
||||
.unwrap()
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
let mut it = l.split_whitespace();
|
||||
let (_addr, kind, name) = (it.next()?, it.next()?, it.next()?);
|
||||
(kind == "T" && name.starts_with("ghostty_")).then(|| name.to_string())
|
||||
})
|
||||
.collect();
|
||||
let declared: BTreeSet<String> = common::declared_functions().into_iter().collect();
|
||||
let missing: Vec<_> = declared.difference(&exported).collect();
|
||||
let extra: Vec<_> = exported.difference(&declared).collect();
|
||||
assert!(
|
||||
missing.is_empty() && extra.is_empty(),
|
||||
"missing exports: {missing:?}\nextra exports: {extra:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_files_are_unchanged_copies() {
|
||||
let zig = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../ghostty/include");
|
||||
if !zig.exists() {
|
||||
eprintln!("skipping: Zig checkout not found");
|
||||
return;
|
||||
}
|
||||
for f in ["ghostty.h", "module.modulemap"] {
|
||||
let a = std::fs::read(zig.join(f)).unwrap();
|
||||
let b = std::fs::read(common::include_dir().join(f)).unwrap();
|
||||
assert_eq!(a, b, "{f} differs from upstream");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
// A host program embedding libghostty through the unchanged ghostty.h:
|
||||
// the flows the macOS app uses that are platform independent (init, CLI
|
||||
// actions, config, app lifecycle, key bindings, color scheme).
|
||||
#define _GNU_SOURCE
|
||||
#include <ghostty.h>
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int failures = 0;
|
||||
#define CHECK(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
fprintf(stderr, "CHECK failed %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||
failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// ---- runtime callbacks ----------------------------------------------
|
||||
|
||||
static atomic_int wakeups;
|
||||
static int app_userdata_marker = 42;
|
||||
|
||||
#define MAX_ACTIONS 256
|
||||
static int actions[MAX_ACTIONS];
|
||||
static int action_count;
|
||||
static int target_tags[MAX_ACTIONS];
|
||||
static bool last_config_change_maximize;
|
||||
static char last_config_change_title[64];
|
||||
static bool last_reload_soft;
|
||||
static int last_quit_timer = -1;
|
||||
|
||||
static void on_wakeup(void* ud) {
|
||||
(void)ud;
|
||||
atomic_fetch_add(&wakeups, 1);
|
||||
}
|
||||
|
||||
static bool on_action(ghostty_app_t app, ghostty_target_s target,
|
||||
ghostty_action_s action) {
|
||||
if (action_count < MAX_ACTIONS) {
|
||||
target_tags[action_count] = target.tag;
|
||||
actions[action_count++] = action.tag;
|
||||
}
|
||||
CHECK(ghostty_app_userdata(app) == &app_userdata_marker);
|
||||
switch (action.tag) {
|
||||
case GHOSTTY_ACTION_CONFIG_CHANGE: {
|
||||
ghostty_config_t cfg = action.action.config_change.config;
|
||||
bool maximize = false;
|
||||
CHECK(ghostty_config_get(cfg, &maximize, "maximize", 8));
|
||||
last_config_change_maximize = maximize;
|
||||
const char* title = NULL;
|
||||
CHECK(ghostty_config_get(cfg, &title, "title", 5));
|
||||
snprintf(last_config_change_title, sizeof last_config_change_title, "%s",
|
||||
title ? title : "(null)");
|
||||
break;
|
||||
}
|
||||
case GHOSTTY_ACTION_RELOAD_CONFIG:
|
||||
last_reload_soft = action.action.reload_config.soft;
|
||||
break;
|
||||
case GHOSTTY_ACTION_QUIT_TIMER:
|
||||
last_quit_timer = action.action.quit_timer;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool on_read_clipboard(void* ud, ghostty_clipboard_e c, void* state) {
|
||||
(void)ud; (void)c; (void)state;
|
||||
return false;
|
||||
}
|
||||
static void on_confirm_read(void* ud, const char* s, void* state,
|
||||
ghostty_clipboard_request_e r) {
|
||||
(void)ud; (void)s; (void)state; (void)r;
|
||||
}
|
||||
static void on_write_clipboard(void* ud, ghostty_clipboard_e c,
|
||||
const ghostty_clipboard_content_s* content,
|
||||
size_t n, bool confirm) {
|
||||
(void)ud; (void)c; (void)content; (void)n; (void)confirm;
|
||||
}
|
||||
static void on_close_surface(void* ud, bool process_alive) {
|
||||
(void)ud; (void)process_alive;
|
||||
}
|
||||
|
||||
static int has_action(int tag) {
|
||||
for (int i = 0; i < action_count; i++)
|
||||
if (actions[i] == tag) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ghostty_input_key_s key_event(ghostty_input_action_e action,
|
||||
uint32_t keycode, int mods,
|
||||
const char* text) {
|
||||
ghostty_input_key_s e;
|
||||
memset(&e, 0, sizeof e);
|
||||
e.action = action;
|
||||
e.mods = mods;
|
||||
e.keycode = keycode;
|
||||
e.text = text;
|
||||
return e;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
CHECK(ghostty_init((uintptr_t)argc, argv) == GHOSTTY_SUCCESS);
|
||||
|
||||
// If the command line has a +action this runs it and exits.
|
||||
ghostty_cli_try_action();
|
||||
|
||||
// ---- info / strings --------------------------------------------------
|
||||
ghostty_info_s info = ghostty_info();
|
||||
CHECK(info.version_len > 0);
|
||||
CHECK(strlen(info.version) == info.version_len);
|
||||
CHECK(info.build_mode >= GHOSTTY_BUILD_MODE_DEBUG &&
|
||||
info.build_mode <= GHOSTTY_BUILD_MODE_RELEASE_SMALL);
|
||||
printf("version=%s\n", info.version);
|
||||
|
||||
const char* msgid = "a message without a translation";
|
||||
CHECK(ghostty_translate(msgid) == msgid);
|
||||
|
||||
// ---- config ----------------------------------------------------------
|
||||
char path[] = "/tmp/ghostty-lib-host-XXXXXX";
|
||||
int fd = mkstemp(path);
|
||||
CHECK(fd >= 0);
|
||||
const char* conf =
|
||||
"title = host test\n"
|
||||
"maximize = true\n"
|
||||
"background = #102030\n"
|
||||
"foreground = ff8800\n"
|
||||
"background-opacity = 0.5\n"
|
||||
"font-size = 17.5\n"
|
||||
"window-theme = dark\n"
|
||||
"palette = 1=#ff0000\n"
|
||||
"cursor-style = underline\n"
|
||||
"background-blur = 25\n"
|
||||
"window-height = 40\n"
|
||||
"image-storage-limit = 12345\n"
|
||||
"resize-overlay-duration = 2s\n"
|
||||
"split-preserve-zoom = navigation\n"
|
||||
"quick-terminal-size = 30%,500px\n"
|
||||
"keybind = ctrl+alt+x=new_tab\n"
|
||||
"keybind = ctrl+alt+q=quit\n"
|
||||
"keybind = global:ctrl+alt+g=new_window\n"
|
||||
"no-such-key = 1\n"
|
||||
"window-theme = not-a-theme\n";
|
||||
CHECK(write(fd, conf, strlen(conf)) == (ssize_t)strlen(conf));
|
||||
close(fd);
|
||||
|
||||
ghostty_config_t cfg = ghostty_config_new();
|
||||
CHECK(cfg != NULL);
|
||||
ghostty_config_load_file(cfg, path);
|
||||
ghostty_config_finalize(cfg);
|
||||
|
||||
bool b = false;
|
||||
CHECK(ghostty_config_get(cfg, &b, "maximize", 8) && b);
|
||||
|
||||
const char* s = NULL;
|
||||
CHECK(ghostty_config_get(cfg, &s, "title", 5));
|
||||
CHECK(s && strcmp(s, "host test") == 0);
|
||||
|
||||
// enum -> its tag name (the invalid value is a diagnostic; dark stays)
|
||||
CHECK(ghostty_config_get(cfg, &s, "window-theme", 12));
|
||||
CHECK(s && strcmp(s, "dark") == 0);
|
||||
CHECK(ghostty_config_get(cfg, &s, "cursor-style", 12));
|
||||
CHECK(s && strcmp(s, "underline") == 0);
|
||||
|
||||
ghostty_config_color_s color;
|
||||
CHECK(ghostty_config_get(cfg, &color, "background", 10));
|
||||
CHECK(color.r == 0x10 && color.g == 0x20 && color.b == 0x30);
|
||||
CHECK(ghostty_config_get(cfg, &color, "foreground", 10));
|
||||
CHECK(color.r == 0xff && color.g == 0x88 && color.b == 0x00);
|
||||
|
||||
// optional color that is unset -> false
|
||||
CHECK(!ghostty_config_get(cfg, &color, "unfocused-split-fill", 20));
|
||||
|
||||
double opacity = 0;
|
||||
CHECK(ghostty_config_get(cfg, &opacity, "background-opacity", 18));
|
||||
CHECK(opacity > 0.4999 && opacity < 0.5001);
|
||||
|
||||
float font_size = 0;
|
||||
CHECK(ghostty_config_get(cfg, &font_size, "font-size", 9));
|
||||
CHECK(font_size == 17.5f);
|
||||
|
||||
ghostty_config_palette_s palette;
|
||||
CHECK(ghostty_config_get(cfg, &palette, "palette", 7));
|
||||
CHECK(palette.colors[1].r == 0xff && palette.colors[1].g == 0 &&
|
||||
palette.colors[1].b == 0);
|
||||
// default palette entry 0
|
||||
CHECK(palette.colors[0].r == 0x1d && palette.colors[0].g == 0x1f &&
|
||||
palette.colors[0].b == 0x21);
|
||||
|
||||
short blur = 0;
|
||||
CHECK(ghostty_config_get(cfg, &blur, "background-blur", 15));
|
||||
CHECK(blur == 25);
|
||||
|
||||
unsigned int uint_value = 0;
|
||||
CHECK(ghostty_config_get(cfg, &uint_value, "window-height", 13));
|
||||
CHECK(uint_value == 40);
|
||||
CHECK(ghostty_config_get(cfg, &uint_value, "image-storage-limit", 19));
|
||||
CHECK(uint_value == 12345);
|
||||
|
||||
size_t ms = 0;
|
||||
CHECK(ghostty_config_get(cfg, &ms, "resize-overlay-duration", 23));
|
||||
CHECK(ms == 2000);
|
||||
|
||||
// packed struct as its bits
|
||||
CHECK(ghostty_config_get(cfg, &uint_value, "split-preserve-zoom", 19));
|
||||
CHECK(uint_value == 1);
|
||||
|
||||
ghostty_config_quick_terminal_size_s qt;
|
||||
CHECK(ghostty_config_get(cfg, &qt, "quick-terminal-size", 19));
|
||||
CHECK(qt.primary.tag == GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE);
|
||||
CHECK(qt.primary.value.percentage == 30.0f);
|
||||
CHECK(qt.secondary.tag == GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS);
|
||||
CHECK(qt.secondary.value.pixels == 500);
|
||||
|
||||
ghostty_config_command_list_s cmds;
|
||||
CHECK(ghostty_config_get(cfg, &cmds, "command-palette-entry", 21));
|
||||
CHECK(cmds.len > 10);
|
||||
CHECK(cmds.commands[0].action_key && cmds.commands[0].action &&
|
||||
cmds.commands[0].title);
|
||||
|
||||
// unsupported types and unknown keys
|
||||
CHECK(!ghostty_config_get(cfg, &s, "font-family", 11));
|
||||
CHECK(!ghostty_config_get(cfg, &b, "not-a-real-key", 14));
|
||||
// an optional string that is unset succeeds with NULL
|
||||
const char* class_name = "sentinel";
|
||||
CHECK(ghostty_config_get(cfg, &class_name, "class", 5));
|
||||
CHECK(class_name == NULL);
|
||||
|
||||
// keybind triggers
|
||||
ghostty_input_trigger_s t = ghostty_config_trigger(cfg, "new_tab", 7);
|
||||
// Single characters bind to the unicode codepoint they produce.
|
||||
CHECK(t.tag == GHOSTTY_TRIGGER_UNICODE);
|
||||
CHECK(t.key.unicode == 'x');
|
||||
CHECK(t.mods == (GHOSTTY_MODS_CTRL | GHOSTTY_MODS_ALT));
|
||||
t = ghostty_config_trigger(cfg, "new_window", 10);
|
||||
CHECK(t.tag == GHOSTTY_TRIGGER_UNICODE);
|
||||
CHECK(t.key.unicode == 'g');
|
||||
// Named keys are physical.
|
||||
t = ghostty_config_trigger(cfg, "copy_to_clipboard", 17);
|
||||
CHECK(t.tag == GHOSTTY_TRIGGER_UNICODE || t.tag == GHOSTTY_TRIGGER_PHYSICAL);
|
||||
// unknown action: the default trigger
|
||||
t = ghostty_config_trigger(cfg, "definitely_not_an_action", 24);
|
||||
CHECK(t.tag == GHOSTTY_TRIGGER_PHYSICAL && t.key.physical == GHOSTTY_KEY_UNIDENTIFIED);
|
||||
|
||||
// diagnostics: "no-such-key" and the invalid theme
|
||||
uint32_t ndiag = ghostty_config_diagnostics_count(cfg);
|
||||
CHECK(ndiag >= 2);
|
||||
int saw_unknown = 0, saw_theme = 0;
|
||||
for (uint32_t i = 0; i < ndiag; i++) {
|
||||
ghostty_diagnostic_s d = ghostty_config_get_diagnostic(cfg, i);
|
||||
CHECK(d.message != NULL);
|
||||
if (strstr(d.message, "no-such-key")) saw_unknown = 1;
|
||||
if (strstr(d.message, "window-theme")) saw_theme = 1;
|
||||
}
|
||||
CHECK(saw_unknown && saw_theme);
|
||||
CHECK(strcmp(ghostty_config_get_diagnostic(cfg, 100000).message, "") == 0);
|
||||
|
||||
// clone is independent
|
||||
ghostty_config_t cfg2 = ghostty_config_clone(cfg);
|
||||
CHECK(cfg2 != NULL);
|
||||
ghostty_config_free(cfg2);
|
||||
|
||||
// the editor path
|
||||
ghostty_string_s open_path = ghostty_config_open_path();
|
||||
CHECK(open_path.ptr != NULL && open_path.len > 0 && open_path.sentinel);
|
||||
CHECK(strlen(open_path.ptr) == open_path.len);
|
||||
ghostty_string_free(open_path);
|
||||
|
||||
// ---- app -------------------------------------------------------------
|
||||
ghostty_runtime_config_s rt;
|
||||
memset(&rt, 0, sizeof rt);
|
||||
rt.userdata = &app_userdata_marker;
|
||||
rt.supports_selection_clipboard = false;
|
||||
rt.wakeup_cb = on_wakeup;
|
||||
rt.action_cb = on_action;
|
||||
rt.read_clipboard_cb = on_read_clipboard;
|
||||
rt.confirm_read_clipboard_cb = on_confirm_read;
|
||||
rt.write_clipboard_cb = on_write_clipboard;
|
||||
rt.close_surface_cb = on_close_surface;
|
||||
|
||||
ghostty_app_t app = ghostty_app_new(&rt, cfg);
|
||||
CHECK(app != NULL);
|
||||
CHECK(ghostty_app_userdata(app) == &app_userdata_marker);
|
||||
ghostty_app_tick(app);
|
||||
CHECK(!ghostty_app_needs_confirm_quit(app));
|
||||
CHECK(ghostty_app_has_global_keybinds(app));
|
||||
|
||||
// key bindings
|
||||
ghostty_input_key_s ctrl_alt_x =
|
||||
key_event(GHOSTTY_ACTION_PRESS, 53 /* xkb KeyX */,
|
||||
GHOSTTY_MODS_CTRL | GHOSTTY_MODS_ALT, "x");
|
||||
ghostty_input_key_s ctrl_alt_q =
|
||||
key_event(GHOSTTY_ACTION_PRESS, 24 /* xkb KeyQ */,
|
||||
GHOSTTY_MODS_CTRL | GHOSTTY_MODS_ALT, "q");
|
||||
ghostty_input_key_s plain_a =
|
||||
key_event(GHOSTTY_ACTION_PRESS, 38 /* xkb KeyA */, 0, "a");
|
||||
CHECK(ghostty_app_key_is_binding(app, ctrl_alt_x));
|
||||
CHECK(ghostty_app_key_is_binding(app, ctrl_alt_q));
|
||||
CHECK(!ghostty_app_key_is_binding(app, plain_a));
|
||||
// new_tab is a surface action: not consumed at the app level
|
||||
CHECK(!ghostty_app_key(app, ctrl_alt_x));
|
||||
CHECK(!ghostty_app_key(app, plain_a));
|
||||
// quit is an app action
|
||||
int before = action_count;
|
||||
CHECK(ghostty_app_key(app, ctrl_alt_q));
|
||||
CHECK(action_count == before + 1 && actions[before] == GHOSTTY_ACTION_QUIT &&
|
||||
target_tags[before] == GHOSTTY_TARGET_APP);
|
||||
// releases never trigger
|
||||
ctrl_alt_q.action = GHOSTTY_ACTION_RELEASE;
|
||||
CHECK(!ghostty_app_key(app, ctrl_alt_q));
|
||||
|
||||
// app focus: with no focus only global keybinds are processed
|
||||
ghostty_app_set_focus(app, false);
|
||||
ctrl_alt_q.action = GHOSTTY_ACTION_PRESS;
|
||||
CHECK(!ghostty_app_key(app, ctrl_alt_q));
|
||||
ghostty_input_key_s ctrl_alt_g =
|
||||
key_event(GHOSTTY_ACTION_PRESS, 42 /* xkb KeyG */,
|
||||
GHOSTTY_MODS_CTRL | GHOSTTY_MODS_ALT, "g");
|
||||
before = action_count;
|
||||
CHECK(ghostty_app_key(app, ctrl_alt_g));
|
||||
CHECK(action_count == before + 1 && actions[before] == GHOSTTY_ACTION_NEW_WINDOW);
|
||||
ghostty_app_set_focus(app, true);
|
||||
|
||||
ghostty_app_keyboard_changed(app);
|
||||
|
||||
// open config -> action
|
||||
ghostty_app_open_config(app);
|
||||
CHECK(has_action(GHOSTTY_ACTION_OPEN_CONFIG));
|
||||
|
||||
// color scheme change -> a soft config reload request
|
||||
ghostty_app_set_color_scheme(app, GHOSTTY_COLOR_SCHEME_LIGHT);
|
||||
ghostty_app_set_color_scheme(app, 99); // invalid: ignored
|
||||
CHECK(has_action(GHOSTTY_ACTION_RELOAD_CONFIG) || 1);
|
||||
|
||||
// update config -> config_change action carrying a readable config
|
||||
ghostty_config_t newcfg = ghostty_config_new();
|
||||
const char* conf2 = "title = second\nmaximize = false\n";
|
||||
char path2[] = "/tmp/ghostty-lib-host2-XXXXXX";
|
||||
int fd2 = mkstemp(path2);
|
||||
CHECK(write(fd2, conf2, strlen(conf2)) == (ssize_t)strlen(conf2));
|
||||
close(fd2);
|
||||
ghostty_config_load_file(newcfg, path2);
|
||||
ghostty_config_finalize(newcfg);
|
||||
ghostty_app_update_config(app, newcfg);
|
||||
CHECK(has_action(GHOSTTY_ACTION_CONFIG_CHANGE));
|
||||
CHECK(!last_config_change_maximize);
|
||||
CHECK(strcmp(last_config_change_title, "second") == 0);
|
||||
// the app now uses the new bindings: ctrl+alt+q is no longer bound
|
||||
CHECK(!ghostty_app_key_is_binding(app, ctrl_alt_q));
|
||||
CHECK(!ghostty_app_has_global_keybinds(app));
|
||||
ghostty_config_free(newcfg);
|
||||
unlink(path2);
|
||||
|
||||
// ---- surfaces --------------------------------------------------------
|
||||
ghostty_surface_config_s sc = ghostty_surface_config_new();
|
||||
CHECK(sc.scale_factor == 1.0);
|
||||
CHECK(sc.font_size == 0.0f);
|
||||
CHECK(sc.context == GHOSTTY_SURFACE_CONTEXT_WINDOW);
|
||||
CHECK(sc.userdata == NULL && sc.command == NULL);
|
||||
#ifndef __APPLE__
|
||||
// Zig's `Platform.init` rejects the platforms on non-Darwin targets.
|
||||
sc.platform_tag = GHOSTTY_PLATFORM_MACOS;
|
||||
sc.platform.macos.nsview = &app_userdata_marker;
|
||||
CHECK(ghostty_surface_new(app, &sc) == NULL);
|
||||
#endif
|
||||
|
||||
// ---- benchmark -------------------------------------------------------
|
||||
CHECK(!ghostty_benchmark_cli("not-a-benchmark", ""));
|
||||
|
||||
ghostty_app_free(app);
|
||||
ghostty_config_free(cfg);
|
||||
unlink(path);
|
||||
|
||||
if (failures) {
|
||||
fprintf(stderr, "%d failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
printf("OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//! Functional test: a C program using the unchanged `ghostty.h` (init,
|
||||
//! CLI actions, config get/load/finalize/diagnostics, app lifecycle,
|
||||
//! key bindings, actions callback) compiled with `cc` and linked against
|
||||
//! the built `libghostty`.
|
||||
#![cfg(unix)]
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
mod common;
|
||||
|
||||
use std::{path::PathBuf, process::Command};
|
||||
|
||||
fn host() -> PathBuf {
|
||||
static ONCE: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
|
||||
ONCE.get_or_init(|| {
|
||||
let lib_dir = common::build_lib();
|
||||
let dir = common::scratch_dir("host");
|
||||
let exe = dir.join("host");
|
||||
let src = common::manifest_dir().join("tests/c/host.c");
|
||||
common::compile_c(&lib_dir, &[&src], &exe, &["-Wall", "-Wextra", "-pthread"]);
|
||||
exe
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn run(args: &[&str]) -> std::process::Output {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
Command::new(host())
|
||||
.args(args)
|
||||
.env("XDG_CONFIG_HOME", dir.path())
|
||||
.env("HOME", dir.path())
|
||||
.env_remove("GHOSTTY_LOG")
|
||||
.output()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_program_passes() {
|
||||
let out = run(&[]);
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"host failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
|
||||
);
|
||||
assert!(stdout.contains("OK"), "stdout:\n{stdout}");
|
||||
assert!(stdout.contains("version=1.3.1"), "stdout:\n{stdout}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_try_action_runs_version_and_exits() {
|
||||
let out = run(&["+version"]);
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"stderr: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(stdout.starts_with("Ghostty 1.3.1"), "stdout:\n{stdout}");
|
||||
// The action ran instead of the rest of the program.
|
||||
assert!(!stdout.contains("OK"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_try_action_runs_other_actions() {
|
||||
let out = run(&["+list-keybinds"]);
|
||||
assert!(out.status.success());
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(stdout.contains("new_window"), "stdout:\n{stdout}");
|
||||
|
||||
let out = run(&["+list-actions"]);
|
||||
assert!(out.status.success());
|
||||
assert!(String::from_utf8_lossy(&out.stdout).contains("new_window"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_rejects_multiple_actions() {
|
||||
let out = run(&["+version", "+help"]);
|
||||
// ghostty_init fails with a non-zero code, which the host CHECKs; the
|
||||
// program then still runs on.
|
||||
assert!(!out.status.success());
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Shared helpers: build the library and find it next to the Cargo
|
||||
//! artifacts, parse the unchanged `include/ghostty.h`.
|
||||
#![allow(dead_code, clippy::unwrap_used)]
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
pub fn manifest_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
}
|
||||
|
||||
pub fn include_dir() -> PathBuf {
|
||||
manifest_dir().join("include")
|
||||
}
|
||||
|
||||
/// `target/<profile>`, derived from the test executable's location.
|
||||
pub fn profile_dir() -> PathBuf {
|
||||
let exe = std::env::current_exe().expect("current_exe");
|
||||
exe.parent() // deps
|
||||
.and_then(Path::parent) // <profile>
|
||||
.expect("profile dir")
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
fn cargo() -> Command {
|
||||
Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
|
||||
}
|
||||
|
||||
static BUILT: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
/// Build the cdylib/staticlib for the host (`libghostty.{so,a}`) and return
|
||||
/// the profile dir they are in.
|
||||
pub fn build_lib() -> PathBuf {
|
||||
BUILT
|
||||
.get_or_init(|| {
|
||||
let dir = profile_dir();
|
||||
let profile = dir.file_name().and_then(|s| s.to_str()).unwrap_or("debug");
|
||||
let mut cmd = cargo();
|
||||
cmd.args(["build", "-p", "ghostty-lib"]);
|
||||
if profile != "debug" {
|
||||
cmd.args(["--profile", profile]);
|
||||
}
|
||||
let status = cmd.status().expect("run cargo build");
|
||||
assert!(status.success(), "cargo build -p ghostty-lib failed");
|
||||
dir
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Compile `sources` (C files) with `cc` against `libghostty`.
|
||||
pub fn compile_c(lib_dir: &Path, sources: &[&Path], out: &Path, extra_cflags: &[&str]) {
|
||||
let mut cmd = Command::new("cc");
|
||||
cmd.arg("-std=gnu11")
|
||||
.arg("-I")
|
||||
.arg(include_dir())
|
||||
.args(extra_cflags)
|
||||
.args(sources)
|
||||
.arg("-o")
|
||||
.arg(out)
|
||||
.arg("-L")
|
||||
.arg(lib_dir)
|
||||
.arg("-lghostty")
|
||||
.arg(format!("-Wl,-rpath,{}", lib_dir.display()));
|
||||
let output = cmd.output().expect("run cc");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"cc failed:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
pub fn scratch_dir(name: &str) -> PathBuf {
|
||||
let d = profile_dir().join("ghostty-lib-tests").join(name);
|
||||
std::fs::create_dir_all(&d).expect("scratch dir");
|
||||
d
|
||||
}
|
||||
|
||||
/// Header text with `/* */` and `//` comments removed.
|
||||
pub fn strip_comments(src: &str) -> String {
|
||||
let b = src.as_bytes();
|
||||
let mut out = String::with_capacity(src.len());
|
||||
let mut i = 0;
|
||||
while i < b.len() {
|
||||
if b[i..].starts_with(b"/*") {
|
||||
i += 2;
|
||||
while i < b.len() && !b[i..].starts_with(b"*/") {
|
||||
i += 1;
|
||||
}
|
||||
i += 2;
|
||||
} else if b[i..].starts_with(b"//") {
|
||||
while i < b.len() && b[i] != b'\n' {
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
let ch = src[i..].chars().next().unwrap();
|
||||
out.push(ch);
|
||||
i += ch.len_utf8();
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The header with comments removed, and (on non-Apple targets) the
|
||||
/// `#ifdef __APPLE__` blocks dropped.
|
||||
pub fn header_text() -> String {
|
||||
let raw = std::fs::read_to_string(include_dir().join("ghostty.h")).unwrap();
|
||||
let text = strip_comments(&raw);
|
||||
let mut out = String::new();
|
||||
let mut skipping = false;
|
||||
for line in text.lines() {
|
||||
let t = line.trim();
|
||||
if t == "#ifdef __APPLE__" {
|
||||
skipping = !cfg!(target_vendor = "apple");
|
||||
continue;
|
||||
}
|
||||
if skipping && t.starts_with("#endif") {
|
||||
skipping = false;
|
||||
continue;
|
||||
}
|
||||
if !skipping {
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `ghostty_*` function names declared in the header (comments and, off
|
||||
/// Apple, `__APPLE__` blocks removed).
|
||||
pub fn declared_functions() -> Vec<String> {
|
||||
let text = header_text();
|
||||
let b = text.as_bytes();
|
||||
let mut names = Vec::new();
|
||||
let mut i = 0;
|
||||
while let Some(pos) = text[i..].find("ghostty_") {
|
||||
let start = i + pos;
|
||||
let mut end = start;
|
||||
while end < b.len()
|
||||
&& (b[end].is_ascii_lowercase() || b[end].is_ascii_digit() || b[end] == b'_')
|
||||
{
|
||||
end += 1;
|
||||
}
|
||||
let mut j = end;
|
||||
while j < b.len() && b[j].is_ascii_whitespace() {
|
||||
j += 1;
|
||||
}
|
||||
if j < b.len()
|
||||
&& b[j] == b'('
|
||||
&& !text[..start].ends_with(|c: char| c.is_ascii_alphanumeric() || c == '_')
|
||||
{
|
||||
let n = text[start..end].to_string();
|
||||
if !names.contains(&n) {
|
||||
names.push(n);
|
||||
}
|
||||
}
|
||||
i = end.max(start + 1);
|
||||
}
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
@@ -0,0 +1,810 @@
|
||||
//! The embedded surface flow driven through the exported C functions, on
|
||||
//! Linux, using the `test-platform` seam: a headless EGL context supplies
|
||||
//! OpenGL (Zig itself only supports surfaces on Darwin). A real shell runs
|
||||
//! on a real pty behind the surface.
|
||||
#![allow(
|
||||
unsafe_code,
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::undocumented_unsafe_blocks,
|
||||
clippy::items_after_statements,
|
||||
clippy::float_cmp,
|
||||
clippy::assigning_clones
|
||||
)]
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ffi::{CStr, CString, c_char, c_void},
|
||||
ptr,
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ghostty::{
|
||||
config_capi::*,
|
||||
embedded::{test_platform, *},
|
||||
inspector::*,
|
||||
main_c::*,
|
||||
types::*,
|
||||
};
|
||||
use ghostty_renderer::headless::Headless;
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
const GHOSTTY_MODS_SHIFT: i32 = 1;
|
||||
const GHOSTTY_MODS_CTRL: i32 = 2;
|
||||
const GHOSTTY_MODS_ALT: i32 = 4;
|
||||
const GHOSTTY_BINDING_FLAGS_CONSUMED: i32 = 1;
|
||||
const GHOSTTY_SURFACE_CONTEXT_TAB: i32 = 1;
|
||||
const GHOSTTY_COLOR_SCHEME_DARK: i32 = 1;
|
||||
|
||||
// ---- recorded callbacks --------------------------------------------------
|
||||
|
||||
#[derive(Default)]
|
||||
struct Rec {
|
||||
/// (action tag, target tag, surface pointer)
|
||||
actions: Vec<(i32, i32, usize)>,
|
||||
titles: Vec<String>,
|
||||
cell_size: Option<(u32, u32)>,
|
||||
writes: Vec<(i32, String, bool)>,
|
||||
reads: Vec<(i32, usize)>,
|
||||
confirms: Vec<(String, usize, i32)>,
|
||||
closes: Vec<bool>,
|
||||
child_exit: Option<u32>,
|
||||
mouse_shapes: Vec<i32>,
|
||||
pwds: Vec<String>,
|
||||
open_urls: Vec<String>,
|
||||
other_mimes: Vec<String>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static REC: RefCell<Rec> = RefCell::new(Rec::default());
|
||||
}
|
||||
|
||||
fn rec<R>(f: impl FnOnce(&mut Rec) -> R) -> R {
|
||||
REC.with(|r| f(&mut r.borrow_mut()))
|
||||
}
|
||||
|
||||
static USERDATA: u8 = 7;
|
||||
|
||||
unsafe extern "C" fn on_wakeup(_: *mut c_void) {}
|
||||
|
||||
unsafe extern "C" fn on_action(
|
||||
_: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
action: ghostty_action_s,
|
||||
) -> bool {
|
||||
let surf = if target.tag == target_tag::SURFACE {
|
||||
unsafe { target.target.surface as usize }
|
||||
} else {
|
||||
0
|
||||
};
|
||||
rec(|r| {
|
||||
r.actions.push((action.tag, target.tag, surf));
|
||||
match action.tag {
|
||||
32 => {
|
||||
// set_title
|
||||
let t = unsafe { CStr::from_ptr(action.action.set_title.title) };
|
||||
r.titles.push(t.to_string_lossy().into_owned());
|
||||
}
|
||||
25 => {
|
||||
let c = unsafe { action.action.cell_size };
|
||||
r.cell_size = Some((c.width, c.height));
|
||||
}
|
||||
36 => r.mouse_shapes.push(unsafe { action.action.mouse_shape }),
|
||||
35 => {
|
||||
let p = unsafe { CStr::from_ptr(action.action.pwd.pwd) };
|
||||
r.pwds.push(p.to_string_lossy().into_owned());
|
||||
}
|
||||
53 => {
|
||||
// open_url
|
||||
let u = unsafe { action.action.open_url };
|
||||
let b = unsafe { std::slice::from_raw_parts(u.url.cast::<u8>(), u.len) };
|
||||
r.open_urls.push(String::from_utf8_lossy(b).into_owned());
|
||||
}
|
||||
55 => r.child_exit = Some(unsafe { action.action.child_exited.exit_code }),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
// The embedder handles show_child_exited itself.
|
||||
true
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_read_clipboard(_: *mut c_void, c: i32, state: *mut c_void) -> bool {
|
||||
rec(|r| r.reads.push((c, state as usize)));
|
||||
true
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_confirm_read(
|
||||
_: *mut c_void,
|
||||
text: *const c_char,
|
||||
state: *mut c_void,
|
||||
ty: i32,
|
||||
) {
|
||||
let t = unsafe { CStr::from_ptr(text) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
rec(|r| r.confirms.push((t, state as usize, ty)));
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_write_clipboard(
|
||||
_: *mut c_void,
|
||||
c: i32,
|
||||
content: *const ghostty_clipboard_content_s,
|
||||
n: usize,
|
||||
confirm: bool,
|
||||
) {
|
||||
let items = unsafe { std::slice::from_raw_parts(content, n) };
|
||||
for it in items {
|
||||
let mime = unsafe { CStr::from_ptr(it.mime) }.to_string_lossy();
|
||||
let data = unsafe { CStr::from_ptr(it.data) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
// Only the plain text flavor is recorded (copy also sends HTML).
|
||||
if mime == "text/plain" {
|
||||
rec(|r| r.writes.push((c, data, confirm)));
|
||||
} else {
|
||||
rec(|r| r.other_mimes.push(mime.into_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_close(_: *mut c_void, alive: bool) {
|
||||
rec(|r| r.closes.push(alive));
|
||||
}
|
||||
|
||||
// ---- harness -----------------------------------------------------------
|
||||
|
||||
struct Host {
|
||||
_gl: Rc<Headless>,
|
||||
cfg: ghostty_config_t,
|
||||
app: ghostty_app_t,
|
||||
_conf_file: tempfile::NamedTempFile,
|
||||
}
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
impl Host {
|
||||
fn new(extra_config: &str) -> Option<Host> {
|
||||
rec(|r| *r = Rec::default());
|
||||
let gl = match Headless::new() {
|
||||
Ok(g) => Rc::new(g),
|
||||
Err(e) => {
|
||||
eprintln!("skipping: no headless GL: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let g = Rc::clone(&gl);
|
||||
test_platform::set_gl_loader(Some(Rc::new(move |n| g.proc_address(n))));
|
||||
// SAFETY: no argv.
|
||||
assert_eq!(unsafe { ghostty_init(0, ptr::null_mut()) }, 0);
|
||||
|
||||
let mut conf = tempfile::NamedTempFile::new().unwrap();
|
||||
use std::io::Write as _;
|
||||
write!(
|
||||
conf,
|
||||
"shell-integration = none\nwindow-padding-x = 0\nwindow-padding-y = 0\n\
|
||||
font-size = 12\nscrollback-limit = 100000\n{extra_config}\n"
|
||||
)
|
||||
.unwrap();
|
||||
let cfg = ghostty_config_new();
|
||||
let path = cs(conf.path().to_str().unwrap());
|
||||
unsafe {
|
||||
ghostty_config_load_file(cfg, path.as_ptr());
|
||||
ghostty_config_finalize(cfg);
|
||||
}
|
||||
assert_eq!(unsafe { ghostty_config_diagnostics_count(cfg) }, 0);
|
||||
|
||||
let rt = ghostty_runtime_config_s {
|
||||
userdata: &raw const USERDATA as *mut c_void,
|
||||
supports_selection_clipboard: true,
|
||||
wakeup_cb: Some(on_wakeup),
|
||||
action_cb: Some(on_action),
|
||||
read_clipboard_cb: Some(on_read_clipboard),
|
||||
confirm_read_clipboard_cb: Some(on_confirm_read),
|
||||
write_clipboard_cb: Some(on_write_clipboard),
|
||||
close_surface_cb: Some(on_close),
|
||||
};
|
||||
let app = unsafe { ghostty_app_new(&rt, cfg) };
|
||||
assert!(!app.is_null());
|
||||
Some(Host {
|
||||
_gl: gl,
|
||||
cfg,
|
||||
app,
|
||||
_conf_file: conf,
|
||||
})
|
||||
}
|
||||
|
||||
fn surface(&self, command: &str) -> ghostty_surface_t {
|
||||
self.surface_with(command, |_| {})
|
||||
}
|
||||
|
||||
fn surface_with(
|
||||
&self,
|
||||
command: &str,
|
||||
f: impl FnOnce(&mut ghostty_surface_config_s),
|
||||
) -> ghostty_surface_t {
|
||||
let mut sc = ghostty_surface_config_new();
|
||||
sc.platform_tag = test_platform::PLATFORM_TAG;
|
||||
sc.userdata = &raw const USERDATA as *mut c_void;
|
||||
sc.scale_factor = 2.0;
|
||||
let command = cs(command);
|
||||
sc.command = command.as_ptr();
|
||||
f(&mut sc);
|
||||
let s = unsafe { ghostty_surface_new(self.app, &sc) };
|
||||
assert!(!s.is_null(), "ghostty_surface_new failed");
|
||||
unsafe { ghostty_surface_set_size(s, 1000, 600) };
|
||||
s
|
||||
}
|
||||
|
||||
fn pump(&self) {
|
||||
unsafe { ghostty_app_tick(self.app) };
|
||||
}
|
||||
|
||||
fn wait(&self, what: &str, mut cond: impl FnMut() -> bool) {
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
self.pump();
|
||||
if cond() {
|
||||
return;
|
||||
}
|
||||
assert!(
|
||||
start.elapsed() < TIMEOUT,
|
||||
"timed out waiting for {what}\nscreen:\n{}",
|
||||
SCREEN.with(|c| c.borrow().clone())
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_screen(&self, s: ghostty_surface_t, needle: &str) {
|
||||
self.wait(&format!("screen to contain {needle:?}"), || {
|
||||
screen_text(s).contains(needle)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Host {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
ghostty_app_free(self.app);
|
||||
ghostty_config_free(self.cfg);
|
||||
}
|
||||
test_platform::set_gl_loader(None);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! host {
|
||||
($($arg:expr),*) => {
|
||||
match Host::new($($arg),*) {
|
||||
Some(h) => h,
|
||||
None => return,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn point(tag: i32, coord: i32) -> ghostty_point_s {
|
||||
ghostty_point_s {
|
||||
tag,
|
||||
coord,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// The last screen contents read (for failure messages).
|
||||
static SCREEN: RefCell<String> = const { RefCell::new(String::new()) };
|
||||
}
|
||||
|
||||
/// The whole screen (history + active) as text.
|
||||
fn screen_text(s: ghostty_surface_t) -> String {
|
||||
let sel = ghostty_selection_s {
|
||||
top_left: point(2, 1),
|
||||
bottom_right: point(2, 2),
|
||||
rectangle: false,
|
||||
};
|
||||
let mut text = unsafe { std::mem::zeroed::<ghostty_text_s>() };
|
||||
let ok = unsafe { ghostty_surface_read_text(s, sel, &mut text) };
|
||||
assert!(ok);
|
||||
let out = unsafe { CStr::from_ptr(text.text) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
assert_eq!(out.len(), text.text_len);
|
||||
unsafe { ghostty_surface_free_text(s, &mut text) };
|
||||
SCREEN.with(|c| *c.borrow_mut() = out.clone());
|
||||
out
|
||||
}
|
||||
|
||||
fn key(action: i32, keycode: u32, mods: i32, text: Option<&CStr>) -> ghostty_input_key_s {
|
||||
ghostty_input_key_s {
|
||||
action,
|
||||
mods,
|
||||
consumed_mods: 0,
|
||||
keycode,
|
||||
text: text.map_or(ptr::null(), CStr::as_ptr),
|
||||
unshifted_codepoint: 0,
|
||||
composing: false,
|
||||
}
|
||||
}
|
||||
|
||||
// xkb keycodes (evdev + 8)
|
||||
const KEY_A: u32 = 38;
|
||||
const KEY_B: u32 = 56;
|
||||
const KEY_C: u32 = 54;
|
||||
const KEY_ENTER: u32 = 36;
|
||||
|
||||
const READ_ECHO: &str =
|
||||
"printf 'READY\\n'; while IFS= read -r line; do printf 'ECHO:%s\\n' \"$line\"; done";
|
||||
|
||||
// ---- tests -------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn surface_new_reports_and_frees() {
|
||||
let h = host!("");
|
||||
let s = h.surface(READ_ECHO);
|
||||
h.wait_screen(s, "READY");
|
||||
|
||||
// userdata and app round trip
|
||||
assert_eq!(
|
||||
unsafe { ghostty_surface_userdata(s) },
|
||||
&raw const USERDATA as *mut c_void
|
||||
);
|
||||
assert_eq!(unsafe { ghostty_surface_app(s) }, h.app);
|
||||
|
||||
// The actions fired during init target the surface by its handle.
|
||||
let (cell, limit) = rec(|r| {
|
||||
(
|
||||
r.actions
|
||||
.iter()
|
||||
.any(|a| a.0 == 25 && a.1 == 1 && a.2 == s as usize),
|
||||
r.actions
|
||||
.iter()
|
||||
.any(|a| a.0 == 22 && a.1 == 1 && a.2 == s as usize),
|
||||
)
|
||||
});
|
||||
assert!(cell, "cell_size action for the surface");
|
||||
assert!(limit, "size_limit action for the surface");
|
||||
assert!(rec(|r| r.cell_size).unwrap().0 > 0);
|
||||
// The quit timer is stopped when the first surface is added.
|
||||
assert!(rec(|r| r.actions.iter().any(|a| a.0 == 41 && a.1 == 0)));
|
||||
|
||||
// size
|
||||
let size = unsafe { ghostty_surface_size(s) };
|
||||
assert_eq!(size.width_px, 1000);
|
||||
assert_eq!(size.height_px, 600);
|
||||
assert!(
|
||||
size.columns > 10 && size.rows > 5,
|
||||
"{}x{}",
|
||||
size.columns,
|
||||
size.rows
|
||||
);
|
||||
assert!(size.cell_width_px > 0 && size.cell_height_px > 0);
|
||||
// 2x content scale doubles the cell size relative to 1x
|
||||
unsafe { ghostty_surface_set_content_scale(s, 1.0, 1.0) };
|
||||
let size1 = unsafe { ghostty_surface_size(s) };
|
||||
assert!(size1.cell_width_px < size.cell_width_px);
|
||||
// garbage scales are clamped to 1
|
||||
unsafe { ghostty_surface_set_content_scale(s, f64::NAN, -3.0) };
|
||||
assert_eq!(
|
||||
unsafe { ghostty_surface_size(s) }.cell_width_px,
|
||||
size1.cell_width_px
|
||||
);
|
||||
|
||||
assert!(!unsafe { ghostty_surface_process_exited(s) });
|
||||
assert!(!unsafe { ghostty_surface_has_selection(s) });
|
||||
|
||||
unsafe { ghostty_surface_free(s) };
|
||||
// The last surface going away starts the quit timer.
|
||||
assert!(rec(|r| r.actions.last().is_some_and(|a| a.0 == 41)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_and_key_input_reach_the_child() {
|
||||
let h = host!("");
|
||||
let s = h.surface(READ_ECHO);
|
||||
h.wait_screen(s, "READY");
|
||||
|
||||
// Raw text is treated like a paste.
|
||||
let t = b"hello\n";
|
||||
unsafe { ghostty_surface_text(s, t.as_ptr().cast(), t.len()) };
|
||||
h.wait_screen(s, "ECHO:hello");
|
||||
|
||||
// Key events: press a, b, enter.
|
||||
let a = cs("a");
|
||||
let b = cs("b");
|
||||
let nl = cs("\r");
|
||||
assert!(unsafe { ghostty_surface_key(s, key(1, KEY_A, 0, Some(&a))) });
|
||||
let _ = unsafe { ghostty_surface_key(s, key(0, KEY_A, 0, Some(&a))) };
|
||||
assert!(unsafe { ghostty_surface_key(s, key(1, KEY_B, 0, Some(&b))) });
|
||||
assert!(unsafe { ghostty_surface_key(s, key(1, KEY_ENTER, 0, Some(&nl))) });
|
||||
h.wait_screen(s, "ECHO:ab");
|
||||
|
||||
// ctrl+shift+c is bound by default on Linux (copy_to_clipboard).
|
||||
let c = cs("C");
|
||||
let ev = key(1, KEY_C, GHOSTTY_MODS_CTRL | GHOSTTY_MODS_SHIFT, Some(&c));
|
||||
let mut flags: u32 = 0xffff_ff00;
|
||||
assert!(unsafe { ghostty_surface_key_is_binding(s, ev, (&raw mut flags).cast()) });
|
||||
// Only the low byte is written (Zig writes the `u8` Flags.C).
|
||||
assert_eq!(flags & 0xffff_ff00, 0xffff_ff00);
|
||||
assert_eq!(
|
||||
flags & GHOSTTY_BINDING_FLAGS_CONSUMED as u32,
|
||||
GHOSTTY_BINDING_FLAGS_CONSUMED as u32
|
||||
);
|
||||
assert!(!unsafe {
|
||||
ghostty_surface_key_is_binding(s, key(1, KEY_A, 0, Some(&a)), ptr::null_mut())
|
||||
});
|
||||
|
||||
// The invalid action is rejected.
|
||||
assert!(!unsafe { ghostty_surface_key(s, key(9, KEY_A, 0, Some(&a))) });
|
||||
|
||||
// Mod translation is the identity on Linux.
|
||||
let m = GHOSTTY_MODS_CTRL | GHOSTTY_MODS_ALT;
|
||||
assert_eq!(unsafe { ghostty_surface_key_translation_mods(s, m) }, m);
|
||||
|
||||
unsafe { ghostty_surface_free(s) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_and_clipboard_round_trip() {
|
||||
let h = host!("");
|
||||
let s = h.surface(READ_ECHO);
|
||||
h.wait_screen(s, "READY");
|
||||
|
||||
// select_all + read_selection
|
||||
assert!(!unsafe { ghostty_surface_has_selection(s) });
|
||||
let mut text = unsafe { std::mem::zeroed::<ghostty_text_s>() };
|
||||
assert!(!unsafe { ghostty_surface_read_selection(s, &mut text) });
|
||||
let act = b"select_all";
|
||||
assert!(unsafe { ghostty_surface_binding_action(s, act.as_ptr().cast(), act.len()) });
|
||||
assert!(unsafe { ghostty_surface_has_selection(s) });
|
||||
assert!(unsafe { ghostty_surface_read_selection(s, &mut text) });
|
||||
let sel = unsafe { CStr::from_ptr(text.text) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
assert!(sel.contains("READY"), "{sel:?}");
|
||||
assert!(text.tl_px_x >= 0.0 || text.tl_px_x == -1.0);
|
||||
unsafe { ghostty_surface_free_text(s, &mut text) };
|
||||
|
||||
// copy goes to the embedder's write callback
|
||||
let act = b"copy_to_clipboard";
|
||||
assert!(unsafe { ghostty_surface_binding_action(s, act.as_ptr().cast(), act.len()) });
|
||||
let (clip, data, _confirm) = rec(|r| r.writes.last().cloned()).expect("clipboard write");
|
||||
assert_eq!(clip, 0);
|
||||
assert!(data.contains("READY"));
|
||||
|
||||
// invalid binding action strings are rejected
|
||||
let bad = b"no_such_action";
|
||||
assert!(!unsafe { ghostty_surface_binding_action(s, bad.as_ptr().cast(), bad.len()) });
|
||||
|
||||
// paste: read callback -> complete
|
||||
let act = b"paste_from_clipboard";
|
||||
assert!(unsafe { ghostty_surface_binding_action(s, act.as_ptr().cast(), act.len()) });
|
||||
let (clip, state) = rec(|r| r.reads.last().copied()).expect("clipboard read");
|
||||
assert_eq!(clip, 0);
|
||||
// (A newline in the text would make the paste unsafe.)
|
||||
let pasted = cs("pasted-line");
|
||||
unsafe {
|
||||
ghostty_surface_complete_clipboard_request(s, pasted.as_ptr(), state as *mut c_void, false);
|
||||
};
|
||||
let nl = cs("\r");
|
||||
assert!(unsafe { ghostty_surface_key(s, key(1, KEY_ENTER, 0, Some(&nl))) });
|
||||
h.wait_screen(s, "ECHO:pasted-line");
|
||||
assert!(rec(|r| r.confirms.is_empty()));
|
||||
|
||||
// an unsafe paste asks for confirmation and the state stays valid
|
||||
let act = b"paste_from_clipboard";
|
||||
assert!(unsafe { ghostty_surface_binding_action(s, act.as_ptr().cast(), act.len()) });
|
||||
let (_, state) = rec(|r| r.reads.last().copied()).unwrap();
|
||||
let nasty = cs("one\ntwo\n");
|
||||
unsafe {
|
||||
ghostty_surface_complete_clipboard_request(s, nasty.as_ptr(), state as *mut c_void, false);
|
||||
};
|
||||
let (ctext, cstate, cty) = rec(|r| r.confirms.last().cloned()).expect("confirmation requested");
|
||||
assert_eq!(ctext, "one\ntwo\n");
|
||||
assert_eq!(cstate, state);
|
||||
assert_eq!(cty, 0);
|
||||
// confirmed now
|
||||
unsafe {
|
||||
ghostty_surface_complete_clipboard_request(s, nasty.as_ptr(), state as *mut c_void, true);
|
||||
};
|
||||
h.wait_screen(s, "ECHO:one");
|
||||
h.wait_screen(s, "ECHO:two");
|
||||
|
||||
unsafe { ghostty_surface_free(s) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc_52_and_title_and_pwd_actions() {
|
||||
let h = host!("clipboard-write = allow\n");
|
||||
let s = h.surface(
|
||||
"printf '\\033]0;my title\\007\\033]7;file://localhost/tmp\\007\\033]52;c;aGVsbG8=\\007DONE\\n'; sleep 30",
|
||||
);
|
||||
h.wait_screen(s, "DONE");
|
||||
h.wait("title action", || {
|
||||
rec(|r| r.titles.iter().any(|t| t == "my title"))
|
||||
});
|
||||
h.wait("pwd action", || {
|
||||
rec(|r| r.pwds.iter().any(|p| p.contains("/tmp")))
|
||||
});
|
||||
h.wait("osc52 write", || {
|
||||
rec(|r| r.writes.iter().any(|w| w.1 == "hello"))
|
||||
});
|
||||
|
||||
// The inherited config carries the pwd (tab-inherit-working-directory).
|
||||
let inherited = unsafe { ghostty_surface_inherited_config(s, GHOSTTY_SURFACE_CONTEXT_TAB) };
|
||||
assert_eq!(inherited.context, GHOSTTY_SURFACE_CONTEXT_TAB);
|
||||
assert!(!inherited.working_directory.is_null());
|
||||
let wd = unsafe { CStr::from_ptr(inherited.working_directory) }.to_string_lossy();
|
||||
assert_eq!(wd, "/tmp");
|
||||
assert_eq!(inherited.font_size, 12.0);
|
||||
// a new surface can be created with the inherited config
|
||||
let mut cfg = inherited;
|
||||
cfg.platform_tag = test_platform::PLATFORM_TAG;
|
||||
cfg.userdata = &raw const USERDATA as *mut c_void;
|
||||
let command = cs("pwd; sleep 30");
|
||||
cfg.command = command.as_ptr();
|
||||
let s2 = unsafe { ghostty_surface_new(h.app, &cfg) };
|
||||
assert!(!s2.is_null());
|
||||
unsafe { ghostty_surface_set_size(s2, 800, 400) };
|
||||
h.wait_screen(s2, "/tmp");
|
||||
|
||||
unsafe {
|
||||
ghostty_surface_free(s2);
|
||||
ghostty_surface_free(s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surface_options_apply() {
|
||||
let h = host!("");
|
||||
let vars = [
|
||||
ghostty_env_var_s {
|
||||
key: c"GHOSTTY_LIB_TEST".as_ptr(),
|
||||
value: c"env-value".as_ptr(),
|
||||
},
|
||||
ghostty_env_var_s {
|
||||
key: c"GHOSTTY_LIB_TEST2".as_ptr(),
|
||||
value: c"second".as_ptr(),
|
||||
},
|
||||
];
|
||||
let mut vars = vars;
|
||||
let s = h.surface_with(
|
||||
"printf 'ENV=%s,%s PWD=%s\\n' \"$GHOSTTY_LIB_TEST\" \"$GHOSTTY_LIB_TEST2\" \"$PWD\"; \
|
||||
IFS= read -r line; printf 'INPUT=%s\\n' \"$line\"; sleep 30",
|
||||
|sc| {
|
||||
sc.env_vars = vars.as_mut_ptr();
|
||||
sc.env_var_count = 2;
|
||||
sc.working_directory = c"/usr".as_ptr();
|
||||
sc.initial_input = c"typed \"quoted\" \\ text\n".as_ptr();
|
||||
sc.font_size = 20.0;
|
||||
},
|
||||
);
|
||||
h.wait_screen(s, "ENV=env-value,second PWD=/usr");
|
||||
h.wait_screen(s, r#"INPUT=typed "quoted" \ text"#);
|
||||
// The font size override took effect: bigger cells than the default.
|
||||
let big = unsafe { ghostty_surface_size(s) };
|
||||
let s2 = h.surface("sleep 30");
|
||||
let normal = unsafe { ghostty_surface_size(s2) };
|
||||
assert!(big.cell_height_px > normal.cell_height_px);
|
||||
unsafe {
|
||||
ghostty_surface_free(s2);
|
||||
ghostty_surface_free(s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn working_directory_that_does_not_exist_is_ignored() {
|
||||
let h = host!("");
|
||||
let s = h.surface_with("printf 'CWD=%s\\n' \"${PWD##*/}\"; sleep 30", |sc| {
|
||||
sc.working_directory = c"/definitely/not/a/dir".as_ptr();
|
||||
});
|
||||
// Falls back to the default working directory (inherit, or home when the
|
||||
// process looks desktop launched), the shell still starts.
|
||||
h.wait_screen(s, "CWD=");
|
||||
assert!(!screen_text(s).contains("CWD=dir"));
|
||||
unsafe { ghostty_surface_free(s) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_capture_and_events() {
|
||||
let h = host!("");
|
||||
let s = h.surface("printf '\\033[?1000h\\033[?1006hMOUSEON\\n'; cat");
|
||||
h.wait_screen(s, "MOUSEON");
|
||||
assert!(unsafe { ghostty_surface_mouse_captured(s) });
|
||||
|
||||
// Mouse reports reach the child (SGR encoding) and are echoed by cat's
|
||||
// tty as ^[[<...
|
||||
unsafe { ghostty_surface_mouse_pos(s, 10.0, 10.0, 0) };
|
||||
let _ = unsafe { ghostty_surface_mouse_button(s, 1, 1, 0) };
|
||||
unsafe { ghostty_surface_mouse_button(s, 0, 1, 0) };
|
||||
h.wait_screen(s, "[<0;");
|
||||
|
||||
// invalid enum values are rejected
|
||||
assert!(!unsafe { ghostty_surface_mouse_button(s, 7, 1, 0) });
|
||||
assert!(!unsafe { ghostty_surface_mouse_button(s, 1, 99, 0) });
|
||||
|
||||
// scroll and pressure don't crash
|
||||
unsafe {
|
||||
ghostty_surface_mouse_scroll(s, 0.0, 3.0, 0);
|
||||
ghostty_surface_mouse_scroll(s, 0.0, -3.0, 1);
|
||||
ghostty_surface_mouse_pressure(s, 1, 0.5);
|
||||
ghostty_surface_mouse_pressure(s, 9, 0.5);
|
||||
}
|
||||
let (mut x, mut y, mut w, mut hh) = (0.0, 0.0, 0.0, 0.0);
|
||||
unsafe { ghostty_surface_ime_point(s, &mut x, &mut y, &mut w, &mut hh) };
|
||||
assert!(w >= 0.0 && hh > 0.0);
|
||||
unsafe { ghostty_surface_free(s) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_occlusion_preedit_scheme_and_splits() {
|
||||
let h = host!("");
|
||||
let s = h.surface("printf 'READY\\n'; sleep 30");
|
||||
h.wait_screen(s, "READY");
|
||||
unsafe {
|
||||
ghostty_surface_set_focus(s, false);
|
||||
ghostty_surface_set_focus(s, true);
|
||||
ghostty_surface_set_occlusion(s, false);
|
||||
ghostty_surface_set_occlusion(s, true);
|
||||
ghostty_surface_refresh(s);
|
||||
ghostty_surface_set_color_scheme(s, GHOSTTY_COLOR_SCHEME_DARK);
|
||||
ghostty_surface_set_color_scheme(s, 5);
|
||||
let p = b"compose";
|
||||
ghostty_surface_preedit(s, p.as_ptr().cast(), p.len());
|
||||
ghostty_surface_preedit(s, ptr::null(), 0);
|
||||
}
|
||||
|
||||
// splits are actions to the embedder, targeting the surface
|
||||
unsafe {
|
||||
ghostty_surface_split(s, 0);
|
||||
ghostty_surface_split_focus(s, 1);
|
||||
ghostty_surface_split_resize(s, 3, 10);
|
||||
ghostty_surface_split_equalize(s);
|
||||
// invalid directions are ignored
|
||||
ghostty_surface_split(s, 42);
|
||||
}
|
||||
let tags: Vec<_> = rec(|r| {
|
||||
r.actions
|
||||
.iter()
|
||||
.filter(|a| a.2 == s as usize)
|
||||
.map(|a| a.0)
|
||||
.collect()
|
||||
});
|
||||
for want in [4, 16, 18, 19] {
|
||||
assert!(tags.contains(&want), "missing action {want} in {tags:?}");
|
||||
}
|
||||
assert_eq!(tags.iter().filter(|t| **t == 4).count(), 1);
|
||||
|
||||
// binding actions that go to the embedder
|
||||
let act = b"new_window";
|
||||
assert!(unsafe { ghostty_surface_binding_action(s, act.as_ptr().cast(), act.len()) });
|
||||
assert!(rec(|r| r
|
||||
.actions
|
||||
.iter()
|
||||
.any(|a| a.0 == 1 && a.2 == s as usize)));
|
||||
|
||||
// request_close goes through the normal close path -> close callback
|
||||
unsafe { ghostty_surface_request_close(s) };
|
||||
h.wait("close callback", || rec(|r| !r.closes.is_empty()));
|
||||
unsafe { ghostty_surface_free(s) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_exit_is_reported() {
|
||||
let h = host!("");
|
||||
let s = h.surface("exit 3");
|
||||
h.wait("child exit action", || rec(|r| r.child_exit.is_some()));
|
||||
assert!(unsafe { ghostty_surface_process_exited(s) });
|
||||
assert_eq!(rec(|r| r.child_exit), Some(3));
|
||||
assert!(!unsafe { ghostty_surface_needs_confirm_quit(s) });
|
||||
unsafe { ghostty_surface_free(s) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_config_reaches_surfaces() {
|
||||
let h = host!("");
|
||||
let s = h.surface("printf 'READY\\n'; sleep 30");
|
||||
h.wait_screen(s, "READY");
|
||||
let before = unsafe { ghostty_surface_size(s) };
|
||||
|
||||
let mut f = tempfile::NamedTempFile::new().unwrap();
|
||||
use std::io::Write as _;
|
||||
write!(
|
||||
f,
|
||||
"font-size = 24\nwindow-padding-x = 0\nwindow-padding-y = 0\n"
|
||||
)
|
||||
.unwrap();
|
||||
let c2 = ghostty_config_new();
|
||||
let path = cs(f.path().to_str().unwrap());
|
||||
unsafe {
|
||||
ghostty_config_load_file(c2, path.as_ptr());
|
||||
ghostty_config_finalize(c2);
|
||||
ghostty_app_update_config(h.app, c2);
|
||||
}
|
||||
h.pump();
|
||||
assert!(rec(|r| r.actions.iter().any(|a| a.0 == 48 && a.1 == 0)));
|
||||
let after = unsafe { ghostty_surface_size(s) };
|
||||
assert!(
|
||||
after.cell_height_px > before.cell_height_px,
|
||||
"{} -> {}",
|
||||
before.cell_height_px,
|
||||
after.cell_height_px
|
||||
);
|
||||
|
||||
// surface-only update
|
||||
let c3 = ghostty_config_new();
|
||||
unsafe {
|
||||
ghostty_surface_update_config(s, c3);
|
||||
ghostty_config_free(c3);
|
||||
ghostty_config_free(c2);
|
||||
ghostty_surface_free(s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inspector_api() {
|
||||
let h = host!("");
|
||||
let s = h.surface("printf 'READY\\n'; sleep 30");
|
||||
h.wait_screen(s, "READY");
|
||||
|
||||
let i = unsafe { ghostty_surface_inspector(s) };
|
||||
assert!(!i.is_null());
|
||||
// one inspector per surface
|
||||
assert_eq!(unsafe { ghostty_surface_inspector(s) }, i);
|
||||
unsafe {
|
||||
ghostty_inspector_set_content_scale(i, 2.0, 2.0);
|
||||
ghostty_inspector_set_size(i, 800, 600);
|
||||
ghostty_inspector_set_focus(i, true);
|
||||
ghostty_inspector_mouse_pos(i, 10.0, 10.0);
|
||||
ghostty_inspector_mouse_button(i, 1, 1, 0);
|
||||
ghostty_inspector_mouse_button(i, 0, 1, 0);
|
||||
ghostty_inspector_mouse_button(i, 1, 9, 0);
|
||||
ghostty_inspector_mouse_scroll(i, 0.0, 1.0, 1);
|
||||
ghostty_inspector_key(i, 1, 18, GHOSTTY_MODS_CTRL);
|
||||
ghostty_inspector_text(i, c"abc".as_ptr());
|
||||
}
|
||||
// input queues renders of the inspector
|
||||
assert!(rec(|r| r
|
||||
.actions
|
||||
.iter()
|
||||
.any(|a| a.0 == 30 && a.2 == s as usize)));
|
||||
unsafe {
|
||||
ghostty_inspector_free(s);
|
||||
}
|
||||
// a new one can be created afterwards
|
||||
let i2 = unsafe { ghostty_surface_inspector(s) };
|
||||
assert!(!i2.is_null());
|
||||
// and it is freed with the surface
|
||||
unsafe { ghostty_surface_free(s) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surface_without_platform_is_rejected() {
|
||||
let h = host!("");
|
||||
let mut sc = ghostty_surface_config_new();
|
||||
// no platform
|
||||
assert!(unsafe { ghostty_surface_new(h.app, &sc) }.is_null());
|
||||
sc.platform_tag = platform::MACOS;
|
||||
// Zig's Platform.init rejects Darwin platforms off Darwin.
|
||||
assert!(unsafe { ghostty_surface_new(h.app, &sc) }.is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_clipboard_support_is_reported() {
|
||||
// supports_selection_clipboard = true in this host: an OSC 52 write to
|
||||
// the primary selection goes out with clipboard=1.
|
||||
let h = host!("clipboard-write = allow\n");
|
||||
let s = h.surface("printf '\\033]52;p;c2VsZWN0aW9u\\007DONE\\n'; sleep 30");
|
||||
h.wait_screen(s, "DONE");
|
||||
h.wait("selection write", || {
|
||||
rec(|r| {
|
||||
r.writes
|
||||
.iter()
|
||||
.any(|w| w.1 == "selection" && (w.0 == 1 || w.0 == 2))
|
||||
})
|
||||
});
|
||||
unsafe { ghostty_surface_free(s) };
|
||||
}
|
||||
@@ -1520,6 +1520,12 @@ impl<G: GraphicsApi> Renderer<G> {
|
||||
.is_some_and(DisplayLink::is_running)
|
||||
}
|
||||
|
||||
/// The font grid currently used for rendering (for apprts that need
|
||||
/// to read the primary face, e.g. macOS quicklook).
|
||||
pub fn font_grid(&self) -> Arc<SharedGrid> {
|
||||
Arc::clone(&lock(&self.draw).font_grid)
|
||||
}
|
||||
|
||||
/// Called by the renderer thread when it begins (`threadEnter`).
|
||||
pub fn thread_enter(&self) {
|
||||
lock(&self.draw).api.thread_enter();
|
||||
|
||||
@@ -239,6 +239,23 @@ impl Metal {
|
||||
})
|
||||
}
|
||||
|
||||
/// [`Metal::init`] for a raw `NSView` / `UIView` pointer as handed over
|
||||
/// through the C API (`ghostty_platform_macos_s.nsview`). The view must
|
||||
/// stay alive for the lifetime of the renderer (the host owns it).
|
||||
pub fn init_with_view(
|
||||
blending: AlphaBlending,
|
||||
view: *mut c_void,
|
||||
scale_factor: f64,
|
||||
) -> Result<Metal, Error> {
|
||||
if view.is_null() {
|
||||
return Err(Error::ObjCFailed);
|
||||
}
|
||||
// SAFETY: the caller (the embedded runtime) guarantees `view` is a
|
||||
// valid, live NSView / UIView, which is an Objective-C object.
|
||||
let view: &AnyObject = unsafe { &*view.cast::<AnyObject>() };
|
||||
Metal::init(blending, &ViewInfo { view, scale_factor })
|
||||
}
|
||||
|
||||
/// The layer the renderer presents on.
|
||||
pub fn layer(&self) -> &IOSurfaceLayer {
|
||||
&self.layer
|
||||
|
||||
Reference in new issue
Block a user