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 BrightnessService 1.0 BrightnessService.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.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 reentrancy 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()
}
}
}

View File

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

View File

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

View File

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

View File

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