- 01.Starting Point
- 02.Auditing the Font Setup
- 03.Testing Glyph Coverage
- 04.Found the Bug: Two Different Font Contexts
- 05.The Fix
- 06.Where We Landed
- 07.Takeaways
Font Cascade Fallback for Terminal Symbols
User noticed the ⏺ record symbol (used as bullet points in Claude Code output) wasn't rendering correctly in the Settings preview. The app already had a font fallback cascade in `Theme.swift`, so the
Commit: 667e3b4
Starting Point
User noticed the ⏺ record symbol (used as bullet points in Claude Code output) wasn't rendering correctly in the Settings preview. The app already had a font fallback cascade in Theme.swift, so the question was: why isn't it working?
Auditing the Font Setup
First, traced where fonts are configured:
grep -r "font|Font" Pocket/Pocket/UI/*.swiftFound the cascade in Theme.swift:6-27:
private func terminalFont(size: CGFloat) -> UIFont {
let baseFont = UIFont(name: "Menlo", size: size) ?? UIFont.monospacedSystemFont(ofSize: size, weight: .regular)
let fallbackDescriptors: [[CFString: Any]] = [
[kCTFontNameAttribute: "STIXTwoMath-Regular" as CFString],
[kCTFontNameAttribute: "Apple Symbols" as CFString],
[kCTFontNameAttribute: "Apple Color Emoji" as CFString],
[kCTFontNameAttribute: "Helvetica" as CFString]
]
let cascadeList = fallbackDescriptors.map { CTFontDescriptorCreateWithAttributes($0 as CFDictionary) }
var attributes: [UIFontDescriptor.AttributeName: Any] = [:]
attributes[UIFontDescriptor.AttributeName(rawValue: kCTFontCascadeListAttribute as String)] = cascadeList
let descriptor = baseFont.fontDescriptor.addingAttributes(attributes)
return UIFont(descriptor: descriptor, size: size)
}The cascade chain: Menlo → STIX Two Math → Apple Symbols → Apple Color Emoji → Helvetica.
This is applied to SwiftTerm at TerminalView.swift:277:
terminalView.font = theme.fontSo the real terminal should work. Where's the bug?
Testing Glyph Coverage
Wrote a quick Swift script to test which fonts contain which glyphs:
import CoreText
let testGlyphs = [
("⏺", "U+23FA", "Record symbol"),
("●", "U+25CF", "Black circle"),
// ...
]
let fontsToCheck = ["Menlo", "STIXTwoMath-Regular", "Apple Symbols", "AppleColorEmoji", "SF Mono"]
for (glyph, code, name) in testGlyphs {
let scalar = glyph.unicodeScalars.first!
for fontName in fontsToCheck {
if let font = CTFontCreateWithName(fontName as CFString, 12, nil) as CTFont? {
var glyphRef: CGGlyph = 0
let hasGlyph = CTFontGetGlyphsForCharacters(font, [UniChar(scalar.value)], &glyphRef, 1)
if hasGlyph && glyphRef != 0 {
print(" ✓ \(fontName)")
}
}
}
}Results:
⏺ U+23FA (Record symbol):
✓ STIXTwoMath-Regular
✓ AppleColorEmoji
● U+25CF (Black circle):
✓ Menlo
✓ STIXTwoMath-Regular
✓ Apple Symbols
✓ Helvetica
✓ SF Mono
Key finding: ⏺ is only in STIX Two Math and Apple Color Emoji — NOT in Menlo or SF Mono.
Found the Bug: Two Different Font Contexts
The problem is there are two contexts rendering terminal-like content:
| Context | Font | Has ⏺? |
|---|---|---|
| SwiftTerm (real terminal) | terminalFont() with CTFont cascade | ✓ |
| MiniTerminalPreview (Settings) | .system(design: .monospaced) | ✗ |
In SettingsView.swift:317:
Text(parseANSI(ansiPreview, theme: theme))
.font(.system(size: min(fontSize, 10), design: .monospaced)) // SF Mono, no cascade!SwiftUI's .font(.system(design: .monospaced)) gives you SF Mono with no fallback chain. The CTFont cascade only exists in the UIKit terminalFont() function, which isn't being used here.
The preview ANSI content includes ⏺ at line 305:
[38;2;255;255;255m⏺[39m Three things:
The Fix
Bridge the UIFont (with cascade) to SwiftUI Font:
- Made
terminalFont(size:)public (wasprivate) - Added bold/italic variants that preserve the cascade
- Used
Font(terminalFont(size:))in the preview
// Theme.swift - expose and add variants
func terminalFont(size: CGFloat) -> UIFont { ... }
func terminalFontBold(size: CGFloat) -> UIFont {
let base = terminalFont(size: size)
let boldDescriptor = base.fontDescriptor.withSymbolicTraits(.traitBold) ?? base.fontDescriptor
return UIFont(descriptor: boldDescriptor, size: size)
}
func terminalFontItalic(size: CGFloat) -> UIFont {
let base = terminalFont(size: size)
let italicDescriptor = base.fontDescriptor.withSymbolicTraits(.traitItalic) ?? base.fontDescriptor
return UIFont(descriptor: italicDescriptor, size: size)
}// SettingsView.swift - use bridged font
Text(parseANSI(ansiPreview, theme: theme))
.font(Font(terminalFont(size: min(fontSize, 10))))
// In the ANSI parser's AttributedString handling:
if isBold { attr.font = Font(terminalFontBold(size: min(fontSize, 10))) }
if isItalic { attr.font = Font(terminalFontItalic(size: min(fontSize, 10))) }Where We Landed
The fix preserves the font cascade chain when bridging to SwiftUI, so ⏺ renders correctly in both the real terminal (SwiftTerm/UIKit) and the settings preview (SwiftUI Text).
Takeaways
-
SwiftUI
.monospacedhas no fallback cascade. It's just SF Mono. If you need glyph fallbacks, you must build them yourself with CTFont descriptors and bridge viaFont(uiFont). -
CTFont cascade list is the magic. The
kCTFontCascadeListAttributeon a UIFontDescriptor tells CoreText "if glyph not found in primary, try these fonts in order." SwiftUI Font has no equivalent API. -
Test glyph coverage directly.
CTFontGetGlyphsForCharacterstells you exactly which fonts have which glyphs. Don't guess. -
The problematic glyph: U+23FA (⏺) is Claude Code's bullet point. Only STIX Two Math and Apple Color Emoji have it on iOS — not Menlo, SF Mono, or Apple Symbols.