Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4ad3040
Add failing test for terminal zoom session restore
austinywang Jul 21, 2026
1f6f30d
Persist explicit terminal zoom across session restore
austinywang Jul 21, 2026
f62c456
Fix terminal zoom lineage lifecycle edge cases
austinywang Jul 21, 2026
5672f8b
Preserve mutable runtime surface config
austinywang Jul 21, 2026
87733e9
Clear stale terminal zoom inheritance
austinywang Jul 21, 2026
107771c
Make terminal zoom capture allocation-free
austinywang Jul 21, 2026
e20e7ab
Preserve terminal zoom ownership during mobile fitting
austinywang Jul 21, 2026
980924b
Merge remote-tracking branch 'origin/main' into issue-8515-font-zoom-…
austinywang Jul 21, 2026
1e52747
Keep unzoomed restores on current terminal config
austinywang Jul 21, 2026
781ef04
Keep terminal zoom inheritance source coherent
austinywang Jul 21, 2026
96b4123
test: reject unmounted terminal zoom sources
austinywang Jul 21, 2026
66fd1be
fix: record terminal zoom source after insertion
austinywang Jul 21, 2026
e2086cc
Merge remote-tracking branch 'origin/main' into issue-8515-font-zoom-…
austinywang Jul 21, 2026
a1d3134
Move terminal font creation policy into core
austinywang Jul 21, 2026
08eef76
test: preserve initial terminal font template
austinywang Jul 21, 2026
11eb1c8
Preserve terminal font size on first runtime creation
austinywang Jul 21, 2026
d6c2e46
test: cover font ownership and crash pruning
austinywang Jul 22, 2026
5af3691
Preserve explicit terminal font ownership
austinywang Jul 22, 2026
a9baff1
test: cover font bounds and live lineage cache
austinywang Jul 22, 2026
a1b2482
fix: bound font persistence and refresh lineage cache
austinywang Jul 22, 2026
d604ea1
test: preserve terminal runtime default sentinel
austinywang Jul 22, 2026
39ddc62
fix: preserve terminal runtime default sentinel
austinywang Jul 22, 2026
fe081b4
Merge origin/main into issue-8515-font-zoom-persistence
austinywang Jul 22, 2026
425ff2c
Merge origin/main into issue-8515-font-zoom-persistence
austinywang Jul 22, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// Temporary runtime font state owned by mobile viewport fitting.
struct MobileViewportFontFitState: Equatable {
var baseRuntimePointSize: Float32
var fittedRuntimePointSize: Float32

func matchesFittedRuntimePointSize(_ runtimePointSize: Float32) -> Bool {
abs(runtimePointSize - fittedRuntimePointSize) <= 0.05
}

mutating func rebase(to runtimePointSize: Float32) {
baseRuntimePointSize = runtimePointSize
fittedRuntimePointSize = runtimePointSize
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
public import CmuxTerminalCore
internal import GhosttyKit

extension TerminalSurface {
/// Captures the current font size and its surface-local ownership state.
///
/// Live Ghostty state is authoritative. When the runtime is unavailable,
/// the last captured lineage survives hibernation and session restoration.
///
/// - Returns: Current font-size lineage, or nil before a size is known.
@MainActor
public func fontSizeLineageSnapshot() -> TerminalFontSizeLineage? {
guard let runtimeSurface = liveSurfaceForGhosttyAccess(
reason: "fontSizeLineage.snapshot"
) else {
return lastKnownFontSizeLineage
}
guard let runtimePoints = GhosttySurfaceRuntimeProbe.currentSurfaceFontSizePoints(
runtimeSurface
) else {
return lastKnownFontSizeLineage
}

return recordObservedFontSizeLineage(
runtimePoints: runtimePoints,
isExplicitOverride: ghostty_surface_font_size_adjusted(runtimeSurface),
globalFontMagnificationPercent: globalFontMagnificationPercent()
)
}

/// Reconciles observed runtime points with durable surface ownership.
///
/// A live value matching the active mobile fit is temporary and leaves the
/// pre-fit lineage unchanged. A different live value came from outside the
/// fitter, so it becomes the new durable base and restore point.
@MainActor
func recordObservedFontSizeLineage(
runtimePoints: Float32,
isExplicitOverride: Bool,
globalFontMagnificationPercent: Int
) -> TerminalFontSizeLineage? {
guard runtimePoints.isFinite, runtimePoints > 0 else {
return lastKnownFontSizeLineage
}
if var fitState = mobileViewportFontFitState {
guard !isExplicitOverride
|| !fitState.matchesFittedRuntimePointSize(runtimePoints) else {
return lastKnownFontSizeLineage
}
fitState.rebase(to: runtimePoints)
mobileViewportFontFitState = fitState
}
Comment thread
austinywang marked this conversation as resolved.

let lineage = TerminalFontSizeLineage(
basePoints: CmuxSurfaceConfigTemplate.baseFontSize(
fromRuntimePoints: runtimePoints,
percent: globalFontMagnificationPercent
),
isExplicitOverride: isExplicitOverride
)
recordCurrentFontSizeLineage(lineage)
return lineage
}

/// Records live font-size lineage for hibernation and split inheritance.
///
/// A non-explicit value is retained as the last known split-inheritance
/// value, while separately recording that this surface must follow current
/// config when its own runtime is recreated.
@MainActor
func recordCurrentFontSizeLineage(_ lineage: TerminalFontSizeLineage) {
guard lastKnownFontSizeLineage != lineage else { return }
lastKnownFontSizeLineage = lineage
onFontSizeLineageChanged?(lineage)
}

/// Resolves the Swift-owned template used to create this surface's runtime.
///
/// Initial non-explicit lineage seeds the first native runtime. After a
/// native lifetime, non-explicit lineage remains available to descendants
/// but must not seed this surface again because Cmd+0 and ordinary unzoomed
/// terminals follow the then-current terminal config.
@MainActor
func runtimeCreationConfigTemplate() -> CmuxSurfaceConfigTemplate {
var template = configTemplate ?? CmuxSurfaceConfigTemplate()
if lastKnownFontSizeLineage?.isExplicitOverride == false,
runtimeSurfaceGeneration > 0 {
template.fontSizeLineage = nil
} else if let lastKnownFontSizeLineage {
template.fontSizeLineage = lastKnownFontSizeLineage
}
return template
}

/// Returns the explicit unscaled font override to persist in a session snapshot.
///
/// Nil means the terminal follows the current config and should not pin a
/// font size across relaunches.
@MainActor
public func sessionFontSizeOverrideBasePoints() -> Float32? {
guard let lineage = fontSizeLineageSnapshot(),
lineage.isExplicitOverride,
TerminalFontSizePolicy().acceptsPersistedBasePoints(lineage.basePoints) else {
return nil
}
return lineage.basePoints
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,10 @@ extension TerminalSurface {
paneHost.setMobileViewportBorder(size: nil, drawRight: false, drawBottom: false)

guard let surface = liveSurfaceForGhosttyAccess(reason: "clearMobileViewportLimit") else {
mobileFitBaseFontPointSize = nil
mobileFittedFontPointSize = nil
mobileViewportFontFitState = nil
return false
}
_ = fontSizeLineageSnapshot()
let fontRestored = restoreMobileViewportFitFontIfNeeded()
let uncappedWidth = lastUncappedPixelWidth
let uncappedHeight = lastUncappedPixelHeight
Expand Down Expand Up @@ -229,8 +229,9 @@ extension TerminalSurface {
let grantedRows = max(1, mobileViewportCellLimit.rows)
let paneWidth = max(1, Int(width))
let paneHeight = max(1, Int(height))
_ = fontSizeLineageSnapshot()
let baseFont = resolvedMobileViewportBaseFontPointSize(surface: surface)
var currentFont = mobileFittedFontPointSize
var currentFont = mobileViewportFontFitState?.fittedRuntimePointSize
?? GhosttySurfaceRuntimeProbe.currentSurfaceFontSizePoints(surface)
?? baseFont
var measurement = mobileViewportMeasurement(surface: surface)
Expand All @@ -247,11 +248,7 @@ extension TerminalSurface {
for _ in 0..<3 {
let fontFloor = min(baseFont, MobileViewportFitGeometry.defaultFontFloorPointSize)
if abs(targetFont - currentFont) >= 0.25 {
if mobileFitBaseFontPointSize == nil {
mobileFitBaseFontPointSize = baseFont
}
if applyMobileViewportFontPointSize(targetFont) {
mobileFittedFontPointSize = targetFont
if applyMobileViewportFontPointSize(targetFont, baseFont: baseFont) {
currentFont = targetFont
fontChanged = true
measurement = mobileViewportMeasurement(surface: surface)
Expand All @@ -276,11 +273,7 @@ extension TerminalSurface {
guard abs(nextTarget - currentFont) > 0.001 else {
break
}
if mobileFitBaseFontPointSize == nil {
mobileFitBaseFontPointSize = baseFont
}
if applyMobileViewportFontPointSize(nextTarget) {
mobileFittedFontPointSize = nextTarget
if applyMobileViewportFontPointSize(nextTarget, baseFont: baseFont) {
currentFont = nextTarget
fontChanged = true
measurement = mobileViewportMeasurement(surface: surface)
Expand All @@ -301,14 +294,10 @@ extension TerminalSurface {
// This force-to-floor step can be the first font change of the fit
// (every earlier apply may have been skipped or broken out of), so
// it must capture the restore point like the loop branches do.
if mobileFitBaseFontPointSize == nil {
mobileFitBaseFontPointSize = baseFont
}
guard applyMobileViewportFontPointSize(fontFloor) else {
guard applyMobileViewportFontPointSize(fontFloor, baseFont: baseFont) else {
let fallback = geometry.cappedFallbackGrant(grantedColumns: grantedColumns, grantedRows: grantedRows)
return .fallback(width: fallback.width, height: fallback.height, columns: fallback.columns, rows: fallback.rows, grant: appliedBox, baseFont: baseFont, currentFont: currentFont, fontChanged: fontChanged)
}
mobileFittedFontPointSize = fontFloor
currentFont = fontFloor
fontChanged = true
measurement = mobileViewportMeasurement(surface: surface)
Expand Down Expand Up @@ -397,8 +386,8 @@ extension TerminalSurface {

@MainActor
private func resolvedMobileViewportBaseFontPointSize(surface: ghostty_surface_t) -> Float {
if let mobileFitBaseFontPointSize {
return mobileFitBaseFontPointSize
if let mobileViewportFontFitState {
return mobileViewportFontFitState.baseRuntimePointSize
}
if let current = GhosttySurfaceRuntimeProbe.currentSurfaceFontSizePoints(surface),
current.isFinite,
Expand All @@ -415,26 +404,49 @@ extension TerminalSurface {
@discardableResult
@MainActor
private func restoreMobileViewportFitFontIfNeeded() -> Bool {
guard mobileFittedFontPointSize != nil,
let baseFont = mobileFitBaseFontPointSize else {
mobileFitBaseFontPointSize = nil
mobileFittedFontPointSize = nil
guard mobileViewportFontFitState != nil else {
return false
}
guard applyMobileViewportFontPointSize(baseFont) else {
let restored: Bool
if let lineage = lastKnownFontSizeLineage,
lineage.isExplicitOverride {
// Lineage stores unscaled base points, so restoration intentionally
// reapplies the current global magnification.
let runtimePoints = CmuxSurfaceConfigTemplate.runtimeFontSize(
fromBasePoints: lineage.basePoints,
percent: globalFontMagnificationPercent()
)
restored = performMobileViewportFontPointSizeAction(runtimePoints)
} else {
restored = performInternalBindingAction("reset_font_size")
}
guard restored else {
// Keep the fit state when the binding action fails so a later
// clear or fit pass can retry; dropping it here would leave the
// pane at the shrunken font with no way back to the base size.
return false
}
mobileFitBaseFontPointSize = nil
mobileFittedFontPointSize = nil
mobileViewportFontFitState = nil
return true
}

@MainActor
@discardableResult
private func applyMobileViewportFontPointSize(_ points: Float) -> Bool {
private func applyMobileViewportFontPointSize(_ points: Float, baseFont: Float) -> Bool {
guard performMobileViewportFontPointSizeAction(points) else { return false }
if mobileViewportFontFitState == nil {
mobileViewportFontFitState = MobileViewportFontFitState(
baseRuntimePointSize: baseFont,
fittedRuntimePointSize: points
)
} else {
mobileViewportFontFitState?.fittedRuntimePointSize = points
}
return true
}

@MainActor
private func performMobileViewportFontPointSizeAction(_ points: Float) -> Bool {
let action = String(format: "set_font_size:%.3f", points)
return performInternalBindingAction(action)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ extension TerminalSurface {
/// agent-hibernation resume.
@MainActor
public func suspendRuntimeSurfaceForAgentHibernation(reason: String) {
_ = fontSizeLineageSnapshot()
mobileViewportFontFitState = nil
runtimeSurfaceSuspendedForAgentHibernation = true
backgroundSurfaceStartQueued = false
backgroundSurfaceStartSource = .normal
Expand Down Expand Up @@ -609,21 +611,18 @@ extension TerminalSurface {
// wrapping at Ghostty's default grid.
flushPendingRemoteOutput(to: createdSurface)

// Some GhosttyKit builds can drop inherited font_size during post-create
// config/scale reconciliation. Re-apply runtime points so all creation
// paths preserve zoom from the source terminal.
if let inheritedBaseFontPoints = configTemplate?.fontSize,
inheritedBaseFontPoints > 0 {
// Some GhosttyKit builds can drop explicit font_size during post-create
// config/scale reconciliation. Re-apply explicit runtime points so
// Ghostty retains surface-local ownership; otherwise Cmd+0 could not
// clear the restored override for the next snapshot. Non-explicit
// lineage intentionally reconciles to the current terminal config.
if let inheritedFontSizeLineage = lastKnownFontSizeLineage,
inheritedFontSizeLineage.isExplicitOverride,
inheritedFontSizeLineage.basePoints > 0 {
let inheritedBaseFontPoints = inheritedFontSizeLineage.basePoints
let inheritedRuntimeFontPoints = CmuxSurfaceConfigTemplate.runtimeFontSize(fromBasePoints: inheritedBaseFontPoints, percent: globalFontMagnificationPercent())
let currentFontPoints = GhosttySurfaceRuntimeProbe.currentSurfaceFontSizePoints(createdSurface)
let shouldReapply = {
guard let currentFontPoints else { return true }
return abs(currentFontPoints - inheritedRuntimeFontPoints) > 0.05
}()
if shouldReapply {
let action = String(format: "set_font_size:%.3f", inheritedRuntimeFontPoints)
_ = performInternalBindingAction(action)
}
let action = String(format: "set_font_size:%.3f", inheritedRuntimeFontPoints)
_ = performInternalBindingAction(action)
}

// Re-apply the desired focus state after creation so the live runtime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension TerminalSurface {
scaleFactors: (x: CGFloat, y: CGFloat, layer: CGFloat),
claudeShim: ClaudeCommandShim?
) -> (createdSurface: ghostty_surface_t?, runtimeInitialInput: String?) {
var baseConfig = configTemplate ?? CmuxSurfaceConfigTemplate()
var baseConfig = runtimeCreationConfigTemplate()
var surfaceConfig = ghostty_surface_config_new()
let magnificationPercent = globalFontMagnificationPercent()
surfaceConfig.font_size = CmuxSurfaceConfigTemplate.runtimeFontSize(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ public final class TerminalSurface: Identifiable, ObservableObject {
let portOrdinal: Int
let surfaceContext: ghostty_surface_context_e
let configTemplate: CmuxSurfaceConfigTemplate?
var lastKnownFontSizeLineage: TerminalFontSizeLineage?
let workingDirectory: String?

/// The command to run instead of the default shell, if any.
Expand Down Expand Up @@ -184,6 +185,8 @@ public final class TerminalSurface: Identifiable, ObservableObject {
/// Remote tmux manual-I/O resize and runtime-readiness hooks.
@MainActor public var onManualSizeApplied: (@MainActor (TerminalSurfaceRawSizingSample) -> Void)?
@MainActor public var onRuntimeReady: (@MainActor () -> Void)?
/// Called after durable font-size lineage changes.
@MainActor public var onFontSizeLineageChanged: (@MainActor (TerminalFontSizeLineage) -> Void)?
@MainActor var manualSizeReportPendingWindowAttach = false
/// For MANUAL-I/O remote tmux display surfaces: whether to suppress
/// ghostty primary-screen reflow on resize.
Expand Down Expand Up @@ -231,10 +234,8 @@ public final class TerminalSurface: Identifiable, ObservableObject {
/// the pinned grid and clips or letterboxes the difference — the same
/// answer tmux gives a client whose size disagrees with the window.
var assignedGrid: (columns: Int, rows: Int)?
/// Runtime font size to restore when mobile viewport fitting clears.
var mobileFitBaseFontPointSize: Float?
/// Last runtime font size applied by mobile viewport fitting.
var mobileFittedFontPointSize: Float?
/// Temporary runtime font-size ownership while a mobile viewport is fitted.
var mobileViewportFontFitState: MobileViewportFontFitState?
// Debug metadata is read from debug/CLI paths off the main thread; the
// lock is the sanctioned carve-out for tiny values shared with
// synchronous off-isolation readers.
Expand Down Expand Up @@ -474,6 +475,7 @@ public final class TerminalSurface: Identifiable, ObservableObject {
self.tabId = tabId
self.surfaceContext = context
self.configTemplate = configTemplate
self.lastKnownFontSizeLineage = configTemplate?.fontSizeLineage
self.workingDirectory = workingDirectory?.trimmingCharacters(in: .whitespacesAndNewlines)
self.portOrdinal = portOrdinal
self.initialCommand = initialCommand.flatMap {
Expand Down
Loading