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
5.7 KiB
ghostty-core notes
Keyboard (fork K) - crates/ghostty-core/src/surface/keyboard.rs
Ported: Keyboard state, keyEventIsBinding, keyCallback, maybeHandleBinding (sequences/leader, key tables incl. once,
catch_all ignore, chained actions, performable, consumed/global/all, last_trigger release suppression), endKeySequence,
encodeKey (+ encode_options_from_terminal = key_encode.Options.fromTerminal), key-table helpers
(activate_key_table, deactivate_key_table, deactivate_all_key_tables), release_pressed_key_on_unfocus,
SurfaceMouse.keyToMouseShape (private copy, because it is only used here at the moment).
Deviations: sequences/tables hold paths/names resolved against the current config (no raw pointers);
global/all chained actions are re-implemented locally (perform_all_chained_action) as the surface has no App handle;
no inspector; macOS option-as-alt skipped; once table deactivation calls deactivate_key_table directly.
Needs from the mouse module: Mouse.{mods: Mods, over_link: bool, hidden: bool}, mods_changed(Mods),
mouse_shift_capture(bool) -> bool, mouse_refresh_links(CursorPos, Coordinate, bool) -> Result<(), CoreError>.
Tests: 8 (Zig has none for this code): OwnedKeyEvent round trip, closing actions, sequence path resolution, table lookup,
leader path, encode options from terminal modes, mouse shape for keys.
Binding actions (fork B) - surface/binding.rs
- Ported:
Search(+ search-thread event callback),Text/dump_text/dump_text_locked(viewport math in free fntext_viewport),perform_binding_actionfor every action,closing_action,writeScreenFile(free fnsscreen_file_contents/write_screen_temp_file; the temp dir is deliberately left on disk like Zig),adjust_selection(free fn on&mut Terminal). - App-scoped actions are mapped locally (
perform_app_action) becauseApp::perform_actionneeds anApp, not the sharedAppState;new_windowfrom a surface targets the surface. toggle_mouse_reportingrebuilds theDerivedConfigwith a clonedConfig(Zig mutated in place).- Inspector action only forwards
RtAction::Inspector(inspector is a no-op). - Cross-crate change:
ghostty_terminal::search::thread::Options.terminalis nowArc<dyn TerminalLock>(trait implemented forMutex<Terminal>; core implements it for the shared render state) because the search thread must lock the terminal insideghostty_renderer::state::State. - Requires from the mouse module:
link_url_at_cursor(&self) -> Option<String>(returns the link text under the cursor incl. clipboard trimming and OSC8 URI; None when not over a link); alsomods_changed,mouse_shift_capture,mouse_refresh_linksused by keyboard.rs. - Tests: 7 (no Zig tests in this range).
Mouse (fork M)
Files: crates/ghostty-core/src/surface/mouse.rs (Mouse state, mouse reporting x10/utf8/sgr/urxvt/sgr-pixels, button/cursor/scroll/pressure callbacks, click-to-move, multi-click select, right-click actions, middle-click paste, links incl. OSC8, selection scrolling) and src/surface_mouse.rs.
- Tests:
key_to_mouse_shape,surface_selection_logic,surface_rectangle_selection_logicported 1:1 (selection math is the puremouse_selection(&PageList, ..)); 3 extra. - Deviations: mouse reports/IO messages/runtime actions are collected and run after the renderer lock is released (Zig held the lock and used
.lockedsends).process_linkstakes the lock itself (Zig called it unlocked). Left-release copy-on-select copies viacopy_on_selectdirectly: Zig'ssetSelection(prev-equal)early-returns oneql, which would never copy. - Extra API for binding fork:
link_url_at_cursor().
Core overview (integration, agent CORE)
Done: trait boundary moved to ghostty_apprt::runtime_api (apprt now depends on terminal+renderer and re-exports the real Scrollbar/RendererHealth);
termio uses ghostty_renderer::state::{State,SharedState} (SharedStateExt::lock_terminal); renderer update_kitty calls image::State::kitty_update;
termio applies image-storage-limit (init + config change) and deletes kitty images on clear-screen; app.rs (App.zig, CoreApp impl, app mailbox),
global.rs (logging in Zig format, GHOSTTY_LOG, rlimits, resources dir, i18n), surface/mod.rs (init/teardown with renderer+IO threads, message handler,
config updates, sizing, fonts, focus, preedit, selection, CoreSurface impl), surface/{clipboard,keyboard,mouse,binding,config}.rs, surface_mouse.rs; root src/main.rs.
Wiring details: renderer->app via Host (scrollbar/health/redraw as app mailbox messages); termio->renderer via RendererLink + Wakeup::with_hook;
apprt-gtk got RtSurface::gl_proc_address (libepoxy epoxy_* pointer variables) and create_scope_fn (cgroup scope over D-Bus from the IO thread).
Deviations/stubs: inspector no-op; locale only via GTK's setlocale (no unsafe in core/root); crash reporting (sentry) not ported; Rust version row of +version empty;
default DPI 96 (Zig linux).
How to run: GSETTINGS_BACKEND=memory GHOSTTY_RESOURCES_DIR=/usr/share/ghostty cargo run -- -e sh -c '...' (debug builds only find resources via the env var here).
Dev screenshot: GHOSTTY_DEBUG_SCREENSHOT=/abs/out.png [GHOSTTY_DEBUG_SCREENSHOT_MS=2500] dumps the renderer's last frame (compositor screenshots under niri come out black / are blocked).
Verified manually: window opens on Wayland and X11, shell prompt and SGR colored output render (target/screenshots/frame.png, shell.png), child exit closes window, clean shutdown.
NOT verified: keyboard input end-to-end (no way to inject keys into the window here; xdotool/niri did not deliver), mouse, clipboard, IME, splits/tabs.
Known warnings at runtime: unimplemented action=CellSize (same as Zig GTK), cgroup scope creation fails with UnixProcessIdUnknown in this sandbox (non-fatal).