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)
1386 lines
37 KiB
Rust
1386 lines
37 KiB
Rust
//! 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");
|
|
}
|
|
}
|