Skip to content

Commit 721e378

Browse files
authored
Merge pull request #647 from nexxai/add-more-fonts
feat: support all monospaced fonts installed on the local system
2 parents e155af7 + 22aa476 commit 721e378

File tree

8 files changed

+132
-57
lines changed

8 files changed

+132
-57
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- Fix idle ping spin loop caused by exhausted AsyncStream iterator (#618)
2020
- Skip exact row count for large tables — use database statistics estimate (#519)
2121

22+
### Changed
23+
24+
- Theme font pickers now list installed monospaced fonts dynamically instead of a fixed built-in list
25+
2226
## [0.28.0] - 2026-04-07
2327

2428
### Added

TablePro/Models/Settings/EditorSettings.swift

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,52 +6,58 @@
66
import AppKit
77
import Foundation
88

9-
/// Available monospace fonts for the SQL editor
10-
enum EditorFont: String, Codable, CaseIterable, Identifiable {
11-
case systemMono = "System Mono"
12-
case sfMono = "SF Mono"
13-
case menlo = "Menlo"
14-
case monaco = "Monaco"
15-
case courierNew = "Courier New"
16-
17-
var id: String { rawValue }
18-
19-
var displayName: String { rawValue }
20-
21-
/// Get the actual NSFont for this option
22-
func font(size: CGFloat) -> NSFont {
23-
switch self {
24-
case .systemMono:
9+
internal struct FontFamilyOption: Equatable, Identifiable, Sendable {
10+
let id: String
11+
let displayName: String
12+
}
13+
14+
internal enum EditorFontResolver {
15+
static let systemMonoId = "System Mono"
16+
17+
static let availableMonospacedFamilies: [FontFamilyOption] = {
18+
var options: [FontFamilyOption] = [
19+
FontFamilyOption(id: systemMonoId, displayName: systemMonoId)
20+
]
21+
22+
let familyNames = NSFontManager.shared.availableFontFamilies
23+
.filter { $0 != systemMonoId }
24+
.filter(isMonospacedFamily)
25+
.sorted { lhs, rhs in
26+
lhs.localizedCaseInsensitiveCompare(rhs) == .orderedAscending
27+
}
28+
29+
var seen: Set<String> = [systemMonoId]
30+
for family in familyNames where !seen.contains(family) {
31+
seen.insert(family)
32+
options.append(FontFamilyOption(id: family, displayName: family))
33+
}
34+
35+
return options
36+
}()
37+
38+
static func resolve(familyId: String, size: CGFloat) -> NSFont {
39+
guard familyId != systemMonoId else {
2540
return NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
26-
case .sfMono:
27-
return NSFont(name: "SFMono-Regular", size: size)
28-
?? NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
29-
case .menlo:
30-
return NSFont(name: "Menlo", size: size)
31-
?? NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
32-
case .monaco:
33-
return NSFont(name: "Monaco", size: size)
34-
?? NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
35-
case .courierNew:
36-
return NSFont(name: "Courier New", size: size)
37-
?? NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
3841
}
39-
}
4042

41-
/// Check if this font is available on the system
42-
var isAvailable: Bool {
43-
switch self {
44-
case .systemMono:
45-
return true
46-
case .sfMono:
47-
return NSFont(name: "SFMono-Regular", size: 12) != nil
48-
case .menlo:
49-
return NSFont(name: "Menlo", size: 12) != nil
50-
case .monaco:
51-
return NSFont(name: "Monaco", size: 12) != nil
52-
case .courierNew:
53-
return NSFont(name: "Courier New", size: 12) != nil
43+
let descriptor = NSFontDescriptor(fontAttributes: [.family: familyId])
44+
if let font = NSFont(descriptor: descriptor, size: size),
45+
font.fontDescriptor.symbolicTraits.contains(.monoSpace) {
46+
return font
5447
}
48+
49+
return NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
50+
}
51+
52+
static func isAvailable(familyId: String) -> Bool {
53+
guard familyId != systemMonoId else { return true }
54+
return isMonospacedFamily(familyId)
55+
}
56+
57+
private static func isMonospacedFamily(_ familyId: String) -> Bool {
58+
let descriptor = NSFontDescriptor(fontAttributes: [.family: familyId])
59+
guard let font = NSFont(descriptor: descriptor, size: 12) else { return false }
60+
return font.fontDescriptor.symbolicTraits.contains(.monoSpace)
5561
}
5662
}
5763

TablePro/Theme/ThemeEngine.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ internal struct EditorFontCache {
3232
let scale = Self.computeAccessibilityScale()
3333
scaleFactor = scale
3434
let scaledSize = round(CGFloat(min(max(fonts.editorFontSize, 11), 18)) * scale)
35-
font = EditorFont(rawValue: fonts.editorFontFamily)?.font(size: scaledSize)
36-
?? NSFont.monospacedSystemFont(ofSize: scaledSize, weight: .regular)
35+
font = EditorFontResolver.resolve(familyId: fonts.editorFontFamily, size: scaledSize)
3736
let lineNumSize = max(round((scaledSize - 2)), 9)
3837
lineNumberFont = NSFont.monospacedSystemFont(ofSize: lineNumSize, weight: .regular)
3938
}
@@ -55,8 +54,7 @@ internal struct DataGridFontCacheResolved {
5554
init(from fonts: ThemeFonts) {
5655
let scale = EditorFontCache.computeAccessibilityScale()
5756
let scaledSize = round(CGFloat(min(max(fonts.dataGridFontSize, 10), 18)) * scale)
58-
regular = EditorFont(rawValue: fonts.dataGridFontFamily)?.font(size: scaledSize)
59-
?? NSFont.monospacedSystemFont(ofSize: scaledSize, weight: .regular)
57+
regular = EditorFontResolver.resolve(familyId: fonts.dataGridFontFamily, size: scaledSize)
6058
italic = regular.withTraits(.italic)
6159
medium = NSFontManager.shared.convert(regular, toHaveTrait: .boldFontMask)
6260
let rowNumSize = max(round(scaledSize - 1), 9)

TablePro/Views/Settings/Appearance/ThemeEditorFontsSection.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ struct ThemeEditorFontsSection: View {
7676
private var previewSection: some View {
7777
Section(String(localized: "Preview")) {
7878
let fonts = currentThemeFonts
79-
let editorFont = EditorFont(rawValue: fonts.editorFontFamily)?
80-
.font(size: CGFloat(fonts.editorFontSize))
81-
?? NSFont.monospacedSystemFont(ofSize: CGFloat(fonts.editorFontSize), weight: .regular)
79+
let editorFont = EditorFontResolver.resolve(
80+
familyId: fonts.editorFontFamily,
81+
size: CGFloat(fonts.editorFontSize)
82+
)
8283

8384
Text("SELECT * FROM users WHERE id = 42;")
8485
.font(Font(editorFont))
@@ -97,8 +98,8 @@ struct ThemeEditorFontsSection: View {
9798
get: { selection },
9899
set: { onChange($0) }
99100
)) {
100-
ForEach(EditorFont.allCases.filter(\.isAvailable)) { font in
101-
Text(font.displayName).tag(font.rawValue)
101+
ForEach(EditorFontResolver.availableMonospacedFamilies) { font in
102+
Text(font.displayName).tag(font.id)
102103
}
103104
}
104105
}

TableProTests/Theme/ThemeDefinitionTests.swift

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
// currentStatementHighlight field and Codable backward compatibility.
77
//
88

9+
import AppKit
910
import Foundation
1011
import Testing
1112
@testable import TablePro
1213

1314
@Suite("Theme Definition")
1415
struct ThemeDefinitionTests {
15-
1616
// MARK: - Default light theme
1717

1818
@Test("Default light editor colors include currentStatementHighlight")
@@ -134,4 +134,71 @@ struct ThemeDefinitionTests {
134134

135135
#expect(decoded.currentStatementHighlight == "#AABBCC")
136136
}
137+
138+
// MARK: - Editor font resolver
139+
140+
@Test("Font resolver always exposes System Mono")
141+
func resolverExposesSystemMono() {
142+
let families = EditorFontResolver.availableMonospacedFamilies
143+
#expect(families.contains { $0.id == EditorFontResolver.systemMonoId })
144+
}
145+
146+
@Test("System Mono is first in picker list")
147+
func systemMonoFirst() {
148+
let families = EditorFontResolver.availableMonospacedFamilies
149+
#expect(families.first?.id == EditorFontResolver.systemMonoId)
150+
}
151+
152+
@Test("Editor font cache falls back for unknown font family")
153+
func editorCacheFallsBackForUnknownFamily() {
154+
let fonts = ThemeFonts(
155+
editorFontFamily: "NoSuchFamily-XYZ",
156+
editorFontSize: 13,
157+
dataGridFontFamily: "System Mono",
158+
dataGridFontSize: 13
159+
)
160+
let cache = EditorFontCache(from: fonts)
161+
#expect(cache.font.pointSize > 0)
162+
}
163+
164+
@Test("Data grid cache falls back for unknown font family")
165+
func dataGridCacheFallsBackForUnknownFamily() {
166+
let fonts = ThemeFonts(
167+
editorFontFamily: "System Mono",
168+
editorFontSize: 13,
169+
dataGridFontFamily: "NoSuchFamily-XYZ",
170+
dataGridFontSize: 13
171+
)
172+
let cache = DataGridFontCacheResolved(from: fonts)
173+
#expect(cache.regular.pointSize > 0)
174+
#expect(cache.monoCharWidth > 0)
175+
}
176+
177+
@Test("Resolver list has unique IDs")
178+
func resolverListHasUniqueIds() {
179+
let ids = EditorFontResolver.availableMonospacedFamilies.map(\.id)
180+
#expect(Set(ids).count == ids.count)
181+
}
182+
183+
@Test("Unknown family reports unavailable")
184+
func unknownFamilyUnavailable() {
185+
#expect(EditorFontResolver.isAvailable(familyId: "NoSuchFamily-XYZ") == false)
186+
}
187+
188+
@Test("ThemeFonts decode keeps legacy family strings")
189+
func themeFontsDecodeKeepsLegacyStrings() throws {
190+
let json = #"{"editorFontFamily":"Menlo","editorFontSize":13,"dataGridFontFamily":"Monaco","dataGridFontSize":13}"#
191+
let decoded = try JSONDecoder().decode(ThemeFonts.self, from: Data(json.utf8))
192+
#expect(decoded.editorFontFamily == "Menlo")
193+
#expect(decoded.dataGridFontFamily == "Monaco")
194+
}
195+
196+
@Test("All resolver font families are monospaced")
197+
func allResolverFamiliesAreMonospaced() {
198+
let families = EditorFontResolver.availableMonospacedFamilies
199+
for family in families where family.id != EditorFontResolver.systemMonoId {
200+
let font = EditorFontResolver.resolve(familyId: family.id, size: 12)
201+
#expect(font.fontDescriptor.symbolicTraits.contains(.monoSpace))
202+
}
203+
}
137204
}

docs/customization/appearance.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ When you select a theme from the list, it becomes the preferred theme for that t
7676

7777
## Customizing Themes
7878

79-
Select a theme and edit directly. Built-in themes auto-duplicate when you change colors or layout, preserving the original. Font changes apply without duplication.
79+
Select a theme and edit directly. Built-in themes auto-duplicate when you change colors or layout, preserving the original. Font changes apply without duplication, and font family pickers list installed monospaced fonts from your Mac.
8080

8181
## Import, Export & Registry
8282

@@ -175,4 +175,3 @@ These appear on:
175175
alt="Database type colors"
176176
/>
177177
</Frame>
178-

docs/customization/editor-settings.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Configure the SQL editor in **Settings** > **Editor**.
2525

2626
### Font Family
2727

28-
Available monospace fonts:
28+
The font picker shows installed monospaced families on your Mac.
2929

3030
| Font | Description |
3131
|------|-------------|
@@ -39,7 +39,7 @@ Available monospace fonts:
3939
**System Mono** automatically uses the best available system monospace font.
4040
</Tip>
4141

42-
If a selected font isn't available, TablePro falls back to System Mono.
42+
If a saved font is no longer available, TablePro falls back to System Mono.
4343

4444
{/* Screenshot: Font family selector */}
4545
<Frame caption="Font family selector with available fonts">

docs/customization/settings.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ No personal information, queries, or database content is transmitted. Toggle off
155155

156156
| Setting | Default | Range | Description |
157157
|---------|---------|-------|-------------|
158-
| **Font family** | System Mono | System Mono, SF Mono, Menlo, Monaco, Courier New | Monospace font for data grid cells |
158+
| **Font family** | System Mono | Installed monospaced fonts (dynamic list) | Monospace font for data grid cells |
159159
| **Font size** | 13 pt | 10-18 pt | Text size in data grid cells |
160160

161161
Affects all data grid cells, including NULL placeholders and cell editing overlays. A live preview is shown in the settings panel.

0 commit comments

Comments
 (0)