macos: starting to work on new libghostty data models

This commit is contained in:
Mitchell Hashimoto
2025-06-19 07:07:32 -07:00
parent bbb69c8f27
commit 5259d0fa55
10 changed files with 171 additions and 46 deletions

View File

@@ -53,7 +53,6 @@
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */; };
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */; };
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; };
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */; };
A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */; };
A5593FE32DF8D78600B47B10 /* TerminalHiddenTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */; };
@@ -124,6 +123,9 @@
A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */; };
A5E408322E02FEDF0035FEAC /* TerminalEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */; };
A5E408342E0320140035FEAC /* GetTerminalDetailsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */; };
A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */; };
A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */; };
A5E4083C2E044DB50035FEAC /* Ghostty.Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */; };
A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; };
AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; };
C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; };
@@ -175,7 +177,6 @@
A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIcon.swift; sourceTree = "<group>"; };
A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconImage.swift; sourceTree = "<group>"; };
A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = "<group>"; };
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; };
A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenTitlebarTerminalWindow.swift; sourceTree = "<group>"; };
A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalHiddenTitlebar.xib; sourceTree = "<group>"; };
@@ -248,6 +249,9 @@
A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyIntentError.swift; sourceTree = "<group>"; };
A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalEntity.swift; sourceTree = "<group>"; };
A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTerminalDetailsIntent.swift; sourceTree = "<group>"; };
A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Surface.swift; sourceTree = "<group>"; };
A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Command.swift; sourceTree = "<group>"; };
A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Error.swift; sourceTree = "<group>"; };
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = "<group>"; };
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = "<group>"; };
@@ -440,12 +444,14 @@
A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */,
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */,
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */,
A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */,
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */,
A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */,
A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */,
A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */,
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */,
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */,
A55685DF29A03A9F004303CE /* AppError.swift */,
A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */,
A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */,
A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */,
);
@@ -766,6 +772,7 @@
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */,
A5593FE72DF927D200B47B10 /* TransparentTitlebarTerminalWindow.swift in Sources */,
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */,
A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */,
@@ -800,6 +807,7 @@
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */,
A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */,
A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */,
A5FEB3002ABB69450068369E /* main.swift in Sources */,
A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */,
A5E4082E2E0237460035FEAC /* NewTerminalIntent.swift in Sources */,
@@ -809,13 +817,13 @@
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */,
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */,
A5E4083C2E044DB50035FEAC /* Ghostty.Error.swift in Sources */,
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */,
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */,
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */,
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */,
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */,
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */,
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */,

View File

