noctalia-shell/Services/FontService.qml
2025-09-14 21:04:46 +02:00

194 lines
4.9 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 var fontconfigMonospaceFonts: []
// -------------------------------------------
function init() {
Logger.log("Font", "Service started")
loadFontconfigMonospaceFonts()
}
function loadFontconfigMonospaceFonts() {
fontconfigProcess.command = ["fc-list", ":mono", "family"]
fontconfigProcess.running = true
}
function loadSystemFonts() {
Logger.log("Font", "Loading system fonts...")
var fontFamilies = Qt.fontFamilies()
availableFonts.clear()
monospaceFonts.clear()
displayFonts.clear()
for (var i = 0; i < fontFamilies.length; i++) {
var fontName = fontFamilies[i]
if (fontName && fontName.trim() !== "") {
availableFonts.append({
"key": fontName,
"name": fontName
})
if (isMonospaceFont(fontName)) {
monospaceFonts.append({
"key": fontName,
"name": fontName
})
}
if (isDisplayFont(fontName)) {
displayFonts.append({
"key": fontName,
"name": fontName
})
}
}
}
sortModel(availableFonts)
sortModel(monospaceFonts)
sortModel(displayFonts)
if (monospaceFonts.count === 0) {
addFallbackFonts(monospaceFonts, ["DejaVu Sans Mono"])
}
if (displayFonts.count === 0) {
addFallbackFonts(displayFonts, ["Inter", "Roboto", "DejaVu Sans"])
}
fontsLoaded = true
Logger.log("Font", "Loaded", availableFonts.count, "fonts:", monospaceFonts.count, "monospace,", displayFonts.count, "display")
}
function isMonospaceFont(fontName) {
// First, check if fontconfig detected this as monospace
if (fontconfigMonospaceFonts.indexOf(fontName) !== -1) {
return true
}
// Minimal fallback: only check for basic monospace patterns
var lowerFontName = fontName.toLowerCase()
if (lowerFontName.includes("mono") || lowerFontName.includes("monospace")) {
return true
}
return false
}
function isDisplayFont(fontName) {
// Minimal fallback: only check for basic display patterns
var lowerFontName = fontName.toLowerCase()
if (lowerFontName.includes("display") || lowerFontName.includes("headline") || lowerFontName.includes("title")) {
return true
}
// Essential fallback fonts only
var essentialFonts = ["Inter", "Roboto", "DejaVu Sans"]
return essentialFonts.includes(fontName)
}
function sortModel(model) {
var fontsArray = []
for (var i = 0; i < model.count; i++) {
fontsArray.push({
"key": model.get(i).key,
"name": model.get(i).name
})
}
fontsArray.sort(function (a, b) {
return a.name.localeCompare(b.name)
})
model.clear()
for (var j = 0; j < fontsArray.length; j++) {
model.append(fontsArray[j])
}
}
function addFallbackFonts(model, fallbackFonts) {
for (var i = 0; i < fallbackFonts.length; i++) {
var fontName = fallbackFonts[i]
var exists = false
for (var j = 0; j < model.count; j++) {
if (model.get(j).name === fontName) {
exists = true
break
}
}
if (!exists) {
model.append({
"key": fontName,
"name": fontName
})
}
}
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')
fontconfigMonospaceFonts = []
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim()
if (line && line !== "") {
if (fontconfigMonospaceFonts.indexOf(line) === -1) {
fontconfigMonospaceFonts.push(line)
}
}
}
}
loadSystemFonts()
}
}
onExited: function (exitCode, exitStatus) {
if (exitCode !== 0) {
fontconfigMonospaceFonts = []
}
loadSystemFonts()
}
}
}