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:
1 parent
6f970bfe26
commit
ac0e05def1
9 files changed
+1529
-1
No files matched your search
Generated
+25
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# ghostty-apprt notes
|
||||
|
||||
## Mapping (ZIG/src/apprt -> crates/ghostty-apprt/src)
|
||||
- action.zig -> action.rs: `Target<S>` (+`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<S, A>`, `AppMailbox<S>` 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<Config>` 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<S>::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<String>` 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<S>` for its app mailbox (S = core surface handle) and instantiates `Target<S>`.
|
||||
- Selection (structs) is the pixel-based apprt struct; terminal::Selection is not used by any payload here.
|
||||
Reference in new issue
Block a user