@@ -17,33 +17,19 @@ struct TerminalCommandPaletteView: View {
// The commands available to the command palette.
private var commandOptions: [CommandOption] {
guard let surface = surfaceView.surface else { return [] }
var ptr: UnsafeMutablePointer<ghostty_command_s>? = nil
var count: Int = 0
ghostty_surface_commands(surface, &ptr, &count)
guard let ptr else { return [] }
let buffer = UnsafeBufferPointer(start: ptr, count: count)
return Array(buffer).filter { c in
let key = String(cString: c.action_key)
switch (key) {
case "toggle_tab_overview",
"toggle_window_decorations",
"show_gtk_inspector":
return false
default:
return true
}
}.map { c in
let action = String(cString: c.action)
return CommandOption(
title: String(cString: c.title),
description: String(cString: c.description),
symbols: ghosttyConfig.keyboardShortcut(for: action)?.keyList
) {
onAction(action)
guard let surface = surfaceView.surfaceModel else { return [] }
do {
return try surface.commands().map { c in
return CommandOption(
title: c.title,
description: c.description,
symbols: ghosttyConfig.keyboardShortcut(for: c.action)?.keyList
) {
onAction(c.action)
}
}
} catch {
return []
}
}

View File

@@ -1,3 +0,0 @@
enum AppError: Error {
case surfaceCreateError
}

View File

@@ -0,0 +1,46 @@
import GhosttyKit
extension Ghostty {
/// `ghostty_command_s`
struct Command: Sendable {
private let cValue: ghostty_command_s
/// The title of the command.
var title: String {
String(cString: cValue.title)
}
/// Human-friendly description of what this command will do.
var description: String {
String(cString: cValue.description)
}
/// The full action that must be performed to invoke this command.
var action: String {
String(cString: cValue.action)
}
/// Only the key portion of the action so you can compare action types, e.g. `goto_split`
/// instead of `goto_split:left`.
var actionKey: String {
String(cString: cValue.action_key)
}
/// True if this can be performed on this target.
var isSupported: Bool {
!Self.unsupportedActionKeys.contains(actionKey)
}
/// Unsupported action keys, because they either don't make sense in the context of our
/// target platform or they just aren't implemented yet.
static let unsupportedActionKeys: [String] = [
"toggle_tab_overview",
"toggle_window_decorations",
"show_gtk_inspector",
]
init(cValue: ghostty_command_s) {
self.cValue = cValue
}
}
}

View File

@@ -0,0 +1,12 @@
extension Ghostty {
/// Possible errors from internal Ghostty calls.
enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
case apiFailed
var localizedStringResource: LocalizedStringResource {
switch self {
case .apiFailed: return "libghostty API call failed"
}
}
}
}

View File

@@ -0,0 +1,64 @@
import GhosttyKit
extension Ghostty {
/// Represents a single surface within Ghostty.
///
/// NOTE(mitchellh): This is a work-in-progress class as part of a general refactor
/// of our Ghostty data model. At the time of writing there's still a ton of surface
/// functionality that is not encapsulated in this class. It is planned to migrate that
/// all over.
///
/// Wraps a `ghostty_surface_t`
final class Surface: Sendable {
private let surface: ghostty_surface_t
/// Read the underlying C value for this surface. This is unsafe because the value will be
/// freed when the Surface class is deinitialized.
var unsafeCValue: ghostty_surface_t {
surface
}
/// Initialize from the C structure.
init(cSurface: ghostty_surface_t) {
self.surface = cSurface
}
deinit {
// deinit is not guaranteed to happen on the main actor and our API
// calls into libghostty must happen there so we capture the surface
// value so we don't capture `self` and then we detach it in a task.
// We can't wait for the task to succeed so this will happen sometime
// but that's okay.
let surface = self.surface
Task.detached { @MainActor in
ghostty_surface_free(surface)
}
}
/// Perform a keybinding action.
///
/// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4`
/// you can perform `goto_tab:4` with this.
///
/// Returns true if the action was performed. Invalid actions return false.
@MainActor
func perform(action: String) -> Bool {
let len = action.utf8CString.count
if (len == 0) { return false }
return action.withCString { cString in
ghostty_surface_binding_action(surface, cString, UInt(len - 1))
}
}
/// Command options for this surface.
@MainActor
func commands() throws -> [Command] {
var ptr: UnsafeMutablePointer<ghostty_command_s>? = nil
var count: Int = 0
ghostty_surface_commands(surface, &ptr, &count)
guard let ptr else { throw Error.apiFailed }
let buffer = UnsafeBufferPointer(start: ptr, count: count)
return Array(buffer).map { Command(cValue: $0) }.filter { $0.isSupported }
}
}
}

View File

@@ -19,6 +19,15 @@ struct Ghostty {
static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show"
}
// MARK: C Extensions
/// A command is fully self-contained so it is Sendable.
extension ghostty_command_s: @unchecked @retroactive Sendable {}
/// A surface is sendable because it is just a reference type. Using the surface in parameters
/// may be unsafe but the value itself is safe to send across threads.
extension ghostty_surface_t: @unchecked @retroactive Sendable {}
// MARK: Build Info
extension Ghostty {

View File

@@ -79,7 +79,7 @@ extension Ghostty {
let pubResign = center.publisher(for: NSWindow.didResignKeyNotification)
#endif
Surface(view: surfaceView, size: geo.size)
SurfaceRepresentable(view: surfaceView, size: geo.size)
.focused($surfaceFocus)
.focusedValue(\.ghosttySurfacePwd, surfaceView.pwd)
.focusedValue(\.ghosttySurfaceView, surfaceView)
@@ -381,7 +381,7 @@ extension Ghostty {
/// We just wrap an AppKit NSView here at the moment so that we can behave as low level as possible
/// since that is what the Metal renderer in Ghostty expects. In the future, it may make more sense to
/// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with.
struct Surface: OSViewRepresentable {
struct SurfaceRepresentable: OSViewRepresentable {
/// The view to render for the terminal surface.
let view: SurfaceView

View File

@@ -115,10 +115,20 @@ extension Ghostty {
}
}
/// Returns the data model for this surface.
///
/// Note: eventually, all surface access will be through this, but presently its in a transition
/// state so we're mixing this with direct surface access.
private(set) var surfaceModel: Ghostty.Surface?
/// Returns the underlying C value for the surface. See "note" on surfaceModel.
var surface: ghostty_surface_t? {
surfaceModel?.unsafeCValue
}
// Notification identifiers associated with this surface
var notificationIdentifiers: Set<String> = []
private(set) var surface: ghostty_surface_t?
private var markedText: NSMutableAttributedString
private(set) var focused: Bool = true
private var prevPressureStage: Int = 0
@@ -282,10 +292,10 @@ extension Ghostty {
let surface_cfg = baseConfig ?? SurfaceConfiguration()
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
guard let surface = ghostty_surface_new(app, &surface_cfg_c) else {
self.error = AppError.surfaceCreateError
self.error = Ghostty.Error.apiFailed
return
}
self.surface = surface;
self.surfaceModel = Ghostty.Surface(cSurface: surface)
// Setup our tracking area so we get mouse moved events
updateTrackingAreas()
@@ -340,11 +350,6 @@ extension Ghostty {
// Remove any notifications associated with this surface
let identifiers = Array(self.notificationIdentifiers)
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
// Free our core surface resources
if let surface = self.surface {
ghostty_surface_free(surface)
}
}
func focusDidChange(_ focused: Bool) {

View File

@@ -1837,12 +1837,10 @@ pub const CAPI = struct {
return false;
};
_ = ptr.core_surface.performBindingAction(action) catch |err| {
return ptr.core_surface.performBindingAction(action) catch |err| {
log.err("error performing binding action action={} err={}", .{ action, err });
return false;
};
return true;
}
/// Complete a clipboard read request started via the read callback.