Fix ARC workspace inheritance crash and native Zig helper builds (#2283)

* Fix ARC workspace inheritance crash and native Zig helper builds

* Fix Nightly Cmd+N workspace creation crash

* Restore safe terminal config snapshots for Intel Nightly
This commit is contained in:
Austin Wang 2026-03-28 03:05:00 -07:00 committed by GitHub
parent f0c3ccc314
commit c4bc18d906
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 265 additions and 213 deletions

View file

@ -2973,7 +2973,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
return val > 0 ? val : 10
}()
private let surfaceContext: ghostty_surface_context_e
private let configTemplate: ghostty_surface_config_s?
private let configTemplate: CmuxSurfaceConfigTemplate?
private let workingDirectory: String?
private let initialCommand: String?
private let initialEnvironmentOverrides: [String: String]
@ -3061,7 +3061,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
init(
tabId: UUID,
context: ghostty_surface_context_e,
configTemplate: ghostty_surface_config_s?,
configTemplate: CmuxSurfaceConfigTemplate?,
workingDirectory: String? = nil,
initialCommand: String? = nil,
initialEnvironmentOverrides: [String: String] = [:],
@ -3585,7 +3585,10 @@ final class TerminalSurface: Identifiable, ObservableObject {
let scaleFactors = scaleFactors(for: view)
var surfaceConfig = configTemplate ?? ghostty_surface_config_new()
let baseConfig = configTemplate ?? CmuxSurfaceConfigTemplate()
var surfaceConfig = ghostty_surface_config_new()
surfaceConfig.font_size = baseConfig.fontSize
surfaceConfig.wait_after_command = baseConfig.waitAfterCommand
surfaceConfig.platform_tag = GHOSTTY_PLATFORM_MACOS
surfaceConfig.platform = ghostty_platform_u(macos: ghostty_platform_macos_s(
nsview: Unmanaged.passUnretained(view).toOpaque()
@ -3612,19 +3615,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
}
}
var env: [String: String] = [:]
if surfaceConfig.env_var_count > 0, let existingEnv = surfaceConfig.env_vars {
let count = Int(surfaceConfig.env_var_count)
if count > 0 {
for i in 0..<count {
let item = existingEnv[i]
if let key = String(cString: item.key, encoding: .utf8),
let value = String(cString: item.value, encoding: .utf8) {
env[key] = value
}
}
}
}
var env = baseConfig.environmentVariables
var protectedStartupEnvironmentKeys: Set<String> = []
func setManagedEnvironmentValue(_ key: String, _ value: String) {
@ -3762,26 +3753,36 @@ final class TerminalSurface: Identifiable, ObservableObject {
}
}
let createWithCommandAndWorkingDirectory = { [self] in
let resolvedWorkingDirectory: String? = {
if let workingDirectory, !workingDirectory.isEmpty {
return workingDirectory
}
return baseConfig.workingDirectory
}()
let resolvedCommand: String? = {
if let initialCommand, !initialCommand.isEmpty {
initialCommand.withCString { cCommand in
surfaceConfig.command = cCommand
if let workingDirectory, !workingDirectory.isEmpty {
workingDirectory.withCString { cWorkingDir in
surfaceConfig.working_directory = cWorkingDir
createSurface()
}
} else {
return initialCommand
}
return baseConfig.command
}()
let resolvedInitialInput = baseConfig.initialInput
func withOptionalCString<T>(_ value: String?, _ body: (UnsafePointer<CChar>?) -> T) -> T {
guard let value else {
return body(nil)
}
return value.withCString(body)
}
let createWithCommandAndWorkingDirectory = {
withOptionalCString(resolvedCommand) { cCommand in
surfaceConfig.command = cCommand
withOptionalCString(resolvedWorkingDirectory) { cWorkingDir in
surfaceConfig.working_directory = cWorkingDir
withOptionalCString(resolvedInitialInput) { cInitialInput in
surfaceConfig.initial_input = cInitialInput
createSurface()
}
}
} else if let workingDirectory, !workingDirectory.isEmpty {
workingDirectory.withCString { cWorkingDir in
surfaceConfig.working_directory = cWorkingDir
createSurface()
}
} else {
createSurface()
}
}
@ -3844,7 +3845,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
// config/scale reconciliation. If runtime points don't match the inherited
// template points, re-apply via binding action so all creation paths
// (new surface, split, new workspace) preserve zoom from the source terminal.
if let inheritedFontPoints = configTemplate?.font_size,
if let inheritedFontPoints = configTemplate?.fontSize,
inheritedFontPoints > 0 {
let currentFontPoints = cmuxCurrentSurfaceFontSizePoints(createdSurface)
let shouldReapply = {

View file

@ -90,7 +90,7 @@ final class TerminalPanel: Panel, ObservableObject {
convenience init(
workspaceId: UUID,
context: ghostty_surface_context_e = GHOSTTY_SURFACE_CONTEXT_SPLIT,
configTemplate: ghostty_surface_config_s? = nil,
configTemplate: CmuxSurfaceConfigTemplate? = nil,
workingDirectory: String? = nil,
portOrdinal: Int = 0,
initialCommand: String? = nil,

View file

@ -1155,7 +1155,7 @@ class TabManager: ObservableObject {
title: String,
workingDirectory: String?,
portOrdinal: Int,
configTemplate: ghostty_surface_config_s?,
configTemplate: CmuxSurfaceConfigTemplate?,
initialTerminalCommand: String?,
initialTerminalEnvironment: [String: String]
) -> Workspace {
@ -1208,105 +1208,106 @@ class TabManager: ObservableObject {
placementOverride: NewWorkspacePlacement? = nil,
autoWelcomeIfNeeded: Bool = true
) -> Workspace {
// Extract Workspace-dependent data through `self` BEFORE capturing locals.
// Accessing through `self` is safe because the method retains `self` for its
// duration, keeping all Workspace objects reachable via `self.tabs`. Local copies
// of Workspace references (like a `selectedWorkspace` local) are vulnerable to
// Xcode 16.x's aggressive ARC optimizer eliding retains through inlined call
// chains (workspace panel surface C pointer), causing use-after-free.
let preferredDir = preferredWorkingDirectoryForNewTab()
let inheritedFontPoints = inheritedTerminalFontPointsForNewWorkspace()
let sourceWorkspace = selectedWorkspace
let capturedTabs = tabs
let capturedSelectedTabId = selectedTabId
let snapshot = workspaceCreationSnapshotLite(
currentTabs: capturedTabs,
currentSelectedTabId: capturedSelectedTabId,
preferredWorkingDirectory: preferredDir,
inheritedTerminalFontPoints: inheritedFontPoints
)
didCaptureWorkspaceCreationSnapshot()
#if DEBUG
maybeMutateSelectionDuringWorkspaceCreationForDev(snapshot: snapshot)
#endif
let nextTabCount = snapshot.tabs.count + 1
sentryBreadcrumb("workspace.create", data: ["tabCount": nextTabCount])
let explicitWorkingDirectory = normalizedWorkingDirectory(overrideWorkingDirectory)
let workingDirectory = explicitWorkingDirectory ?? snapshot.preferredWorkingDirectory
let inheritedConfig = workspaceCreationConfigTemplate(
inheritedTerminalFontPoints: snapshot.inheritedTerminalFontPoints
)
// Resolve placement against the pre-creation snapshot before Workspace init
// boots terminal state. The ssh/new-workspace path can otherwise crash while
// reading @Published placement state from existing workspaces mid-creation.
let insertIndex = newTabInsertIndex(snapshot: snapshot, placementOverride: placementOverride)
let ordinal = Self.nextPortOrdinal
Self.nextPortOrdinal += 1
let newWorkspace = makeWorkspaceForCreation(
title: title ?? "Terminal \(nextTabCount)",
workingDirectory: workingDirectory,
portOrdinal: ordinal,
configTemplate: inheritedConfig,
initialTerminalCommand: initialTerminalCommand,
initialTerminalEnvironment: initialTerminalEnvironment
)
newWorkspace.owningTabManager = self
if title != nil {
newWorkspace.setCustomTitle(title)
}
wireClosedBrowserTracking(for: newWorkspace)
if eagerLoadTerminal && !select {
requestBackgroundWorkspaceLoad(for: newWorkspace.id)
}
// Apply insertion to the current live array so post-snapshot closes/reorders
// are preserved instead of reintroducing stale workspace instances.
var updatedTabs = tabs
if insertIndex >= 0 && insertIndex <= updatedTabs.count {
updatedTabs.insert(newWorkspace, at: insertIndex)
} else {
updatedTabs.append(newWorkspace)
}
tabs = updatedTabs
if let explicitWorkingDirectory,
let terminalPanel = newWorkspace.focusedTerminalPanel {
scheduleInitialWorkspaceGitMetadataRefresh(
workspaceId: newWorkspace.id,
panelId: terminalPanel.id,
directory: explicitWorkingDirectory
// Snapshot the selected tab from the pinned workspace instead of rereading the
// @Published selectedTabId storage after the inheritance helpers. The arm64 Nightly
// Cmd+N crash is in PublishedSubject.value.getter on that second getter read.
let capturedSelectedTabId = sourceWorkspace?.id
// Keep both the source workspace and the pre-creation workspace array alive for the
// entire creation path. Release ARC can otherwise drop retains early across the
// helper/insertion chain, which reintroduces use-after-free crashes in optimized builds.
return withExtendedLifetime((capturedTabs, sourceWorkspace)) {
let dir = preferredWorkingDirectoryForNewTab(workspace: sourceWorkspace)
let font = inheritedTerminalFontPointsForNewWorkspace(workspace: sourceWorkspace)
let snapshot = workspaceCreationSnapshotLite(
currentTabs: capturedTabs,
currentSelectedTabId: capturedSelectedTabId,
preferredWorkingDirectory: dir,
inheritedTerminalFontPoints: font
)
}
if eagerLoadTerminal {
if select {
newWorkspace.focusedTerminalPanel?.surface.requestBackgroundSurfaceStartIfNeeded()
didCaptureWorkspaceCreationSnapshot()
#if DEBUG
maybeMutateSelectionDuringWorkspaceCreationForDev(snapshot: snapshot)
#endif
let nextTabCount = snapshot.tabs.count + 1
sentryBreadcrumb("workspace.create", data: ["tabCount": nextTabCount])
let explicitWorkingDirectory = normalizedWorkingDirectory(overrideWorkingDirectory)
let workingDirectory = explicitWorkingDirectory ?? snapshot.preferredWorkingDirectory
let inheritedConfig = workspaceCreationConfigTemplate(
inheritedTerminalFontPoints: snapshot.inheritedTerminalFontPoints
)
// Resolve placement against the pre-creation snapshot before Workspace init
// boots terminal state. The ssh/new-workspace path can otherwise crash while
// reading @Published placement state from existing workspaces mid-creation.
let insertIndex = newTabInsertIndex(snapshot: snapshot, placementOverride: placementOverride)
let ordinal = Self.nextPortOrdinal
Self.nextPortOrdinal += 1
let newWorkspace = makeWorkspaceForCreation(
title: title ?? "Terminal \(nextTabCount)",
workingDirectory: workingDirectory,
portOrdinal: ordinal,
configTemplate: inheritedConfig,
initialTerminalCommand: initialTerminalCommand,
initialTerminalEnvironment: initialTerminalEnvironment
)
newWorkspace.owningTabManager = self
if title != nil {
newWorkspace.setCustomTitle(title)
}
}
if select {
#if DEBUG
debugPrimeWorkspaceSwitchTrigger("create", to: newWorkspace.id)
#endif
selectedTabId = newWorkspace.id
NotificationCenter.default.post(
name: .ghosttyDidFocusTab,
object: nil,
userInfo: [GhosttyNotificationKey.tabId: newWorkspace.id]
)
}
#if DEBUG
UITestRecorder.incrementInt("addTabInvocations")
UITestRecorder.record([
"tabCount": String(updatedTabs.count),
"selectedTabId": select ? newWorkspace.id.uuidString : (snapshot.selectedTabId?.uuidString ?? "")
])
#endif
if autoWelcomeIfNeeded && select && !UserDefaults.standard.bool(forKey: WelcomeSettings.shownKey) {
if let appDelegate = AppDelegate.shared {
appDelegate.sendWelcomeCommandWhenReady(to: newWorkspace, markShownOnSend: true)
wireClosedBrowserTracking(for: newWorkspace)
if eagerLoadTerminal && !select {
requestBackgroundWorkspaceLoad(for: newWorkspace.id)
}
// Apply insertion to the current live array so post-snapshot closes/reorders
// are preserved instead of reintroducing stale workspace instances.
var updatedTabs = tabs
if insertIndex >= 0 && insertIndex <= updatedTabs.count {
updatedTabs.insert(newWorkspace, at: insertIndex)
} else {
sendWelcomeWhenReady(to: newWorkspace)
updatedTabs.append(newWorkspace)
}
tabs = updatedTabs
if let explicitWorkingDirectory,
let terminalPanel = newWorkspace.focusedTerminalPanel {
scheduleInitialWorkspaceGitMetadataRefresh(
workspaceId: newWorkspace.id,
panelId: terminalPanel.id,
directory: explicitWorkingDirectory
)
}
if eagerLoadTerminal {
if select {
newWorkspace.focusedTerminalPanel?.surface.requestBackgroundSurfaceStartIfNeeded()
}
}
if select {
#if DEBUG
debugPrimeWorkspaceSwitchTrigger("create", to: newWorkspace.id)
#endif
selectedTabId = newWorkspace.id
NotificationCenter.default.post(
name: .ghosttyDidFocusTab,
object: nil,
userInfo: [GhosttyNotificationKey.tabId: newWorkspace.id]
)
}
#if DEBUG
UITestRecorder.incrementInt("addTabInvocations")
UITestRecorder.record([
"tabCount": String(updatedTabs.count),
"selectedTabId": select ? newWorkspace.id.uuidString : (snapshot.selectedTabId?.uuidString ?? "")
])
#endif
if autoWelcomeIfNeeded && select && !UserDefaults.standard.bool(forKey: WelcomeSettings.shownKey) {
if let appDelegate = AppDelegate.shared {
appDelegate.sendWelcomeCommandWhenReady(to: newWorkspace, markShownOnSend: true)
} else {
sendWelcomeWhenReady(to: newWorkspace)
}
}
return newWorkspace
}
return newWorkspace
}
@MainActor
@ -2206,7 +2207,17 @@ class TabManager: ObservableObject {
preferredWorkingDirectory: String?,
inheritedTerminalFontPoints: Float?
) -> WorkspaceCreationSnapshot {
let tabSnapshots = currentTabs.map { WorkspaceCreationTabSnapshot(workspace: $0) }
var tabSnapshots: [WorkspaceCreationTabSnapshot] = []
tabSnapshots.reserveCapacity(currentTabs.count)
for workspace in currentTabs {
// Keep each Workspace alive while copying the tiny value snapshot out of it.
// The optimized arm64 Nightly build can otherwise over-release during
// Collection.map, crashing here in swift_release / snapshot creation.
let snapshot = withExtendedLifetime(workspace) {
WorkspaceCreationTabSnapshot(workspace: workspace)
}
tabSnapshots.append(snapshot)
}
let selectedTabSnapshot = currentSelectedTabId.flatMap { selectedTabId in
tabSnapshots.first(where: { $0.id == selectedTabId })
}
@ -2281,26 +2292,36 @@ class TabManager: ObservableObject {
return candidates.first
}
private func inheritedTerminalConfigForNewWorkspace() -> ghostty_surface_config_s? {
private func inheritedTerminalConfigForNewWorkspace() -> CmuxSurfaceConfigTemplate? {
inheritedTerminalConfigForNewWorkspace(workspace: selectedWorkspace)
}
private func cachedInheritedTerminalFontPointsForNewWorkspace(
workspace: Workspace?
) -> Float? {
guard let workspace else { return nil }
// New workspace creation only seeds font size into a fresh Swift-owned template.
// Avoid reading live panel/surface state here; the arm64 Nightly Cmd+N crash path
// was repeatedly dereferencing pointer-backed terminal objects while preparing the
// new workspace. The workspace already caches the rooted font lineage we need.
return withExtendedLifetime(workspace) {
guard let fontPoints = workspace.lastRememberedTerminalFontPointsForConfigInheritance(),
fontPoints > 0 else {
return nil
}
return fontPoints
}
}
func inheritedTerminalConfigForNewWorkspace(
workspace: Workspace?
) -> ghostty_surface_config_s? {
if let panel = terminalPanelForWorkspaceConfigInheritanceSource(workspace: workspace),
let sourceSurface = panel.surface.surface {
return cmuxInheritedSurfaceConfig(
sourceSurface: sourceSurface,
context: GHOSTTY_SURFACE_CONTEXT_TAB
)
) -> CmuxSurfaceConfigTemplate? {
guard let fontPoints = cachedInheritedTerminalFontPointsForNewWorkspace(workspace: workspace) else {
return nil
}
if let fallbackFontPoints = workspace?.lastRememberedTerminalFontPointsForConfigInheritance() {
var config = ghostty_surface_config_new()
config.font_size = fallbackFontPoints
return config
}
return nil
var config = CmuxSurfaceConfigTemplate()
config.fontSize = fontPoints
return config
}
private func inheritedTerminalFontPointsForNewWorkspace() -> Float? {
@ -2310,24 +2331,19 @@ class TabManager: ObservableObject {
private func inheritedTerminalFontPointsForNewWorkspace(
workspace: Workspace?
) -> Float? {
guard let inheritedConfig = inheritedTerminalConfigForNewWorkspace(workspace: workspace),
inheritedConfig.font_size > 0 else {
return nil
}
return inheritedConfig.font_size
cachedInheritedTerminalFontPointsForNewWorkspace(workspace: workspace)
}
private func workspaceCreationConfigTemplate(
inheritedTerminalFontPoints: Float?
) -> ghostty_surface_config_s? {
) -> CmuxSurfaceConfigTemplate? {
guard let inheritedTerminalFontPoints, inheritedTerminalFontPoints > 0 else {
return nil
}
// ghostty_surface_config_s can carry raw C pointers owned by the source surface.
// New workspace creation only needs the inherited zoom level, so rebuild a clean
// config instead of snapshotting pointer-backed fields across workspace creation.
var config = ghostty_surface_config_new()
config.font_size = inheritedTerminalFontPoints
// Rebuild a clean Swift-owned template instead of carrying over any pointer-backed
// inherited config state from the source workspace.
var config = CmuxSurfaceConfigTemplate()
config.fontSize = inheritedTerminalFontPoints
return config
}

View file

@ -8,6 +8,40 @@ import Darwin
import Network
import CoreText
struct CmuxSurfaceConfigTemplate {
var fontSize: Float32 = 0
var workingDirectory: String?
var command: String?
var environmentVariables: [String: String] = [:]
var initialInput: String?
var waitAfterCommand: Bool = false
init() {}
init(cConfig: ghostty_surface_config_s) {
fontSize = cConfig.font_size
if let workingDirectory = cConfig.working_directory {
self.workingDirectory = String(cString: workingDirectory, encoding: .utf8)
}
if let command = cConfig.command {
self.command = String(cString: command, encoding: .utf8)
}
if let initialInput = cConfig.initial_input {
self.initialInput = String(cString: initialInput, encoding: .utf8)
}
if cConfig.env_var_count > 0, let envVars = cConfig.env_vars {
for index in 0..<Int(cConfig.env_var_count) {
let envVar = envVars[index]
if let key = String(cString: envVar.key, encoding: .utf8),
let value = String(cString: envVar.value, encoding: .utf8) {
environmentVariables[key] = value
}
}
}
waitAfterCommand = cConfig.wait_after_command
}
}
func cmuxSurfaceContextName(_ context: ghostty_surface_context_e) -> String {
switch context {
case GHOSTTY_SURFACE_CONTEXT_WINDOW:
@ -54,21 +88,21 @@ func cmuxCurrentSurfaceFontSizePoints(_ surface: ghostty_surface_t) -> Float? {
func cmuxInheritedSurfaceConfig(
sourceSurface: ghostty_surface_t,
context: ghostty_surface_context_e
) -> ghostty_surface_config_s {
) -> CmuxSurfaceConfigTemplate {
let inherited = ghostty_surface_inherited_config(sourceSurface, context)
var config = inherited
var config = CmuxSurfaceConfigTemplate(cConfig: inherited)
// Make runtime zoom inheritance explicit, even when Ghostty's
// inherit-font-size config is disabled.
let runtimePoints = cmuxCurrentSurfaceFontSizePoints(sourceSurface)
if let points = runtimePoints {
config.font_size = points
config.fontSize = points
}
#if DEBUG
let inheritedText = String(format: "%.2f", inherited.font_size)
let runtimeText = runtimePoints.map { String(format: "%.2f", $0) } ?? "nil"
let finalText = String(format: "%.2f", config.font_size)
let finalText = String(format: "%.2f", config.fontSize)
dlog(
"zoom.inherit context=\(cmuxSurfaceContextName(context)) " +
"inherited=\(inheritedText) runtime=\(runtimeText) final=\(finalText)"
@ -5664,7 +5698,7 @@ final class Workspace: Identifiable, ObservableObject {
title: String = "Terminal",
workingDirectory: String? = nil,
portOrdinal: Int = 0,
configTemplate: ghostty_surface_config_s? = nil,
configTemplate: CmuxSurfaceConfigTemplate? = nil,
initialTerminalCommand: String? = nil,
initialTerminalEnvironment: [String: String] = [:]
) {
@ -7221,9 +7255,9 @@ final class Workspace: Identifiable, ObservableObject {
private func seedTerminalInheritanceFontPoints(
panelId: UUID,
configTemplate: ghostty_surface_config_s?
configTemplate: CmuxSurfaceConfigTemplate?
) {
guard let fontPoints = configTemplate?.font_size, fontPoints > 0 else { return }
guard let fontPoints = configTemplate?.fontSize, fontPoints > 0 else { return }
terminalInheritanceFontPointsByPanelId[panelId] = fontPoints
lastTerminalConfigInheritanceFontPoints = fontPoints
}
@ -7231,7 +7265,7 @@ final class Workspace: Identifiable, ObservableObject {
private func resolvedTerminalInheritanceFontPoints(
for terminalPanel: TerminalPanel,
sourceSurface: ghostty_surface_t,
inheritedConfig: ghostty_surface_config_s
inheritedConfig: CmuxSurfaceConfigTemplate
) -> Float? {
let runtimePoints = cmuxCurrentSurfaceFontSizePoints(sourceSurface)
if let rooted = terminalInheritanceFontPointsByPanelId[terminalPanel.id], rooted > 0 {
@ -7242,8 +7276,8 @@ final class Workspace: Identifiable, ObservableObject {
}
return rooted
}
if inheritedConfig.font_size > 0 {
return inheritedConfig.font_size
if inheritedConfig.fontSize > 0 {
return inheritedConfig.fontSize
}
return runtimePoints
}
@ -7341,14 +7375,20 @@ final class Workspace: Identifiable, ObservableObject {
private func inheritedTerminalConfig(
preferredPanelId: UUID? = nil,
inPane preferredPaneId: PaneID? = nil
) -> ghostty_surface_config_s? {
) -> CmuxSurfaceConfigTemplate? {
// Walk candidates in priority order and use the first panel that still exposes
// a runtime surface pointer.
for terminalPanel in terminalPanelConfigInheritanceCandidates(
preferredPanelId: preferredPanelId,
inPane: preferredPaneId
) {
guard let sourceSurface = terminalPanel.surface.surface else { continue }
// Pin the panel and its TerminalSurface wrapper for the duration of
// this iteration. The raw ghostty_surface_t extracted below is owned
// by `surface` (the TerminalSurface) ARC must not release it while
// ghostty_surface_inherited_config or cmuxCurrentSurfaceFontSizePoints
// is still reading through the pointer.
let surface = terminalPanel.surface
guard let sourceSurface = surface.surface else { continue }
var config = cmuxInheritedSurfaceConfig(
sourceSurface: sourceSurface,
context: GHOSTTY_SURFACE_CONTEXT_SPLIT
@ -7358,19 +7398,21 @@ final class Workspace: Identifiable, ObservableObject {
sourceSurface: sourceSurface,
inheritedConfig: config
), rootedFontPoints > 0 {
config.font_size = rootedFontPoints
config.fontSize = rootedFontPoints
terminalInheritanceFontPointsByPanelId[terminalPanel.id] = rootedFontPoints
}
// Prevent ARC from releasing panel/surface before the C calls above complete.
withExtendedLifetime((terminalPanel, surface)) {}
rememberTerminalConfigInheritanceSource(terminalPanel)
if config.font_size > 0 {
lastTerminalConfigInheritanceFontPoints = config.font_size
if config.fontSize > 0 {
lastTerminalConfigInheritanceFontPoints = config.fontSize
}
return config
}
if let fallbackFontPoints = lastTerminalConfigInheritanceFontPoints {
var config = ghostty_surface_config_new()
config.font_size = fallbackFontPoints
var config = CmuxSurfaceConfigTemplate()
config.fontSize = fallbackFontPoints
#if DEBUG
dlog(
"zoom.inherit fallback=lastKnownFont context=split font=\(String(format: "%.2f", fallbackFontPoints))"

View file

@ -306,7 +306,7 @@ final class WorkspaceCreationPlacementTests: XCTestCase {
title: String,
workingDirectory: String?,
portOrdinal: Int,
configTemplate: ghostty_surface_config_s?,
configTemplate: CmuxSurfaceConfigTemplate?,
initialTerminalCommand: String?,
initialTerminalEnvironment: [String: String]
) -> Workspace {
@ -543,47 +543,21 @@ final class WorkspaceCreationPlacementTests: XCTestCase {
@MainActor
final class WorkspaceCreationConfigSanitizationTests: XCTestCase {
private final class UnsafeConfigSnapshotTabManager: TabManager {
private var retainedCStringPointers: [UnsafeMutablePointer<CChar>] = []
private var retainedEnvVars: UnsafeMutablePointer<ghostty_env_var_s>?
private var injectedConfig: ghostty_surface_config_s?
var capturedConfigTemplate: ghostty_surface_config_s?
deinit {
retainedEnvVars?.deinitialize(count: 1)
retainedEnvVars?.deallocate()
for pointer in retainedCStringPointers {
free(pointer)
}
}
private var injectedConfig: CmuxSurfaceConfigTemplate?
var capturedConfigTemplate: CmuxSurfaceConfigTemplate?
func installInjectedConfig(fontSize: Float) {
let workingDirectory = strdup("/tmp/cmux-workspace-snapshot")
let command = strdup("echo snapshot")
let envKey = strdup("CMUX_INHERITED_ENV")
let envValue = strdup("1")
let envVars = UnsafeMutablePointer<ghostty_env_var_s>.allocate(capacity: 1)
envVars.initialize(
to: ghostty_env_var_s(
key: UnsafePointer(envKey),
value: UnsafePointer(envValue)
)
)
retainedCStringPointers = [workingDirectory, command, envKey, envValue].compactMap { $0 }
retainedEnvVars = envVars
var config = ghostty_surface_config_new()
config.font_size = fontSize
config.working_directory = UnsafePointer(workingDirectory)
config.command = UnsafePointer(command)
config.env_vars = envVars
config.env_var_count = 1
var config = CmuxSurfaceConfigTemplate()
config.fontSize = fontSize
config.workingDirectory = "/tmp/cmux-workspace-snapshot"
config.command = "echo snapshot"
config.environmentVariables = ["CMUX_INHERITED_ENV": "1"]
injectedConfig = config
}
override func inheritedTerminalConfigForNewWorkspace(
workspace: Workspace?
) -> ghostty_surface_config_s? {
) -> CmuxSurfaceConfigTemplate? {
injectedConfig ?? super.inheritedTerminalConfigForNewWorkspace(workspace: workspace)
}
@ -591,7 +565,7 @@ final class WorkspaceCreationConfigSanitizationTests: XCTestCase {
title: String,
workingDirectory: String?,
portOrdinal: Int,
configTemplate: ghostty_surface_config_s?,
configTemplate: CmuxSurfaceConfigTemplate?,
initialTerminalCommand: String?,
initialTerminalEnvironment: [String: String]
) -> Workspace {
@ -618,11 +592,10 @@ final class WorkspaceCreationConfigSanitizationTests: XCTestCase {
return
}
XCTAssertEqual(capturedConfig.font_size, 19, accuracy: 0.001)
XCTAssertNil(capturedConfig.working_directory)
XCTAssertEqual(capturedConfig.fontSize, 19, accuracy: 0.001)
XCTAssertNil(capturedConfig.workingDirectory)
XCTAssertNil(capturedConfig.command)
XCTAssertNil(capturedConfig.env_vars)
XCTAssertEqual(capturedConfig.env_var_count, 0)
XCTAssertTrue(capturedConfig.environmentVariables.isEmpty)
}
}

View file

@ -78,6 +78,16 @@ if [[ -n "$TARGET_TRIPLE" ]]; then
exit 1
;;
esac
# When the requested target matches zig's native output arch, drop -Dtarget
# so zig uses native compilation. This avoids cross-linker issues on newer
# SDKs (e.g., macOS Tahoe + zig 0.15.x). Note: zig may run under Rosetta,
# so we detect native output arch from the zig binary itself, not uname -m.
ZIG_ARCH="$(file "$(command -v zig)" 2>/dev/null | grep -oE '(arm64|x86_64)' | head -1)"
case "$TARGET_TRIPLE" in
aarch64-macos) [[ "$ZIG_ARCH" == "arm64" ]] && TARGET_TRIPLE="" ;;
x86_64-macos) [[ "$ZIG_ARCH" == "x86_64" ]] && TARGET_TRIPLE="" ;;
esac
fi
if ! command -v zig >/dev/null 2>&1; then
@ -122,8 +132,18 @@ mkdir -p "$(dirname "$OUTPUT_PATH")"
if [[ "$UNIVERSAL" == "true" ]]; then
ARM64_PREFIX="$TMP_DIR/arm64"
X86_PREFIX="$TMP_DIR/x86_64"
build_helper "$ARM64_PREFIX" "aarch64-macos"
build_helper "$X86_PREFIX" "x86_64-macos"
ZIG_ARCH="$(file "$(command -v zig)" 2>/dev/null | grep -oE '(arm64|x86_64)' | head -1)"
# Use native compilation for the matching arch to avoid cross-linker issues
if [[ "$ZIG_ARCH" == "arm64" ]]; then
build_helper "$ARM64_PREFIX" ""
build_helper "$X86_PREFIX" "x86_64-macos"
elif [[ "$ZIG_ARCH" == "x86_64" ]]; then
build_helper "$ARM64_PREFIX" "aarch64-macos"
build_helper "$X86_PREFIX" ""
else
build_helper "$ARM64_PREFIX" "aarch64-macos"
build_helper "$X86_PREFIX" "x86_64-macos"
fi
/usr/bin/lipo -create \
"$ARM64_PREFIX/bin/ghostty" \
"$X86_PREFIX/bin/ghostty" \