feat(macos): foundation of the macOS app ported from Swift to Rust

Phase 9, MA1: crates/ghostty-macos (AppKit via objc2; empty stub on
non-Apple targets) ports the foundation of Ghostty 1.3.1's Swift macOS
app (ghostty/macos): main, AppDelegate (+Ghostty) with lifecycle, dock
menu, open-file, notifications, shortcut sync and quit/reopen paths;
MainMenu.xib as generated menu tables built into NSMenus; the
Ghostty.App/Config/Surface wrappers driving libghostty in-process via
ghostty-lib; the generic helpers; scripts/macos-bundle.sh to assemble
Ghostty.app (dry-run on Linux). Feature areas are stubbed for MA2-MA5.
Design and type mapping: docs/rewrite/notes/macos-app-design.md
(App Intents is a Swift-only framework and cannot be ported).

Compile-checked only (no macOS here): just check-macos passes for
aarch64/x86_64-apple-darwin with -D warnings; 125 platform-independent
tests run on Linux.

Test Plan:
- just check-macos
- cargo test -p ghostty-macos
This commit is contained in:
ddidderr committed 2026-10-12 19:25:44 +02:00
1 parent feacba2d69
commit 4861015105
112 files changed
+18438 -2

No files matched your search

@@ -0,0 +1,80 @@
//! `Helpers/Extensions/Array+Extension.swift`.
use std::ffi::CString;
/// `extension Array`.
pub trait ArrayExt<T> {
/// `subscript(safe:)`.
fn get_safe(&self, index: isize) -> Option<&T>;
/// Returns the index before i, with wraparound. Assumes i is a valid index.
fn index_wrapping_before(&self, i: usize) -> usize;
/// Returns the index after i, with wraparound. Assumes i is a valid index.
fn index_wrapping_after(&self, i: usize) -> usize;
}
impl<T> ArrayExt<T> for [T] {
fn get_safe(&self, index: isize) -> Option<&T> {
usize::try_from(index).ok().and_then(|i| self.get(i))
}
fn index_wrapping_before(&self, i: usize) -> usize {
if i == 0 {
return self.len() - 1;
}
i - 1
}
fn index_wrapping_after(&self, i: usize) -> usize {
if i == self.len() - 1 {
return 0;
}
i + 1
}
}
/// `Array<String>.withCStrings`: NUL terminated copies of the strings (the
/// returned `CString`s own the memory the pointers of `as_ptr()` refer to).
/// A string containing an interior NUL is truncated at it, as a C consumer
/// would read it.
#[must_use]
pub fn to_c_strings<S: AsRef<str>>(strings: &[S]) -> Vec<CString> {
strings
.iter()
.map(|s| {
let bytes = s.as_ref().as_bytes();
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
CString::new(&bytes[..end]).unwrap_or_default()
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn safe_subscript() {
let a = [1, 2, 3];
assert_eq!(a.get_safe(0), Some(&1));
assert_eq!(a.get_safe(3), None);
assert_eq!(a.get_safe(-1), None);
}
#[test]
fn wrapping_indices() {
let a = [1, 2, 3];
assert_eq!(a.index_wrapping_before(0), 2);
assert_eq!(a.index_wrapping_before(2), 1);
assert_eq!(a.index_wrapping_after(2), 0);
assert_eq!(a.index_wrapping_after(0), 1);
}
#[test]
fn c_strings() {
let c = to_c_strings(&["a", "b\0c", ""]);
assert_eq!(c[0].to_str().unwrap(), "a");
assert_eq!(c[1].to_str().unwrap(), "b");
assert_eq!(c[2].as_bytes().len(), 0);
assert_eq!(to_c_strings::<&str>(&[]).len(), 0);
}
}