import QtQuick import Quickshell import Quickshell.Io import "../../../../Helpers/FuzzySort.js" as Fuzzysort import qs.Commons Item { property var launcher: null property string name: I18n.tr("plugins.applications") property bool handleSearch: true property var entries: [] // Persistent usage tracking stored in cacheDir property string usageFilePath: Settings.cacheDir + "launcher_app_usage.json" // Debounced saver to avoid excessive IO Timer { id: saveTimer interval: 750 repeat: false onTriggered: usageFile.writeAdapter() } FileView { id: usageFile path: usageFilePath printErrors: false watchChanges: false onLoadFailed: function (error) { if (error.toString().includes("No such file") || error === 2) { writeAdapter(); } } onAdapterUpdated: saveTimer.start() JsonAdapter { id: usageAdapter // key: app id/command, value: integer count property var counts: ({}) } } function init() { loadApplications(); } function onOpened() { // Refresh apps when launcher opens loadApplications(); } function loadApplications() { if (typeof DesktopEntries === 'undefined') { Logger.w("ApplicationsPlugin", "DesktopEntries service not available"); return; } const allApps = DesktopEntries.applications.values || []; entries = allApps.filter(app => app && !app.noDisplay).map(app => { // Add executable name property for search app.executableName = getExecutableName(app); return app; }); Logger.d("ApplicationsPlugin", `Loaded ${entries.length} applications`); } function getExecutableName(app) { if (!app) return ""; // Try to get executable name from command array if (app.command && Array.isArray(app.command) && app.command.length > 0) { const cmd = app.command[0]; // Extract just the executable name from the full path const parts = cmd.split('/'); const executable = parts[parts.length - 1]; // Remove any arguments or parameters return executable.split(' ')[0]; } // Try to get from exec property if available if (app.exec) { const parts = app.exec.split('/'); const executable = parts[parts.length - 1]; return executable.split(' ')[0]; } // Fallback to app id (desktop file name without .desktop) if (app.id) { return app.id.replace('.desktop', ''); } return ""; } function getResults(query) { if (!entries || entries.length === 0) return []; if (!query || query.trim() === "") { // Return all apps, optionally sorted by usage const favoriteApps = Settings.data.appLauncher.favoriteApps || []; let sorted; if (Settings.data.appLauncher.sortByMostUsed) { sorted = entries.slice().sort((a, b) => { // Favorites first const aFav = favoriteApps.includes(getAppKey(a)); const bFav = favoriteApps.includes(getAppKey(b)); if (aFav !== bFav) return aFav ? -1 : 1; const ua = getUsageCount(a); const ub = getUsageCount(b); if (ub !== ua) return ub - ua; return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()); }); } else { sorted = entries.slice().sort((a, b) => { const aFav = favoriteApps.includes(getAppKey(a)); const bFav = favoriteApps.includes(getAppKey(b)); if (aFav !== bFav) return aFav ? -1 : 1; return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()); }); } return sorted.map(app => createResultEntry(app)); } // Use fuzzy search if available, fallback to simple search if (typeof Fuzzysort !== 'undefined') { const fuzzyResults = Fuzzysort.go(query, entries, { "keys": ["name", "comment", "genericName", "executableName"], "threshold": -1000, "limit": 20 }); // Sort favorites first within fuzzy results while preserving fuzzysort order otherwise const favoriteApps = Settings.data.appLauncher.favoriteApps || []; const fav = []; const nonFav = []; for (const r of fuzzyResults) { const app = r.obj; if (favoriteApps.includes(getAppKey(app))) fav.push(r); else nonFav.push(r); } return fav.concat(nonFav).map(result => createResultEntry(result.obj)); } else { // Fallback to simple search const searchTerm = query.toLowerCase(); return entries.filter(app => { const name = (app.name || "").toLowerCase(); const comment = (app.comment || "").toLowerCase(); const generic = (app.genericName || "").toLowerCase(); const executable = getExecutableName(app).toLowerCase(); return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm) || executable.includes(searchTerm); }).sort((a, b) => { // Prioritize name matches, then executable matches const aName = a.name.toLowerCase(); const bName = b.name.toLowerCase(); const aExecutable = getExecutableName(a).toLowerCase(); const bExecutable = getExecutableName(b).toLowerCase(); const aStarts = aName.startsWith(searchTerm); const bStarts = bName.startsWith(searchTerm); const aExecStarts = aExecutable.startsWith(searchTerm); const bExecStarts = bExecutable.startsWith(searchTerm); // Prioritize name matches first if (aStarts && !bStarts) return -1; if (!aStarts && bStarts) return 1; // Then prioritize executable matches if (aExecStarts && !bExecStarts) return -1; if (!aExecStarts && bExecStarts) return 1; return aName.localeCompare(bName); }).slice(0, 20).map(app => createResultEntry(app)); } } function createResultEntry(app) { return { "appId": getAppKey(app), "name": app.name || "Unknown", "description": app.genericName || app.comment || "", "icon": app.icon || "application-x-executable", "isImage": false, "onActivate": function () { // Close the launcher/SmartPanel immediately without any animations. // Ensures we are not preventing the future focusing of the app launcher.close(); Logger.d("ApplicationsPlugin", `Launching: ${app.name}`); // Record usage and persist asynchronously if (Settings.data.appLauncher.sortByMostUsed) recordUsage(app); if (Settings.data.appLauncher.customLaunchPrefixEnabled && Settings.data.appLauncher.customLaunchPrefix) { // Use custom launch prefix const prefix = Settings.data.appLauncher.customLaunchPrefix.split(" "); if (app.runInTerminal) { const terminal = Settings.data.appLauncher.terminalCommand.split(" "); const command = prefix.concat(terminal.concat(app.command)); Quickshell.execDetached(command); } else { const command = prefix.concat(app.command); Quickshell.execDetached(command); } } else if (Settings.data.appLauncher.useApp2Unit && app.id) { Logger.d("ApplicationsPlugin", `Using app2unit for: ${app.id}`); if (app.runInTerminal) Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"]); else Quickshell.execDetached(["app2unit", "--"].concat(app.command)); } else { // Fallback logic when app2unit is not used if (app.runInTerminal) { // If app.execute() fails for terminal apps, we handle it manually. Logger.d("ApplicationsPlugin", "Executing terminal app manually: " + app.name); const terminal = Settings.data.appLauncher.terminalCommand.split(" "); const command = terminal.concat(app.command); Quickshell.execDetached(command); } else if (app.execute) { // Default execution for GUI apps app.execute(); } else { Logger.w("ApplicationsPlugin", `Could not launch: ${app.name}. No valid launch method.`); } } } }; } // ------------------------- // Usage tracking helpers function getAppKey(app) { if (app && app.id) return String(app.id); if (app && app.command && app.command.join) return app.command.join(" "); return String(app && app.name ? app.name : "unknown"); } function getUsageCount(app) { const key = getAppKey(app); const m = usageAdapter && usageAdapter.counts ? usageAdapter.counts : null; if (!m) return 0; const v = m[key]; return typeof v === 'number' && isFinite(v) ? v : 0; } function recordUsage(app) { const key = getAppKey(app); if (!usageAdapter.counts) usageAdapter.counts = ({}); const current = getUsageCount(app); usageAdapter.counts[key] = current + 1; // Trigger save via debounced timer saveTimer.restart(); } }