feat(apprt): port the core/runtime contract types

Ports ZIG/src/apprt/{action,structs,surface,ipc,runtime}.zig into
ghostty-apprt: the Action enum performed by the core on the app runtime
(all 65 variants with docs), Target<S> generic over the runtime's
surface handle, clipboard/size/scale/selection structs, the core->surface
Message enum with a Mailbox over an AppMailbox trait, and IPC action
types for +new-window.

Owned Strings and Arc<Config> replace Zig's borrowed slices/pointers.
Scrollbar and RendererHealth are local stand-ins until core integration.
The embedded C API and browser runtime are out of scope.

Test Plan:
- cargo test -p ghostty-apprt (16 tests)
This commit is contained in:
ddidderr committed 2026-10-11 22:25:05 +02:00
1 parent 6f970bfe26
commit ac0e05def1
9 files changed
+1529 -1

No files matched your search

+883
View File
@@ -0,0 +1,883 @@
//! Port of `action.zig`: the actions an apprt has to react to.
//!
//! C-ABI plumbing (`C`, `CValue`, `cval`, `ghostty.h` sync tests) is not
//! ported; the enums keep their C integer discriminants via `as i32`.
use crate::surface::{ChildExited, RendererHealth, Scrollbar};
use ghostty_config::config::Config;
use ghostty_config::types_b::Duration;
use ghostty_input::binding::Trigger;
use ghostty_vt::mouse_shape::MouseShape;
use ghostty_vt::osc::ProgressReport;
use std::sync::Arc;
/// The target for an action. This is generally the thing that had focus
/// while the action was made but the concept of "focus" is not guaranteed
/// since actions can also be triggered by timers, scripts, etc.
///
/// Generic over the surface handle `S` (Zig: `*CoreSurface`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Target<S> {
App,
Surface(S),
}
/// Mirrors `Target.Key`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum TargetKey {
App,
Surface,
}
impl<S> Target<S> {
#[must_use]
pub fn key(&self) -> TargetKey {
match self {
Target::App => TargetKey::App,
Target::Surface(_) => TargetKey::Surface,
}
}
}
/// The possible actions an apprt has to react to. Actions are one-way
/// messages that are sent to the app runtime to trigger some behavior.
///
/// Actions are very often key binding actions but can also be triggered
/// by lifecycle events. For example, the `quit_timer` action is not bindable.
///
/// Importantly, actions are generally OPTIONAL to implement by an apprt.
/// Required functionality is called directly on the runtime structure so
/// there is a compiler error if an action is not implemented.
#[derive(Debug, Clone)]
pub enum Action {
/// Quit the application.
Quit,
/// Open a new window. The target determines whether properties such
/// as font size should be inherited.
NewWindow,
/// Open a new tab. If the target is a surface it should be opened in
/// the same window as the surface. If the target is the app then
/// the tab should be opened in a new window.
NewTab,
/// Closes the tab belonging to the currently focused split, or all other
/// tabs, depending on the mode.
CloseTab(CloseTabMode),
/// Create a new split. The value determines the location of the split
/// relative to the target.
NewSplit(SplitDirection),
/// Close all open windows.
CloseAllWindows,
/// Toggle maximized window state.
ToggleMaximize,
/// Toggle fullscreen mode.
ToggleFullscreen(Fullscreen),
/// Toggle tab overview.
ToggleTabOverview,
/// Toggle whether window directions are shown.
ToggleWindowDecorations,
/// Toggle the quick terminal in or out.
ToggleQuickTerminal,
/// Toggle the command palette.
ToggleCommandPalette,
/// Toggle the visibility of all Ghostty terminal windows.
ToggleVisibility,
/// Toggle the window background opacity. This only has an effect
/// if the window started as transparent (non-opaque), and toggles
/// it between fully opaque and the configured background opacity.
ToggleBackgroundOpacity,
/// Moves a tab by a relative offset.
///
/// Adjusts the tab position based on `offset` (e.g., -1 for left, +1
/// for right). If the new position is out of bounds, it wraps around
/// cyclically within the tab range.
MoveTab(MoveTab),
/// Jump to a specific tab. Must handle the scenario that the tab
/// value is invalid.
GotoTab(GotoTab),
/// Jump to a specific split.
GotoSplit(GotoSplit),
/// Jump to next/previous window.
GotoWindow(GotoWindow),
/// Resize the split in the given direction.
ResizeSplit(ResizeSplit),
/// Equalize all the splits in the target window.
EqualizeSplits,
/// Toggle whether a split is zoomed or not. A zoomed split is resized
/// to take up the entire window.
ToggleSplitZoom,
/// Present the target terminal whether its a tab, split, or window.
PresentTerminal,
/// Sets a size limit (in pixels) for the target terminal.
SizeLimit(SizeLimit),
/// Resets the window size to the default size. See the
/// `reset_window_size` keybinding for more information.
ResetWindowSize,
/// Specifies the initial size of the target terminal.
///
/// This may be sent once during the initialization of a surface
/// (as part of the init call) to indicate the initial size requested
/// for the window if it is not maximized or fullscreen.
///
/// This may also be sent at any time after the surface is initialized
/// to note the new "default size" of the window. This should in general
/// be ignored, but may be useful if the apprt wants to support
/// a "return to default size" action.
InitialSize(InitialSize),
/// The cell size has changed to the given dimensions in pixels.
CellSize(CellSize),
/// The scrollbar is updating.
Scrollbar(Scrollbar),
/// The target should be re-rendered. This usually has a specific
/// surface target but if the app is targeted then all active
/// surfaces should be redrawn.
Render,
/// Control whether the inspector is shown or hidden.
Inspector(Inspector),
/// Show the GTK inspector.
ShowGtkInspector,
/// The inspector for the given target has changes and should be
/// rendered at the next opportunity.
RenderInspector,
/// Show a desktop notification.
DesktopNotification(DesktopNotification),
/// Set the title of the target to the requested value.
SetTitle(SetTitle),
/// Set the tab title override for the target's tab.
SetTabTitle(SetTitle),
/// Set the title of the target to a prompted value. It is up to
/// the apprt to prompt. The value specifies whether to prompt for the
/// surface title or the tab title.
PromptTitle(PromptTitle),
/// The current working directory has changed for the target terminal.
Pwd(Pwd),
/// Set the mouse cursor shape.
MouseShape(MouseShape),
/// Set whether the mouse cursor is visible or not.
MouseVisibility(MouseVisibility),
/// Called when the mouse is over or recently left a link.
MouseOverLink(MouseOverLink),
/// The health of the renderer has changed.
RendererHealth(RendererHealth),
/// Open the Ghostty configuration. This is platform-specific about
/// what it means; it can mean opening a dedicated UI or just opening
/// a file in a text editor.
OpenConfig,
/// Called when there are no more surfaces and the app should quit
/// after the configured delay.
///
/// Despite the name, this is the notification that libghostty sends
/// when there are no more surfaces regardless of if the configuration
/// wants to quit after close, has any delay set, etc. It's up to the
/// apprt to implement the proper logic based on the config.
///
/// This can be cancelled by sending another quit_timer action with "stop".
/// Multiple "starts" shouldn't happen and can be ignored or cause a
/// restart it isn't that important.
QuitTimer(QuitTimer),
/// Set the window floating state. A floating window is one that is
/// always on top of other windows even when not focused.
FloatWindow(FloatWindow),
/// Set the secure input functionality on or off. "Secure input" means
/// that the user is currently at some sort of prompt where they may be
/// entering a password or other sensitive information. This can be used
/// by the app runtime to change the appearance of the cursor, setup
/// system APIs to not log the input, etc.
SecureInput(SecureInput),
/// A sequenced key binding has started, continued, or stopped.
/// The UI should show some indication that the user is in a sequenced
/// key mode because other input may be ignored.
KeySequence(KeySequence),
/// A key table has been activated or deactivated.
KeyTable(KeyTable),
/// A terminal color was changed programmatically through things
/// such as OSC 10/11.
ColorChange(ColorChange),
/// A request to reload the configuration. The reload request can be
/// from a user or for some internal reason. The reload request may
/// request it is a soft reload or a full reload. See the struct for
/// more documentation.
///
/// The configuration should be passed to updateConfig either at the
/// app or surface level depending on the target.
ReloadConfig(ReloadConfig),
/// The configuration has changed. The value is the new configuration
/// (shared via `Arc`; in Zig a pointer valid only for the duration of
/// the action).
///
/// This should be used by apprts to update any internal state that
/// depends on configuration for the given target (i.e. headerbar colors).
///
/// This allows an apprt to have config-dependent state reactively
/// change without having to store the entire configuration or poll
/// for changes.
ConfigChange(ConfigChange),
/// Closes the currently focused window.
CloseWindow,
/// Called when the bell character is seen. The apprt should do whatever
/// it needs to ring the bell. This is usually a sound or visual effect.
RingBell,
/// Undo the last action. See the "undo" keybinding for more
/// details on what can and cannot be undone.
Undo,
/// Redo the last undone action.
Redo,
CheckForUpdates,
/// Open a URL using the native OS mechanisms. On macOS this might be `open`
/// or on Linux this might be `xdg-open`. The exact mechanism is up to the
/// apprt.
OpenUrl(OpenUrl),
/// Show a native GUI notification that the child process has exited.
ShowChildExited(ChildExited),
/// Show a native GUI notification about the progress of some TUI operation.
ProgressReport(ProgressReport),
/// Show the on-screen keyboard.
ShowOnScreenKeyboard,
/// A command has finished,
CommandFinished(CommandFinished),
/// Start the search overlay with an optional initial needle. If the
/// search is already active and the needle is non-empty, update the
/// current search needle and focus the search input.
StartSearch(StartSearch),
/// End the search overlay, clearing the search state and hiding it.
EndSearch,
/// The total number of matches found by the search.
SearchTotal(SearchTotal),
/// The currently selected search match index (1-based).
SearchSelected(SearchSelected),
/// The readonly state of the surface has changed.
Readonly(Readonly),
/// Copy the effective title of the surface to the clipboard.
/// The effective title is the user-overridden title if set,
/// otherwise the terminal-set title.
CopyTitleToClipboard,
}
/// Mirrors `Action.Key`. The order matches the libghostty C enum.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum Key {
Quit,
NewWindow,
NewTab,
CloseTab,
NewSplit,
CloseAllWindows,
ToggleMaximize,
ToggleFullscreen,
ToggleTabOverview,
ToggleWindowDecorations,
ToggleQuickTerminal,
ToggleCommandPalette,
ToggleVisibility,
ToggleBackgroundOpacity,
MoveTab,
GotoTab,
GotoSplit,
GotoWindow,
ResizeSplit,
EqualizeSplits,
ToggleSplitZoom,
PresentTerminal,
SizeLimit,
ResetWindowSize,
InitialSize,
CellSize,
Scrollbar,
Render,
Inspector,
ShowGtkInspector,
RenderInspector,
DesktopNotification,
SetTitle,
SetTabTitle,
PromptTitle,
Pwd,
MouseShape,
MouseVisibility,
MouseOverLink,
RendererHealth,
OpenConfig,
QuitTimer,
FloatWindow,
SecureInput,
KeySequence,
KeyTable,
ColorChange,
ReloadConfig,
ConfigChange,
CloseWindow,
RingBell,
Undo,
Redo,
CheckForUpdates,
OpenUrl,
ShowChildExited,
ProgressReport,
ShowOnScreenKeyboard,
CommandFinished,
StartSearch,
EndSearch,
SearchTotal,
SearchSelected,
Readonly,
CopyTitleToClipboard,
}
impl Action {
/// The tag of this action.
#[must_use]
pub fn key(&self) -> Key {
match self {
Action::Quit => Key::Quit,
Action::NewWindow => Key::NewWindow,
Action::NewTab => Key::NewTab,
Action::CloseTab(_) => Key::CloseTab,
Action::NewSplit(_) => Key::NewSplit,
Action::CloseAllWindows => Key::CloseAllWindows,
Action::ToggleMaximize => Key::ToggleMaximize,
Action::ToggleFullscreen(_) => Key::ToggleFullscreen,
Action::ToggleTabOverview => Key::ToggleTabOverview,
Action::ToggleWindowDecorations => Key::ToggleWindowDecorations,
Action::ToggleQuickTerminal => Key::ToggleQuickTerminal,
Action::ToggleCommandPalette => Key::ToggleCommandPalette,
Action::ToggleVisibility => Key::ToggleVisibility,
Action::ToggleBackgroundOpacity => Key::ToggleBackgroundOpacity,
Action::MoveTab(_) => Key::MoveTab,
Action::GotoTab(_) => Key::GotoTab,
Action::GotoSplit(_) => Key::GotoSplit,
Action::GotoWindow(_) => Key::GotoWindow,
Action::ResizeSplit(_) => Key::ResizeSplit,
Action::EqualizeSplits => Key::EqualizeSplits,
Action::ToggleSplitZoom => Key::ToggleSplitZoom,
Action::PresentTerminal => Key::PresentTerminal,
Action::SizeLimit(_) => Key::SizeLimit,
Action::ResetWindowSize => Key::ResetWindowSize,
Action::InitialSize(_) => Key::InitialSize,
Action::CellSize(_) => Key::CellSize,
Action::Scrollbar(_) => Key::Scrollbar,
Action::Render => Key::Render,
Action::Inspector(_) => Key::Inspector,
Action::ShowGtkInspector => Key::ShowGtkInspector,
Action::RenderInspector => Key::RenderInspector,
Action::DesktopNotification(_) => Key::DesktopNotification,
Action::SetTitle(_) => Key::SetTitle,
Action::SetTabTitle(_) => Key::SetTabTitle,
Action::PromptTitle(_) => Key::PromptTitle,
Action::Pwd(_) => Key::Pwd,
Action::MouseShape(_) => Key::MouseShape,
Action::MouseVisibility(_) => Key::MouseVisibility,
Action::MouseOverLink(_) => Key::MouseOverLink,
Action::RendererHealth(_) => Key::RendererHealth,
Action::OpenConfig => Key::OpenConfig,
Action::QuitTimer(_) => Key::QuitTimer,
Action::FloatWindow(_) => Key::FloatWindow,
Action::SecureInput(_) => Key::SecureInput,
Action::KeySequence(_) => Key::KeySequence,
Action::KeyTable(_) => Key::KeyTable,
Action::ColorChange(_) => Key::ColorChange,
Action::ReloadConfig(_) => Key::ReloadConfig,
Action::ConfigChange(_) => Key::ConfigChange,
Action::CloseWindow => Key::CloseWindow,
Action::RingBell => Key::RingBell,
Action::Undo => Key::Undo,
Action::Redo => Key::Redo,
Action::CheckForUpdates => Key::CheckForUpdates,
Action::OpenUrl(_) => Key::OpenUrl,
Action::ShowChildExited(_) => Key::ShowChildExited,
Action::ProgressReport(_) => Key::ProgressReport,
Action::ShowOnScreenKeyboard => Key::ShowOnScreenKeyboard,
Action::CommandFinished(_) => Key::CommandFinished,
Action::StartSearch(_) => Key::StartSearch,
Action::EndSearch => Key::EndSearch,
Action::SearchTotal(_) => Key::SearchTotal,
Action::SearchSelected(_) => Key::SearchSelected,
Action::Readonly(_) => Key::Readonly,
Action::CopyTitleToClipboard => Key::CopyTitleToClipboard,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum SplitDirection {
Right,
Down,
Left,
Up,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum GotoSplit {
Previous,
Next,
Up,
Left,
Down,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum GotoWindow {
Previous,
Next,
}
/// The amount to resize the split by and the direction to resize it in.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResizeSplit {
pub amount: u16,
pub direction: ResizeSplitDirection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum ResizeSplitDirection {
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MoveTab {
pub amount: isize,
}
/// The tab to jump to. Non-negative integer values represent the index
/// (zero-based) of the tab to jump to. Negative values are special values
/// (Zig: non-exhaustive enum; unknown negatives are `Index(n)` with n < -3
/// and must be treated as invalid by the apprt).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GotoTab {
Previous,
Next,
Last,
Index(i32),
}
impl GotoTab {
#[must_use]
pub fn from_int(v: i32) -> Self {
match v {
-1 => Self::Previous,
-2 => Self::Next,
-3 => Self::Last,
n => Self::Index(n),
}
}
#[must_use]
pub fn int(self) -> i32 {
match self {
Self::Previous => -1,
Self::Next => -2,
Self::Last => -3,
Self::Index(n) => n,
}
}
}
/// The fullscreen mode to toggle to if we're moving to fullscreen.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Fullscreen {
Native,
/// macOS has a non-native fullscreen mode that is more like a maximized
/// window. This is much faster to enter and exit than the native mode.
MacosNonNative,
MacosNonNativeVisibleMenu,
MacosNonNativePaddedNotch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum FloatWindow {
On,
Off,
Toggle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum SecureInput {
On,
Off,
Toggle,
}
/// The inspector mode to toggle to if we're toggling the inspector.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Inspector {
Toggle,
Show,
Hide,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum QuitTimer {
Start,
Stop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Readonly {
Off,
On,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum MouseVisibility {
Visible,
Hidden,
}
/// Whether to prompt for the surface title or tab title.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum PromptTitle {
Surface,
Tab,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MouseOverLink {
pub url: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SizeLimit {
pub min_width: u32,
pub min_height: u32,
pub max_width: u32,
pub max_height: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InitialSize {
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellSize {
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetTitle {
pub title: String,
}
impl std::fmt::Display for SetTitle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SetTitle{{ {} }}", self.title)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pwd {
pub pwd: String,
}
impl std::fmt::Display for Pwd {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Pwd{{ {} }}", self.pwd)
}
}
/// The desktop notification to show.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DesktopNotification {
pub title: String,
pub body: String,
}
impl std::fmt::Display for DesktopNotification {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"DesktopNotification{{ title: {}, body: {} }}",
self.title, self.body
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeySequence {
Trigger(Trigger),
End,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyTable {
Activate(String),
Deactivate,
DeactivateAll,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ColorChange {
pub kind: ColorKind,
pub r: u8,
pub g: u8,
pub b: u8,
}
/// Negative numbers indicate some named kind; 0+ values indicate a palette
/// index (Zig: non-exhaustive enum).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorKind {
Foreground,
Background,
Cursor,
Palette(i32),
}
impl ColorKind {
#[must_use]
pub fn from_int(v: i32) -> Self {
match v {
-1 => Self::Foreground,
-2 => Self::Background,
-3 => Self::Cursor,
n => Self::Palette(n),
}
}
#[must_use]
pub fn int(self) -> i32 {
match self {
Self::Foreground => -1,
Self::Background => -2,
Self::Cursor => -3,
Self::Palette(n) => n,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ReloadConfig {
/// A soft reload means that the configuration doesn't need to be
/// read off disk, but libghostty needs the full config again so call
/// updateConfig with it.
pub soft: bool,
}
#[derive(Debug, Clone)]
pub struct ConfigChange {
pub config: Arc<Config>,
}
/// Open a URL
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenUrl {
/// The type of data that the URL refers to.
pub kind: OpenUrlKind,
/// The URL.
pub url: String,
}
/// The type of the data at the URL to open. This is used as a hint to
/// potentially open the URL in a different way.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(i32)]
pub enum OpenUrlKind {
/// The type is unknown. This is the default and apprts should
/// open the URL in the most generic way possible. For example,
/// on macOS this would be the equivalent of `open` or on Linux
/// this would be `xdg-open`.
#[default]
Unknown,
/// The URL is known to be a text file. In this case, the apprt
/// should try to open the URL in a text editor or viewer or
/// some equivalent, if possible.
Text,
/// The URL is known to contain HTML content.
Html,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum CloseTabMode {
/// Close the current tab.
This,
/// Close all other tabs.
Other,
/// Close all tabs to the right of the current tab.
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CommandFinished {
pub exit_code: Option<u8>,
pub duration: Duration,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StartSearch {
pub needle: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SearchTotal {
pub total: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SearchSelected {
pub selected: Option<usize>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn target_key() {
assert_eq!(Target::<u32>::App.key(), TargetKey::App);
assert_eq!(Target::Surface(1u32).key(), TargetKey::Surface);
assert_eq!(TargetKey::Surface as i32, 1);
}
#[test]
fn action_key_order_matches_c_enum() {
assert_eq!(Key::Quit as i32, 0);
assert_eq!(Key::CopyTitleToClipboard as i32, 64);
assert_eq!(Action::Quit.key(), Key::Quit);
assert_eq!(Action::EndSearch.key(), Key::EndSearch);
assert_eq!(
Action::ResizeSplit(ResizeSplit {
amount: 1,
direction: ResizeSplitDirection::Up
})
.key(),
Key::ResizeSplit
);
assert_eq!(
Action::QuitTimer(QuitTimer::Start).key() as i32,
Key::QuitTimer as i32
);
}
#[test]
fn goto_tab_roundtrip() {
assert_eq!(GotoTab::from_int(-1), GotoTab::Previous);
assert_eq!(GotoTab::from_int(-2), GotoTab::Next);
assert_eq!(GotoTab::from_int(-3), GotoTab::Last);
assert_eq!(GotoTab::from_int(4), GotoTab::Index(4));
assert_eq!(GotoTab::Index(4).int(), 4);
assert_eq!(GotoTab::Last.int(), -3);
}
#[test]
fn color_kind_roundtrip() {
assert_eq!(ColorKind::from_int(-2), ColorKind::Background);
assert_eq!(ColorKind::from_int(7), ColorKind::Palette(7));
assert_eq!(ColorKind::Cursor.int(), -3);
}
#[test]
fn enum_discriminants() {
assert_eq!(SplitDirection::Up as i32, 3);
assert_eq!(GotoSplit::Right as i32, 5);
assert_eq!(Fullscreen::MacosNonNativePaddedNotch as i32, 3);
assert_eq!(OpenUrlKind::default(), OpenUrlKind::Unknown);
assert_eq!(CloseTabMode::Right as i32, 2);
assert_eq!(Readonly::On as i32, 1);
}
#[test]
fn display_formats() {
assert_eq!(SetTitle { title: "x".into() }.to_string(), "SetTitle{ x }");
assert_eq!(Pwd { pwd: "/a".into() }.to_string(), "Pwd{ /a }");
assert_eq!(
DesktopNotification {
title: "t".into(),
body: "b".into()
}
.to_string(),
"DesktopNotification{ title: t, body: b }"
);
}
#[test]
fn reload_config_default_hard() {
assert!(!ReloadConfig::default().soft);
}
}
+81
View File
@@ -0,0 +1,81 @@
//! Inter-process Communication to a running Ghostty instance from a separate
//! process (port of `ipc.zig`). The transport (D-Bus) lives in apprt-gtk.
/// Errors from IPC.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum Error {
/// The IPC failed. If a function returns this error, it's expected that
/// an a more specific error message will have been written to stderr (or
/// otherwise shown to the user in an appropriate way).
#[error("IPC failed")]
IpcFailed,
}
/// Which instance to talk to.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Target {
/// Open up a new window in a custom instance of Ghostty.
Class(String),
/// Detect which instance to open a new window in.
Detect,
}
/// The arguments to pass to Ghostty as the command.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewWindow {
/// A list of command arguments to launch in the new window. If this is
/// `None` the command configured in the config or the user's default
/// shell should be launched.
///
/// It is an error for this to be `Some`, but zero length.
pub arguments: Option<Vec<String>>,
}
/// IPC actions.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
/// The arguments to pass to Ghostty as the command.
NewWindow(NewWindow),
}
/// Mirrors `Action.Key`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActionKey {
NewWindow,
}
impl Action {
#[must_use]
pub fn key(&self) -> ActionKey {
match self {
Action::NewWindow(_) => ActionKey::NewWindow,
}
}
}
impl Target {
#[must_use]
pub fn is_detect(&self) -> bool {
matches!(self, Target::Detect)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn action_key() {
let a = Action::NewWindow(NewWindow {
arguments: Some(vec!["a".into()]),
});
assert_eq!(a.key(), ActionKey::NewWindow);
}
#[test]
fn target_variants() {
assert!(Target::Detect.is_detect());
assert!(!Target::Class("x".into()).is_detect());
}
}
+17 -1
View File
@@ -1 +1,17 @@
//! ghostty-apprt: the contract between the core and app runtimes (ZIG/src/apprt/{action,structs,surface,ipc}.zig).
//! ghostty-apprt: the contract between the core and app runtimes
//! (ZIG/src/apprt/{action,structs,surface,ipc,runtime,none}.zig).
//!
//! The C-ABI parts of the Zig sources (`C` structs, `cval`, `CValue`,
//! `ghostty.h` sync tests) belong to the embedded runtime and are not ported.
pub mod action;
pub mod ipc;
pub mod runtime;
pub mod structs;
pub mod surface;
pub use action::{Action, Target};
pub use structs::{
Clipboard, ClipboardContent, ClipboardRequest, ClipboardRequestType, ColorScheme, ContentScale,
CursorPos, ImePos, Selection, SurfaceSize,
};
+61
View File
@@ -0,0 +1,61 @@
//! Port of `runtime.zig` (and `none.zig`).
/// Runtime is the runtime to use for Ghostty. All runtimes do not provide
/// equivalent feature sets.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Runtime {
/// Will not produce an executable at all (lib only). Matches `none.zig`:
/// there is no apprt to communicate with, so IPC always returns `false`.
None,
/// GTK4. Rich windowed application. This uses a full GObject-based
/// approach to building the application.
Gtk,
}
impl Runtime {
/// The default runtime for the current target. The Linux and FreeBSD
/// default is GTK because it is a full featured application; otherwise
/// `None`.
#[must_use]
pub fn default_for_target() -> Self {
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
Runtime::Gtk
} else {
Runtime::None
}
}
}
/// `none.zig` `App::performIpc`: always false as there is no apprt to
/// communicate with.
///
/// # Errors
/// Never fails.
pub fn none_perform_ipc(
_target: &crate::ipc::Target,
_action: &crate::ipc::Action,
) -> Result<bool, crate::ipc::Error> {
Ok(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_runtime_is_gtk_on_linux() {
if cfg!(target_os = "linux") {
assert_eq!(Runtime::default_for_target(), Runtime::Gtk);
}
}
#[test]
fn none_ipc_returns_false() {
let r = none_perform_ipc(
&crate::ipc::Target::Detect,
&crate::ipc::Action::NewWindow(crate::ipc::NewWindow { arguments: None }),
);
assert!(matches!(r, Ok(false)));
}
}
+131
View File
@@ -0,0 +1,131 @@
//! Port of `structs.zig`.
/// ContentScale is the ratio between the current DPI and the platform's
/// default DPI. This is used to determine how much certain rendered elements
/// need to be scaled up or down.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ContentScale {
pub x: f32,
pub y: f32,
}
/// The size of the surface in pixels.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SurfaceSize {
pub width: u32,
pub height: u32,
}
/// The position of the cursor in pixels.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CursorPos {
pub x: f32,
pub y: f32,
}
/// Input Method Editor (IME) position.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ImePos {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
/// The clipboard type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Clipboard {
/// ctrl+c/v
Standard = 0,
Selection = 1,
Primary = 2,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClipboardContent {
pub mime: String,
pub data: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClipboardRequestType {
Paste,
Osc52Read,
Osc52Write,
}
/// Clipboard request. This is used to request clipboard contents and must
/// be sent as a response to a ClipboardRequest event.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClipboardRequest {
/// A direct paste of clipboard contents.
Paste,
/// A request to read clipboard contents via OSC 52.
Osc52Read(Clipboard),
/// A request to write clipboard contents via OSC 52.
Osc52Write(Clipboard),
}
impl ClipboardRequest {
#[must_use]
pub fn request_type(&self) -> ClipboardRequestType {
match self {
Self::Paste => ClipboardRequestType::Paste,
Self::Osc52Read(_) => ClipboardRequestType::Osc52Read,
Self::Osc52Write(_) => ClipboardRequestType::Osc52Write,
}
}
}
/// The color scheme in use (light vs dark).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ColorScheme {
Light = 0,
Dark = 1,
}
/// Selection information
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Selection {
/// Top-left point of the selection in the viewport in scaled
/// window pixels. (0,0) is the top-left of the window.
pub tl_x_px: f64,
pub tl_y_px: f64,
/// The offset of the selection start in cells from the top-left
/// of the viewport.
///
/// This is a strange metric but its used by macOS.
pub offset_start: u32,
pub offset_len: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enum_values() {
assert_eq!(Clipboard::Standard as u8, 0);
assert_eq!(Clipboard::Selection as u8, 1);
assert_eq!(Clipboard::Primary as u8, 2);
assert_eq!(ColorScheme::Light as u8, 0);
assert_eq!(ColorScheme::Dark as u8, 1);
}
#[test]
fn clipboard_request_type() {
assert_eq!(
ClipboardRequest::Osc52Read(Clipboard::Primary).request_type(),
ClipboardRequestType::Osc52Read
);
assert_eq!(
ClipboardRequest::Paste.request_type(),
ClipboardRequestType::Paste
);
}
}
+304
View File
@@ -0,0 +1,304 @@
//! Port of `surface.zig`: messages sent from the core to a surface's apprt.
use crate::structs::Clipboard;
use ghostty_config::config::Config;
use ghostty_config::types_a::WorkingDirectory;
use ghostty_datastruct::MessageData;
use ghostty_datastruct::blocking_queue::Timeout;
use ghostty_vt::mouse_shape::MouseShape;
use ghostty_vt::osc::ProgressReport;
use ghostty_vt::osc::color::ColoredTarget;
use std::sync::Arc;
/// Represents a write request. Magic number comes from the max size
/// we want this union to be.
pub type WriteReq = MessageData<u8, 255>;
/// Maximum title size (including NUL sentinel space) of `set_title`.
pub const SET_TITLE_LEN: usize = 256;
/// `[63:0]u8` => 64 bytes including the sentinel.
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,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReportTitleStyle {
Csi21T,
// This enum is a placeholder for future title styles.
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChildExited {
pub exit_code: u32,
pub runtime_ms: u64,
}
/// Fixed-size desktop notification (title `[63:0]u8`, body `[255:0]u8`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DesktopNotification {
/// NUL-terminated.
pub title: [u8; NOTIFICATION_TITLE_LEN],
/// NUL-terminated.
pub body: [u8; NOTIFICATION_BODY_LEN],
}
fn cstr(buf: &[u8]) -> &[u8] {
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
&buf[..end]
}
fn fill<const N: usize>(src: &[u8]) -> [u8; N] {
let mut out = [0u8; N];
let n = src.len().min(N - 1);
out[..n].copy_from_slice(&src[..n]);
out
}
impl DesktopNotification {
/// Build from byte strings, truncating to fit (keeping the sentinel).
#[must_use]
pub fn new(title: &[u8], body: &[u8]) -> Self {
Self {
title: fill(title),
body: fill(body),
}
}
#[must_use]
pub fn title(&self) -> &[u8] {
cstr(&self.title)
}
#[must_use]
pub fn body(&self) -> &[u8] {
cstr(&self.body)
}
}
/// The message types that can be sent to a single surface.
#[derive(Debug, Clone)]
pub enum Message {
/// Set the title of the surface (NUL-terminated within 256 bytes).
/// TODO(upstream): should become a `WriteReq`.
SetTitle([u8; SET_TITLE_LEN]),
/// Report the window title back to the terminal
ReportTitle(ReportTitleStyle),
/// Set the mouse shape.
SetMouseShape(MouseShape),
/// Read the clipboard and write to the pty.
ClipboardRead(Clipboard),
/// Write the clipboard contents.
ClipboardWrite {
clipboard_type: Clipboard,
req: WriteReq,
},
/// Change the configuration to the given configuration. In Zig the
/// pointer is not valid after receiving; here it is shared via `Arc`.
ChangeConfig(Arc<Config>),
/// Close the surface. This will only close the current surface that
/// receives this, not the full application.
Close,
/// The child process running in the surface has exited. This may trigger
/// a surface close, it may not. Additional details about the child
/// command are given in the `ChildExited` struct.
ChildExited(ChildExited),
/// Show a desktop notification.
DesktopNotification(DesktopNotification),
/// Health status change for the renderer.
RendererHealth(RendererHealth),
/// Tell the surface to present itself to the user. This may require raising
/// a window and switching tabs.
PresentSurface,
/// Notifies the surface that password input has started within
/// the terminal. This should always be followed by a false value
/// unless the surface exits.
PasswordInput(bool),
/// A terminal color was changed using OSC sequences.
ColorChange(ColoredTarget),
/// Notifies the surface that a tick of the timer that is timing
/// out selection scrolling has occurred. "selection scrolling"
/// is when the user has clicked and dragged the mouse outside
/// the viewport of the terminal and the terminal is scrolling
/// the viewport to follow the mouse cursor.
SelectionScrollTick(bool),
/// The terminal has reported a change in the working directory.
PwdChange(WriteReq),
/// The terminal encountered a bell character.
RingBell,
/// Report the progress of an action using a GUI element
ProgressReport(ProgressReport),
/// A command has started in the shell, start a timer.
StartCommand,
/// A command has finished in the shell, stop the timer and send out
/// notifications as appropriate. The optional u8 is the exit code
/// of the command.
StopCommand(Option<u8>),
/// The scrollbar state changed for the surface.
Scrollbar(Scrollbar),
/// Search progress update
SearchTotal(Option<usize>),
/// Selected search index change
SearchSelected(Option<usize>),
}
/// The app-level mailbox a surface mailbox forwards into.
///
/// TODO(integration): in Zig this is `App.Mailbox` taking an
/// `App.Message::surface_message { surface, message }`. ghostty-core
/// implements this trait for its app mailbox (the `S` handle being the core
/// surface pointer/id).
pub trait AppMailbox<S> {
/// Push a surface-directed message to the app thread. Returns the queue
/// size after the push; zero means the push failed.
fn push_surface_message(&self, surface: S, message: Message, timeout: Timeout) -> usize;
}
/// A surface mailbox.
#[derive(Debug, Clone)]
pub struct Mailbox<S, A> {
pub surface: S,
pub app: A,
}
impl<S: Clone, A: AppMailbox<S>> Mailbox<S, A> {
/// Send a message to the surface. Surface message sending is actually
/// implemented on the app thread, so we rewrap the message with our
/// surface handle and send it to the app thread.
pub fn push(&self, msg: Message, timeout: Timeout) -> usize {
self.app
.push_surface_message(self.surface.clone(), msg, timeout)
}
}
/// Context for new surface creation to determine inheritance behavior
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum NewSurfaceContext {
Window = 0,
Tab = 1,
Split = 2,
}
#[must_use]
pub fn should_inherit_working_directory(context: NewSurfaceContext, config: &Config) -> bool {
match context {
NewSurfaceContext::Window => config.window_inherit_working_directory,
NewSurfaceContext::Tab => config.tab_inherit_working_directory,
NewSurfaceContext::Split => config.split_inherit_working_directory,
}
}
/// Returns a new config for a surface that should be used for any new
/// surfaces.
///
/// `prev_pwd` is the pwd of the previously focused surface (Zig:
/// `app.focusedSurface()?.pwd()`), which the caller (core) must look up.
// TODO(integration): core could wrap this to resolve the focused surface.
#[must_use]
pub fn new_config(config: &Config, context: NewSurfaceContext, prev_pwd: Option<String>) -> Config {
let mut copy = config.clone();
if let Some(pwd) = prev_pwd
&& should_inherit_working_directory(context, config)
{
copy.working_directory = Some(WorkingDirectory::Path(pwd));
}
copy
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct Rec(RefCell<Vec<(u32, bool)>>);
impl AppMailbox<u32> for Rec {
fn push_surface_message(&self, s: u32, m: Message, _t: Timeout) -> usize {
self.0.borrow_mut().push((s, matches!(m, Message::Close)));
self.0.borrow().len()
}
}
#[test]
fn mailbox_wraps_surface() {
let mb = Mailbox {
surface: 7u32,
app: Rec(RefCell::new(vec![])),
};
assert_eq!(mb.push(Message::Close, Timeout::Instant), 1);
assert_eq!(mb.app.0.borrow()[0], (7, true));
}
#[test]
fn notification_truncates() {
let n = DesktopNotification::new(&[b'a'; 100], b"hi");
assert_eq!(n.title().len(), 63);
assert_eq!(n.body(), b"hi");
}
#[test]
fn inherit_and_new_config() {
let c = Config {
tab_inherit_working_directory: false,
..Config::default()
};
assert!(should_inherit_working_directory(
NewSurfaceContext::Window,
&c
));
assert!(!should_inherit_working_directory(
NewSurfaceContext::Tab,
&c
));
let n = new_config(&c, NewSurfaceContext::Window, Some("/tmp".into()));
assert_eq!(
n.working_directory,
Some(WorkingDirectory::Path("/tmp".into()))
);
let n = new_config(&c, NewSurfaceContext::Tab, Some("/tmp".into()));
assert_eq!(n.working_directory, None);
}
}