Skip to content

Extract Ghostty config-file discovery from GhosttyTerminalView into CmuxGhosttyConfigLoader - #6177

Closed
azooz2003-bit wants to merge 1 commit into
mainfrom
feat-ghostty-engine-config-loader
Closed

azooz2003-bit wants to merge 1 commit into
mainfrom
feat-ghostty-engine-config-loader

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator

Pulls the pure Ghostty config-file discovery + scanning domain out of GhosttyApp (inside the ~12k-line Sources/GhosttyTerminalView.swift god file) into a new leaf package CmuxGhosttyConfigLoader, as a constructor-injected value type GhosttyConfigDiscovery.

What moved

Now a real type with inverted seams instead of statics welded onto GhosttyApp:

  • CJK font fallback: cjkFontMappings, autoInjectedCJKFontMappings, shouldInjectCJKFontFallback, resolvedInjectedCJKFontName, glyph-coverage probing.
  • User font-config scanning: userFontConfigSummary, config-file include following, parsedConfigEntry / applyConfigFileDirective, UserFontConfigSummary.
  • Scan-path resolution: loadedGhosttyConfigScanPaths / loadedCJKScanPaths, cmuxAppSupportConfigURLs.
  • Legacy-config decisions: shouldLoadLegacyGhosttyConfig, shouldIncludeLegacyGhosttyConfigInScanPaths, shouldIgnoreNativeLegacyBaselineForUnparsedAppearance.
  • Conditional-theme override + managed-default-appearance decisions.

Seams (inverted, constructor-injected; defaults reproduce prior behavior)

  • GhosttyConfigFileReading (default FileManagerGhosttyConfigFileReader) inverts every config file size/contents read that used FileManager.default / String(contentsOfFile:).
  • GhosttyFontProbing (default CoreTextGhosttyFontProbe) inverts the CoreText CTFont family-name discovery and glyph-coverage probing.
  • CmuxGhosttyConfigPathResolver (from CmuxFoundation) injected for app-support URL resolution.

One major public type per file; /// DocC on every public symbol; no caseless-enum namespace, no free functions, no lint:allow.

Byte-identical

Every directive key, scan-path ordering, legacy-fallback size rule, config-file include semantics, CJK range set, and theme-override text is preserved exactly. The C-API config-load instance methods (loadDefaultConfigFilesWithLegacyFallback, loadCJKFontFallbackIfNeeded, reloadConfiguration, synchronizeThemeWithAppearance) stay in the app target and now call the new type to decide what to load while still issuing the ghostty_config_load_* calls themselves. The former GhosttyApp statics remain as thin one-line forwards, so all call sites (including cmuxTests) are unchanged.

Left in the app target on purpose: everything that mutates ghostty_config_t, GhosttyApp instance state, or reaches AppDelegate.shared / NotificationCenter — not pure, not part of this clean subset.

The seam is a synchronous value type (not an actor) because discovery runs inside the synchronous launch-path config-load sequence; an async actor would reorder the C-API calls. Principled over the actor shape the audit suggested.

Tests

17 behavior unit tests with in-memory fakes for both seams (CJK mapping, font-config scanning incl. config-file includes, legacy-config rules, scan-path composition, theme override).

Stats / wiring

  • Giant GhosttyTerminalView.swift shrinks 12093 → 11691 lines (−402); .github/swift-file-length-budget.tsv reconciled.
  • Wired into cmux.xcodeproj (6 pbxproj entries) + app-target dependency.

