From ac0e05def1c42c5deecd6471d5d21604af091ef0 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sun, 11 Oct 2026 22:25:05 +0200 Subject: [PATCH] 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 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 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) --- Cargo.lock | 25 + crates/ghostty-apprt/Cargo.toml | 1 + crates/ghostty-apprt/src/action.rs | 883 ++++++++++++++++++++++++++++ crates/ghostty-apprt/src/ipc.rs | 81 +++ crates/ghostty-apprt/src/lib.rs | 18 +- crates/ghostty-apprt/src/runtime.rs | 61 ++ crates/ghostty-apprt/src/structs.rs | 131 +++++ crates/ghostty-apprt/src/surface.rs | 304 ++++++++++ docs/rewrite/notes/ghostty-apprt.md | 26 + 9 files changed, 1529 insertions(+), 1 deletion(-) create mode 100644 crates/ghostty-apprt/src/action.rs create mode 100644 crates/ghostty-apprt/src/ipc.rs create mode 100644 crates/ghostty-apprt/src/runtime.rs create mode 100644 crates/ghostty-apprt/src/structs.rs create mode 100644 crates/ghostty-apprt/src/surface.rs create mode 100644 docs/rewrite/notes/ghostty-apprt.md diff --git a/Cargo.lock b/Cargo.lock index 542ac96..0ba31cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,6 +321,20 @@ name = "bytemuck" version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "castaway" @@ -876,6 +890,17 @@ dependencies = [ "temp-dir", ] +[[package]] +name = "ghostty-apprt" +version = "0.1.0" +dependencies = [ + "ghostty-config", + "ghostty-datastruct", + "ghostty-input", + "ghostty-vt", + "thiserror 2.0.21", +] + [[package]] name = "ghostty-apprt-gtk" version = "0.1.0" diff --git a/crates/ghostty-apprt/Cargo.toml b/crates/ghostty-apprt/Cargo.toml index 995ab83..88c75c1 100644 --- a/crates/ghostty-apprt/Cargo.toml +++ b/crates/ghostty-apprt/Cargo.toml @@ -9,6 +9,7 @@ ghostty-vt.workspace = true ghostty-input.workspace = true ghostty-config.workspace = true ghostty-datastruct.workspace = true +thiserror = "2.0.21" [lints] workspace = true diff --git a/crates/ghostty-apprt/src/action.rs b/crates/ghostty-apprt/src/action.rs new file mode 100644 index 0000000..55d6e00 --- /dev/null +++ b/crates/ghostty-apprt/src/action.rs @@ -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 { + App, + Surface(S), +} + +/// Mirrors `Target.Key`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(i32)] +pub enum TargetKey { + App, + Surface, +} + +impl Target { + #[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, +} + +/// 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, + 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, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SearchSelected { + pub selected: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn target_key() { + assert_eq!(Target::::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); + } +} diff --git a/crates/ghostty-apprt/src/ipc.rs b/crates/ghostty-apprt/src/ipc.rs new file mode 100644 index 0000000..a58472a --- /dev/null +++ b/crates/ghostty-apprt/src/ipc.rs @@ -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>, +} + +/// 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()); + } +} diff --git a/crates/ghostty-apprt/src/lib.rs b/crates/ghostty-apprt/src/lib.rs index 1693053..6da24b2 100644 --- a/crates/ghostty-apprt/src/lib.rs +++ b/crates/ghostty-apprt/src/lib.rs @@ -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, +}; diff --git a/crates/ghostty-apprt/src/runtime.rs b/crates/ghostty-apprt/src/runtime.rs new file mode 100644 index 0000000..8e8ba89 --- /dev/null +++ b/crates/ghostty-apprt/src/runtime.rs @@ -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 { + 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))); + } +} diff --git a/crates/ghostty-apprt/src/structs.rs b/crates/ghostty-apprt/src/structs.rs new file mode 100644 index 0000000..4600277 --- /dev/null +++ b/crates/ghostty-apprt/src/structs.rs @@ -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 + ); + } +} diff --git a/crates/ghostty-apprt/src/surface.rs b/crates/ghostty-apprt/src/surface.rs new file mode 100644 index 0000000..b743518 --- /dev/null +++ b/crates/ghostty-apprt/src/surface.rs @@ -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; + +/// 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(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), + + /// 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), + + /// The scrollbar state changed for the surface. + Scrollbar(Scrollbar), + + /// Search progress update + SearchTotal(Option), + + /// Selected search index change + SearchSelected(Option), +} + +/// 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 { + /// 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 { + pub surface: S, + pub app: A, +} + +impl> Mailbox { + /// 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) -> 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>); + impl AppMailbox 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); + } +} diff --git a/docs/rewrite/notes/ghostty-apprt.md b/docs/rewrite/notes/ghostty-apprt.md new file mode 100644 index 0000000..30e5215 --- /dev/null +++ b/docs/rewrite/notes/ghostty-apprt.md @@ -0,0 +1,26 @@ +# ghostty-apprt notes + +## Mapping (ZIG/src/apprt -> crates/ghostty-apprt/src) +- action.zig -> action.rs: `Target` (+`TargetKey`), `Action` enum (all 65 variants, docs ported), `Key` (C-enum order, `Action::key()`), helper enums/structs (SplitDirection, GotoSplit, GotoWindow, GotoTab, ResizeSplit, MoveTab, Fullscreen, FloatWindow, SecureInput, Inspector, QuitTimer, Readonly, MouseVisibility, PromptTitle, SizeLimit, InitialSize, CellSize, SetTitle, Pwd, DesktopNotification, KeySequence, KeyTable, ColorChange/ColorKind, ReloadConfig, ConfigChange, OpenUrl/OpenUrlKind, CloseTabMode, CommandFinished, StartSearch, SearchTotal, SearchSelected, MouseOverLink). +- structs.zig -> structs.rs: ContentScale, SurfaceSize, CursorPos, ImePos (Zig `IMEPos`), Clipboard, ClipboardContent, ClipboardRequest(+Type), ColorScheme, Selection. +- surface.zig -> surface.rs: `Message`, `Mailbox`, `AppMailbox` trait, `NewSurfaceContext`, `should_inherit_working_directory`, `new_config`. +- ipc.zig -> ipc.rs: `Target` (Class/Detect), `Action::NewWindow(NewWindow{arguments})`, `ActionKey`, `Error::IpcFailed`. +- runtime.zig/none.zig -> runtime.rs: `Runtime{None,Gtk}`, `none_perform_ipc` (always false). +- Skipped: embedded.zig, browser.zig, gtk.zig (apprt-gtk), all C ABI (`C`, `CValue`, `cval`, `getGObjectType`). + +## Reused types +ghostty_input::binding::Trigger, ghostty_vt::mouse_shape::MouseShape, ghostty_vt::osc::{ProgressReport, color::ColoredTarget}, +ghostty_config::{config::Config, types_b::Duration, types_a::WorkingDirectory}, ghostty_datastruct::{MessageData, blocking_queue::Timeout}. + +## Deviations +- Strings are `String` (not NUL-terminated); `surface::Message::SetTitle` and `DesktopNotification` keep fixed arrays (256 / 64 / 256 bytes, NUL-terminated) with `title()`/`body()` accessors. +- `ConfigChange.config` and `Message::ChangeConfig` hold `Arc` instead of a borrowed pointer. +- `GotoTab`/`ColorKind` (non-exhaustive Zig enums) are Rust enums with an `Index(i32)`/`Palette(i32)` catch-all and `from_int`/`int`. +- `Mailbox::push` forwards through the `AppMailbox::push_surface_message(surface, msg, timeout)` trait (Zig wraps in `App.Message.surface_message`). +- `new_config` takes the previous focused surface's pwd as `Option` rather than an `App`. +- Tests of kind `ghostty.h ...` (checkGhosttyHEnum) skipped (C ABI); `refAllDecls` skipped. Replaced by 16 Rust tests (discriminants, key order, roundtrips, mailbox, config inheritance). + +## TODO(integration) +- `surface::Scrollbar` stands in for ghostty_terminal `page_list::Scrollbar`; `surface::RendererHealth` for ghostty_renderer `backend::Health`. Core should convert or (if dep order permits) re-export the real ones. +- ghostty-core implements `AppMailbox` for its app mailbox (S = core surface handle) and instantiates `Target`. +- Selection (structs) is the pixel-based apprt struct; terminal::Selection is not used by any payload here.