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:
1 parent
9c3eeeba50
commit
3266108c10
42 files changed
+9301
-179
No files matched your search
@@ -9,6 +9,8 @@ ghostty-vt.workspace = true
|
||||
ghostty-input.workspace = true
|
||||
ghostty-config.workspace = true
|
||||
ghostty-datastruct.workspace = true
|
||||
ghostty-terminal.workspace = true
|
||||
ghostty-renderer.workspace = true
|
||||
thiserror = "2.0.21"
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
pub mod action;
|
||||
pub mod ipc;
|
||||
pub mod runtime;
|
||||
pub mod runtime_api;
|
||||
pub mod structs;
|
||||
pub mod surface;
|
||||
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
//! The trait boundary between app runtimes (apprt-gtk) and `ghostty-core`.
|
||||
//!
|
||||
//! In Zig, `apprt/gtk` calls directly into `CoreApp` (App.zig) and
|
||||
//! `CoreSurface` (Surface.zig). `ghostty-core` isn't ported yet (and it can't
|
||||
//! be a dependency of this crate without a cycle once core implements these
|
||||
//! traits), so the exact surface of what GTK needs is expressed here:
|
||||
//!
|
||||
//! * [`CoreApp`] / [`CoreSurface`]: implemented by ghostty-core, called by GTK.
|
||||
//! * [`RtApp`] / [`RtSurface`]: implemented by this crate, handed to core so it
|
||||
//! can call back (Zig: `rt_app.performAction`, `rt_surface.getSize()`, ...).
|
||||
//!
|
||||
//! Threading: everything here is **main-thread only** (`!Send`), like the GTK
|
||||
//! objects behind it. The only cross-thread entry point is [`Waker`]
|
||||
//! (`App.wakeup`), used by core threads to request an app `tick()`.
|
||||
//!
|
||||
//! See `docs/rewrite/notes/ghostty-apprt-gtk.md` for the full spec.
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::action::{Action, Target};
|
||||
use crate::structs::{
|
||||
Clipboard, ClipboardRequest, ColorScheme, ContentScale, CursorPos, ImePos, SurfaceSize,
|
||||
};
|
||||
use ghostty_config::config::Config;
|
||||
use ghostty_input::{Mods, MouseButton, MouseButtonState, ScrollMods};
|
||||
|
||||
/// Identifier for a core surface (Zig: `*CoreSurface` pointer). Allocated by
|
||||
/// this crate for each GTK surface; core stores it in its `Target::Surface`.
|
||||
pub type SurfaceId = u64;
|
||||
|
||||
/// Minimum window size in cells (Zig: `CoreSurface.min_window_{width,height}_cells`).
|
||||
pub const MIN_WINDOW_WIDTH_CELLS: u32 = 10;
|
||||
/// See [`MIN_WINDOW_WIDTH_CELLS`].
|
||||
pub const MIN_WINDOW_HEIGHT_CELLS: u32 = 4;
|
||||
|
||||
/// Result of `CoreSurface::key_callback` (Zig: `Surface.InputEffect`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum InputEffect {
|
||||
/// The key was consumed by a binding that closed the surface; the
|
||||
/// surface may no longer be valid.
|
||||
Closed,
|
||||
/// The key wasn't used.
|
||||
Ignored,
|
||||
/// The key was used (binding performed or sent to the pty).
|
||||
Consumed,
|
||||
}
|
||||
|
||||
/// Errors from `CoreSurface::complete_clipboard_request` that the GTK side
|
||||
/// reacts to (Zig error set: `UnsafePaste`, `UnauthorizedPaste`).
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ClipboardError {
|
||||
#[error("unsafe paste")]
|
||||
UnsafePaste,
|
||||
#[error("unauthorized paste")]
|
||||
UnauthorizedPaste,
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
/// Generic core error (Zig: `anyerror`).
|
||||
pub type CoreError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
/// A message sent by the GTK side to the core app mailbox
|
||||
/// (Zig: `core_app.mailbox.push`).
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AppMessage {
|
||||
/// Open the config file in the default editor (`App.Message.open_config`).
|
||||
OpenConfig,
|
||||
/// `App.Message.new_window` (queued by `activate`; core responds with
|
||||
/// `rt.perform_action(Target::App, Action::NewWindow)`).
|
||||
NewWindow,
|
||||
/// `surface_message{ surface, .present_surface }`; core validates the id.
|
||||
PresentSurface(SurfaceId),
|
||||
}
|
||||
|
||||
/// Desired font size request used when initializing a surface (Zig:
|
||||
/// `font.face.DesiredSize` points).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct DesiredFontSize {
|
||||
pub points: f32,
|
||||
pub xdpi: u16,
|
||||
pub ydpi: u16,
|
||||
}
|
||||
|
||||
/// Input to `CoreSurface::key_callback`: `ghostty_input::KeyEvent` is used
|
||||
/// directly.
|
||||
pub use ghostty_input::KeyEvent;
|
||||
|
||||
/// Cell metrics for a config+DPI combo (Zig: `font_grid_set.ref(..)` ->
|
||||
/// `grid.cellSize()`), used to compute the default window size.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct CellSize {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
/// Implemented by ghostty-core's `App`.
|
||||
pub trait CoreApp {
|
||||
/// `App.tick`: drain the app mailbox and perform pending work. Core calls
|
||||
/// back into `rt` (e.g. `rt.perform_action`). Called on the main thread
|
||||
/// whenever a [`Waker`] wake was requested, and after config updates.
|
||||
fn tick(&self, rt: &dyn RtApp) -> Result<(), CoreError>;
|
||||
|
||||
/// `App.updateConfig`: apply a new config to the app and all surfaces
|
||||
/// (surfaces get `Message::ChangeConfig`). Returns Ok on success.
|
||||
fn update_config(&self, rt: &dyn RtApp, config: &Arc<Config>) -> Result<(), CoreError>;
|
||||
|
||||
/// `App.needsConfirmQuit`: any surface with a running process?
|
||||
fn needs_confirm_quit(&self) -> bool;
|
||||
|
||||
/// `App.colorSchemeEvent`: the system color scheme changed.
|
||||
fn color_scheme_event(&self, rt: &dyn RtApp, scheme: ColorScheme);
|
||||
|
||||
/// `App.performAllAction`: perform `action` on every surface
|
||||
/// (`rt.perform_action(Target::Surface(id), ..)` for each).
|
||||
fn perform_all_action(
|
||||
&self,
|
||||
rt: &dyn RtApp,
|
||||
action: &ghostty_input::binding::Action,
|
||||
) -> Result<(), CoreError>;
|
||||
|
||||
/// `App.mailbox.push(.., .forever)`.
|
||||
fn push_message(&self, msg: AppMessage);
|
||||
|
||||
/// `App.addSurface` + `Surface.init`: create a core surface for a GTK
|
||||
/// surface. `config` is the (possibly modified) surface config. The
|
||||
/// returned surface is owned by the GTK surface. `rt_surface` is the
|
||||
/// back-reference core uses (Zig: `&priv.rt_surface`).
|
||||
fn new_surface(
|
||||
&self,
|
||||
id: SurfaceId,
|
||||
config: Config,
|
||||
rt_app: Rc<dyn RtApp>,
|
||||
rt_surface: Rc<dyn RtSurface>,
|
||||
) -> Result<Rc<dyn CoreSurface>, CoreError>;
|
||||
|
||||
/// `App.deleteSurface`: called when the GTK surface is finalized, after
|
||||
/// the `CoreSurface` was dropped.
|
||||
fn delete_surface(&self, id: SurfaceId);
|
||||
|
||||
/// Ids of all live surfaces (`core_app.surfaces.items`).
|
||||
fn surface_ids(&self) -> Vec<SurfaceId>;
|
||||
|
||||
/// Cell size for the given config and DPI, used for the default window
|
||||
/// size (`font_grid_set.ref(..).cellSize()`); None if fonts fail.
|
||||
fn cell_size(&self, config: &Config, font_size: DesiredFontSize) -> Option<CellSize>;
|
||||
}
|
||||
|
||||
/// Implemented by ghostty-core's `Surface`. All methods take `&self`
|
||||
/// (interior mutability inside core), main thread only.
|
||||
pub trait CoreSurface {
|
||||
/// The config the surface was created with / last updated to.
|
||||
fn config(&self) -> Arc<Config>;
|
||||
|
||||
// ---- input -------------------------------------------------------
|
||||
fn key_callback(&self, event: KeyEvent<'_>) -> Result<InputEffect, CoreError>;
|
||||
/// `preeditCallback(null)` clears.
|
||||
fn preedit_callback(&self, text: Option<&str>) -> Result<(), CoreError>;
|
||||
fn focus_callback(&self, focused: bool) -> Result<(), CoreError>;
|
||||
/// Returns true if the click was consumed.
|
||||
fn mouse_button_callback(
|
||||
&self,
|
||||
state: MouseButtonState,
|
||||
button: MouseButton,
|
||||
mods: Mods,
|
||||
) -> Result<bool, CoreError>;
|
||||
/// `pos` is in surface (logical*scale) pixels; `(-1,-1)` = left window.
|
||||
fn cursor_pos_callback(&self, pos: CursorPos, mods: Option<Mods>) -> Result<(), CoreError>;
|
||||
fn scroll_callback(&self, xoff: f64, yoff: f64, mods: ScrollMods) -> Result<(), CoreError>;
|
||||
fn mouse_pressure_callback(&self, _stage: ghostty_input::MousePressureStage, _pressure: f64) {}
|
||||
fn size_callback(&self, size: SurfaceSize) -> Result<(), CoreError>;
|
||||
fn content_scale_callback(&self, scale: ContentScale) -> Result<(), CoreError>;
|
||||
/// IME candidate window position in surface pixels (`imePoint`).
|
||||
fn ime_point(&self) -> ImePos;
|
||||
fn has_selection(&self) -> bool;
|
||||
/// `colorSchemeCallback`: the system color scheme changed.
|
||||
fn color_scheme_callback(&self, scheme: ColorScheme) -> Result<(), CoreError>;
|
||||
/// `Surface.updateConfig`: apply a new config to just this surface.
|
||||
fn update_config(&self, config: &Arc<Config>) -> Result<(), CoreError>;
|
||||
/// Perform a keybinding action on this surface; returns whether it was
|
||||
/// performed.
|
||||
fn perform_binding_action(
|
||||
&self,
|
||||
action: &ghostty_input::binding::Action,
|
||||
) -> Result<bool, CoreError>;
|
||||
|
||||
// ---- clipboard ---------------------------------------------------
|
||||
/// `completeClipboardRequest(req, data, confirmed)`.
|
||||
fn complete_clipboard_request(
|
||||
&self,
|
||||
req: ClipboardRequest,
|
||||
data: &str,
|
||||
confirmed: bool,
|
||||
) -> Result<(), ClipboardError>;
|
||||
/// After the user confirms (`surface.config.clipboard_read = .allow`).
|
||||
fn set_clipboard_read_allowed(&self, allow: bool);
|
||||
/// See [`CoreSurface::set_clipboard_read_allowed`] (`clipboard_write`).
|
||||
fn set_clipboard_write_allowed(&self, allow: bool);
|
||||
|
||||
// ---- lifecycle / rendering ----------------------------------------
|
||||
/// Whether closing needs confirmation (`needsConfirmQuit`).
|
||||
fn needs_confirm_quit(&self) -> bool;
|
||||
/// Whether the child exited (`child_exited`) - used by close logic.
|
||||
fn child_exited(&self) -> bool;
|
||||
/// `renderer.displayRealized()`: GLArea realized, GL context current.
|
||||
fn renderer_realized(&self) -> Result<(), CoreError>;
|
||||
/// `renderer.displayUnrealized()`: GL context current (if it could be).
|
||||
fn renderer_unrealized(&self);
|
||||
/// `renderer.drawFrame(true)`: draw into the currently bound GLArea
|
||||
/// framebuffer (GL context is current). `default_framebuffer` is the
|
||||
/// GLArea's FBO id (`GL_FRAMEBUFFER_BINDING`).
|
||||
fn draw_frame(&self, default_framebuffer: u32) -> Result<(), CoreError>;
|
||||
|
||||
// ---- misc state read by the surface widget (added by F5) -------------
|
||||
/// The `SurfaceId` this core surface was created with (`new_surface`'s
|
||||
/// `id`); lets the GTK side map a core surface back to its widget.
|
||||
fn id(&self) -> Option<SurfaceId> {
|
||||
None
|
||||
}
|
||||
/// `surface.readonly` (read-only mode state).
|
||||
fn readonly(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// `surface.size.grid()` -> (columns, rows); used by the resize overlay.
|
||||
fn grid_size(&self) -> (u32, u32) {
|
||||
(0, 0)
|
||||
}
|
||||
/// `surface.font_size` (inherited by child surfaces in `setParent`).
|
||||
fn font_size(&self) -> DesiredFontSize {
|
||||
DesiredFontSize {
|
||||
points: 12.0,
|
||||
xdpi: 96,
|
||||
ydpi: 96,
|
||||
}
|
||||
}
|
||||
|
||||
// ---- inspector (deferred) -------------------------------------------
|
||||
fn activate_inspector(&self) -> Result<(), CoreError> {
|
||||
Ok(())
|
||||
}
|
||||
fn deactivate_inspector(&self) {}
|
||||
}
|
||||
|
||||
/// Implemented by this crate (GTK `Application`); handed to core.
|
||||
pub trait RtApp {
|
||||
/// `App.performAction`. Returns whether the action was performed.
|
||||
/// Runs on the main thread.
|
||||
fn perform_action(&self, target: Target<SurfaceId>, action: Action) -> bool;
|
||||
}
|
||||
|
||||
/// Implemented by this crate (GTK `Surface`); handed to core. Mirrors
|
||||
/// `apprt/gtk/Surface.zig`.
|
||||
pub trait RtSurface {
|
||||
/// `close(process_active)`.
|
||||
fn close(&self, process_active: bool);
|
||||
/// Cgroup path of the surface (Linux, `gtk-single-instance` scopes).
|
||||
fn cgroup(&self) -> Option<String>;
|
||||
fn get_title(&self) -> Option<String>;
|
||||
fn get_content_scale(&self) -> ContentScale;
|
||||
fn get_size(&self) -> SurfaceSize;
|
||||
fn get_cursor_pos(&self) -> CursorPos;
|
||||
fn supports_clipboard(&self, clipboard: Clipboard) -> bool {
|
||||
let _ = clipboard;
|
||||
true
|
||||
}
|
||||
/// `clipboardRequest`; the answer arrives later through
|
||||
/// `CoreSurface::complete_clipboard_request`. Returns true if started.
|
||||
fn clipboard_request(&self, clipboard: Clipboard, state: ClipboardRequest) -> bool;
|
||||
/// `setClipboard(type, contents, confirm)`; contents are (mime, data).
|
||||
fn set_clipboard(&self, clipboard: Clipboard, contents: &[(String, String)], confirm: bool);
|
||||
/// `defaultTermioEnv`: env vars to set for the child (GDK/GTK related
|
||||
/// cleanups).
|
||||
fn default_termio_env(&self) -> Vec<(String, String)>;
|
||||
fn redraw_inspector(&self) {}
|
||||
/// Resolve an OpenGL function pointer for the surface's GL context
|
||||
/// (GTK: dlsym on libepoxy). Called with the context current. Null if
|
||||
/// the symbol is unknown.
|
||||
fn gl_proc_address(&self, name: &str) -> *const std::ffi::c_void {
|
||||
let _ = name;
|
||||
std::ptr::null()
|
||||
}
|
||||
/// Callback to create a transient systemd scope for the child process
|
||||
/// (Zig: `apprt/gtk/cgroup.zig` + `post_fork`). Called on the main
|
||||
/// thread when the surface is created; the returned closure is called
|
||||
/// from the IO thread after fork. `None` disables cgroup isolation.
|
||||
fn create_scope_fn(&self) -> Option<CreateScopeFn> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// `fn(pid, memory_high, tasks_max)`: `None` if there is no D-Bus
|
||||
/// connection, `Some(Err(msg))` on failure.
|
||||
pub type CreateScopeFn =
|
||||
Arc<dyn Fn(u32, Option<u64>, Option<u64>) -> Option<Result<(), String>> + Send + Sync>;
|
||||
|
||||
/// Thread-safe handle to wake the app thread (`App.wakeup`). Core's threads
|
||||
/// call [`Waker::wake`]; the GTK main loop then runs `CoreApp::tick`.
|
||||
pub trait Waker: Send + Sync {
|
||||
fn wake(&self);
|
||||
}
|
||||
@@ -21,28 +21,10 @@ pub const NOTIFICATION_TITLE_LEN: usize = 64;
|
||||
/// `[255:0]u8` => 256 bytes including the sentinel.
|
||||
pub const NOTIFICATION_BODY_LEN: usize = 256;
|
||||
|
||||
// TODO(integration): `terminal.Scrollbar` lives in ghostty-terminal
|
||||
// (page_list::Scrollbar); apprt doesn't depend on it. Local stand-in with
|
||||
// identical fields.
|
||||
/// Scrollbar geometry in rows.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Scrollbar {
|
||||
/// Total scrollable rows.
|
||||
pub total: usize,
|
||||
/// Viewport's offset from the oldest row.
|
||||
pub offset: usize,
|
||||
/// Visible row count.
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
// TODO(integration): `renderer.Health` lives in ghostty-renderer
|
||||
// (backend::Health); apprt doesn't depend on it.
|
||||
/// The health status of a renderer.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RendererHealth {
|
||||
Healthy,
|
||||
Unhealthy,
|
||||
}
|
||||
/// The health status of a renderer (`renderer.Health`).
|
||||
pub use ghostty_renderer::backend::Health as RendererHealth;
|
||||
/// Scrollbar geometry in rows (`terminal.Scrollbar`).
|
||||
pub use ghostty_terminal::page_list::Scrollbar;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ReportTitleStyle {
|
||||
|
||||
Reference in new issue
Block a user