NOTE: full app build is validated by GitHub CI; local app xcodebuild was skipped intentionally (parallel refactor agents). The new package builds and tests pass locally, and scripts/lint-ios-package-conventions.sh is clean with zero new lint:allow.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Extracted Ghostty config discovery/scanning into a new leaf package CmuxGhosttyConfigLoader with GhosttyConfigDiscovery, reducing GhosttyTerminalView.swift by 402 lines and keeping behavior identical. Improves testability and isolates pure logic from app state.

  • Refactors
    • Moved: CJK font fallback, user font-config scanning (with includes), scan-path resolution, legacy-config decisions, and theme override/managed default appearance into GhosttyConfigDiscovery.
    • Added seams: GhosttyConfigFileReading (default FileManagerGhosttyConfigFileReader), GhosttyFontProbing (default CoreTextGhosttyFontProbe); injects CmuxGhosttyConfigPathResolver.
    • App’s C-API config-loaders remain and now delegate to the new type while performing ghostty_config_* calls.
    • Preserves behavior: same scan-path ordering, legacy size rule, include semantics, CJK ranges, and theme override text.
    • Added 17 unit tests with in-memory fakes and small C stubs; wired CmuxGhosttyConfigLoader into cmux.xcodeproj.

Written for commit b56a7bb. Summary will update on new commits.

Review in cubic

…muxGhosttyConfigLoader

Pull the pure config-file discovery + scanning domain out of GhosttyApp
(in the 12k-line GhosttyTerminalView.swift god file) into a new leaf
package, CmuxGhosttyConfigLoader, as a constructor-injected value type
GhosttyConfigDiscovery.

What moved (now a real type with inverted seams, not statics on GhosttyApp):
- CJK font-fallback mapping (cjkFontMappings, autoInjectedCJKFontMappings,
  shouldInjectCJKFontFallback, resolvedInjectedCJKFontName, font coverage)
- user font-config scanning (userFontConfigSummary, config-file include
  following, parsedConfigEntry/applyConfigFileDirective, UserFontConfigSummary)
- scan-path resolution (loadedGhosttyConfigScanPaths/loadedCJKScanPaths,
  cmuxAppSupportConfigURLs)
- legacy-config decisions (shouldLoadLegacyGhosttyConfig,
  shouldIncludeLegacyGhosttyConfigInScanPaths,
  shouldIgnoreNativeLegacyBaselineForUnparsedAppearance)
- conditional-theme override + managed-default-appearance decisions

Seams (inverted, constructor-injected, defaults reproduce prior behavior):
- GhosttyConfigFileReading (default FileManagerGhosttyConfigFileReader) for
  every config file size/contents read that used FileManager.default /
  String(contentsOfFile:)
- GhosttyFontProbing (default CoreTextGhosttyFontProbe) for the CoreText
  CTFont family-name discovery and glyph-coverage probing
- CmuxGhosttyConfigPathResolver injected for app-support URL resolution

Byte-identical: every directive key, scan-path ordering, legacy-fallback
size rule, config-file include semantics, CJK range set, and theme-override
text is preserved exactly. The C-API config-load instance methods
(loadDefaultConfigFilesWithLegacyFallback, loadCJKFontFallbackIfNeeded,
reloadConfiguration, synchronizeThemeWithAppearance) stay in the app target
and now call the new type to decide *what* to load, performing the actual
ghostty_config_load_* calls themselves. The former GhosttyApp statics remain
as thin one-line forwards so all call sites (incl. cmuxTests) are unchanged.

Left in the app target on purpose: everything that mutates ghostty_config_t,
GhosttyApp instance state (config/app/lastAppearanceColorScheme/default
background scope), or reaches AppDelegate.shared / NotificationCenter — those
are not pure and not part of this clean subset.

Seam is synchronous (value type, not actor) because discovery runs inside the
synchronous launch-path config-load sequence; making it async would change
the ordering of the C-API calls. Principled.

Adds behavior unit tests (17) with in-memory fakes for both seams.
Wires the package into cmux.xcodeproj (6 pbxproj entries) + app target dep.
Giant shrinks 12093 -> 11691 lines; budget reconciled.

NOTE: full app build is validated by GitHub CI (local app xcodebuild skipped
to avoid melting the machine across parallel refactor agents). New package
builds + tests pass locally; package-conventions lint is clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 15, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 15, 2026 8:03pm
cmux-staging Building Building Preview, Comment Jun 15, 2026 8:03pm

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@azooz2003-bit, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 2 minutes and 11 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0db17373-6ebf-41da-80d2-867c9c8dabf9

