fix(terminal): preserve theme fallbacks for preferred fonts
This commit is contained in:
1 parent
632d913bb3
commit
82daa906a1
7 files changed
+132
-32
No files matched your search
@@ -26,13 +26,20 @@ nonisolated extension TermindTerminalPalette {
|
||||
}
|
||||
|
||||
nonisolated extension TerminalCustomConfiguration {
|
||||
func applying(to base: GhosttyTerminal.TerminalConfiguration)
|
||||
func applying(to base: GhosttyTerminal.TerminalConfiguration, preferredFontFamily: String)
|
||||
-> GhosttyTerminal.TerminalConfiguration
|
||||
{
|
||||
GhosttyTerminal.TerminalConfiguration(startingFrom: base) { builder in
|
||||
if !preferredFontFamily.isEmpty { builder.withFontFamily(preferredFontFamily) }
|
||||
// Command alerts are owned by the app's shared settings and dispatcher.
|
||||
for entry in entries where entry.key != "notify-on-command-finish" {
|
||||
builder.withCustom(entry.key, entry.value)
|
||||
if !preferredFontFamily.isEmpty && entry.key == "font-family"
|
||||
&& (entry.value.isEmpty || entry.value == "\"\"")
|
||||
{
|
||||
// Keep the theme's reset semantics without dropping the device's first choice.
|
||||
builder.withFontFamily(preferredFontFamily)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,14 +83,10 @@ public nonisolated enum TermindTerminalThemeCatalog {
|
||||
) -> GhosttyTerminal.TerminalConfiguration {
|
||||
let selected = resolvedTheme(named: themeName, isDark: isDark)
|
||||
let base = selected.toTerminalConfiguration()
|
||||
let themed = customEnabled ? TerminalCustomConfiguration(customText).applying(to: base) : base
|
||||
// Ghostty renders theme commands after session overrides. Typography must
|
||||
// come last here, and font-family is a list that needs an explicit reset.
|
||||
let themed = TerminalCustomConfiguration(customEnabled ? customText : "")
|
||||
.applying(to: base, preferredFontFamily: font.family)
|
||||
// Ghostty renders theme commands after session overrides, so manual size must come last here.
|
||||
return GhosttyTerminal.TerminalConfiguration(startingFrom: themed) { builder in
|
||||
if !font.family.isEmpty {
|
||||
builder.withFontFamily("")
|
||||
builder.withFontFamily(font.family)
|
||||
}
|
||||
if !font.sizeFollowsTheme { builder.withFontSize(Float(font.size)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ final class TerminalFontCatalog {
|
||||
func refresh() {
|
||||
families = UIFont.familyNames.filter { family in
|
||||
UIFont.fontNames(forFamilyName: family).contains { name in
|
||||
UIFont(name: name, size: 12)?.fontDescriptor.symbolicTraits.contains(.traitMonoSpace) == true
|
||||
UIFont(name: name, size: 12) != nil
|
||||
}
|
||||
}.sorted { $0.localizedStandardCompare($1) == .orderedAscending }
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import UIKit
|
||||
|
||||
@Suite(.serialized) @MainActor
|
||||
struct MobileTerminalFontTests {
|
||||
@Test func fontCatalogIncludesOnlyAvailableMonospacedFamilies() {
|
||||
@Test func fontCatalogIncludesEveryLoadableFamilyInLocalizedOrder() {
|
||||
TerminalFontCatalog.shared.refresh()
|
||||
let catalogFamilies = TerminalFontCatalog.shared.families
|
||||
#expect(!catalogFamilies.isEmpty)
|
||||
@@ -18,9 +18,13 @@ struct MobileTerminalFontTests {
|
||||
for family in catalogFamilies {
|
||||
#expect(
|
||||
UIFont.fontNames(forFamilyName: family).contains {
|
||||
UIFont(name: $0, size: 14)?.fontDescriptor.symbolicTraits.contains(.traitMonoSpace) == true
|
||||
UIFont(name: $0, size: 14) != nil
|
||||
})
|
||||
}
|
||||
let loadableFamilies = UIFont.familyNames.filter { family in
|
||||
UIFont.fontNames(forFamilyName: family).contains { UIFont(name: $0, size: 14) != nil }
|
||||
}
|
||||
#expect(Set(catalogFamilies) == Set(loadableFamilies))
|
||||
|
||||
let proportionalFamily = UIFont.familyNames.first { family in
|
||||
let fonts = UIFont.fontNames(forFamilyName: family).compactMap { UIFont(name: $0, size: 14) }
|
||||
@@ -29,7 +33,7 @@ struct MobileTerminalFontTests {
|
||||
}
|
||||
#expect(proportionalFamily != nil)
|
||||
if let proportionalFamily {
|
||||
#expect(!catalogFamilies.contains(proportionalFamily))
|
||||
#expect(catalogFamilies.contains(proportionalFamily))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +42,7 @@ struct MobileTerminalFontTests {
|
||||
arguments: ["en", "zh-Hans", "zh-Hant"])
|
||||
func fontNamesPreviewTheirOwnFamilies(_ language: String) async throws {
|
||||
let view = TerminalFontOptions(
|
||||
family: .constant("Menlo"), families: ["Courier", "Menlo", "Courier New"], didSelect: {}
|
||||
family: .constant("Helvetica"), families: ["Courier", "Courier New", "Helvetica", "Menlo"], didSelect: {}
|
||||
)
|
||||
.environment(\.locale, Locale(identifier: language))
|
||||
.frame(width: 280, height: 320)
|
||||
@@ -50,7 +54,7 @@ struct MobileTerminalFontTests {
|
||||
|
||||
@Test(
|
||||
.requiresKeyWindow,
|
||||
arguments: ["en", "zh-Hans", "zh-Hant"], ["", "Menlo"])
|
||||
arguments: ["en", "zh-Hans", "zh-Hant"], ["", "Menlo", "Helvetica"])
|
||||
func terminalFontSettingsRenderInSupportedLanguages(_ language: String, family: String) async throws {
|
||||
let domain = "MobileTerminalFontRendering.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: domain))
|
||||
|
||||
@@ -12,7 +12,7 @@ final class TerminalFontCatalog {
|
||||
families = NSFontManager.shared.availableFontFamilies.filter { family in
|
||||
NSFontManager.shared.availableMembers(ofFontFamily: family)?.contains { member in
|
||||
guard let fontName = member.first as? String else { return false }
|
||||
return NSFont(name: fontName, size: 12)?.isFixedPitch == true
|
||||
return NSFont(name: fontName, size: 12) != nil
|
||||
} == true
|
||||
}.sorted { $0.localizedStandardCompare($1) == .orderedAscending }
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import Testing
|
||||
|
||||
@Suite(.requiresVisibleWindow, .serialized) @MainActor
|
||||
struct TerminalSettingsRenderingTests {
|
||||
@Test(arguments: ["en", "zh-Hans", "zh-Hant"], ["", "Menlo"])
|
||||
@Test(arguments: ["en", "zh-Hans", "zh-Hant"], ["", "Menlo", "Helvetica"])
|
||||
func terminalSettingsRenderInSupportedLanguages(_ language: String, family: String) async throws {
|
||||
let domain = "TerminalFontRendering.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: domain))
|
||||
@@ -38,7 +38,7 @@ struct TerminalSettingsRenderingTests {
|
||||
@Test(arguments: ["en", "zh-Hans", "zh-Hant"])
|
||||
func fontNamesPreviewTheirOwnFamilies(_ language: String) async throws {
|
||||
let view = TerminalFontOptions(
|
||||
family: .constant("Menlo"), families: ["Courier", "Menlo", "Monaco"], didSelect: {}
|
||||
family: .constant("Helvetica"), families: ["Courier", "Helvetica", "Menlo", "Monaco"], didSelect: {}
|
||||
)
|
||||
.environment(\.locale, Locale(identifier: language))
|
||||
.frame(width: 280, height: 320)
|
||||
|
||||
@@ -9,12 +9,91 @@ import Testing
|
||||
@Suite @MainActor
|
||||
struct TerminalFontRenderingTests {
|
||||
@Test(arguments: [false, true])
|
||||
func explicitTypographyOverridesCustomConfigurationForBothAppearances(_ isDark: Bool) {
|
||||
func preferredFontPrecedesThemeFallbacksAndManualSizeWins(_ isDark: Bool) {
|
||||
let configuration = TermindTerminalThemeCatalog.terminalConfiguration(
|
||||
themeName: isDark ? "Termind Dark" : "Termind Light",
|
||||
customEnabled: true, customText: "font-family = Courier\nfont-size = 30", isDark: isDark,
|
||||
customEnabled: true,
|
||||
customText: "font-family = Courier\nfont-family = Helvetica\nfont-size = 30", isDark: isDark,
|
||||
font: .init(family: "Menlo", size: 18, sizeFollowsTheme: false))
|
||||
#expect(configuration.rendered.hasSuffix("font-family = \nfont-family = Menlo\nfont-size = 18"))
|
||||
#expect(
|
||||
configuration.rendered.hasSuffix(
|
||||
"font-family = Menlo\nfont-family = Courier\nfont-family = Helvetica\nfont-size = 30\nfont-size = 18"))
|
||||
}
|
||||
|
||||
@Test(arguments: [false, true], ["", "\"\""])
|
||||
func themeResetsRetainPreferredFontAndDoNotRestoreClearedFallbacks(_ isDark: Bool, reset: String) {
|
||||
let text = """
|
||||
font-family = Courier
|
||||
font-family = \(reset)
|
||||
font-family = Helvetica
|
||||
font-family = \(reset)
|
||||
font-family = "Courier New"
|
||||
"""
|
||||
let preferences = TerminalPreferences(
|
||||
darkCustomEnabled: true, lightCustomEnabled: true,
|
||||
darkCustomConfiguration: text, lightCustomConfiguration: text)
|
||||
let state = TerminalViewState()
|
||||
state.applyPreferences(preferences, font: .init(family: "Menlo"))
|
||||
let configuration = isDark ? state.theme.dark : state.theme.light
|
||||
#expect(
|
||||
configuration.rendered.hasSuffix(
|
||||
"""
|
||||
font-family = Menlo
|
||||
font-family = Courier
|
||||
font-family = \(reset)
|
||||
font-family = Menlo
|
||||
font-family = Helvetica
|
||||
font-family = \(reset)
|
||||
font-family = Menlo
|
||||
font-family = "Courier New"
|
||||
"""))
|
||||
#expect(state.controller.lastConfigurationIssue == nil)
|
||||
}
|
||||
|
||||
@Test(arguments: [false, true], ["", "Missing Font"])
|
||||
func followingThemeOrUnavailableFontPreservesThemeEntries(_ isDark: Bool, family: String) {
|
||||
let text = "font-family = Courier\nfont-family = \"\"\nfont-family = Helvetica"
|
||||
let configuration = TermindTerminalThemeCatalog.terminalConfiguration(
|
||||
themeName: isDark ? "Termind Dark" : "Termind Light",
|
||||
customEnabled: true, customText: text, isDark: isDark,
|
||||
font: TerminalFontPreferences(family: family).resolvingFamily(in: ["Menlo"]))
|
||||
#expect(configuration.rendered.hasSuffix(text))
|
||||
#expect(fontFamilies(in: configuration) == ["Courier", "\"\"", "Helvetica"])
|
||||
}
|
||||
|
||||
@Test(arguments: [false, true])
|
||||
func disabledCustomConfigurationDoesNotContributeFallbacks(_ isDark: Bool) {
|
||||
let configuration = TermindTerminalThemeCatalog.terminalConfiguration(
|
||||
themeName: isDark ? "Termind Dark" : "Termind Light",
|
||||
customEnabled: false, customText: "font-family = Courier", isDark: isDark,
|
||||
font: .init(family: "Helvetica"))
|
||||
#expect(fontFamilies(in: configuration) == ["Helvetica"])
|
||||
}
|
||||
|
||||
@Test(arguments: [false, true])
|
||||
func themeWithoutFontFamiliesStillUsesPreferredFamily(_ isDark: Bool) {
|
||||
let configuration = TermindTerminalThemeCatalog.terminalConfiguration(
|
||||
themeName: isDark ? "Termind Dark" : "Termind Light",
|
||||
customEnabled: true, customText: "background = 123456\nfont-size = 30", isDark: isDark,
|
||||
font: .init(family: "Helvetica"))
|
||||
#expect(configuration.rendered.hasSuffix("font-family = Helvetica\nbackground = 123456\nfont-size = 30"))
|
||||
}
|
||||
|
||||
@Test func themeStylesOtherOptionsAndDuplicateFamiliesRemainUnchanged() {
|
||||
let text = """
|
||||
font-family = "Courier New"
|
||||
font-family-bold = Courier
|
||||
font-family-italic = Helvetica
|
||||
font-family-bold-italic = "Courier New"
|
||||
font-family = Menlo
|
||||
font-family = "Courier New"
|
||||
background = 123456
|
||||
"""
|
||||
let configuration = TermindTerminalThemeCatalog.terminalConfiguration(
|
||||
themeName: "Termind Dark", customEnabled: true, customText: text, isDark: true,
|
||||
font: .init(family: "Menlo"))
|
||||
#expect(configuration.rendered.hasSuffix("font-family = Menlo\n" + text))
|
||||
#expect(fontFamilies(in: configuration) == ["Menlo", "\"Courier New\"", "Menlo", "\"Courier New\""])
|
||||
}
|
||||
|
||||
@Test func familyAndSizeFollowThemeIndependently() {
|
||||
@@ -25,7 +104,9 @@ struct TerminalFontRenderingTests {
|
||||
).rendered
|
||||
}
|
||||
#expect(configuration(.init()).hasSuffix("font-family = Courier\nfont-size = 30"))
|
||||
#expect(configuration(.init(family: "Menlo")).hasSuffix("font-family = \nfont-family = Menlo"))
|
||||
#expect(
|
||||
configuration(.init(family: "Menlo"))
|
||||
.hasSuffix("font-family = Menlo\nfont-family = Courier\nfont-size = 30"))
|
||||
let manualSize = configuration(.init(size: 18, sizeFollowsTheme: false))
|
||||
#expect(manualSize.hasSuffix("font-size = 18"))
|
||||
#expect(!manualSize.contains("font-family = \n"))
|
||||
@@ -38,27 +119,27 @@ struct TerminalFontRenderingTests {
|
||||
state.applyPreferences(preferences, font: .init(family: "Menlo", size: 18, sizeFollowsTheme: false))
|
||||
#expect(state.controller === controller)
|
||||
#expect(state.controller.lastConfigurationIssue == nil)
|
||||
#expect(state.theme.dark.rendered.hasSuffix("font-family = \nfont-family = Menlo\nfont-size = 18"))
|
||||
#expect(state.theme.dark.rendered.hasSuffix("font-family = Menlo\nfont-family = Courier\nfont-size = 18"))
|
||||
state.applyPreferences(preferences)
|
||||
#expect(state.controller === controller)
|
||||
#expect(state.controller.lastConfigurationIssue == nil)
|
||||
#expect(state.theme.dark.rendered.hasSuffix("font-family = Courier"))
|
||||
}
|
||||
|
||||
@Test func fontCatalogIncludesEveryLoadableMonospacedFamilyInLocalizedOrder() {
|
||||
@Test func fontCatalogIncludesEveryLoadableFamilyInLocalizedOrder() {
|
||||
TerminalFontCatalog.shared.refresh()
|
||||
let catalogFamilies = TerminalFontCatalog.shared.families
|
||||
let manager = NSFontManager.shared
|
||||
|
||||
let fixedPitchFamilies = manager.availableFontFamilies.filter { family in
|
||||
let loadableFamilies = manager.availableFontFamilies.filter { family in
|
||||
NSFontDescriptor(fontAttributes: [.family: family])
|
||||
.matchingFontDescriptors(withMandatoryKeys: [.family])
|
||||
.contains { NSFont(descriptor: $0, size: 14)?.isFixedPitch == true }
|
||||
.contains { NSFont(descriptor: $0, size: 14) != nil }
|
||||
}
|
||||
.sorted { $0.localizedStandardCompare($1) == .orderedAscending }
|
||||
|
||||
#expect(!catalogFamilies.isEmpty)
|
||||
#expect(catalogFamilies == fixedPitchFamilies)
|
||||
#expect(catalogFamilies == loadableFamilies)
|
||||
|
||||
let proportionalFamily = manager.availableFontFamilies.first { family in
|
||||
let fonts = NSFontDescriptor(fontAttributes: [.family: family])
|
||||
@@ -68,8 +149,13 @@ struct TerminalFontRenderingTests {
|
||||
}
|
||||
#expect(proportionalFamily != nil)
|
||||
if let proportionalFamily {
|
||||
#expect(!catalogFamilies.contains(proportionalFamily))
|
||||
#expect(catalogFamilies.contains(proportionalFamily))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func fontFamilies(in configuration: GhosttyTerminal.TerminalConfiguration) -> [String] {
|
||||
TerminalCustomConfiguration(configuration.rendered).entries
|
||||
.filter { $0.key == "font-family" }.map(\.value)
|
||||
}
|
||||
}
|
||||
@@ -165,13 +165,20 @@ struct TerminalThemeBrowserTests {
|
||||
#expect(model.issue == nil)
|
||||
#expect(model.terminal.theme == lastValid)
|
||||
#expect(model.palette == lastPalette)
|
||||
#expect(
|
||||
model.update(
|
||||
themeName: name, text: "font-size = 30\nbackground = ABCDEF", isDark: isDark,
|
||||
font: .init(family: "Menlo", size: 18, sizeFollowsTheme: false)))
|
||||
let fontText = "font-family = Courier\nfont-family = Helvetica\nfont-size = 30\nbackground = ABCDEF"
|
||||
let font = TerminalFontPreferences(family: "Menlo", size: 18, sizeFollowsTheme: false)
|
||||
#expect(model.update(themeName: name, text: fontText, isDark: isDark, font: font))
|
||||
#expect(model.issue == nil)
|
||||
#expect(model.terminal.controller === controller)
|
||||
#expect(model.terminal.theme.dark.rendered.hasSuffix("font-family = \nfont-family = Menlo\nfont-size = 18"))
|
||||
#expect(model.terminal.theme.dark.rendered.hasSuffix("font-family = Menlo\n" + fontText + "\nfont-size = 18"))
|
||||
let state = TerminalViewState()
|
||||
state.applyPreferences(
|
||||
.init(
|
||||
darkTheme: name, lightTheme: name,
|
||||
darkCustomEnabled: true, lightCustomEnabled: true,
|
||||
darkCustomConfiguration: fontText, lightCustomConfiguration: fontText),
|
||||
font: font)
|
||||
#expect(model.terminal.theme.dark == (isDark ? state.theme.dark : state.theme.light))
|
||||
#expect(!model.terminal.isFocused)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user