feat(core): port App/Surface and wire up the runnable ghostty binary

This is the integration step that turns the ported crates into a working
terminal: `cargo run` now opens a GTK window running the user's shell.

ghostty-core ports App.zig, Surface.zig (split into keyboard, mouse,
binding, clipboard and config submodules), surface_mouse.zig and
global.zig, and implements the runtime traits. perform_binding_action
handles every binding action.

Cross-crate integration:
- The core<->runtime traits (CoreApp, CoreSurface, RtApp, RtSurface,
  Waker) move from apprt-gtk into ghostty_apprt::runtime_api so the core
  stays runtime-agnostic; Scrollbar/RendererHealth stand-ins are replaced
  by the real terminal/renderer types.
- termio now uses ghostty_renderer::state::{State, SharedState}; the
  renderer calls kitty_update; image-storage-limit is applied at init
  and on config change.
- The search thread locks the terminal through a TerminalLock trait so
  it can share the render state mutex; termio's Wakeup gained a hook to
  wake the renderer thread.
- apprt-gtk: GL proc addresses are resolved through libepoxy's
  epoxy_glFoo pointer variables (plain glFoo symbols resolved to null),
  default DPI fixed to 96, libadwaita version read without GTK init so
  --version works headless.

The root binary dispatches +actions to ghostty-cli (list-fonts,
show-face and new-window are now wired) and otherwise runs the GTK app
with the real core.

Verified at runtime on Wayland and X11: shell prompt and colored output
render, child exit closes the window cleanly. Not yet verified: keyboard
and mouse input, clipboard, IME, tabs and splits (no key injection was
possible in this environment). GHOSTTY_DEBUG_SCREENSHOT=<png> dumps the
renderer's last frame for headless checks.

Test Plan:
- cargo test --workspace (2890 passed)
- cargo clippy --workspace --all-targets -- -D warnings
- GSETTINGS_BACKEND=memory GHOSTTY_RESOURCES_DIR=/usr/share/ghostty \
  cargo run
This commit is contained in:
ddidderr committed 2026-10-12 01:33:44 +02:00
1 parent 9c3eeeba50
commit 3266108c10
42 files changed
+9301 -179

No files matched your search

+26 -7
View File
@@ -13,7 +13,8 @@
//! based refresh timer. The terminal and its mutex are combined into an
//! `Arc<Mutex<Terminal>>`.
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::ops::DerefMut;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
use crossbeam_channel::{Receiver, Sender, TrySendError};
@@ -50,7 +51,7 @@ pub type EventCallback = Arc<dyn Fn(Event) + Send + Sync>;
pub struct Options {
/// The terminal data to search. The mutex must be held while
/// reading/writing the terminal.
pub terminal: Arc<Mutex<Terminal>>,
pub terminal: Arc<dyn TerminalLock>,
/// The callback for events from the search thread. This can be `None`
/// if you don't want to receive events, which could be useful for a
@@ -146,8 +147,26 @@ impl Handle {
}
}
fn lock(m: &Mutex<Terminal>) -> MutexGuard<'_, Terminal> {
m.lock().unwrap_or_else(PoisonError::into_inner)
/// A locked terminal (any guard that derefs to a [`Terminal`]).
pub trait TerminalRef: DerefMut<Target = Terminal> {}
impl<T: DerefMut<Target = Terminal>> TerminalRef for T {}
/// Something that owns a terminal behind a lock. Implemented for
/// `Mutex<Terminal>`; the core implements it for the shared renderer state
/// (where the terminal lives next to preedit/mouse state).
pub trait TerminalLock: Send + Sync {
/// Lock and borrow the terminal (poisoning is ignored).
fn lock_terminal(&self) -> Box<dyn TerminalRef + '_>;
}
impl TerminalLock for Mutex<Terminal> {
fn lock_terminal(&self) -> Box<dyn TerminalRef + '_> {
Box::new(self.lock().unwrap_or_else(PoisonError::into_inner))
}
}
fn lock(m: &Arc<dyn TerminalLock>) -> Box<dyn TerminalRef + '_> {
m.lock_terminal()
}
/// The search thread state.
@@ -677,7 +696,7 @@ impl Search {
/// This doesn't require any locking as it only reads internal state
/// (the terminal mutex is only passed through for API symmetry and is
/// not locked).
fn notify(&mut self, _terminal: &Arc<Mutex<Terminal>>, cb: &EventCallback) {
fn notify(&mut self, _terminal: &Arc<dyn TerminalLock>, cb: &EventCallback) {
let Some(screen_search) = self.screens[key_index(self.last_screen.key)].as_ref() else {
return;
};
@@ -787,7 +806,7 @@ mod tests {
});
let (handle, join) = Thread::spawn(Options {
terminal: Arc::clone(&terminal),
terminal: terminal.clone(),
event_cb: Some(cb),
})
.unwrap();
@@ -845,7 +864,7 @@ mod tests {
});
let (handle, join) = Thread::spawn(Options {
terminal: Arc::clone(&terminal),
terminal: terminal.clone(),
event_cb: Some(cb),
})
.unwrap();