feat(lib): port libghostty, Ghostty's embedded C API
Ports apprt/embedded.zig, main_c.zig and config/CApi.zig + c_get.zig as crates/ghostty-lib (cdylib/staticlib/rlib `libghostty`), exporting every function of the unchanged include/ghostty.h - the ABI Ghostty's Swift macOS/iOS app links against. The embedded runtime implements the runtime traits over the host's C callbacks, so the real core drives it. - ghostty-core selects the renderer per target: Metal on Apple (surface NSView/UIView via RtSurface::platform_view), OpenGL elsewhere. - ghostty_config_get follows Zig's c_get rules via a CValue type (143 supported keys, checked against the Zig field types). - The inspector C API is complete; its Metal backend replaces ImGui's Metal renderer and has not run. Verification: generated C ABI probe (sizeof/alignof/offsetof of every struct/union, 755 enum values, every function linked, nm exports, headers byte-identical to upstream); a C host on Linux covering init, CLI actions, config, app, keys, focus and color scheme; an opt-in test-platform feature creating real surfaces on Linux with headless EGL and a pty (13 tests). Apple targets are clippy-clean but not run; the Swift app has not been built against it. Test Plan: - cargo test -p ghostty-lib (and --features test-platform) - cargo clippy --target aarch64-apple-darwin -p ghostty-lib (cross env in docs/rewrite/notes/macos-M1.md)
This commit is contained in:
1 parent
4787565397
commit
49d4ecefae
47 files changed
+10296
-34
No files matched your search
@@ -0,0 +1,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");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user