mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
281 lines
7.0 KiB
QML
281 lines
7.0 KiB
QML
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()
|
|
}
|
|
}
|
|
}
|