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:
1 parent
fb79cc8d5c
commit
0137168c66
112 files changed
+18438
-2
No files matched your search
@@ -0,0 +1,120 @@
|
||||
//! `Helpers/Extensions/String+Extension.swift`.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// `String.truncate(length:trailing:)`: counts Characters (extended grapheme
|
||||
/// clusters in Swift; Unicode scalars here, which is identical for the text
|
||||
/// this is used on).
|
||||
#[must_use]
|
||||
pub fn truncate(s: &str, length: usize, trailing: &str) -> String {
|
||||
let trailing_len = trailing.chars().count();
|
||||
let count = s.chars().count();
|
||||
if length <= trailing_len || s.is_empty() || count <= length {
|
||||
return s.to_string();
|
||||
}
|
||||
let max_length = length - trailing_len;
|
||||
let mut out: String = s.chars().take(max_length).collect();
|
||||
out.push_str(trailing);
|
||||
out
|
||||
}
|
||||
|
||||
/// `truncate(length:)` with the default `"…"` trailing text.
|
||||
#[must_use]
|
||||
pub fn truncate_default(s: &str, length: usize) -> String {
|
||||
truncate(s, length, "…")
|
||||
}
|
||||
|
||||
/// `abbreviatedPath`: the path with the home directory abbreviated as `~`.
|
||||
#[must_use]
|
||||
pub fn abbreviated_path(path: &str, home: &str) -> String {
|
||||
match path.strip_prefix(home) {
|
||||
Some(rest) => format!("~{rest}"),
|
||||
None => path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `String.temporaryFile(_:)`: write the string to `<tmp>/<filename>.txt`
|
||||
/// (errors are ignored like `try?`) and return the URL path.
|
||||
#[must_use]
|
||||
pub fn temporary_file(contents: &str, temp_dir: &std::path::Path, filename: &str) -> PathBuf {
|
||||
let mut path = temp_dir.join(filename);
|
||||
path.set_extension("txt");
|
||||
let _ = std::fs::write(&path, contents);
|
||||
path
|
||||
}
|
||||
|
||||
/// `String.fourCharCode`: a four-character ASCII string as a big-endian
|
||||
/// `FourCharCode` (`UInt32`).
|
||||
#[must_use]
|
||||
pub fn four_char_code(s: &str) -> u32 {
|
||||
debug_assert!(
|
||||
s.chars().count() <= 4,
|
||||
"FourCharCode string must be at most 4 characters"
|
||||
);
|
||||
s.bytes()
|
||||
.take(4)
|
||||
.fold(0u32, |acc, b| (acc << 8) | u32::from(b))
|
||||
}
|
||||
|
||||
/// The home-dependent wrapper (`FileManager.homeDirectoryForCurrentUser`).
|
||||
#[must_use]
|
||||
pub fn abbreviated_path_for_current_user(path: &str) -> String {
|
||||
let home = std::env::var("HOME").unwrap_or_default();
|
||||
abbreviated_path(path, &home)
|
||||
}
|
||||
|
||||
pub trait StringExt {
|
||||
fn truncate(&self, length: usize) -> String;
|
||||
fn abbreviated_path(&self) -> String;
|
||||
fn four_char_code(&self) -> u32;
|
||||
}
|
||||
|
||||
impl StringExt for str {
|
||||
fn truncate(&self, length: usize) -> String {
|
||||
truncate_default(self, length)
|
||||
}
|
||||
fn abbreviated_path(&self) -> String {
|
||||
abbreviated_path_for_current_user(self)
|
||||
}
|
||||
fn four_char_code(&self) -> u32 {
|
||||
four_char_code(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn truncate_cases() {
|
||||
assert_eq!(truncate_default("hello", 10), "hello");
|
||||
assert_eq!(truncate_default("hello world", 8), "hello w…");
|
||||
assert_eq!(truncate_default("hello", 5), "hello");
|
||||
assert_eq!(truncate_default("", 3), "");
|
||||
// maxLength <= 0 returns self
|
||||
assert_eq!(truncate("abcdef", 1, "…"), "abcdef");
|
||||
assert_eq!(truncate("abcdef", 3, ".."), "a..");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abbreviates_home() {
|
||||
assert_eq!(abbreviated_path("/Users/x/dev", "/Users/x"), "~/dev");
|
||||
assert_eq!(abbreviated_path("/opt/x", "/Users/x"), "/opt/x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_cc() {
|
||||
assert_eq!(four_char_code("why?"), 0x7768_793F);
|
||||
assert_eq!(four_char_code("ab"), 0x6162);
|
||||
assert_eq!(four_char_code(""), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temp_file_written() {
|
||||
let dir = std::env::temp_dir();
|
||||
let p = temporary_file("data", &dir, "ghostty-macos-test-temp");
|
||||
assert_eq!(p.extension().unwrap(), "txt");
|
||||
assert_eq!(std::fs::read_to_string(&p).unwrap(), "data");
|
||||
let _ = std::fs::remove_file(p);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user