📥 Commits

Reviewing files that changed from the base of the PR and between 5321bec and b56a7bb.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (10)
  • Packages/CmuxGhosttyConfigLoader/Package.swift
  • Packages/CmuxGhosttyConfigLoader/Sources/CmuxGhosttyConfigLoader/GhosttyConfigDiscovery.swift
  • Packages/CmuxGhosttyConfigLoader/Sources/CmuxGhosttyConfigLoader/GhosttyConfigFileReading.swift
  • Packages/CmuxGhosttyConfigLoader/Sources/CmuxGhosttyConfigLoader/GhosttyFontProbing.swift
  • Packages/CmuxGhosttyConfigLoader/Sources/CmuxGhosttyConfigLoader/UserFontConfigSummary.swift
  • Packages/CmuxGhosttyConfigLoader/Tests/CmuxGhosttyConfigLoaderTests/GhosttyConfigDiscoveryTests.swift
  • Packages/CmuxGhosttyConfigLoader/Tests/GhosttyRuntimeTestStubs/GhosttyRuntimeTestStubs.c
  • Packages/CmuxGhosttyConfigLoader/Tests/GhosttyRuntimeTestStubs/include/GhosttyRuntimeTestStubs.h
  • Sources/GhosttyTerminalView.swift
  • cmux.xcodeproj/project.pbxproj
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ghostty-engine-config-loader

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extracts the pure Ghostty config-file discovery and CJK font-fallback logic from the GhosttyTerminalView.swift god file into a new leaf SwiftPM package, CmuxGhosttyConfigLoader, with a clean constructor-injected value type GhosttyConfigDiscovery.

  • GhosttyConfigDiscovery (561 lines) becomes the single source of truth for CJK font mappings, user font-config scanning with config-file include following, legacy-config decisions, scan-path resolution, and conditional-theme override text — all of which previously lived as private statics on GhosttyApp.
  • Two seam protocols, GhosttyConfigFileReading and GhosttyFontProbing, invert the FileManager.default / CoreText globals so the logic can be driven by in-memory fakes in the 17 new unit tests.
  • The GhosttyApp statics are thinned to one-line forwards into configDiscovery, preserving all call-site signatures; GhosttyTerminalView.swift shrinks by 402 lines.

Confidence Score: 4/5

The extraction is byte-identical to the original logic, all call sites are unchanged, and 17 new unit tests cover the core decision paths. The three findings are non-blocking quality notes.

