1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-07 14:05:38 -05:00

App launcher fixes

This commit is contained in:
bbedward
2025-07-11 20:08:51 -04:00
parent 2fc9fa9f8d
commit 2d2f3040b7
9 changed files with 579 additions and 516 deletions

View File

@@ -0,0 +1,213 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property list<DesktopEntry> 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
}
}

View File

@@ -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()
}

View File

@@ -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()
}
}

View File

@@ -8,4 +8,7 @@ singleton AudioService 1.0 AudioService.qml
singleton BluetoothService 1.0 BluetoothService.qml singleton BluetoothService 1.0 BluetoothService.qml
singleton BrightnessService 1.0 BrightnessService.qml singleton BrightnessService 1.0 BrightnessService.qml
singleton BatteryService 1.0 BatteryService.qml singleton BatteryService 1.0 BatteryService.qml
singleton SystemMonitorService 1.0 SystemMonitorService.qml singleton SystemMonitorService 1.0 SystemMonitorService.qml
singleton AppSearchService 1.0 AppSearchService.qml
singleton PreferencesService 1.0 PreferencesService.qml
singleton LauncherService 1.0 LauncherService.qml

View File

@@ -6,47 +6,13 @@ import Quickshell.Widgets
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Io import Quickshell.Io
import "../Common" import "../Common"
import "../Services"
// Fixed version icon loaders now swap to fallback components instead of showing the magenta checkerboard
PanelWindow { PanelWindow {
id: launcher id: launcher
property var theme
property bool isVisible: false 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 // Full screen overlay setup for proper focus
anchors { anchors {
top: true top: true
@@ -64,44 +30,15 @@ PanelWindow {
visible: isVisible visible: isVisible
color: "transparent" color: "transparent"
// Enhanced app management // App management
property var currentApp: ({}) property var categories: AppSearchService.getAllCategories()
property var allApps: []
property var categories: ["All"]
property string selectedCategory: "All" property string selectedCategory: "All"
property var recentApps: [] property var recentApps: []
property var pinnedApps: ["firefox", "code", "terminal", "file-manager"] property var pinnedApps: ["firefox", "code", "terminal", "file-manager"]
property bool showCategories: false property bool showCategories: false
property string viewMode: "list" // "list" or "grid" 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: filteredModel }
ListModel { id: categoryModel }
// Background dim with click to close // Background dim with click to close
Rectangle { Rectangle {
@@ -112,8 +49,8 @@ PanelWindow {
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: activeTheme.shortDuration duration: Theme.shortDuration
easing.type: activeTheme.standardEasing easing.type: Theme.standardEasing
} }
} }
@@ -124,187 +61,94 @@ PanelWindow {
} }
} }
// Desktop applications scanning Connections {
Process { target: AppSearchService
id: desktopScanner function onReadyChanged() {
command: ["sh", "-c", ` if (AppSearchService.ready) {
for dir in "/usr/share/applications/" "/usr/local/share/applications/" "$HOME/.local/share/applications/" "/run/current-system/sw/share/applications/"; do categories = AppSearchService.getAllCategories()
if [ -d "$dir" ]; then updateFilteredModel()
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
}
} }
} }
}
onExited: {
// Save last app Connections {
if (currentApp.name && currentApp.exec && !currentApp.hidden && !currentApp.noDisplay) { target: LauncherService
allApps.push({ function onShowAppLauncher() {
name: currentApp.name, launcher.show()
exec: currentApp.exec, }
icon: currentApp.icon || "application-x-executable", function onHideAppLauncher() {
comment: currentApp.comment || "", launcher.hide()
categories: currentApp.categories || [] }
}) function onToggleAppLauncher() {
} launcher.toggle()
// 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()
} }
} }
function updateFilteredModel() { function updateFilteredModel() {
filteredModel.clear() filteredModel.clear()
let apps = allApps var apps = []
// Filter by category // Get apps based on category and search
if (selectedCategory !== "All") {
apps = apps.filter(app => {
return app.categories.some(cat => appCategories[cat] === selectedCategory)
})
}
// Filter by search
if (searchField.text.length > 0) { if (searchField.text.length > 0) {
const query = searchField.text.toLowerCase() // Search across all apps or category
apps = apps.filter(app => { var baseApps = selectedCategory === "All" ?
return app.name.toLowerCase().includes(query) || AppSearchService.applications :
(app.comment && app.comment.toLowerCase().includes(query)) AppSearchService.getAppsInCategory(selectedCategory)
}).sort((a, b) => { apps = AppSearchService.searchApplications(searchField.text).filter(app =>
// Sort by relevance baseApps.includes(app)
const aName = a.name.toLowerCase() )
const bName = b.name.toLowerCase() } else {
const aStartsWith = aName.startsWith(query) // Just category filter
const bStartsWith = bName.startsWith(query) apps = AppSearchService.getAppsInCategory(selectedCategory)
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))
} }
// Add to model // Add to model
apps.forEach(app => { apps.forEach(app => {
filteredModel.append(app) filteredModel.append({
}) name: app.name,
} exec: app.execString || "",
icon: app.icon || "application-x-executable",
/* ---------------------------------------------------------------------------- comment: app.comment || "",
* LOADER UTILITIES categories: app.categories || [],
* ---------------------------------------------------------------------------- */ desktopEntry: app
/** 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
}) })
} }
Component { Component {
id: iconComponent id: iconComponent
IconImage { Item {
id: img property var appData: parent.modelData || {}
anchors.fill: parent
source: _iconName ? Quickshell.iconPath(_iconName, "") : ""
smooth: true
asynchronous: true
onStatusChanged: { IconImage {
// Image.Null = 0, Image.Ready = 1, Image.Loading = 2, Image.Error = 3 id: iconImg
if (status === Image.Error || anchors.fill: parent
status === Image.Null || source: appData.icon ? Quickshell.iconPath(appData.icon, "") : ""
(!source && _iconName)) { smooth: true
// defer the swap to avoid reentrancy in Loader asynchronous: true
Qt.callLater(() => img.parent.sourceComponent = fallbackComponent) visible: status === Image.Ready
}
} }
// Add timeout fallback for stuck loading icons Rectangle {
Timer { anchors.fill: parent
interval: 3000 // 3 second timeout visible: !iconImg.visible
running: img.status === Image.Loading color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.10)
onTriggered: { radius: Theme.cornerRadiusLarge
if (img.status === Image.Loading) { border.width: 1
Qt.callLater(() => img.parent.sourceComponent = fallbackComponent) 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 // Main launcher panel with enhanced design
Rectangle { Rectangle {
id: launcherPanel id: launcherPanel
@@ -316,11 +160,11 @@ PanelWindow {
top: parent.top top: parent.top
left: parent.left left: parent.left
topMargin: 50 topMargin: 50
leftMargin: activeTheme.spacingL leftMargin: Theme.spacingL
} }
color: Qt.rgba(activeTheme.surfaceContainer.r, activeTheme.surfaceContainer.g, activeTheme.surfaceContainer.b, 0.98) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
radius: activeTheme.cornerRadiusXLarge radius: Theme.cornerRadiusXLarge
// Material 3 elevation with multiple layers // Material 3 elevation with multiple layers
Rectangle { Rectangle {
@@ -346,7 +190,7 @@ PanelWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: "transparent" 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 border.width: 1
radius: parent.radius radius: parent.radius
z: -1 z: -1
@@ -363,7 +207,7 @@ PanelWindow {
Behavior on xScale { Behavior on xScale {
NumberAnimation { NumberAnimation {
duration: activeTheme.mediumDuration duration: Theme.mediumDuration
easing.type: Easing.OutBack easing.type: Easing.OutBack
easing.overshoot: 1.2 easing.overshoot: 1.2
} }
@@ -371,7 +215,7 @@ PanelWindow {
Behavior on yScale { Behavior on yScale {
NumberAnimation { NumberAnimation {
duration: activeTheme.mediumDuration duration: Theme.mediumDuration
easing.type: Easing.OutBack easing.type: Easing.OutBack
easing.overshoot: 1.2 easing.overshoot: 1.2
} }
@@ -384,15 +228,15 @@ PanelWindow {
Behavior on x { Behavior on x {
NumberAnimation { NumberAnimation {
duration: activeTheme.mediumDuration duration: Theme.mediumDuration
easing.type: activeTheme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
Behavior on y { Behavior on y {
NumberAnimation { NumberAnimation {
duration: activeTheme.mediumDuration duration: Theme.mediumDuration
easing.type: activeTheme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
} }
@@ -402,8 +246,8 @@ PanelWindow {
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: activeTheme.mediumDuration duration: Theme.mediumDuration
easing.type: activeTheme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
@@ -422,8 +266,8 @@ PanelWindow {
Column { Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: activeTheme.spacingXL anchors.margins: Theme.spacingXL
spacing: activeTheme.spacingL spacing: Theme.spacingL
// Header section // Header section
Row { Row {
@@ -434,9 +278,9 @@ PanelWindow {
Text { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Applications" text: "Applications"
font.pixelSize: activeTheme.fontSizeLarge + 4 font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold font.weight: Font.Bold
color: activeTheme.surfaceText color: Theme.surfaceText
} }
Item { width: parent.width - 200; height: 1 } Item { width: parent.width - 200; height: 1 }
@@ -445,8 +289,8 @@ PanelWindow {
Text { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: filteredModel.count + " apps" text: filteredModel.count + " apps"
font.pixelSize: activeTheme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: activeTheme.surfaceVariantText color: Theme.surfaceVariantText
} }
} }
@@ -455,42 +299,42 @@ PanelWindow {
id: searchContainer id: searchContainer
width: parent.width width: parent.width
height: 52 height: 52
radius: activeTheme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.6) color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.6)
border.width: searchField.activeFocus ? 2 : 1 border.width: searchField.activeFocus ? 2 : 1
border.color: searchField.activeFocus ? activeTheme.primary : border.color: searchField.activeFocus ? Theme.primary :
Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.3) Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
Behavior on border.color { Behavior on border.color {
ColorAnimation { ColorAnimation {
duration: activeTheme.shortDuration duration: Theme.shortDuration
easing.type: activeTheme.standardEasing easing.type: Theme.standardEasing
} }
} }
Row { Row {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: activeTheme.spacingL anchors.leftMargin: Theme.spacingL
anchors.rightMargin: activeTheme.spacingL anchors.rightMargin: Theme.spacingL
spacing: activeTheme.spacingM spacing: Theme.spacingM
Text { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "search" text: "search"
font.family: activeTheme.iconFont font.family: Theme.iconFont
font.pixelSize: activeTheme.iconSize font.pixelSize: Theme.iconSize
color: searchField.activeFocus ? activeTheme.primary : activeTheme.surfaceVariantText color: searchField.activeFocus ? Theme.primary : Theme.surfaceVariantText
font.weight: activeTheme.iconFontWeight font.weight: Theme.iconFontWeight
} }
TextInput { TextInput {
id: searchField id: searchField
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width - parent.spacing - activeTheme.iconSize - 32 width: parent.width - parent.spacing - Theme.iconSize - 32
height: parent.height - activeTheme.spacingS height: parent.height - Theme.spacingS
color: activeTheme.surfaceText color: Theme.surfaceText
font.pixelSize: activeTheme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
verticalAlignment: TextInput.AlignVCenter verticalAlignment: TextInput.AlignVCenter
focus: launcher.isVisible focus: launcher.isVisible
@@ -501,8 +345,8 @@ PanelWindow {
Text { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Search applications..." text: "Search applications..."
color: activeTheme.surfaceVariantText color: Theme.surfaceVariantText
font.pixelSize: activeTheme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
visible: searchField.text.length === 0 && !searchField.activeFocus visible: searchField.text.length === 0 && !searchField.activeFocus
} }
@@ -511,7 +355,7 @@ PanelWindow {
width: 24 width: 24
height: 24 height: 24
radius: 12 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.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: searchField.text.length > 0 visible: searchField.text.length > 0
@@ -519,9 +363,9 @@ PanelWindow {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "close" text: "close"
font.family: activeTheme.iconFont font.family: Theme.iconFont
font.pixelSize: 16 font.pixelSize: 16
color: clearSearchArea.containsMouse ? activeTheme.outline : activeTheme.surfaceVariantText color: clearSearchArea.containsMouse ? Theme.outline : Theme.surfaceVariantText
} }
MouseArea { MouseArea {
@@ -537,7 +381,12 @@ PanelWindow {
Keys.onPressed: function (event) { Keys.onPressed: function (event) {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && filteredModel.count) { 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() launcher.hide()
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_Escape) { } else if (event.key === Qt.Key_Escape) {
@@ -553,36 +402,36 @@ PanelWindow {
Row { Row {
width: parent.width width: parent.width
height: 40 height: 40
spacing: activeTheme.spacingM spacing: Theme.spacingM
visible: searchField.text.length === 0 visible: searchField.text.length === 0
// Category filter // Category filter
Rectangle { Rectangle {
width: 200 width: 200
height: 36 height: 36
radius: activeTheme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3) color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: activeTheme.spacingM anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: activeTheme.spacingS spacing: Theme.spacingS
Text { Text {
text: "category" text: "category"
font.family: activeTheme.iconFont font.family: Theme.iconFont
font.pixelSize: 18 font.pixelSize: 18
color: activeTheme.surfaceVariantText color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Text { Text {
text: selectedCategory text: selectedCategory
font.pixelSize: activeTheme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: activeTheme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
font.weight: Font.Medium font.weight: Font.Medium
} }
@@ -590,12 +439,12 @@ PanelWindow {
Text { Text {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: activeTheme.spacingM anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: showCategories ? "expand_less" : "expand_more" text: showCategories ? "expand_less" : "expand_more"
font.family: activeTheme.iconFont font.family: Theme.iconFont
font.pixelSize: 18 font.pixelSize: 18
color: activeTheme.surfaceVariantText color: Theme.surfaceVariantText
} }
MouseArea { MouseArea {
@@ -617,16 +466,16 @@ PanelWindow {
Rectangle { Rectangle {
width: 36 width: 36
height: 36 height: 36
radius: activeTheme.cornerRadius radius: Theme.cornerRadius
color: viewMode === "list" ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) : color: viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
listViewArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.08) : "transparent" listViewArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent"
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "view_list" text: "view_list"
font.family: activeTheme.iconFont font.family: Theme.iconFont
font.pixelSize: 20 font.pixelSize: 20
color: viewMode === "list" ? activeTheme.primary : activeTheme.surfaceText color: viewMode === "list" ? Theme.primary : Theme.surfaceText
} }
MouseArea { MouseArea {
@@ -642,16 +491,16 @@ PanelWindow {
Rectangle { Rectangle {
width: 36 width: 36
height: 36 height: 36
radius: activeTheme.cornerRadius radius: Theme.cornerRadius
color: viewMode === "grid" ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) : color: viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
gridViewArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.08) : "transparent" gridViewArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent"
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "grid_view" text: "grid_view"
font.family: activeTheme.iconFont font.family: Theme.iconFont
font.pixelSize: 20 font.pixelSize: 20
color: viewMode === "grid" ? activeTheme.primary : activeTheme.surfaceText color: viewMode === "grid" ? Theme.primary : Theme.surfaceText
} }
MouseArea { MouseArea {
@@ -682,8 +531,8 @@ PanelWindow {
ListView { ListView {
id: appList id: appList
width: parent.width width: parent.width
anchors.margins: activeTheme.spacingS anchors.margins: Theme.spacingS
spacing: activeTheme.spacingS spacing: Theme.spacingS
model: filteredModel model: filteredModel
delegate: listDelegate delegate: listDelegate
@@ -701,7 +550,7 @@ PanelWindow {
GridView { GridView {
id: appGrid id: appGrid
width: parent.width width: parent.width
anchors.margins: activeTheme.spacingS anchors.margins: Theme.spacingS
// Responsive cell sizes based on screen width // Responsive cell sizes based on screen width
property int baseCellWidth: Math.max(100, Math.min(140, width / 8)) property int baseCellWidth: Math.max(100, Math.min(140, width / 8))
@@ -713,7 +562,7 @@ PanelWindow {
// Center the grid content // Center the grid content
property int columnsCount: Math.floor(width / cellWidth) property int columnsCount: Math.floor(width / cellWidth)
property int remainingSpace: width - (columnsCount * cellWidth) property int remainingSpace: width - (columnsCount * cellWidth)
leftMargin: Math.max(activeTheme.spacingS, remainingSpace / 2) leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
rightMargin: leftMargin rightMargin: leftMargin
model: filteredModel model: filteredModel
@@ -726,10 +575,10 @@ PanelWindow {
Rectangle { Rectangle {
id: categoryDropdown id: categoryDropdown
width: 200 width: 200
height: Math.min(250, categories.length * 40 + activeTheme.spacingM * 2) height: Math.min(250, categories.length * 40 + Theme.spacingM * 2)
radius: activeTheme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: activeTheme.surfaceContainer color: Theme.surfaceContainer
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1 border.width: 1
visible: showCategories visible: showCategories
z: 1000 z: 1000
@@ -757,7 +606,7 @@ PanelWindow {
ScrollView { ScrollView {
anchors.fill: parent anchors.fill: parent
anchors.margins: activeTheme.spacingS anchors.margins: Theme.spacingS
clip: true clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
@@ -769,16 +618,16 @@ PanelWindow {
delegate: Rectangle { delegate: Rectangle {
width: ListView.view.width width: ListView.view.width
height: 36 height: 36
radius: activeTheme.cornerRadiusSmall radius: Theme.cornerRadiusSmall
color: catArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) : "transparent" color: catArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
Text { Text {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: activeTheme.spacingM anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: modelData text: modelData
font.pixelSize: activeTheme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: selectedCategory === modelData ? activeTheme.primary : activeTheme.surfaceText color: selectedCategory === modelData ? Theme.primary : Theme.surfaceText
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
} }
@@ -807,16 +656,16 @@ PanelWindow {
Rectangle { Rectangle {
width: appList.width width: appList.width
height: 72 height: 72
radius: activeTheme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: appMouseArea.hovered ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) color: appMouseArea.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.03) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: 1
Row { Row {
anchors.fill: parent anchors.fill: parent
anchors.margins: activeTheme.spacingM anchors.margins: Theme.spacingM
spacing: activeTheme.spacingL spacing: Theme.spacingL
Item { Item {
width: 56 width: 56
@@ -826,22 +675,21 @@ PanelWindow {
Loader { Loader {
id: listIconLoader id: listIconLoader
anchors.fill: parent anchors.fill: parent
property string _iconName: model.icon property var modelData: model
property string _appName: model.name
sourceComponent: iconComponent sourceComponent: iconComponent
} }
} }
Column { Column {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width - 56 - activeTheme.spacingL width: parent.width - 56 - Theme.spacingL
spacing: activeTheme.spacingXS spacing: Theme.spacingXS
Text { Text {
width: parent.width width: parent.width
text: model.name text: model.name
font.pixelSize: activeTheme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: activeTheme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
elide: Text.ElideRight elide: Text.ElideRight
} }
@@ -849,8 +697,8 @@ PanelWindow {
Text { Text {
width: parent.width width: parent.width
text: model.comment || "Application" text: model.comment || "Application"
font.pixelSize: activeTheme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: activeTheme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
visible: model.comment && model.comment.length > 0 visible: model.comment && model.comment.length > 0
} }
@@ -863,7 +711,11 @@ PanelWindow {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
launcher.launchApp(model.exec) if (model.desktopEntry) {
AppSearchService.launchApp(model.desktopEntry)
} else {
launcher.launchApp(model.exec)
}
launcher.hide() launcher.hide()
} }
} }
@@ -876,15 +728,15 @@ PanelWindow {
Rectangle { Rectangle {
width: appGrid.cellWidth - 8 width: appGrid.cellWidth - 8
height: appGrid.cellHeight - 8 height: appGrid.cellHeight - 8
radius: activeTheme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: gridAppArea.hovered ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) color: gridAppArea.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.03) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: 1
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
spacing: activeTheme.spacingS spacing: Theme.spacingS
Item { Item {
property int iconSize: Math.min(56, Math.max(32, appGrid.cellWidth * 0.6)) property int iconSize: Math.min(56, Math.max(32, appGrid.cellWidth * 0.6))
@@ -895,8 +747,7 @@ PanelWindow {
Loader { Loader {
id: gridIconLoader id: gridIconLoader
anchors.fill: parent anchors.fill: parent
property string _iconName: model.icon property var modelData: model
property string _appName: model.name
sourceComponent: iconComponent sourceComponent: iconComponent
} }
} }
@@ -905,8 +756,8 @@ PanelWindow {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
width: 88 width: 88
text: model.name text: model.name
font.pixelSize: activeTheme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: activeTheme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
elide: Text.ElideRight elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -921,38 +772,28 @@ PanelWindow {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
launcher.launchApp(model.exec) if (model.desktopEntry) {
AppSearchService.launchApp(model.desktopEntry)
} else {
launcher.launchApp(model.exec)
}
launcher.hide() 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) { 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() { function show() {
@@ -977,6 +818,9 @@ PanelWindow {
} }
Component.onCompleted: { Component.onCompleted: {
desktopScanner.running = true if (AppSearchService.ready) {
categories = AppSearchService.getAllCategories()
updateFilteredModel()
}
} }
} }

View File

@@ -1,25 +1,25 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import "../Common"
Rectangle { Rectangle {
id: archLauncher id: archLauncher
property var theme
property var root property var root
width: 40 width: 40
height: 32 height: 32
radius: theme.cornerRadius 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) 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 anchors.verticalCenter: parent.verticalCenter
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: root.osLogo || "apps" text: root.osLogo || "apps"
font.family: root.osLogo ? "NerdFont" : theme.iconFont font.family: root.osLogo ? "NerdFont" : Theme.iconFont
font.pixelSize: root.osLogo ? theme.iconSize - 2 : theme.iconSize - 2 font.pixelSize: root.osLogo ? Theme.iconSize - 2 : Theme.iconSize - 2
font.weight: theme.iconFontWeight font.weight: Theme.iconFontWeight
color: theme.surfaceText color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
@@ -37,8 +37,8 @@ Rectangle {
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: theme.shortDuration duration: Theme.shortDuration
easing.type: theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }

View File

@@ -6,35 +6,18 @@ import Quickshell.Widgets
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Io import Quickshell.Io
import "../Common" import "../Common"
import "../Services"
PanelWindow { PanelWindow {
id: spotlightLauncher id: spotlightLauncher
property bool spotlightOpen: false property bool spotlightOpen: false
property var currentApp: ({})
property var allApps: []
property var recentApps: [] property var recentApps: []
property var filteredApps: [] property var filteredApps: []
property int selectedIndex: 0 property int selectedIndex: 0
property int maxResults: 12 property int maxResults: 12
property var categories: ["All"] property var categories: AppSearchService.getAllCategories()
property string selectedCategory: "All" 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 { anchors {
top: true top: true
@@ -84,80 +67,67 @@ PanelWindow {
} }
function loadRecentApps() { function loadRecentApps() {
recentApps = Prefs.getRecentApps() recentApps = PreferencesService.getRecentApps()
} }
function updateFilteredApps() { function updateFilteredApps() {
filteredApps = [] filteredApps = []
selectedIndex = 0 selectedIndex = 0
var apps = allApps var apps = []
// Filter by category first
if (selectedCategory !== "All") {
apps = apps.filter(app => {
return app.categories.some(cat => appCategories[cat] === selectedCategory)
})
}
if (searchField.text.length === 0) { 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 = [] var combined = []
// Add recent apps first // Add recent apps first if they match category
recentApps.forEach(recentApp => { recentApps.forEach(recentApp => {
var found = apps.find(app => app.exec === recentApp.exec) var found = categoryApps.find(app => app.exec === recentApp.exec)
if (found) { if (found) {
combined.push(found) combined.push(found)
} }
}) })
// Add remaining apps not in recent, sorted alphabetically // Add remaining apps not in recent
var remaining = apps.filter(app => { var remaining = categoryApps.filter(app => {
return !recentApps.some(recentApp => recentApp.exec === app.exec) return !recentApps.some(recentApp => recentApp.exec === app.exec)
}).sort((a, b) => a.name.localeCompare(b.name)) })
combined = combined.concat(remaining) combined = combined.concat(remaining)
filteredApps = combined.slice(0, maxResults) apps = combined.slice(0, maxResults)
} else { } else {
var query = searchField.text.toLowerCase() // Search with category filter
var matches = [] var baseApps = selectedCategory === "All" ?
AppSearchService.applications :
for (var i = 0; i < apps.length; i++) { AppSearchService.getAppsInCategory(selectedCategory)
var app = apps[i] var searchResults = AppSearchService.searchApplications(searchField.text)
var name = app.name.toLowerCase() apps = searchResults.filter(app => baseApps.includes(app)).slice(0, maxResults)
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)
} }
// 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() filteredModel.clear()
for (var i = 0; i < filteredApps.length; i++) { filteredApps.forEach(app => filteredModel.append(app))
filteredModel.append(filteredApps[i])
}
} }
function launchApp(app) { function launchApp(app) {
Prefs.addRecentApp(app) PreferencesService.addRecentApp(app)
appLauncher.start(app.exec) 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() hide()
} }
@@ -181,96 +151,18 @@ PanelWindow {
ListModel { id: filteredModel } ListModel { id: filteredModel }
Process { Connections {
id: desktopScanner target: AppSearchService
command: ["sh", "-c", ` function onReadyChanged() {
for dir in "/usr/share/applications/" "/usr/local/share/applications/" "$HOME/.local/share/applications/" "/run/current-system/sw/share/applications/"; do if (AppSearchService.ready) {
if [ -d "$dir" ]; then categories = AppSearchService.getAllCategories()
find "$dir" -name "*.desktop" 2>/dev/null | while read file; do if (spotlightOpen) {
echo "===FILE:$file" updateFilteredApps()
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
} }
} }
} }
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 { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -609,6 +501,8 @@ PanelWindow {
Component.onCompleted: { Component.onCompleted: {
console.log("SpotlightLauncher: Component.onCompleted called - component loaded successfully!") console.log("SpotlightLauncher: Component.onCompleted called - component loaded successfully!")
desktopScanner.running = true if (AppSearchService.ready) {
categories = AppSearchService.getAllCategories()
}
} }
} }

View File

@@ -223,7 +223,7 @@ EOF`
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
appLauncher.toggle() LauncherService.toggleAppLauncher()
} }
} }

View File

@@ -303,7 +303,6 @@ ShellRoot {
// Application and clipboard components // Application and clipboard components
AppLauncher { AppLauncher {
id: appLauncher id: appLauncher
theme: Theme
} }
SpotlightLauncher { SpotlightLauncher {
@@ -312,6 +311,5 @@ ShellRoot {
ClipboardHistory { ClipboardHistory {
id: clipboardHistoryPopup id: clipboardHistoryPopup
theme: Theme
} }
} }