summaryrefslogtreecommitdiff
path: root/services
diff options
context:
space:
mode:
authorlittleblack111 <littleblack11111@gmail.com>2025-07-29 19:41:13 +0800
committerlittleblack111 <littleblack11111@gmail.com>2025-07-29 19:41:13 +0800
commita657eab0468ef86b398e1612c2a3d5caf7328f1e (patch)
tree7d3822662f37c98f799ebf663affd62f94c9c893 /services
parent7be05c2696c3371caf29ac09dcc2022ccccb82fb (diff)
chore: move AppSearch to services
Diffstat (limited to 'services')
-rw-r--r--services/AppSearch.qml178
1 files changed, 178 insertions, 0 deletions
diff --git a/services/AppSearch.qml b/services/AppSearch.qml
new file mode 100644
index 0000000..afab48f
--- /dev/null
+++ b/services/AppSearch.qml
@@ -0,0 +1,178 @@
+pragma Singleton
+
+import "../utils/fuzzysort.js" as Fuzzy
+import "../utils/levendist.js" as Levendist
+import Quickshell
+import Quickshell.Io
+import qs.config
+
+/**
+ * - Eases fuzzy searching for applications by name
+ * - Guesses icon name for window class name
+ */
+Singleton {
+ id: root
+
+ property bool sloppySearch: General.sloppySearch ?? false
+ property real scoreThreshold: 0.2
+
+ 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: [
+ {
+ regex: /^steam_app_(\d+)$/,
+ replace: "steam_icon_$1"
+ },
+ {
+ regex: /Minecraft.*/,
+ replace: "minecraft"
+ },
+ {
+ regex: /.*polkit.*/,
+ replace: "system-lock-screen"
+ },
+ {
+ regex: /gcr.prompter/,
+ replace: "system-lock-screen"
+ }
+ ]
+
+ property var fuzzyQueryCache: ({})
+ property var iconExistsCache: ({})
+ property var guessIconCache: ({})
+
+ readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values).sort((a, b) => a.name.localeCompare(b.name))
+
+ readonly property var preppedNames: list.map(a => ({
+ name: Fuzzy.prepare(`${a.name} `),
+ entry: a
+ }))
+
+ /**
+ * Returns an Array<DesktopEntry> for a given search.
+ * Results are cached by exact search string.
+ */
+ function fuzzyQuery(search) {
+ if (root.fuzzyQueryCache[search]) {
+ return root.fuzzyQueryCache[search];
+ }
+
+ let results;
+ if (root.sloppySearch) {
+ results = list.map(obj => ({
+ entry: obj,
+ score: Levendist.computeScore(obj.name.toLowerCase(), search.toLowerCase())
+ })).filter(item => item.score > root.scoreThreshold).sort((a, b) => b.score - a.score).map(item => item.entry);
+ } else {
+ results = Fuzzy.go(search, preppedNames, {
+ all: true,
+ key: "name"
+ }).map(r => r.obj.entry);
+ }
+
+ root.fuzzyQueryCache[search] = results;
+ return results;
+ }
+
+ /**
+ * Caches whether an icon actually exists on disk.
+ */
+ function iconExists(iconName) {
+ if (!iconName || iconName.length === 0) {
+ return false;
+ }
+ if (root.iconExistsCache.hasOwnProperty(iconName)) {
+ return root.iconExistsCache[iconName];
+ }
+ const exists = Quickshell.iconPath(iconName, true).length > 0 && !iconName.includes("image-missing");
+ root.iconExistsCache[iconName] = exists;
+ return exists;
+ }
+
+ /**
+ * Heavy logic to guess an icon name. We memoize results by the
+ * exact input string.
+ */
+ function guessIcon(str) {
+ if (!str || str.length === 0) {
+ return "image-missing";
+ }
+ if (root.guessIconCache[str]) {
+ return root.guessIconCache[str];
+ }
+
+ let result;
+
+ // 1) direct substitution table
+ if (substitutions[str]) {
+ result = substitutions[str];
+ } else
+ // 2) regex rules
+ {
+ result = null;
+ for (let i = 0; i < regexSubstitutions.length; i++) {
+ const {
+ regex,
+ replace
+ } = regexSubstitutions[i];
+ const replaced = str.replace(regex, replace);
+ if (replaced !== str) {
+ result = replaced;
+ break;
+ }
+ }
+ }
+
+ // 3) if still not found, see if the original exists
+ if (!result) {
+ if (root.iconExists(str)) {
+ result = str;
+ }
+ }
+
+ // 4) try domain-reverse name
+ if (!result) {
+ let guessStr = str.split(".").slice(-1)[0].toLowerCase();
+ if (root.iconExists(guessStr)) {
+ result = guessStr;
+ }
+ }
+
+ // 5) kebab-case
+ if (!result) {
+ let guessStr = str.toLowerCase().replace(/\s+/g, "-");
+ if (root.iconExists(guessStr)) {
+ result = guessStr;
+ }
+ }
+
+ // 6) fallback to first fuzzy match’s icon
+ if (!result) {
+ const matches = root.fuzzyQuery(str);
+ if (matches.length > 0) {
+ const candidate = matches[0].icon;
+ if (root.iconExists(candidate)) {
+ result = candidate;
+ }
+ }
+ }
+
+ // 7) give up
+ if (!result) {
+ result = str;
+ }
+
+ root.guessIconCache[str] = result;
+ return result;
+ }
+}