The refactor is well-scoped and the new package compiles under Swift 6 strict mode. The only gaps are: resolvedInjectedCJKFontName leaking direct CTFontCopy* calls outside the injected probe (making the face-selection logic hard to test in isolation), a missing positive-path test for the conditional-theme override (the exact regression path fixed in issue #3459), and the GhosttyConfigDiscovery static in GhosttyApp relying on @MainActor isolation to avoid a Swift 6 Sendable constraint that isn't documented at the declaration site.

GhosttyConfigDiscovery.swift — the resolvedInjectedCJKFontName method and the missing Sendable documentation. GhosttyConfigDiscoveryTests.swift — the GhosttyConfigDiscoveryThemeOverrideTests suite needs a positive case.

Important Files Changed

Filename Overview
Packages/CmuxGhosttyConfigLoader/Sources/CmuxGhosttyConfigLoader/GhosttyConfigDiscovery.swift Core extraction; 561 lines, one clear responsibility. resolvedInjectedCJKFontName partially bypasses the GhosttyFontProbing seam via direct CTFontCopy* calls on fonts obtained from the probe.
Packages/CmuxGhosttyConfigLoader/Sources/CmuxGhosttyConfigLoader/GhosttyConfigFileReading.swift Clean seam protocol + default FileManager-backed conformer; intentionally not Sendable per inline comment.
Packages/CmuxGhosttyConfigLoader/Sources/CmuxGhosttyConfigLoader/GhosttyFontProbing.swift Clean seam protocol + CoreText conformer; configuredFont correctly normalizes the requested name before comparing.
Packages/CmuxGhosttyConfigLoader/Sources/CmuxGhosttyConfigLoader/UserFontConfigSummary.swift Small value type with correct Ghostty-reset semantics on empty font-family and font-codepoint-map directives.
Packages/CmuxGhosttyConfigLoader/Tests/CmuxGhosttyConfigLoaderTests/GhosttyConfigDiscoveryTests.swift 17 unit tests with in-memory fakes; all existing behavior paths are covered except the positive-return path of conditionalThemeOverrideConfigContents.
Packages/CmuxGhosttyConfigLoader/Package.swift Swift 6 language mode, ExistentialAny, InternalImportsByDefault, correct platform floor; GhosttyRuntimeTestStubs isolates the link-time stub correctly.
Sources/GhosttyTerminalView.swift −402 lines; all former statics are now one-line forwards into configDiscovery; configPaths defaults changed from eager loadedCJKScanPaths() to nil (lazy), semantically equivalent.
Packages/CmuxGhosttyConfigLoader/Tests/GhosttyRuntimeTestStubs/GhosttyRuntimeTestStubs.c Minimal link-time stub satisfying @_silgen_name symbols needed by CmuxTerminalCore; correct and intentional test scaffolding.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["GhosttyApp static methods\ne.g. cjkFontMappings\nautoInjectedCJKFontMappings\nconditionalThemeOverrideConfigContents"] -->|one-line forward| B["GhosttyConfigDiscovery\nprivate static let configDiscovery"]

    B --> C{Decision logic}

    C -->|scan-path resolution| D["CmuxGhosttyConfigPathResolver\nfrom CmuxFoundation"]
    C -->|file size / contents reads| E["GhosttyConfigFileReading seam\ndefault: FileManagerGhosttyConfigFileReader"]
    C -->|font family / glyph coverage| F["GhosttyFontProbing seam\ndefault: CoreTextGhosttyFontProbe"]
    C -->|appearance + theme helpers| G["GhosttyConfig\nfrom CmuxTerminalCore"]

    E -->|in tests| H["FakeFileReader\nin-memory map"]
    F -->|in tests| I["NoFontProbe\nreturns nil"]

    B -->|returns decisions| A
    A -->|still issues ghostty_config_load_* calls| J["Ghostty C-API\nghotsty_config_t mutation stays in app target"]
Loading

Reviews (1): Last reviewed commit: "Extract Ghostty config-file discovery fr..." | Re-trigger Greptile

Comment on lines +237 to +270
public func resolvedInjectedCJKFontName(
named name: String,
size: CGFloat = 12
) -> String {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return name }
guard let regularWeightFont = fontProbe.discoveredFont(named: trimmed, size: size, weightTrait: 0.0) else {
return trimmed
}

let candidateNames = [
CTFontCopyName(regularWeightFont, kCTFontFullNameKey) as String?,
CTFontCopyName(regularWeightFont, kCTFontPostScriptNameKey) as String?,
].compactMap { $0 }
let expectedFullName = CTFontCopyFullName(regularWeightFont) as String
let expectedPostScriptName = CTFontCopyPostScriptName(regularWeightFont) as String

for candidate in candidateNames {
guard let verifiedFont = fontProbe.discoveredFont(named: candidate, size: size, weightTrait: nil) else { continue }
let verifiedNames = [
CTFontCopyName(verifiedFont, kCTFontFamilyNameKey) as String?,
CTFontCopyName(verifiedFont, kCTFontFullNameKey) as String?,
CTFontCopyName(verifiedFont, kCTFontPostScriptNameKey) as String?,
].compactMap { $0 }
let matchesRegularWeightFace = verifiedNames.contains {
Self.normalizedFontName($0) == Self.normalizedFontName(expectedFullName) ||
Self.normalizedFontName($0) == Self.normalizedFontName(expectedPostScriptName)
}
if matchesRegularWeightFace {
return candidate
}
}

return trimmed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 resolvedInjectedCJKFontName partially bypasses the GhosttyFontProbing seam

After calling fontProbe.discoveredFont(...) to obtain regularWeightFont, the method calls CTFontCopyName, CTFontCopyFullName, CTFontCopyPostScriptName, and another CTFontCopyName set directly on the returned CTFont — outside the injected probe. The verification loop then calls fontProbe.discoveredFont again, but the subsequent name lookups (CTFontCopyName for family/full/PostScript) are also direct CoreText calls.

In practice the existing tests are unaffected because NoFontProbe.discoveredFont returns nil, which short-circuits the function at line 243 and avoids the direct calls entirely. However, a test that wants to verify the face-selection logic (e.g., confirming that "Hiragino Sans" resolves to the regular-weight face rather than W0) cannot do so without a real system font — the probe seam doesn't cover the downstream CTFontCopy* reads. Consider exposing a fontNameAttributes(for: CTFont) -> (full: String, postScript: String, family: String) requirement on GhosttyFontProbing so the entire resolution path is injectable and independently testable.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +163 to +183
@Suite struct GhosttyConfigDiscoveryThemeOverrideTests {
@Test func nonConditionalThemeNeedsNoOverride() {
let path = "/cfg/config"
let reader = FakeFileReader(contentsByPath: [path: "theme = Dracula"])
let discovery = GhosttyConfigDiscovery(fileReader: reader, fontProbe: NoFontProbe())
#expect(discovery.conditionalThemeOverrideConfigContents(
preferredColorScheme: .dark,
configPaths: [path]
) == nil)
}

