import Foundation import GhosttyKit import SwiftUI extension Ghostty { class OSSurfaceView: OSView, ObservableObject { typealias ID = UUID /// Unique ID per surface let id: UUID // The current pwd of the surface as defined by the pty. This can be // changed with escape codes. @Published var pwd: String? // The cell size of this surface. This is set by the core when the // surface is first created and any time the cell size changes (i.e. // when the font size changes). This is used to allow windows to be // resized in discrete steps of a single cell. @Published var cellSize: CGSize = .zero // The health state of the surface. This currently only reflects the // renderer health. In the future we may want to make this an enum. @Published var healthy: Bool = true // Any error while initializing the surface. @Published var error: Error? // The hovered URL string @Published var hoverUrl: String? // The progress report (if any) @Published var progressReport: Action.ProgressReport? // The currently active key tables. Empty if no tables are active. @Published var keyTables: [String] = [] // The current search state. When non-nil, the search overlay should be shown. @Published var searchState: SearchState? // The time this surface last became focused. This is a ContinuousClock.Instant // on supported platforms. @Published var focusInstant: ContinuousClock.Instant? // Returns sizing information for the surface. This is the raw C // structure because I'm lazy. @Published var surfaceSize: ghostty_surface_size_s? /// True when the surface is in readonly mode. @Published private(set) var readonly: Bool = false /// True when the surface should show a highlight effect (e.g., when presented via goto_split). @Published private(set) var highlighted: Bool = false /// A message sent from `ghostty_surface_t` when a child process exited @Published private(set) var childExitedMessage: ChildExitedMessage? var surface: ghostty_surface_t? { nil } init(id: UUID?, frame: CGRect) { self.id = id ?? UUID() super.init(frame: frame) // Before we initialize the surface we want to register our notifications // so there is no window where we can't receive them. let center = NotificationCenter.default center.addObserver( self, selector: #selector(ghosttyDidChangeReadonly(_:)), name: .ghosttyDidChangeReadonly, object: self, ) } required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } deinit { NotificationCenter.default .removeObserver(self) } @objc private func ghosttyDidChangeReadonly(_ notification: Foundation.Notification) { guard let value = notification.userInfo?[Foundation.Notification.Name.ReadonlyKey] as? Bool else { return } readonly = value } /// Triggers a brief highlight animation on this surface. func highlight() { highlighted = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in self?.highlighted = false } } func setChildExitedMessage(_ message: ChildExitedMessage) { self.childExitedMessage = message } @MainActor func endSearch() { searchState = nil } // MARK: - Placeholders func focusDidChange(_ focused: Bool) {} func sizeDidChange(_ size: CGSize) {} } } // MARK: Search State extension Ghostty.OSSurfaceView { @MainActor class SearchState: ObservableObject { /// The pasteboard used to persist the search needle. /// /// The `.find` pasteboard lets us sync our needle across the system and other find bars. private let pasteboard: OSPasteboard @Published var needle: String = "" @Published var selected: UInt? @Published var total: UInt? /// The range of the needle's text selection in the find bar. @Published var needleSelection: Range? init( from startSearch: Ghostty.Action.StartSearch, pasteboard: OSPasteboard = OSPasteboard.find ) { self.pasteboard = pasteboard if let needle = startSearch.needle, !needle.isEmpty { self.needle = needle writePasteboardNeedle() } else { readPasteboardNeedle() } } func readPasteboardNeedle() { let pasteboardNeedle = pasteboard.string if let pasteboardNeedle, pasteboardNeedle != needle { needle = pasteboardNeedle needleSelection = needle.startIndex.. Bool { guard let surface = self.surface else { return false } let action = "navigate_search:next" if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { #if canImport(AppKit) AppDelegate.logger.warning("action failed action=\(action)") #endif return false } return true } func navigateSearchToPrevious() -> Bool { guard let surface = self.surface else { return false } let action = "navigate_search:previous" if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { #if canImport(AppKit) AppDelegate.logger.warning("action failed action=\(action)") #endif return false } return true } }