From 2d2f3040b7e61ae12193e8304821e5bb7c8193aa Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 11 Jul 2025 20:08:51 -0400 Subject: [PATCH] App launcher fixes --- Services/AppSearchService.qml | 213 +++++++++++++ Services/LauncherService.qml | 20 ++ Services/PreferencesService.qml | 91 ++++++ Services/qmldir | 5 +- Widgets/AppLauncher.qml | 544 ++++++++++++-------------------- Widgets/AppLauncherButton.qml | 18 +- Widgets/SpotlightLauncher.qml | 200 +++--------- Widgets/TopBar.qml | 2 +- shell.qml | 2 - 9 files changed, 579 insertions(+), 516 deletions(-) create mode 100644 Services/AppSearchService.qml create mode 100644 Services/LauncherService.qml create mode 100644 Services/PreferencesService.qml diff --git a/Services/AppSearchService.qml b/Services/AppSearchService.qml new file mode 100644 index 00000000..290287b3 --- /dev/null +++ b/Services/AppSearchService.qml @@ -0,0 +1,213 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + id: root + + property list applications: [] + property var applicationsByName: ({}) + property var applicationsByExec: ({}) + property bool ready: false + + Component.onCompleted: { + loadApplications() + } + + function loadApplications() { + var allApps = Array.from(DesktopEntries.applications.values) + + // Debug: Check what properties are available + if (allApps.length > 0) { + var firstApp = allApps[0] + console.log("AppSearchService: Sample DesktopEntry properties:") + console.log(" name:", firstApp.name) + console.log(" id:", firstApp.id) + if (firstApp.exec !== undefined) console.log(" exec:", firstApp.exec) + if (firstApp.execString !== undefined) console.log(" execString:", firstApp.execString) + if (firstApp.executable !== undefined) console.log(" executable:", firstApp.executable) + if (firstApp.command !== undefined) console.log(" command:", firstApp.command) + } + + applications = allApps + .filter(app => !app.noDisplay) + .sort((a, b) => a.name.localeCompare(b.name)) + + // Build lookup maps + var byName = {} + var byExec = {} + + for (var i = 0; i < applications.length; i++) { + var app = applications[i] + byName[app.name.toLowerCase()] = app + + // Clean exec string for lookup + var execProp = app.execString || "" + var cleanExec = execProp ? execProp.replace(/%[fFuU]/g, "").trim() : "" + if (cleanExec) { + byExec[cleanExec] = app + } + } + + applicationsByName = byName + applicationsByExec = byExec + ready = true + + console.log("AppSearchService: Loaded", applications.length, "applications") + } + + function searchApplications(query) { + if (!query || query.length === 0) { + return applications + } + + var lowerQuery = query.toLowerCase() + var results = [] + + for (var i = 0; i < applications.length; i++) { + var app = applications[i] + var score = 0 + + // Check name + var nameLower = app.name.toLowerCase() + if (nameLower === lowerQuery) { + score = 1000 + } else if (nameLower.startsWith(lowerQuery)) { + score = 500 + } else if (nameLower.includes(lowerQuery)) { + score = 100 + } + + // Check comment/description + if (app.comment) { + var commentLower = app.comment.toLowerCase() + if (commentLower.includes(lowerQuery)) { + score += 50 + } + } + + // Check generic name + if (app.genericName) { + var genericLower = app.genericName.toLowerCase() + if (genericLower.includes(lowerQuery)) { + score += 25 + } + } + + // Check keywords + if (app.keywords && app.keywords.length > 0) { + for (var j = 0; j < app.keywords.length; j++) { + if (app.keywords[j].toLowerCase().includes(lowerQuery)) { + score += 10 + break + } + } + } + + if (score > 0) { + results.push({ + app: app, + score: score + }) + } + } + + // Sort by score descending + results.sort((a, b) => b.score - a.score) + + // Return just the apps + return results.map(r => r.app) + } + + function getAppByName(name) { + return applicationsByName[name.toLowerCase()] || null + } + + function getAppByExec(exec) { + var cleanExec = exec.replace(/%[fFuU]/g, "").trim() + return applicationsByExec[cleanExec] || null + } + + function getCategoriesForApp(app) { + if (!app || !app.categories) return [] + + var categoryMap = { + "AudioVideo": "Media", + "Audio": "Media", + "Video": "Media", + "Development": "Development", + "TextEditor": "Development", + "IDE": "Development", + "Education": "Education", + "Game": "Games", + "Graphics": "Graphics", + "Photography": "Graphics", + "Network": "Internet", + "WebBrowser": "Internet", + "Email": "Internet", + "Office": "Office", + "WordProcessor": "Office", + "Spreadsheet": "Office", + "Presentation": "Office", + "Science": "Science", + "Settings": "Settings", + "System": "System", + "Utility": "Utilities", + "Accessories": "Utilities", + "FileManager": "Utilities", + "TerminalEmulator": "Utilities" + } + + var mappedCategories = new Set() + + for (var i = 0; i < app.categories.length; i++) { + var cat = app.categories[i] + if (categoryMap[cat]) { + mappedCategories.add(categoryMap[cat]) + } + } + + return Array.from(mappedCategories) + } + + function getAllCategories() { + var categories = new Set(["All"]) + + for (var i = 0; i < applications.length; i++) { + var appCategories = getCategoriesForApp(applications[i]) + appCategories.forEach(cat => categories.add(cat)) + } + + return Array.from(categories).sort() + } + + function getAppsInCategory(category) { + if (category === "All") { + return applications + } + + return applications.filter(app => { + var appCategories = getCategoriesForApp(app) + return appCategories.includes(category) + }) + } + + function launchApp(app) { + if (!app) { + console.warn("AppSearchService: Cannot launch app, app is null") + return false + } + + // DesktopEntry objects have an execute() method + if (typeof app.execute === "function") { + app.execute() + return true + } + + console.warn("AppSearchService: Cannot launch app, no execute method") + return false + } +} \ No newline at end of file diff --git a/Services/LauncherService.qml b/Services/LauncherService.qml new file mode 100644 index 00000000..1c87134b --- /dev/null +++ b/Services/LauncherService.qml @@ -0,0 +1,20 @@ +import QtQuick +import Quickshell +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + id: root + + signal showAppLauncher() + signal hideAppLauncher() + signal toggleAppLauncher() + + signal showSpotlight() + signal hideSpotlight() + signal toggleSpotlight() + + signal showClipboardHistory() + signal hideClipboardHistory() + signal toggleClipboardHistory() +} \ No newline at end of file diff --git a/Services/PreferencesService.qml b/Services/PreferencesService.qml new file mode 100644 index 00000000..94d9e70a --- /dev/null +++ b/Services/PreferencesService.qml @@ -0,0 +1,91 @@ +import QtQuick +import Quickshell +import Quickshell.Io +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + id: root + + property string configDir: (Quickshell.env["XDG_CONFIG_HOME"] || Quickshell.env["HOME"] + "/.config") + "/DankMaterialShell" + property string recentAppsFile: configDir + "/recentApps.json" + property int maxRecentApps: 10 + property var recentApps: [] + + // Create config directory on startup + Process { + id: mkdirProcess + command: ["mkdir", "-p", root.configDir] + running: true + onExited: { + loadRecentApps() + } + } + + FileView { + id: recentAppsFileView + path: root.recentAppsFile + + onTextChanged: { + if (text && text.length > 0) { + try { + var data = JSON.parse(text) + if (Array.isArray(data)) { + root.recentApps = data + } + } catch (e) { + console.log("PreferencesService: Invalid recent apps format") + root.recentApps = [] + } + } + } + } + + function loadRecentApps() { + // FileView will automatically load and trigger onTextChanged + if (!recentAppsFileView.text || recentAppsFileView.text.length === 0) { + recentApps = [] + } + } + + function saveRecentApps() { + recentAppsFileView.text = JSON.stringify(recentApps, null, 2) + } + + function addRecentApp(app) { + if (!app) return + + var execProp = app.execString || "" + if (!execProp) return + + // Create a minimal app object to store + var appData = { + name: app.name, + exec: execProp, + icon: app.icon || "application-x-executable", + comment: app.comment || "" + } + + // Remove existing entry if present + recentApps = recentApps.filter(a => a.exec !== execProp) + + // Add to front + recentApps.unshift(appData) + + // Limit size + if (recentApps.length > maxRecentApps) { + recentApps = recentApps.slice(0, maxRecentApps) + } + + saveRecentApps() + } + + function getRecentApps() { + return recentApps + } + + function clearRecentApps() { + recentApps = [] + saveRecentApps() + } +} \ No newline at end of file diff --git a/Services/qmldir b/Services/qmldir index d3734d7e..d87166ac 100644 --- a/Services/qmldir +++ b/Services/qmldir @@ -8,4 +8,7 @@ singleton AudioService 1.0 AudioService.qml singleton BluetoothService 1.0 BluetoothService.qml singleton BrightnessService 1.0 BrightnessService.qml singleton BatteryService 1.0 BatteryService.qml -singleton SystemMonitorService 1.0 SystemMonitorService.qml \ No newline at end of file +singleton SystemMonitorService 1.0 SystemMonitorService.qml +singleton AppSearchService 1.0 AppSearchService.qml +singleton PreferencesService 1.0 PreferencesService.qml +singleton LauncherService 1.0 LauncherService.qml \ No newline at end of file diff --git a/Widgets/AppLauncher.qml b/Widgets/AppLauncher.qml index f145a947..8671c969 100644 --- a/Widgets/AppLauncher.qml +++ b/Widgets/AppLauncher.qml @@ -6,47 +6,13 @@ import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Io import "../Common" +import "../Services" -// Fixed version – icon loaders now swap to fallback components instead of showing the magenta checkerboard PanelWindow { id: launcher - property var theme property bool isVisible: false - // Default theme fallback - property var defaultTheme: QtObject { - property color primary: "#D0BCFF" - property color background: "#10121E" - property color surfaceContainer: "#1D1B20" - property color surfaceText: "#E6E0E9" - property color surfaceVariant: "#49454F" - property color surfaceVariantText: "#CAC4D0" - property color outline: "#938F99" - property real cornerRadius: 12 - property real cornerRadiusLarge: 16 - property real cornerRadiusXLarge: 24 - property real spacingXS: 4 - property real spacingS: 8 - property real spacingM: 12 - property real spacingL: 16 - property real spacingXL: 24 - property real fontSizeLarge: 16 - property real fontSizeMedium: 14 - property real fontSizeSmall: 12 - property real iconSize: 24 - property real iconSizeLarge: 32 - property real barHeight: 48 - property string iconFont: "Material Symbols Rounded" - property int iconFontWeight: Font.Normal - property int shortDuration: 150 - property int mediumDuration: 300 - property int standardEasing: Easing.OutCubic - property int emphasizedEasing: Easing.OutQuart - } - - property var activeTheme: theme || defaultTheme - // Full screen overlay setup for proper focus anchors { top: true @@ -64,44 +30,15 @@ PanelWindow { visible: isVisible color: "transparent" - // Enhanced app management - property var currentApp: ({}) - property var allApps: [] - property var categories: ["All"] + // App management + property var categories: AppSearchService.getAllCategories() property string selectedCategory: "All" property var recentApps: [] property var pinnedApps: ["firefox", "code", "terminal", "file-manager"] property bool showCategories: false property string viewMode: "list" // "list" or "grid" - property var appCategories: ({ - "AudioVideo": "Media", - "Audio": "Media", - "Video": "Media", - "Development": "Development", - "TextEditor": "Development", - "IDE": "Development", - "Programming": "Development", - "Education": "Education", - "Game": "Games", - "Graphics": "Graphics", - "Photography": "Graphics", - "Network": "Internet", - "WebBrowser": "Internet", - "Office": "Office", - "WordProcessor": "Office", - "Spreadsheet": "Office", - "Presentation": "Office", - "Science": "Science", - "Settings": "Settings", - "System": "System", - "Utility": "Utilities", - "Accessories": "Utilities", - "FileManager": "Utilities", - "TerminalEmulator": "Utilities" - }) ListModel { id: filteredModel } - ListModel { id: categoryModel } // Background dim with click to close Rectangle { @@ -112,8 +49,8 @@ PanelWindow { Behavior on opacity { NumberAnimation { - duration: activeTheme.shortDuration - easing.type: activeTheme.standardEasing + duration: Theme.shortDuration + easing.type: Theme.standardEasing } } @@ -124,187 +61,94 @@ PanelWindow { } } - // Desktop applications scanning - Process { - id: desktopScanner - command: ["sh", "-c", ` - for dir in "/usr/share/applications/" "/usr/local/share/applications/" "$HOME/.local/share/applications/" "/run/current-system/sw/share/applications/"; 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; /^Hidden=/p; /^NoDisplay=/p; /^Categories=/p; /^Comment=/p}' "$file" 2>/dev/null || true - done - fi - done - `] - - stdout: SplitParser { - splitMarker: "\n" - onRead: (line) => { - if (line.startsWith("===FILE:")) { - // Save previous app if valid - if (currentApp.name && currentApp.exec && !currentApp.hidden && !currentApp.noDisplay) { - allApps.push({ - name: currentApp.name, - exec: currentApp.exec, - icon: currentApp.icon || "application-x-executable", - comment: currentApp.comment || "", - categories: currentApp.categories || [] - }) - } - // Start new app - currentApp = { name: "", exec: "", icon: "", comment: "", categories: [], hidden: false, noDisplay: false } - } else if (line.startsWith("Name=")) { - currentApp.name = line.substring(5) - } else if (line.startsWith("Exec=")) { - currentApp.exec = line.substring(5) - } else if (line.startsWith("Icon=")) { - currentApp.icon = line.substring(5) - } else if (line.startsWith("Comment=")) { - currentApp.comment = line.substring(8) - } else if (line.startsWith("Categories=")) { - currentApp.categories = line.substring(11).split(";").filter(cat => cat.length > 0) - } else if (line === "Hidden=true") { - currentApp.hidden = true - } else if (line === "NoDisplay=true") { - currentApp.noDisplay = true - } + Connections { + target: AppSearchService + function onReadyChanged() { + if (AppSearchService.ready) { + categories = AppSearchService.getAllCategories() + updateFilteredModel() } } - - onExited: { - // Save last app - if (currentApp.name && currentApp.exec && !currentApp.hidden && !currentApp.noDisplay) { - allApps.push({ - name: currentApp.name, - exec: currentApp.exec, - icon: currentApp.icon || "application-x-executable", - comment: currentApp.comment || "", - categories: currentApp.categories || [] - }) - } - - // Extract unique categories - let uniqueCategories = new Set(["All"]) - allApps.forEach(app => { - app.categories.forEach(cat => { - if (appCategories[cat]) { - uniqueCategories.add(appCategories[cat]) - } - }) - }) - categories = Array.from(uniqueCategories) - - console.log("Loaded", allApps.length, "applications with", categories.length, "categories") - updateFilteredModel() + } + + Connections { + target: LauncherService + function onShowAppLauncher() { + launcher.show() + } + function onHideAppLauncher() { + launcher.hide() + } + function onToggleAppLauncher() { + launcher.toggle() } } function updateFilteredModel() { filteredModel.clear() - let apps = allApps + var apps = [] - // Filter by category - if (selectedCategory !== "All") { - apps = apps.filter(app => { - return app.categories.some(cat => appCategories[cat] === selectedCategory) - }) - } - - // Filter by search + // Get apps based on category and search if (searchField.text.length > 0) { - const query = searchField.text.toLowerCase() - apps = apps.filter(app => { - return app.name.toLowerCase().includes(query) || - (app.comment && app.comment.toLowerCase().includes(query)) - }).sort((a, b) => { - // Sort by relevance - const aName = a.name.toLowerCase() - const bName = b.name.toLowerCase() - const aStartsWith = aName.startsWith(query) - const bStartsWith = bName.startsWith(query) - - if (aStartsWith && !bStartsWith) return -1 - if (!aStartsWith && bStartsWith) return 1 - return aName.localeCompare(bName) - }) - } - - // Sort alphabetically if no search - if (searchField.text.length === 0) { - apps.sort((a, b) => a.name.localeCompare(b.name)) + // Search across all apps or category + var baseApps = selectedCategory === "All" ? + AppSearchService.applications : + AppSearchService.getAppsInCategory(selectedCategory) + apps = AppSearchService.searchApplications(searchField.text).filter(app => + baseApps.includes(app) + ) + } else { + // Just category filter + apps = AppSearchService.getAppsInCategory(selectedCategory) } // Add to model apps.forEach(app => { - filteredModel.append(app) - }) - } - - /* ---------------------------------------------------------------------------- - * LOADER UTILITIES - * ---------------------------------------------------------------------------- */ - /** Returns an IconImage component or the fallback badge depending on availability. */ - function makeIconLoader(iconName, appName, fallbackId) { - return Qt.createComponent("", { - "anchors.fill": parent, - "_iconName": iconName, - "_appName": appName, - "sourceComponent": iconComponent + filteredModel.append({ + name: app.name, + exec: app.execString || "", + icon: app.icon || "application-x-executable", + comment: app.comment || "", + categories: app.categories || [], + desktopEntry: app + }) }) } Component { id: iconComponent - IconImage { - id: img - anchors.fill: parent - source: _iconName ? Quickshell.iconPath(_iconName, "") : "" - smooth: true - asynchronous: true + Item { + property var appData: parent.modelData || {} - onStatusChanged: { - // Image.Null = 0, Image.Ready = 1, Image.Loading = 2, Image.Error = 3 - if (status === Image.Error || - status === Image.Null || - (!source && _iconName)) { - // defer the swap to avoid re‑entrancy in Loader - Qt.callLater(() => img.parent.sourceComponent = fallbackComponent) - } + IconImage { + id: iconImg + anchors.fill: parent + source: appData.icon ? Quickshell.iconPath(appData.icon, "") : "" + smooth: true + asynchronous: true + visible: status === Image.Ready } - // Add timeout fallback for stuck loading icons - Timer { - interval: 3000 // 3 second timeout - running: img.status === Image.Loading - onTriggered: { - if (img.status === Image.Loading) { - Qt.callLater(() => img.parent.sourceComponent = fallbackComponent) - } + Rectangle { + anchors.fill: parent + visible: !iconImg.visible + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.10) + radius: Theme.cornerRadiusLarge + border.width: 1 + border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.20) + + Text { + anchors.centerIn: parent + text: appData.name ? appData.name.charAt(0).toUpperCase() : "A" + font.pixelSize: 28 + color: Theme.primary + font.weight: Font.Bold } } } } - Component { - id: fallbackComponent - Rectangle { - color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.10) - radius: activeTheme.cornerRadiusLarge - border.width: 1 - border.color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.20) - - Text { - anchors.centerIn: parent - text: _appName ? _appName.charAt(0).toUpperCase() : "A" - font.pixelSize: 28 - color: activeTheme.primary - font.weight: Font.Bold - } - } - } - // Main launcher panel with enhanced design Rectangle { id: launcherPanel @@ -316,11 +160,11 @@ PanelWindow { top: parent.top left: parent.left topMargin: 50 - leftMargin: activeTheme.spacingL + leftMargin: Theme.spacingL } - color: Qt.rgba(activeTheme.surfaceContainer.r, activeTheme.surfaceContainer.g, activeTheme.surfaceContainer.b, 0.98) - radius: activeTheme.cornerRadiusXLarge + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) + radius: Theme.cornerRadiusXLarge // Material 3 elevation with multiple layers Rectangle { @@ -346,7 +190,7 @@ PanelWindow { Rectangle { anchors.fill: parent color: "transparent" - border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.12) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.width: 1 radius: parent.radius z: -1 @@ -363,7 +207,7 @@ PanelWindow { Behavior on xScale { NumberAnimation { - duration: activeTheme.mediumDuration + duration: Theme.mediumDuration easing.type: Easing.OutBack easing.overshoot: 1.2 } @@ -371,7 +215,7 @@ PanelWindow { Behavior on yScale { NumberAnimation { - duration: activeTheme.mediumDuration + duration: Theme.mediumDuration easing.type: Easing.OutBack easing.overshoot: 1.2 } @@ -384,15 +228,15 @@ PanelWindow { Behavior on x { NumberAnimation { - duration: activeTheme.mediumDuration - easing.type: activeTheme.emphasizedEasing + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing } } Behavior on y { NumberAnimation { - duration: activeTheme.mediumDuration - easing.type: activeTheme.emphasizedEasing + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing } } } @@ -402,8 +246,8 @@ PanelWindow { Behavior on opacity { NumberAnimation { - duration: activeTheme.mediumDuration - easing.type: activeTheme.emphasizedEasing + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing } } @@ -422,8 +266,8 @@ PanelWindow { Column { anchors.fill: parent - anchors.margins: activeTheme.spacingXL - spacing: activeTheme.spacingL + anchors.margins: Theme.spacingXL + spacing: Theme.spacingL // Header section Row { @@ -434,9 +278,9 @@ PanelWindow { Text { anchors.verticalCenter: parent.verticalCenter text: "Applications" - font.pixelSize: activeTheme.fontSizeLarge + 4 + font.pixelSize: Theme.fontSizeLarge + 4 font.weight: Font.Bold - color: activeTheme.surfaceText + color: Theme.surfaceText } Item { width: parent.width - 200; height: 1 } @@ -445,8 +289,8 @@ PanelWindow { Text { anchors.verticalCenter: parent.verticalCenter text: filteredModel.count + " apps" - font.pixelSize: activeTheme.fontSizeMedium - color: activeTheme.surfaceVariantText + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText } } @@ -455,42 +299,42 @@ PanelWindow { id: searchContainer width: parent.width height: 52 - radius: activeTheme.cornerRadiusLarge - color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.6) + radius: Theme.cornerRadiusLarge + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.6) border.width: searchField.activeFocus ? 2 : 1 - border.color: searchField.activeFocus ? activeTheme.primary : - Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.3) + border.color: searchField.activeFocus ? Theme.primary : + Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) Behavior on border.color { ColorAnimation { - duration: activeTheme.shortDuration - easing.type: activeTheme.standardEasing + duration: Theme.shortDuration + easing.type: Theme.standardEasing } } Row { anchors.fill: parent - anchors.leftMargin: activeTheme.spacingL - anchors.rightMargin: activeTheme.spacingL - spacing: activeTheme.spacingM + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + spacing: Theme.spacingM Text { anchors.verticalCenter: parent.verticalCenter text: "search" - font.family: activeTheme.iconFont - font.pixelSize: activeTheme.iconSize - color: searchField.activeFocus ? activeTheme.primary : activeTheme.surfaceVariantText - font.weight: activeTheme.iconFontWeight + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize + color: searchField.activeFocus ? Theme.primary : Theme.surfaceVariantText + font.weight: Theme.iconFontWeight } TextInput { id: searchField anchors.verticalCenter: parent.verticalCenter - width: parent.width - parent.spacing - activeTheme.iconSize - 32 - height: parent.height - activeTheme.spacingS + width: parent.width - parent.spacing - Theme.iconSize - 32 + height: parent.height - Theme.spacingS - color: activeTheme.surfaceText - font.pixelSize: activeTheme.fontSizeLarge + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeLarge verticalAlignment: TextInput.AlignVCenter focus: launcher.isVisible @@ -501,8 +345,8 @@ PanelWindow { Text { anchors.verticalCenter: parent.verticalCenter text: "Search applications..." - color: activeTheme.surfaceVariantText - font.pixelSize: activeTheme.fontSizeLarge + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeLarge visible: searchField.text.length === 0 && !searchField.activeFocus } @@ -511,7 +355,7 @@ PanelWindow { width: 24 height: 24 radius: 12 - color: clearSearchArea.containsMouse ? Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.12) : "transparent" + color: clearSearchArea.containsMouse ? Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) : "transparent" anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter visible: searchField.text.length > 0 @@ -519,9 +363,9 @@ PanelWindow { Text { anchors.centerIn: parent text: "close" - font.family: activeTheme.iconFont + font.family: Theme.iconFont font.pixelSize: 16 - color: clearSearchArea.containsMouse ? activeTheme.outline : activeTheme.surfaceVariantText + color: clearSearchArea.containsMouse ? Theme.outline : Theme.surfaceVariantText } MouseArea { @@ -537,7 +381,12 @@ PanelWindow { Keys.onPressed: function (event) { if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && filteredModel.count) { - launcher.launchApp(filteredModel.get(0).exec) + var firstApp = filteredModel.get(0) + if (firstApp.desktopEntry) { + AppSearchService.launchApp(firstApp.desktopEntry) + } else { + launcher.launchApp(firstApp.exec) + } launcher.hide() event.accepted = true } else if (event.key === Qt.Key_Escape) { @@ -553,36 +402,36 @@ PanelWindow { Row { width: parent.width height: 40 - spacing: activeTheme.spacingM + spacing: Theme.spacingM visible: searchField.text.length === 0 // Category filter Rectangle { width: 200 height: 36 - radius: activeTheme.cornerRadius - color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3) - border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2) + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 1 Row { anchors.left: parent.left - anchors.leftMargin: activeTheme.spacingM + anchors.leftMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter - spacing: activeTheme.spacingS + spacing: Theme.spacingS Text { text: "category" - font.family: activeTheme.iconFont + font.family: Theme.iconFont font.pixelSize: 18 - color: activeTheme.surfaceVariantText + color: Theme.surfaceVariantText anchors.verticalCenter: parent.verticalCenter } Text { text: selectedCategory - font.pixelSize: activeTheme.fontSizeMedium - color: activeTheme.surfaceText + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter font.weight: Font.Medium } @@ -590,12 +439,12 @@ PanelWindow { Text { anchors.right: parent.right - anchors.rightMargin: activeTheme.spacingM + anchors.rightMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter text: showCategories ? "expand_less" : "expand_more" - font.family: activeTheme.iconFont + font.family: Theme.iconFont font.pixelSize: 18 - color: activeTheme.surfaceVariantText + color: Theme.surfaceVariantText } MouseArea { @@ -617,16 +466,16 @@ PanelWindow { Rectangle { width: 36 height: 36 - radius: activeTheme.cornerRadius - color: viewMode === "list" ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) : - listViewArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.08) : "transparent" + radius: Theme.cornerRadius + color: viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : + listViewArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent" Text { anchors.centerIn: parent text: "view_list" - font.family: activeTheme.iconFont + font.family: Theme.iconFont font.pixelSize: 20 - color: viewMode === "list" ? activeTheme.primary : activeTheme.surfaceText + color: viewMode === "list" ? Theme.primary : Theme.surfaceText } MouseArea { @@ -642,16 +491,16 @@ PanelWindow { Rectangle { width: 36 height: 36 - radius: activeTheme.cornerRadius - color: viewMode === "grid" ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) : - gridViewArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.08) : "transparent" + radius: Theme.cornerRadius + color: viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : + gridViewArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent" Text { anchors.centerIn: parent text: "grid_view" - font.family: activeTheme.iconFont + font.family: Theme.iconFont font.pixelSize: 20 - color: viewMode === "grid" ? activeTheme.primary : activeTheme.surfaceText + color: viewMode === "grid" ? Theme.primary : Theme.surfaceText } MouseArea { @@ -682,8 +531,8 @@ PanelWindow { ListView { id: appList width: parent.width - anchors.margins: activeTheme.spacingS - spacing: activeTheme.spacingS + anchors.margins: Theme.spacingS + spacing: Theme.spacingS model: filteredModel delegate: listDelegate @@ -701,7 +550,7 @@ PanelWindow { GridView { id: appGrid width: parent.width - anchors.margins: activeTheme.spacingS + anchors.margins: Theme.spacingS // Responsive cell sizes based on screen width property int baseCellWidth: Math.max(100, Math.min(140, width / 8)) @@ -713,7 +562,7 @@ PanelWindow { // Center the grid content property int columnsCount: Math.floor(width / cellWidth) property int remainingSpace: width - (columnsCount * cellWidth) - leftMargin: Math.max(activeTheme.spacingS, remainingSpace / 2) + leftMargin: Math.max(Theme.spacingS, remainingSpace / 2) rightMargin: leftMargin model: filteredModel @@ -726,10 +575,10 @@ PanelWindow { Rectangle { id: categoryDropdown width: 200 - height: Math.min(250, categories.length * 40 + activeTheme.spacingM * 2) - radius: activeTheme.cornerRadiusLarge - color: activeTheme.surfaceContainer - border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2) + height: Math.min(250, categories.length * 40 + Theme.spacingM * 2) + radius: Theme.cornerRadiusLarge + color: Theme.surfaceContainer + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 1 visible: showCategories z: 1000 @@ -757,7 +606,7 @@ PanelWindow { ScrollView { anchors.fill: parent - anchors.margins: activeTheme.spacingS + anchors.margins: Theme.spacingS clip: true ScrollBar.vertical.policy: ScrollBar.AsNeeded ScrollBar.horizontal.policy: ScrollBar.AlwaysOff @@ -769,16 +618,16 @@ PanelWindow { delegate: Rectangle { width: ListView.view.width height: 36 - radius: activeTheme.cornerRadiusSmall - color: catArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) : "transparent" + radius: Theme.cornerRadiusSmall + color: catArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" Text { anchors.left: parent.left - anchors.leftMargin: activeTheme.spacingM + anchors.leftMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter text: modelData - font.pixelSize: activeTheme.fontSizeMedium - color: selectedCategory === modelData ? activeTheme.primary : activeTheme.surfaceText + font.pixelSize: Theme.fontSizeMedium + color: selectedCategory === modelData ? Theme.primary : Theme.surfaceText font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal } @@ -807,16 +656,16 @@ PanelWindow { Rectangle { width: appList.width height: 72 - radius: activeTheme.cornerRadiusLarge - color: appMouseArea.hovered ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) - : Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.03) - border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08) + radius: Theme.cornerRadiusLarge + color: appMouseArea.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) + : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.width: 1 Row { anchors.fill: parent - anchors.margins: activeTheme.spacingM - spacing: activeTheme.spacingL + anchors.margins: Theme.spacingM + spacing: Theme.spacingL Item { width: 56 @@ -826,22 +675,21 @@ PanelWindow { Loader { id: listIconLoader anchors.fill: parent - property string _iconName: model.icon - property string _appName: model.name + property var modelData: model sourceComponent: iconComponent } } Column { anchors.verticalCenter: parent.verticalCenter - width: parent.width - 56 - activeTheme.spacingL - spacing: activeTheme.spacingXS + width: parent.width - 56 - Theme.spacingL + spacing: Theme.spacingXS Text { width: parent.width text: model.name - font.pixelSize: activeTheme.fontSizeLarge - color: activeTheme.surfaceText + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText font.weight: Font.Medium elide: Text.ElideRight } @@ -849,8 +697,8 @@ PanelWindow { Text { width: parent.width text: model.comment || "Application" - font.pixelSize: activeTheme.fontSizeMedium - color: activeTheme.surfaceVariantText + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText elide: Text.ElideRight visible: model.comment && model.comment.length > 0 } @@ -863,7 +711,11 @@ PanelWindow { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - launcher.launchApp(model.exec) + if (model.desktopEntry) { + AppSearchService.launchApp(model.desktopEntry) + } else { + launcher.launchApp(model.exec) + } launcher.hide() } } @@ -876,15 +728,15 @@ PanelWindow { Rectangle { width: appGrid.cellWidth - 8 height: appGrid.cellHeight - 8 - radius: activeTheme.cornerRadiusLarge - color: gridAppArea.hovered ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) - : Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.03) - border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08) + radius: Theme.cornerRadiusLarge + color: gridAppArea.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) + : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.width: 1 Column { anchors.centerIn: parent - spacing: activeTheme.spacingS + spacing: Theme.spacingS Item { property int iconSize: Math.min(56, Math.max(32, appGrid.cellWidth * 0.6)) @@ -895,8 +747,7 @@ PanelWindow { Loader { id: gridIconLoader anchors.fill: parent - property string _iconName: model.icon - property string _appName: model.name + property var modelData: model sourceComponent: iconComponent } } @@ -905,8 +756,8 @@ PanelWindow { anchors.horizontalCenter: parent.horizontalCenter width: 88 text: model.name - font.pixelSize: activeTheme.fontSizeSmall - color: activeTheme.surfaceText + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText font.weight: Font.Medium elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter @@ -921,38 +772,28 @@ PanelWindow { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - launcher.launchApp(model.exec) + if (model.desktopEntry) { + AppSearchService.launchApp(model.desktopEntry) + } else { + launcher.launchApp(model.exec) + } launcher.hide() } } } } - Process { - id: appLauncher - - function start(exec) { - // Clean up exec command (remove field codes) - var cleanExec = exec.replace(/%[fFuU]/g, "").trim() - console.log("Launching app - Original:", exec, "Cleaned:", cleanExec) - - // Use setsid to fully detach from shell session - command = ["setsid", "sh", "-c", cleanExec] - running = true - } - - onExited: (exitCode) => { - if (exitCode !== 0) { - console.log("Failed to launch application, exit code:", exitCode) - console.log("Command was:", command) - } else { - console.log("App launch command completed successfully") - } - } - } - function launchApp(exec) { - appLauncher.start(exec) + // Try to find the desktop entry + var app = AppSearchService.getAppByExec(exec) + if (app) { + AppSearchService.launchApp(app) + } else { + // Fallback to direct execution + var cleanExec = exec.replace(/%[fFuU]/g, "").trim() + console.log("Launching app directly:", cleanExec) + Quickshell.execDetached(["sh", "-c", cleanExec]) + } } function show() { @@ -977,6 +818,9 @@ PanelWindow { } Component.onCompleted: { - desktopScanner.running = true + if (AppSearchService.ready) { + categories = AppSearchService.getAllCategories() + updateFilteredModel() + } } } \ No newline at end of file diff --git a/Widgets/AppLauncherButton.qml b/Widgets/AppLauncherButton.qml index 6a7aaf26..120fdc71 100644 --- a/Widgets/AppLauncherButton.qml +++ b/Widgets/AppLauncherButton.qml @@ -1,25 +1,25 @@ import QtQuick import QtQuick.Controls +import "../Common" Rectangle { id: archLauncher - property var theme property var root width: 40 height: 32 - radius: theme.cornerRadius - color: launcherArea.containsMouse ? Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.12) : Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.08) + radius: Theme.cornerRadius + color: launcherArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) anchors.verticalCenter: parent.verticalCenter Text { anchors.centerIn: parent text: root.osLogo || "apps" - font.family: root.osLogo ? "NerdFont" : theme.iconFont - font.pixelSize: root.osLogo ? theme.iconSize - 2 : theme.iconSize - 2 - font.weight: theme.iconFontWeight - color: theme.surfaceText + font.family: root.osLogo ? "NerdFont" : Theme.iconFont + font.pixelSize: root.osLogo ? Theme.iconSize - 2 : Theme.iconSize - 2 + font.weight: Theme.iconFontWeight + color: Theme.surfaceText horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } @@ -37,8 +37,8 @@ Rectangle { Behavior on color { ColorAnimation { - duration: theme.shortDuration - easing.type: theme.standardEasing + duration: Theme.shortDuration + easing.type: Theme.standardEasing } } } \ No newline at end of file diff --git a/Widgets/SpotlightLauncher.qml b/Widgets/SpotlightLauncher.qml index 74b61b8b..c042c831 100644 --- a/Widgets/SpotlightLauncher.qml +++ b/Widgets/SpotlightLauncher.qml @@ -6,35 +6,18 @@ import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Io import "../Common" +import "../Services" PanelWindow { id: spotlightLauncher property bool spotlightOpen: false - property var currentApp: ({}) - property var allApps: [] property var recentApps: [] property var filteredApps: [] property int selectedIndex: 0 property int maxResults: 12 - property var categories: ["All"] + property var categories: AppSearchService.getAllCategories() property string selectedCategory: "All" - property var appCategories: ({ - "AudioVideo": "Media", - "Audio": "Media", - "Video": "Media", - "Development": "Development", - "TextEditor": "Development", - "Education": "Education", - "Game": "Games", - "Graphics": "Graphics", - "Network": "Internet", - "Office": "Office", - "Science": "Science", - "Settings": "Settings", - "System": "System", - "Utility": "Utilities" - }) anchors { top: true @@ -84,80 +67,67 @@ PanelWindow { } function loadRecentApps() { - recentApps = Prefs.getRecentApps() + recentApps = PreferencesService.getRecentApps() } function updateFilteredApps() { filteredApps = [] selectedIndex = 0 - var apps = allApps - - // Filter by category first - if (selectedCategory !== "All") { - apps = apps.filter(app => { - return app.categories.some(cat => appCategories[cat] === selectedCategory) - }) - } + var apps = [] if (searchField.text.length === 0) { - // Show recent apps first, then all apps, limited to maxResults + // Show recent apps first, then all apps from category + var categoryApps = AppSearchService.getAppsInCategory(selectedCategory) var combined = [] - // Add recent apps first + // Add recent apps first if they match category recentApps.forEach(recentApp => { - var found = apps.find(app => app.exec === recentApp.exec) + var found = categoryApps.find(app => app.exec === recentApp.exec) if (found) { combined.push(found) } }) - // Add remaining apps not in recent, sorted alphabetically - var remaining = apps.filter(app => { + // Add remaining apps not in recent + var remaining = categoryApps.filter(app => { return !recentApps.some(recentApp => recentApp.exec === app.exec) - }).sort((a, b) => a.name.localeCompare(b.name)) + }) combined = combined.concat(remaining) - filteredApps = combined.slice(0, maxResults) + apps = combined.slice(0, maxResults) } else { - var query = searchField.text.toLowerCase() - var matches = [] - - for (var i = 0; i < apps.length; i++) { - var app = apps[i] - var name = app.name.toLowerCase() - var comment = (app.comment || "").toLowerCase() - - if (name.includes(query) || comment.includes(query)) { - var score = 0 - if (name.startsWith(query)) score += 100 - if (name.includes(query)) score += 50 - if (comment.includes(query)) score += 25 - - matches.push({ - name: app.name, - exec: app.exec, - icon: app.icon, - comment: app.comment, - categories: app.categories, - score: score - }) - } - } - - matches.sort(function(a, b) { return b.score - a.score }) - filteredApps = matches.slice(0, maxResults) + // Search with category filter + var baseApps = selectedCategory === "All" ? + AppSearchService.applications : + AppSearchService.getAppsInCategory(selectedCategory) + var searchResults = AppSearchService.searchApplications(searchField.text) + apps = searchResults.filter(app => baseApps.includes(app)).slice(0, maxResults) } + // Convert to our format + filteredApps = apps.map(app => ({ + name: app.name, + exec: app.execString || "", + icon: app.icon || "application-x-executable", + comment: app.comment || "", + categories: app.categories || [], + desktopEntry: app + })) + filteredModel.clear() - for (var i = 0; i < filteredApps.length; i++) { - filteredModel.append(filteredApps[i]) - } + filteredApps.forEach(app => filteredModel.append(app)) } function launchApp(app) { - Prefs.addRecentApp(app) - appLauncher.start(app.exec) + PreferencesService.addRecentApp(app) + if (app.desktopEntry) { + AppSearchService.launchApp(app.desktopEntry) + } else { + var cleanExec = app.exec.replace(/%[fFuU]/g, "").trim() + console.log("Spotlight: Launching app directly:", cleanExec) + Quickshell.execDetached(["sh", "-c", cleanExec]) + } hide() } @@ -181,96 +151,18 @@ PanelWindow { ListModel { id: filteredModel } - Process { - id: desktopScanner - command: ["sh", "-c", ` - for dir in "/usr/share/applications/" "/usr/local/share/applications/" "$HOME/.local/share/applications/" "/run/current-system/sw/share/applications/"; 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; /^Hidden=/p; /^NoDisplay=/p; /^Categories=/p; /^Comment=/p}' "$file" 2>/dev/null || true - done - fi - done - `] - - stdout: SplitParser { - splitMarker: "\n" - onRead: (line) => { - if (line.startsWith("===FILE:")) { - if (currentApp.name && currentApp.exec && !currentApp.hidden && !currentApp.noDisplay) { - allApps.push({ - name: currentApp.name, - exec: currentApp.exec, - icon: currentApp.icon || "application-x-executable", - comment: currentApp.comment || "", - categories: currentApp.categories || [] - }) - } - currentApp = { name: "", exec: "", icon: "", comment: "", categories: [], hidden: false, noDisplay: false } - } else if (line.startsWith("Name=")) { - currentApp.name = line.substring(5) - } else if (line.startsWith("Exec=")) { - currentApp.exec = line.substring(5) - } else if (line.startsWith("Icon=")) { - currentApp.icon = line.substring(5) - } else if (line.startsWith("Comment=")) { - currentApp.comment = line.substring(8) - } else if (line.startsWith("Categories=")) { - currentApp.categories = line.substring(11).split(";").filter(cat => cat.length > 0) - } else if (line === "Hidden=true") { - currentApp.hidden = true - } else if (line === "NoDisplay=true") { - currentApp.noDisplay = true + Connections { + target: AppSearchService + function onReadyChanged() { + if (AppSearchService.ready) { + categories = AppSearchService.getAllCategories() + if (spotlightOpen) { + updateFilteredApps() } } } - - onExited: { - if (currentApp.name && currentApp.exec && !currentApp.hidden && !currentApp.noDisplay) { - allApps.push({ - name: currentApp.name, - exec: currentApp.exec, - icon: currentApp.icon || "application-x-executable", - comment: currentApp.comment || "", - categories: currentApp.categories || [] - }) - } - - // Extract unique categories - var uniqueCategories = new Set(["All"]) - allApps.forEach(app => { - app.categories.forEach(cat => { - if (appCategories[cat]) { - uniqueCategories.add(appCategories[cat]) - } - }) - }) - categories = Array.from(uniqueCategories) - - console.log("Spotlight: Loaded", allApps.length, "applications with", categories.length, "categories") - if (spotlightOpen) { - updateFilteredApps() - } - } } - Process { - id: appLauncher - - function start(exec) { - var cleanExec = exec.replace(/%[fFuU]/g, "").trim() - console.log("Spotlight: Launching app:", cleanExec) - command = ["setsid", "sh", "-c", cleanExec] - running = true - } - - onExited: (exitCode) => { - if (exitCode !== 0) { - console.log("Spotlight: Failed to launch application, exit code:", exitCode) - } - } - } Rectangle { anchors.fill: parent @@ -609,6 +501,8 @@ PanelWindow { Component.onCompleted: { console.log("SpotlightLauncher: Component.onCompleted called - component loaded successfully!") - desktopScanner.running = true + if (AppSearchService.ready) { + categories = AppSearchService.getAllCategories() + } } } \ No newline at end of file diff --git a/Widgets/TopBar.qml b/Widgets/TopBar.qml index 5831ebaf..a7b7c9f0 100644 --- a/Widgets/TopBar.qml +++ b/Widgets/TopBar.qml @@ -223,7 +223,7 @@ EOF` cursorShape: Qt.PointingHandCursor onClicked: { - appLauncher.toggle() + LauncherService.toggleAppLauncher() } } diff --git a/shell.qml b/shell.qml index 94031a0e..6e44769b 100644 --- a/shell.qml +++ b/shell.qml @@ -303,7 +303,6 @@ ShellRoot { // Application and clipboard components AppLauncher { id: appLauncher - theme: Theme } SpotlightLauncher { @@ -312,6 +311,5 @@ ShellRoot { ClipboardHistory { id: clipboardHistoryPopup - theme: Theme } } \ No newline at end of file