COORD: 44.21.90
OFFSET: +12.5°
SYS.READY
BUFFER: 99%
FOCAL_PT
BACK TO DEVLOG
POCKET

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

2026-01-04 // RAW LEARNING CAPTURE
PROJECTPOCKET

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/*.swift

Found 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.font

So 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:

ContextFontHas ⏺?
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:

  1. Made terminalFont(size:) public (was private)
  2. Added bold/italic variants that preserve the cascade
  3. 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 .monospaced has no fallback cascade. It's just SF Mono. If you need glyph fallbacks, you must build them yourself with CTFont descriptors and bridge via Font(uiFont).

  • CTFont cascade list is the magic. The kCTFontCascadeListAttribute on 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. CTFontGetGlyphsForCharacters tells 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.

LOG.ENTRY_END
ref:pocket
RAW