@Test func noThemeDirectiveYieldsNil() {
let path = "/cfg/config"
let reader = FakeFileReader(contentsByPath: [path: "font-family = Mono"])
let discovery = GhosttyConfigDiscovery(fileReader: reader, fontProbe: NoFontProbe())
#expect(discovery.conditionalThemeOverrideConfigContents(
preferredColorScheme: .dark,
configPaths: [path]
) == nil)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 conditionalThemeOverrideConfigContents positive path is untested

Both existing tests in GhosttyConfigDiscoveryThemeOverrideTests only exercise nil-return scenarios — a plain (non-conditional) theme and a config with no theme directive at all. There is no test for the case where the function should return a non-nil override string, for example theme = light:Dracula,dark:Nord with a .dark preference returning "theme = Nord". This is the live bug path that the fix in issue #3459 targets, so a regression here would be invisible to the test suite.

Comment on lines +13 to +21
public protocol GhosttyConfigFileReading {
/// Returns the byte size of the file at `path`, or `nil` when it does not
/// exist or its attributes cannot be read.
func fileSize(atPath path: String) -> Int?

/// Returns the UTF-8 contents of the file at `path`, or `nil` when it cannot
/// be read.
func contents(atPath path: String) -> String?
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 GhosttyConfigFileReading (and GhosttyFontProbing) lack Sendable; GhosttyConfigDiscovery can't synthesize it

The doc comment correctly states that the seam is "intentionally not Sendable" because discovery only runs synchronously on the launch path. However, GhosttyConfigDiscovery stores both protocols as existentials (any GhosttyConfigFileReading, any GhosttyFontProbing), and a struct that holds non-Sendable stored properties cannot synthesize Sendable itself. The result is that private static let configDiscovery = GhosttyConfigDiscovery() in GhosttyApp is only safe today because GhosttyApp is @MainActor-isolated — the static is implicitly actor-bound, so Swift 6 doesn't require the type to be Sendable. If GhosttyApp's isolation is ever relaxed or the static is accessed from a nonisolated context in a future refactor, Swift 6 strict mode will surface a latent error. Documenting the coupling between "non-Sendable by design" and "caller must be @MainActor-isolated" at the static declaration site — or alternatively adding @unchecked Sendable to GhosttyConfigDiscovery with an explanatory comment — would make the invariant explicit and durable.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Superseded by #6227 — consolidated into the existing CmuxTerminalCore package (no new micro-package) per over-engineering review. The extraction is preserved there.

This branch was successfully deployed

1 active deployment
Preview – cmux — b56a7bb7 Deployed Jun 15, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant