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 e256c42e53
commit ad76ae2301
42 files changed
+9301 -179

No files matched your search

+288
View File
@@ -0,0 +1,288 @@
//! SurfaceMouse represents mouse helper functionality for the core surface
//! (port of `surface_mouse.zig`).
//!
//! It's currently small in scope; its purpose is to isolate mouse logic that
//! has gotten a bit complex (e.g. pointer shape handling for key events).
use ghostty_input::{Key, Mods};
use ghostty_terminal::terminal::MouseEvents;
use ghostty_vt::mouse_shape::MouseShape;
/// See the module docs.
#[derive(Debug, Clone, Copy)]
pub struct SurfaceMouse {
/// For processing key events; the key that was physically pressed on the
/// keyboard.
pub physical_key: Key,
/// The mouse event tracking mode, if any.
pub mouse_event: MouseEvents,
/// The current terminal's mouse shape.
pub mouse_shape: MouseShape,
/// The last mods state when the last mouse button (whatever it was) was
/// pressed or release.
pub mods: Mods,
/// True if the mouse position is currently over a link.
pub over_link: bool,
/// True if the mouse pointer is currently hidden.
pub hidden: bool,
}
impl SurfaceMouse {
/// Translates key state to mouse shape (cursor) state, based on a state
/// machine.
///
/// There are 4 current states:
///
/// * text: starting state, displays a text bar.
/// * default: default state when in a mouse tracking mode. (e.g. vim,
/// etc). Displays an arrow pointer.
/// * pointer: default state when over a link. Displays a pointing finger.
/// * crosshair: any above state can transition to this when the
/// rectangle select keys are pressed (ctrl/super+alt).
///
/// Additionally, default can transition back to text if one of the shift
/// keys are pressed during mouse tracking mode.
///
/// Any secondary state transitions back to its default state when the
/// appropriate keys are released.
///
/// `None` is returned when the mouse shape does not need changing.
#[must_use]
pub fn key_to_mouse_shape(&self) -> Option<MouseShape> {
// Filter for appropriate key events
if !eligible_mouse_shape_key_event(self.physical_key) {
return None;
}
// Exceptions: link hover or hidden state overrides any other shape
// processing and does not change state.
if self.over_link || self.hidden {
return None;
}
if self.mouse_event == MouseEvents::None {
// Default terminal mode, should be text (text bar)
if is_rectangle_select_state(self.mods) {
// Crosshair (rectangle select)
Some(MouseShape::Crosshair)
} else {
Some(MouseShape::Text)
}
} else {
// In mouse tracking mode, should be default (arrow pointer)
if is_mouse_mode_override_state(self.mods) && is_rectangle_select_state(self.mods) {
// Crosshair (rectangle select), only set if we are also
// overriding (e.g. shift+ctrl+alt)
Some(MouseShape::Crosshair)
} else if is_mouse_mode_override_state(self.mods) {
// Normal override state
Some(MouseShape::Text)
} else {
Some(MouseShape::Default)
}
}
}
}
fn eligible_mouse_shape_key_event(physical_key: Key) -> bool {
physical_key.ctrl_or_super()
|| physical_key.left_or_right_shift()
|| physical_key.left_or_right_alt()
}
fn is_mouse_mode_override_state(mods: Mods) -> bool {
mods.shift
}
/// Returns true if our modifiers put us in a state where dragging should
/// cause a rectangle select.
#[must_use]
pub fn is_rectangle_select_state(mods: Mods) -> bool {
mods.ctrl_or_super() && mods.alt
}
#[cfg(test)]
mod tests {
use super::*;
fn m(
physical_key: Key,
mouse_event: MouseEvents,
mouse_shape: MouseShape,
mods: Mods,
over_link: bool,
hidden: bool,
) -> SurfaceMouse {
SurfaceMouse {
physical_key,
mouse_event,
mouse_shape,
mods,
over_link,
hidden,
}
}
fn mods(shift: bool, ctrl: bool, alt: bool, super_: bool) -> Mods {
Mods {
shift,
ctrl,
alt,
super_,
..Mods::default()
}
}
#[test]
fn key_to_mouse_shape() {
let none = Mods::default();
// No specific key pressed
let s = m(
Key::Unidentified,
MouseEvents::None,
MouseShape::Progress,
none,
false,
false,
);
assert_eq!(s.key_to_mouse_shape(), None);
// Over a link.
let s = m(
Key::ShiftLeft,
MouseEvents::None,
MouseShape::Progress,
none,
true,
false,
);
assert_eq!(s.key_to_mouse_shape(), None);
// Mouse is currently hidden
let s = m(
Key::ShiftLeft,
MouseEvents::None,
MouseShape::Progress,
none,
true,
true,
);
assert_eq!(s.key_to_mouse_shape(), None);
// default, no mods (mouse tracking)
let s = m(
Key::ShiftLeft,
MouseEvents::X10,
MouseShape::Default,
none,
false,
false,
);
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Default));
// default -> crosshair (mouse tracking)
let s = m(
Key::AltLeft,
MouseEvents::X10,
MouseShape::Default,
mods(true, true, true, true),
false,
false,
);
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Crosshair));
// default -> text (mouse tracking)
let s = m(
Key::ShiftLeft,
MouseEvents::X10,
MouseShape::Default,
mods(true, false, false, false),
false,
false,
);
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Text));
// crosshair -> text (mouse tracking)
let s = m(
Key::AltLeft,
MouseEvents::X10,
MouseShape::Crosshair,
mods(true, false, false, false),
false,
false,
);
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Text));
// crosshair -> default (mouse tracking)
let s = m(
Key::AltLeft,
MouseEvents::X10,
MouseShape::Crosshair,
none,
false,
false,
);
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Default));
// text -> crosshair (mouse tracking)
let s = m(
Key::AltLeft,
MouseEvents::X10,
MouseShape::Text,
mods(true, true, true, true),
false,
false,
);
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Crosshair));
// text -> default (mouse tracking)
let s = m(
Key::ShiftLeft,
MouseEvents::X10,
MouseShape::Text,
none,
false,
false,
);
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Default));
// text, no mods (no mouse tracking)
let s = m(
Key::ShiftLeft,
MouseEvents::None,
MouseShape::Text,
none,
false,
false,
);
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Text));
// text -> crosshair (no mouse tracking)
let s = m(
Key::AltLeft,
MouseEvents::None,
MouseShape::Text,
mods(false, true, true, true),
false,
false,
);
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Crosshair));
// crosshair -> text (no mouse tracking)
let s = m(
Key::AltLeft,
MouseEvents::None,
MouseShape::Crosshair,
none,
false,
false,
);
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Text));
}
}