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