mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-20 22:35:20 +00:00
macOS: clean up duplicated declarations in SurfaceView_AppKit/UIKit (#12250)
Added a shared `OSSurfaceView` as the base class to share common variables and functions across platforms. Each commit contains a small change to move one or two variables or functions to `OSSurfaceView`.
This commit is contained in:
139
macos/Sources/Ghostty/Surface View/OSSurfaceView.swift
Normal file
139
macos/Sources/Ghostty/Surface View/OSSurfaceView.swift
Normal file
@@ -0,0 +1,139 @@
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Placeholders
|
||||
|
||||
func focusDidChange(_ focused: Bool) {}
|
||||
|
||||
func sizeDidChange(_ size: CGSize) {}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Search State
|
||||
|
||||
extension Ghostty.OSSurfaceView {
|
||||
class SearchState: ObservableObject {
|
||||
@Published var needle: String = ""
|
||||
@Published var selected: UInt?
|
||||
@Published var total: UInt?
|
||||
|
||||
init(from startSearch: Ghostty.Action.StartSearch) {
|
||||
self.needle = startSearch.needle ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
func navigateSearchToNext() -> 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
|
||||
}
|
||||
}
|
||||
@@ -1267,41 +1267,3 @@ extension FocusedValues {
|
||||
typealias Value = OSSize
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Search State
|
||||
|
||||
extension Ghostty.SurfaceView {
|
||||
class SearchState: ObservableObject {
|
||||
@Published var needle: String = ""
|
||||
@Published var selected: UInt?
|
||||
@Published var total: UInt?
|
||||
|
||||
init(from startSearch: Ghostty.Action.StartSearch) {
|
||||
self.needle = startSearch.needle ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
func navigateSearchToNext() -> 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,9 @@ import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
/// The NSView implementation for a terminal surface.
|
||||
class SurfaceView: OSView, ObservableObject, Codable, Identifiable {
|
||||
typealias ID = UUID
|
||||
|
||||
/// Unique ID per surface
|
||||
let id: UUID
|
||||
|
||||
class SurfaceView: OSSurfaceView, Codable, Identifiable {
|
||||
// The current title of the surface as defined by the pty. This can be
|
||||
// changed with escape codes. This is public because the callbacks go
|
||||
// to the app level and it is set from there.
|
||||
// changed with escape codes.
|
||||
@Published private(set) var title: String = "" {
|
||||
didSet {
|
||||
if !title.isEmpty {
|
||||
@@ -25,28 +19,8 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
// 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: NSSize = .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? {
|
||||
override var progressReport: Action.ProgressReport? {
|
||||
didSet {
|
||||
// Cancel any existing timer
|
||||
progressReportTimer?.invalidate()
|
||||
@@ -65,11 +39,8 @@ extension Ghostty {
|
||||
// The currently active key sequence. The sequence is not active if this is empty.
|
||||
@Published var keySequence: [KeyboardShortcut] = []
|
||||
|
||||
// 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? {
|
||||
override var searchState: SearchState? {
|
||||
didSet {
|
||||
if let searchState {
|
||||
// I'm not a Combine expert so if there is a better way to do this I'm
|
||||
@@ -105,14 +76,6 @@ extension Ghostty {
|
||||
// Cancellable for search state needle changes
|
||||
private var searchNeedleCancellable: AnyCancellable?
|
||||
|
||||
// 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?
|
||||
|
||||
// Whether the pointer should be visible or not
|
||||
@Published private(set) var pointerStyle: CursorStyle = .horizontalText
|
||||
|
||||
@@ -136,12 +99,6 @@ extension Ghostty {
|
||||
/// True when the bell is active. This is set inactive on focus or event.
|
||||
@Published private(set) var bell: Bool = false
|
||||
|
||||
/// 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
|
||||
|
||||
// An initial size to request for a window. This will only affect
|
||||
// then the view is moved to a new window.
|
||||
var initialSize: NSSize?
|
||||
@@ -207,7 +164,7 @@ extension Ghostty {
|
||||
private(set) var surfaceModel: Ghostty.Surface?
|
||||
|
||||
/// Returns the underlying C value for the surface. See "note" on surfaceModel.
|
||||
var surface: ghostty_surface_t? {
|
||||
override var surface: ghostty_surface_t? {
|
||||
surfaceModel?.unsafeCValue
|
||||
}
|
||||
/// Current scrollbar state, cached here for persistence across rebuilds
|
||||
@@ -255,7 +212,6 @@ extension Ghostty {
|
||||
|
||||
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
|
||||
self.markedText = NSMutableAttributedString()
|
||||
self.id = uuid ?? .init()
|
||||
|
||||
// Our initial config always is our application wide config.
|
||||
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
|
||||
@@ -273,7 +229,7 @@ extension Ghostty {
|
||||
// Initialize with some default frame size. The important thing is that this
|
||||
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||
// can do SOMETHING.
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: 800, height: 600))
|
||||
super.init(id: uuid, frame: NSRect(x: 0, y: 0, width: 800, height: 600))
|
||||
|
||||
// Our cache of screen data
|
||||
cachedScreenContents = .init(duration: .milliseconds(500)) { [weak self] in
|
||||
@@ -362,11 +318,6 @@ extension Ghostty {
|
||||
selector: #selector(ghosttyBellDidRing(_:)),
|
||||
name: .ghosttyBellDidRing,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyDidChangeReadonly(_:)),
|
||||
name: .ghosttyDidChangeReadonly,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(windowDidChangeScreen),
|
||||
@@ -438,7 +389,7 @@ extension Ghostty {
|
||||
progressReportTimer?.invalidate()
|
||||
}
|
||||
|
||||
func focusDidChange(_ focused: Bool) {
|
||||
override func focusDidChange(_ focused: Bool) {
|
||||
guard let surface = self.surface else { return }
|
||||
guard self.focused != focused else { return }
|
||||
self.focused = focused
|
||||
@@ -475,7 +426,7 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
func sizeDidChange(_ size: CGSize) {
|
||||
override func sizeDidChange(_ size: CGSize) {
|
||||
// Ghostty wants to know the actual framebuffer size... It is very important
|
||||
// here that we use "size" and NOT the view frame. If we're in the middle of
|
||||
// an animation (i.e. a fullscreen animation), the frame will not yet be updated.
|
||||
@@ -785,11 +736,6 @@ extension Ghostty {
|
||||
bell = true
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidChangeReadonly(_ notification: SwiftUI.Notification) {
|
||||
guard let value = notification.userInfo?[SwiftUI.Notification.Name.ReadonlyKey] as? Bool else { return }
|
||||
readonly = value
|
||||
}
|
||||
|
||||
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
|
||||
guard let window = self.window else { return }
|
||||
guard let object = notification.object as? NSWindow, window == object else { return }
|
||||
@@ -1627,14 +1573,6 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func splitRight(_ sender: Any) {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_RIGHT)
|
||||
|
||||
@@ -3,74 +3,26 @@ import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
/// The UIView implementation for a terminal surface.
|
||||
class SurfaceView: UIView, ObservableObject {
|
||||
typealias ID = UUID
|
||||
|
||||
/// Unique ID per surface
|
||||
let id: UUID
|
||||
|
||||
class SurfaceView: OSSurfaceView {
|
||||
// The current title of the surface as defined by the pty. This can be
|
||||
// changed with escape codes. This is public because the callbacks go
|
||||
// to the app level and it is set from there.
|
||||
@Published var title: String = "👻"
|
||||
|
||||
// The current pwd of the surface.
|
||||
@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: OSSize = .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
|
||||
@Published var hoverUrl: String?
|
||||
|
||||
// The progress report (if any)
|
||||
@Published var progressReport: Action.ProgressReport?
|
||||
|
||||
// The time this surface last became focused. This is a ContinuousClock.Instant
|
||||
// on supported platforms.
|
||||
@Published var focusInstant: ContinuousClock.Instant?
|
||||
// changed with escape codes.
|
||||
@Published private(set) var title: String = "👻"
|
||||
|
||||
/// True when the bell is active. This is set inactive on focus or event.
|
||||
@Published var bell: Bool = false
|
||||
|
||||
// The current search state. When non-nil, the search overlay should be shown.
|
||||
@Published var searchState: SearchState?
|
||||
private(set) var _surface: ghostty_surface_t?
|
||||
|
||||
// The currently active key tables. Empty if no tables are active.
|
||||
@Published var keyTables: [String] = []
|
||||
|
||||
/// 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
|
||||
|
||||
// Returns sizing information for the surface. This is the raw C
|
||||
// structure because I'm lazy.
|
||||
var surfaceSize: ghostty_surface_size_s? {
|
||||
guard let surface = self.surface else { return nil }
|
||||
return ghostty_surface_size(surface)
|
||||
override var surface: ghostty_surface_t? {
|
||||
_surface
|
||||
}
|
||||
|
||||
private(set) var surface: ghostty_surface_t?
|
||||
|
||||
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
|
||||
self.id = uuid ?? .init()
|
||||
|
||||
// Initialize with some default frame size. The important thing is that this
|
||||
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||
// can do SOMETHING.
|
||||
super.init(frame: CGRect(x: 0, y: 0, width: 800, height: 600))
|
||||
super.init(id: uuid, frame: CGRect(x: 0, y: 0, width: 800, height: 600))
|
||||
|
||||
// Setup our surface. This will also initialize all the terminal IO.
|
||||
let surface_cfg = baseConfig ?? SurfaceConfiguration()
|
||||
@@ -81,7 +33,7 @@ extension Ghostty {
|
||||
// TODO
|
||||
return
|
||||
}
|
||||
self.surface = surface
|
||||
self._surface = surface
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -93,7 +45,7 @@ extension Ghostty {
|
||||
ghostty_surface_free(surface)
|
||||
}
|
||||
|
||||
func focusDidChange(_ focused: Bool) {
|
||||
override func focusDidChange(_ focused: Bool) {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_set_focus(surface, focused)
|
||||
|
||||
@@ -103,7 +55,7 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
func sizeDidChange(_ size: CGSize) {
|
||||
override func sizeDidChange(_ size: CGSize) {
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
// Ghostty wants to know the actual framebuffer size... It is very important
|
||||
|
||||
Reference in New Issue
Block a user