pragma Singleton import QtQuick import QtQuick.Controls import Quickshell import Quickshell.Io import qs.Commons Singleton { id: root property ListModel availableFonts: ListModel {} property ListModel monospaceFonts: ListModel {} property ListModel displayFonts: ListModel {} property bool fontsLoaded: false property bool isLoading: false // Use objects for O(1) lookup instead of arrays property var fontconfigMonospaceFonts: ({}) // Cache for font classification to avoid repeated checks property var fontCache: ({}) // Chunk size for async processing readonly property int chunkSize: 100 // ------------------------------------------- function init() { Logger.i("Font", "Service started") loadFontconfigMonospaceFonts() } function loadFontconfigMonospaceFonts() { fontconfigProcess.command = ["fc-list", ":mono", "family"] fontconfigProcess.running = true } function loadSystemFonts() { if (isLoading) return Logger.d("Font", "Loading system fonts...") isLoading = true var fontFamilies = Qt.fontFamilies() // Pre-sort fonts before processing to ensure consistent order fontFamilies.sort(function (a, b) { return a.localeCompare(b) }) // Clear existing models availableFonts.clear() monospaceFonts.clear() displayFonts.clear() fontCache = {} // Process fonts in chunks to avoid blocking processFontsAsync(fontFamilies, 0) } function processFontsAsync(fontFamilies, startIndex) { var endIndex = Math.min(startIndex + chunkSize, fontFamilies.length) var hasMore = endIndex < fontFamilies.length // Batch arrays to append all at once (much faster than individual appends) var availableBatch = [] var monospaceBatch = [] var displayBatch = [] for (var i = startIndex; i < endIndex; i++) { var fontName = fontFamilies[i] if (!fontName || fontName.trim() === "") continue // Add to available fonts var fontObj = { "key": fontName, "name": fontName } availableBatch.push(fontObj) // Check monospace (with caching) if (isMonospaceFont(fontName)) { monospaceBatch.push(fontObj) } // Check display font (with caching) if (isDisplayFont(fontName)) { displayBatch.push(fontObj) } } // Batch append to models batchAppendToModel(availableFonts, availableBatch) batchAppendToModel(monospaceFonts, monospaceBatch) batchAppendToModel(displayFonts, displayBatch) if (hasMore) { // Continue processing in next frame Qt.callLater(function () { processFontsAsync(fontFamilies, endIndex) }) } else { // Finished loading all fonts finalizeFontLoading() } } function batchAppendToModel(model, items) { for (var i = 0; i < items.length; i++) { model.append(items[i]) } } function finalizeFontLoading() { // Add fallbacks if needed (models are already sorted) if (monospaceFonts.count === 0) { addFallbackFonts(monospaceFonts, ["DejaVu Sans Mono"]) } if (displayFonts.count === 0) { addFallbackFonts(displayFonts, ["Inter", "Roboto", "DejaVu Sans"]) } fontsLoaded = true isLoading = false Logger.d("Font", "Loaded", availableFonts.count, "fonts:", monospaceFonts.count, "monospace,", displayFonts.count, "display") } function isMonospaceFont(fontName) { // Check cache first if (fontCache.hasOwnProperty(fontName)) { return fontCache[fontName].isMonospace } var result = false // O(1) lookup using object instead of indexOf if (fontconfigMonospaceFonts.hasOwnProperty(fontName)) { result = true } else { // Fallback: check for basic monospace patterns var lowerFontName = fontName.toLowerCase() if (lowerFontName.includes("mono") || lowerFontName.includes("monospace")) { result = true } } // Cache the result if (!fontCache[fontName]) { fontCache[fontName] = {} } fontCache[fontName].isMonospace = result return result } function isDisplayFont(fontName) { // Check cache first if (fontCache.hasOwnProperty(fontName) && fontCache[fontName].hasOwnProperty('isDisplay')) { return fontCache[fontName].isDisplay } var result = false var lowerFontName = fontName.toLowerCase() if (lowerFontName.includes("display") || lowerFontName.includes("headline") || lowerFontName.includes("title")) { result = true } // Essential fallback fonts only var essentialFonts = ["Inter", "Roboto", "DejaVu Sans"] if (essentialFonts.indexOf(fontName) !== -1) { result = true } // Cache the result if (!fontCache[fontName]) { fontCache[fontName] = {} } fontCache[fontName].isDisplay = result return result } function sortModel(model) { // Convert to array var fontsArray = [] for (var i = 0; i < model.count; i++) { fontsArray.push({ "key": model.get(i).key, "name": model.get(i).name }) } // Sort fontsArray.sort(function (a, b) { return a.name.localeCompare(b.name) }) // Clear and rebuild model.clear() batchAppendToModel(model, fontsArray) } function addFallbackFonts(model, fallbackFonts) { // Build a set of existing fonts for O(1) lookup var existingFonts = {} for (var i = 0; i < model.count; i++) { existingFonts[model.get(i).name] = true } var toAdd = [] for (var j = 0; j < fallbackFonts.length; j++) { var fontName = fallbackFonts[j] if (!existingFonts[fontName]) { toAdd.push({ "key": fontName, "name": fontName }) } } if (toAdd.length > 0) { batchAppendToModel(model, toAdd) sortModel(model) } } function searchFonts(query) { if (!query || query.trim() === "") return availableFonts var results = [] var lowerQuery = query.toLowerCase() for (var i = 0; i < availableFonts.count; i++) { var font = availableFonts.get(i) if (font.name.toLowerCase().includes(lowerQuery)) { results.push(font) } } return results } // Process for fontconfig commands Process { id: fontconfigProcess running: false stdout: StdioCollector { onStreamFinished: { if (this.text !== "") { var lines = this.text.split('\n') // Use object for O(1) lookup instead of array var monospaceLookup = {} for (var i = 0; i < lines.length; i++) { var line = lines[i].trim() if (line && line !== "") { monospaceLookup[line] = true } } fontconfigMonospaceFonts = monospaceLookup } loadSystemFonts() } } onExited: function (exitCode, exitStatus) { if (exitCode !== 0) { fontconfigMonospaceFonts = {} } loadSystemFonts() } } }