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:
parent
f0c3ccc314
commit
c4bc18d906
6 changed files with 265 additions and 213 deletions
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" \
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue