Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3412,6 +3412,10 @@ final class TerminalSurface: Identifiable, ObservableObject {
private var pendingSocketInputBytes: Int = 0
private let maxPendingSocketInputBytes = 1_048_576
private var backgroundSurfaceStartQueued = false
/// Offscreen window used to host the view hierarchy during background surface creation.
/// Ghostty requires a live NSWindow for Metal layer setup and backing context.
/// Released when the view moves to a real window or on teardown.
private var offscreenHostWindow: NSWindow?
private var surfaceCallbackContext: Unmanaged<GhosttySurfaceCallbackContext>?
/// The desired focus state for the Ghostty C surface. May be set before the
/// C surface exists (e.g. during layout restoration); `createSurface`
Expand Down Expand Up @@ -3844,6 +3848,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
func teardownSurface() {
recordTeardownRequest(reason: "surface.teardown")
markPortalLifecycleClosed(reason: "teardown")
offscreenHostWindow = nil

let callbackContext = surfaceCallbackContext
surfaceCallbackContext = nil
Expand Down Expand Up @@ -3960,6 +3965,10 @@ final class TerminalSurface: Identifiable, ObservableObject {
#if DEBUG
dlog("surface.attach.reuse surface=\(id.uuidString.prefix(5)) view=\(Unmanaged.passUnretained(view).toOpaque())")
#endif
// Release the offscreen host window once the view has moved to a real window.
if let offscreen = offscreenHostWindow, view.window !== offscreen {
offscreenHostWindow = nil
}
if let screen = view.window?.screen ?? NSScreen.main,
let displayID = screen.displayID,
displayID != 0,
Expand Down Expand Up @@ -4610,7 +4619,37 @@ final class TerminalSurface: Identifiable, ObservableObject {
}

guard allowsRuntimeSurfaceCreation() else { return }
guard surface == nil, attachedView != nil else { return }
guard surface == nil else { return }

// Background workspaces may not be in any NSWindow yet — ghostty requires
// a live window for Metal layer setup and backing context. Provide an offscreen
// host window so the standard viewDidMoveToWindow → attachToView → createSurface
// chain fires with a valid window.
if surfaceView.window == nil && offscreenHostWindow == nil {
let window = NSWindow(
contentRect: NSRect(x: -10000, y: -10000, width: 800, height: 600),
styleMask: [.borderless],
backing: .buffered,
defer: false
)
window.isReleasedWhenClosed = false
window.orderOut(nil)
offscreenHostWindow = window
if let contentView = window.contentView {
hostedView.frame = contentView.bounds
hostedView.autoresizingMask = [.width, .height]
contentView.addSubview(hostedView)
}
// viewDidMoveToWindow fires on surfaceView → attachToView → createSurface
if surface != nil {
#if DEBUG
dlog("surface.background_start.offscreen surface=\(id.uuidString.prefix(8)) ready=1")
#endif
return
}
}

guard attachedView != nil else { return }
guard !backgroundSurfaceStartQueued else { return }
backgroundSurfaceStartQueued = true

Expand Down