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
289 lines
8.1 KiB
Rust
289 lines
8.1 KiB
Rust
//! 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));
|
|
}
|
|
}
|