pragma Singleton import qs.modules.common import qs.modules.common.functions as Functions import Quickshell /** * - Eases fuzzy searching for applications by name * - Guesses icon name for window class name */ Singleton { id: root property bool sloppySearch: false // Default value, will be updated when Config is ready property real scoreThreshold: 0.2 // Update sloppySearch when Config becomes available Component.onCompleted: { if (Config && Config.options && Config.options.search) { sloppySearch = Config.options.search.sloppy || false } } property var substitutions: ({ "code-url-handler": "visual-studio-code", "Code": "visual-studio-code", "gnome-tweaks": "org.gnome.tweaks", "pavucontrol-qt": "pavucontrol", "wps": "wps-office2019-kprometheus", "wpsoffice": "wps-office2019-kprometheus", "footclient": "foot", "zen": "zen-browser", "brave-browser": "brave-desktop" }) property var regexSubstitutions: [ { "pattern": "^steam_app_(\\d+)$", "replace": "steam_icon_$1" }, { "pattern": "Minecraft.*", "replace": "minecraft" }, { "pattern": ".*polkit.*", "replace": "system-lock-screen" }, { "pattern": "gcr.prompter", "replace": "system-lock-screen" } ] readonly property list list: Array.from(DesktopEntries.applications.values) .sort((a, b) => a.name.localeCompare(b.name)) readonly property var preppedNames: list.map(a => ({ name: Functions.Fuzzy.prepare(`${a.name} `), entry: a })) readonly property var preppedIcons: list.map(a => ({ name: Functions.Fuzzy.prepare(`${a.icon} `), entry: a })) function fuzzyQuery(search: string): var { // Idk why list doesn't work if (root.sloppySearch) { const results = list.map(obj => ({ entry: obj, score: Functions.Levendist.computeScore(obj.name.toLowerCase(), search.toLowerCase()) })).filter(item => item.score > root.scoreThreshold) .sort((a, b) => b.score - a.score) return results .map(item => item.entry) } return Functions.Fuzzy.go(search, preppedNames, { all: true, key: "name" }).map(r => { return r.obj.entry }); } function iconExists(iconName) { if (!iconName || iconName.length == 0) return false; return (Quickshell.iconPath(iconName, true).length > 0) && !iconName.includes("image-missing"); } function getReverseDomainNameAppName(str) { return str.split('.').slice(-1)[0] } function getKebabNormalizedAppName(str) { return str.toLowerCase().replace(/\s+/g, "-"); } function guessIcon(str) { if (!str || str.length == 0) return "image-missing"; // Normal substitutions if (substitutions[str]) return substitutions[str]; if (substitutions[str.toLowerCase()]) return substitutions[str.toLowerCase()]; // Regex substitutions for (let i = 0; i < regexSubstitutions.length; i++) { const substitution = regexSubstitutions[i]; const regex = new RegExp(substitution.pattern); const replacedName = str.replace( regex, substitution.replace, ); if (replacedName != str) return replacedName; } // Icon exists -> return as is if (iconExists(str)) return str; // Simple guesses const lowercased = str.toLowerCase(); if (iconExists(lowercased)) return lowercased; const reverseDomainNameAppName = getReverseDomainNameAppName(str); if (iconExists(reverseDomainNameAppName)) return reverseDomainNameAppName; const lowercasedDomainNameAppName = reverseDomainNameAppName.toLowerCase(); if (iconExists(lowercasedDomainNameAppName)) return lowercasedDomainNameAppName; const kebabNormalizedGuess = getKebabNormalizedAppName(str); if (iconExists(kebabNormalizedGuess)) return kebabNormalizedGuess; // Search in desktop entries const iconSearchResults = Functions.Fuzzy.go(str, preppedIcons, { all: true, key: "name" }).map(r => { return r.obj.entry }); if (iconSearchResults.length > 0) { const guess = iconSearchResults[0].icon if (iconExists(guess)) return guess; } const nameSearchResults = root.fuzzyQuery(str); if (nameSearchResults.length > 0) { const guess = nameSearchResults[0].icon if (iconExists(guess)) return guess; } // Give up return str; } }