feat: init quickshell
This commit is contained in:
394
modules/desktop/quickshell/qml/Services/AppLauncherService.qml
Normal file
394
modules/desktop/quickshell/qml/Services/AppLauncherService.qml
Normal file
@@ -0,0 +1,394 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
// App launcher service - discovers and manages applications
|
||||
Item {
|
||||
id: appService
|
||||
|
||||
property var applications: []
|
||||
property bool isLoading: false
|
||||
|
||||
// Categories for apps
|
||||
property var categories: {
|
||||
"AudioVideo": "🎵",
|
||||
"Audio": "🎵",
|
||||
"Video": "🎬",
|
||||
"Development": "💻",
|
||||
"Education": "📚",
|
||||
"Game": "🎮",
|
||||
"Graphics": "🎨",
|
||||
"Network": "🌐",
|
||||
"Office": "📄",
|
||||
"Science": "🔬",
|
||||
"Settings": "⚙️",
|
||||
"System": "🔧",
|
||||
"Utility": "🛠️",
|
||||
"Other": "📦"
|
||||
}
|
||||
|
||||
property string userName: ""
|
||||
property string homeDirectory: ""
|
||||
property bool userInfoLoaded: false
|
||||
property var currentApp: ({})
|
||||
property var pendingSearchPaths: []
|
||||
|
||||
Component.onCompleted: {
|
||||
// First get user info, then load applications
|
||||
loadUserInfo();
|
||||
}
|
||||
|
||||
function loadUserInfo() {
|
||||
userNameProcess.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: userNameProcess
|
||||
command: ["whoami"]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => {
|
||||
if (line.trim()) {
|
||||
userName = line.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
// Now get home directory
|
||||
homeDirProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: homeDirProcess
|
||||
command: ["sh", "-c", "echo $HOME"]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => {
|
||||
if (line.trim()) {
|
||||
homeDirectory = line.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
// Now we have user info, start loading applications
|
||||
userInfoLoaded = true;
|
||||
loadApplications();
|
||||
}
|
||||
}
|
||||
|
||||
function loadApplications() {
|
||||
if (!userInfoLoaded) {
|
||||
console.log("User info not loaded yet, skipping application scan");
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
applications = [];
|
||||
|
||||
console.log("DEBUG: Starting application scan with user:", userName, "home:", homeDirectory);
|
||||
|
||||
// Comprehensive search paths for maximum Linux compatibility
|
||||
appService.pendingSearchPaths = [
|
||||
// User-specific locations (highest priority)
|
||||
homeDirectory + "/.local/share/applications/",
|
||||
|
||||
// Standard FreeDesktop.org locations
|
||||
"/usr/share/applications/", "/usr/local/share/applications/",
|
||||
|
||||
// Flatpak locations
|
||||
"/var/lib/flatpak/exports/share/applications/", homeDirectory + "/.local/share/flatpak/exports/share/applications/",
|
||||
|
||||
// Snap locations
|
||||
"/var/lib/snapd/desktop/applications/", "/snap/bin/",
|
||||
|
||||
// AppImage locations (common user directories)
|
||||
homeDirectory + "/Applications/", homeDirectory + "/AppImages/",
|
||||
|
||||
// Distribution-specific paths
|
||||
"/opt/*/share/applications/" // For manually installed software
|
||||
, "/usr/share/applications/kde4/" // KDE4 legacy
|
||||
|
||||
,
|
||||
|
||||
// NixOS-specific (will be ignored on non-NixOS systems)
|
||||
"/run/current-system/sw/share/applications/", "/etc/profiles/per-user/" + userName + "/share/applications/"];
|
||||
|
||||
console.log("DEBUG: Starting with essential paths:", JSON.stringify(appService.pendingSearchPaths));
|
||||
|
||||
// Add XDG and home-manager paths
|
||||
getXdgDataDirs.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getXdgDataDirs
|
||||
command: ["sh", "-c", "echo $XDG_DATA_DIRS"]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => {
|
||||
if (line.trim()) {
|
||||
var xdgDirs = line.trim().split(":");
|
||||
for (var i = 0; i < xdgDirs.length; i++) {
|
||||
if (xdgDirs[i].trim()) {
|
||||
var xdgPath = xdgDirs[i].trim() + "/applications/";
|
||||
if (appService.pendingSearchPaths.indexOf(xdgPath) === -1) {
|
||||
appService.pendingSearchPaths.push(xdgPath);
|
||||
console.log("DEBUG: Added XDG path:", xdgPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
// Now add home-manager path
|
||||
getHomeManagerPaths.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getHomeManagerPaths
|
||||
command: ["sh", "-c", "find /nix/store -maxdepth 1 -name '*home-manager-path*' -type d 2>/dev/null | head -1"]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => {
|
||||
if (line.trim()) {
|
||||
var homeManagerPath = line.trim() + "/share/applications/";
|
||||
appService.pendingSearchPaths.push(homeManagerPath);
|
||||
console.log("DEBUG: Added home-manager path:", homeManagerPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
// CRITICAL: Always ensure these essential directories are included
|
||||
var essentialPaths = ["/run/current-system/sw/share/applications/", "/usr/share/applications/", "/usr/local/share/applications/"];
|
||||
|
||||
for (var i = 0; i < essentialPaths.length; i++) {
|
||||
var path = essentialPaths[i];
|
||||
if (appService.pendingSearchPaths.indexOf(path) === -1) {
|
||||
appService.pendingSearchPaths.push(path);
|
||||
console.log("DEBUG: Added missing essential path:", path);
|
||||
}
|
||||
}
|
||||
|
||||
// Start bulk parsing with all paths including XDG and home-manager
|
||||
startBulkParsing(appService.pendingSearchPaths);
|
||||
}
|
||||
}
|
||||
|
||||
function startBulkParsing(searchPaths) {
|
||||
// BULLETPROOF: Ensure critical system directories are always included
|
||||
var criticalPaths = ["/run/current-system/sw/share/applications/", "/usr/share/applications/", "/usr/local/share/applications/"];
|
||||
|
||||
for (var i = 0; i < criticalPaths.length; i++) {
|
||||
var path = criticalPaths[i];
|
||||
if (searchPaths.indexOf(path) === -1) {
|
||||
searchPaths.push(path);
|
||||
console.log("DEBUG: BULLETPROOF: Added missing critical path:", path);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("DEBUG: Final directories to scan:", searchPaths.join(", "));
|
||||
|
||||
// Single command to parse all .desktop files at once
|
||||
// Only parse fields from the main [Desktop Entry] section, ignore [Desktop Action] sections
|
||||
var cmd = 'for dir in ' + searchPaths.map(p => "'" + p + "'").join(" ") + '; do ' + 'if [ -d "$dir" ]; then ' + 'find "$dir" -name "*.desktop" 2>/dev/null | while read file; do ' + 'echo "===FILE:$file"; ' + 'sed -n \'/^\\[Desktop Entry\\]/,/^\\[.*\\]/{/^\\[Desktop Entry\\]/d; /^\\[.*\\]/q; /^Name=/p; /^Exec=/p; /^Icon=/p; /^Comment=/p; /^Categories=/p; /^Hidden=/p; /^NoDisplay=/p}\' "$file" 2>/dev/null || true; ' + 'done; ' + 'fi; ' + 'done';
|
||||
|
||||
bulkParseProcess.command = ["sh", "-c", cmd];
|
||||
bulkParseProcess.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: bulkParseProcess
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => {
|
||||
if (line.startsWith("===FILE:")) {
|
||||
// Start of new file
|
||||
if (appService.currentApp.name && appService.currentApp.exec && !appService.currentApp.hidden && !appService.currentApp.noDisplay) {
|
||||
applications.push(appService.currentApp);
|
||||
}
|
||||
appService.currentApp = {
|
||||
name: "",
|
||||
exec: "",
|
||||
icon: "",
|
||||
comment: "",
|
||||
categories: [],
|
||||
hidden: false,
|
||||
noDisplay: false,
|
||||
filePath: line.substring(8) // Remove "===FILE:" prefix
|
||||
};
|
||||
} else if (line.startsWith("Name=")) {
|
||||
appService.currentApp.name = line.substring(5);
|
||||
} else if (line.startsWith("Exec=")) {
|
||||
appService.currentApp.exec = line.substring(5);
|
||||
} else if (line.startsWith("Icon=")) {
|
||||
appService.currentApp.icon = line.substring(5);
|
||||
} else if (line.startsWith("Comment=")) {
|
||||
appService.currentApp.comment = line.substring(8);
|
||||
} else if (line.startsWith("Categories=")) {
|
||||
appService.currentApp.categories = line.substring(11).split(";").filter(cat => cat.length > 0);
|
||||
} else if (line === "Hidden=true") {
|
||||
appService.currentApp.hidden = true;
|
||||
} else if (line === "NoDisplay=true") {
|
||||
appService.currentApp.noDisplay = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onStarted: {
|
||||
appService.currentApp = {};
|
||||
}
|
||||
|
||||
onExited: {
|
||||
// Process the last app
|
||||
if (appService.currentApp.name && appService.currentApp.exec && !appService.currentApp.hidden && !appService.currentApp.noDisplay) {
|
||||
applications.push(appService.currentApp);
|
||||
}
|
||||
|
||||
console.log("DEBUG: Before deduplication: Found", applications.length, "applications");
|
||||
|
||||
// Deduplicate applications - prefer user installations over system ones
|
||||
var uniqueApps = {};
|
||||
var finalApps = [];
|
||||
|
||||
for (var i = 0; i < applications.length; i++) {
|
||||
var app = applications[i];
|
||||
var key = app.name + "|" + app.exec.split(" ")[0]; // Use name + base command as key
|
||||
|
||||
if (!uniqueApps[key]) {
|
||||
// First occurrence of this app
|
||||
uniqueApps[key] = app;
|
||||
finalApps.push(app);
|
||||
} else {
|
||||
// Duplicate found - check if this version should replace the existing one
|
||||
var existing = uniqueApps[key];
|
||||
var shouldReplace = false;
|
||||
|
||||
// Priority order (higher priority replaces lower):
|
||||
// 1. User local applications (highest priority)
|
||||
// 2. Home-manager applications
|
||||
// 3. User profile applications
|
||||
// 4. System applications (lowest priority)
|
||||
|
||||
if (app.filePath.includes("/.local/share/applications/")) {
|
||||
shouldReplace = true; // User local always wins
|
||||
} else if (app.filePath.includes("home-manager-path") && !existing.filePath.includes("/.local/share/applications/")) {
|
||||
shouldReplace = true; // Home-manager beats system
|
||||
} else if (app.filePath.includes("/home/") && !existing.filePath.includes("/.local/share/applications/") && !existing.filePath.includes("home-manager-path")) {
|
||||
shouldReplace = true; // User profile beats system
|
||||
}
|
||||
|
||||
if (shouldReplace) {
|
||||
// Replace the existing app in finalApps array
|
||||
for (var j = 0; j < finalApps.length; j++) {
|
||||
if (finalApps[j] === existing) {
|
||||
finalApps[j] = app;
|
||||
uniqueApps[key] = app;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If not replacing, just ignore the duplicate
|
||||
}
|
||||
}
|
||||
|
||||
applications = finalApps;
|
||||
console.log("DEBUG: After deduplication: Found", applications.length, "unique applications");
|
||||
|
||||
isLoading = false;
|
||||
applicationsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
function launchApplication(app) {
|
||||
if (!app || !app.exec)
|
||||
return;
|
||||
|
||||
// Clean up the exec command (remove field codes like %f, %F, %u, %U)
|
||||
var cleanExec = app.exec.replace(/%[fFuU]/g, "").trim();
|
||||
|
||||
launchProcess.command = ["sh", "-c", cleanExec];
|
||||
launchProcess.running = true;
|
||||
|
||||
console.log("Launching:", app.name, "with command:", cleanExec);
|
||||
}
|
||||
|
||||
Process {
|
||||
id: launchProcess
|
||||
running: false
|
||||
|
||||
onExited: {
|
||||
if (exitCode !== 0) {
|
||||
console.log("Failed to launch application, exit code:", exitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fuzzy search function
|
||||
function fuzzySearch(query, apps) {
|
||||
if (!query || query.length === 0) {
|
||||
return apps;
|
||||
}
|
||||
|
||||
query = query.toLowerCase();
|
||||
|
||||
return apps.filter(app => {
|
||||
var searchText = (app.name + " " + app.comment).toLowerCase();
|
||||
|
||||
// Simple fuzzy matching - check if all characters of query appear in order
|
||||
var queryIndex = 0;
|
||||
for (var i = 0; i < searchText.length && queryIndex < query.length; i++) {
|
||||
if (searchText[i] === query[queryIndex]) {
|
||||
queryIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return queryIndex === query.length;
|
||||
}).sort((a, b) => {
|
||||
// Sort by relevance - exact matches first, then by name
|
||||
var aName = a.name.toLowerCase();
|
||||
var bName = b.name.toLowerCase();
|
||||
|
||||
var aExact = aName.includes(query);
|
||||
var bExact = bName.includes(query);
|
||||
|
||||
if (aExact && !bExact)
|
||||
return -1;
|
||||
if (!aExact && bExact)
|
||||
return 1;
|
||||
|
||||
return aName.localeCompare(bName);
|
||||
});
|
||||
}
|
||||
|
||||
function getCategoryIcon(app) {
|
||||
if (!app.categories || app.categories.length === 0) {
|
||||
return categories["Other"];
|
||||
}
|
||||
|
||||
// Find the first matching category
|
||||
for (var i = 0; i < app.categories.length; i++) {
|
||||
var category = app.categories[i];
|
||||
if (categories[category]) {
|
||||
return categories[category];
|
||||
}
|
||||
}
|
||||
|
||||
return categories["Other"];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user