import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Effects import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Widgets import qs.Services import qs.Widgets import "../../Helpers/FuzzySort.js" as Fuzzysort import "../../Helpers/MathHelper.js" as MathHelper NLoader { id: appLauncher isLoaded: false // Clipboard state is persisted in Services/Clipboard.qml content: Component { NPanel { id: appLauncherPanel WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand // No local timer/processes; use persistent Clipboard service // Removed local clipboard processes; handled by Clipboard service // Copy helpers via simple exec; avoid keeping processes alive locally function copyImageBase64(mime, base64) { Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`]) } function copyText(text) { Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`]) } function updateClipboardHistory() { Clipboard.refresh() } function selectNext() { if (filteredEntries.length > 0) { selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1) } } function selectPrev() { if (filteredEntries.length > 0) { selectedIndex = Math.max(selectedIndex - 1, 0) } } function activateSelected() { if (filteredEntries.length === 0) return var modelData = filteredEntries[selectedIndex] if (modelData && modelData.execute) { if (modelData.isCommand) { modelData.execute() return } else { modelData.execute() } appLauncherPanel.hide() } } property var desktopEntries: DesktopEntries.applications.values property string searchText: "" property int selectedIndex: 0 property var filteredEntries: { console.log("[AppLauncher] Total desktop entries:", desktopEntries ? desktopEntries.length : 0) if (!desktopEntries || desktopEntries.length === 0) { console.log("[AppLauncher] No desktop entries available") return [] } // Filter out entries that shouldn't be displayed var visibleEntries = desktopEntries.filter(entry => { if (!entry || entry.noDisplay) { return false } return true }) console.log("[AppLauncher] Visible entries:", visibleEntries.length) var query = searchText ? searchText.toLowerCase() : "" var results = [] // Handle special commands if (query === ">") { results.push({ "isCommand": true, "name": ">calc", "content": "Calculator - evaluate mathematical expressions", "icon": "calculate", "execute": function () { searchText = ">calc " searchInput.cursorPosition = searchText.length } }) results.push({ "isCommand": true, "name": ">clip", "content": "Clipboard history - browse and restore clipboard items", "icon": "content_paste", "execute": function () { searchText = ">clip " searchInput.cursorPosition = searchText.length } }) return results } // Handle clipboard history if (query.startsWith(">clip")) { if (!Clipboard.initialized) { Clipboard.refresh() } const searchTerm = query.slice(5).trim() Clipboard.history.forEach(function (clip, index) { let searchContent = clip.type === 'image' ? clip.mimeType : clip.content || clip if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) { let entry if (clip.type === 'image') { entry = { "isClipboard": true, "name": "Image from " + new Date(clip.timestamp).toLocaleTimeString(), "content": "Image: " + clip.mimeType, "icon": "image", "type": 'image', "data": clip.data, "execute": function () { const base64Data = clip.data.split(',')[1] copyImageBase64(clip.mimeType, base64Data) Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType]) } } } else { const textContent = clip.content || clip let displayContent = textContent let previewContent = "" displayContent = displayContent.replace(/\s+/g, ' ').trim() if (displayContent.length > 50) { previewContent = displayContent displayContent = displayContent.split('\n')[0].substring(0, 50) + "..." } entry = { "isClipboard": true, "name": displayContent, "content": previewContent || textContent, "icon": "content_paste", "execute": function () { Quickshell.clipboardText = String(textContent) copyText(String(textContent)) var preview = (textContent.length > 50) ? textContent.slice(0, 50) + "…" : textContent Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview]) } } } results.push(entry) } }) if (results.length === 0) { results.push({ "isClipboard": true, "name": "No clipboard history", "content": "No matching clipboard entries found", "icon": "content_paste_off" }) } return results } // Handle calculator if (query.startsWith(">calc")) { var expr = searchText.slice(5).trim() if (expr && isMathExpression(expr)) { var value = safeEval(expr) if (value !== null && value !== undefined && value !== "") { var formattedResult = MathHelper.MathHelper.formatResult(value) results.push({ "isCalculator": true, "name": `Calculator: ${expr} = ${formattedResult}`, "result": value, "expr": expr, "icon": "calculate", "execute": function () { Quickshell.clipboardText = String(formattedResult) clipboardTextCopyProcess.copyText(String(formattedResult)) Quickshell.execDetached( ["notify-send", "Calculator Result", `${expr} = ${formattedResult} (copied to clipboard)`]) } }) } } return results } // Regular app search if (!query) { results = results.concat(visibleEntries.sort(function (a, b) { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) })) } else { var fuzzyResults = Fuzzysort.go(query, visibleEntries, { "keys": ["name", "comment", "genericName"] }) results = results.concat(fuzzyResults.map(function (r) { return r.obj })) } console.log("[AppLauncher] Filtered entries:", results.length) return results } Component.onCompleted: { console.log("[AppLauncher] Component completed") console.log("[AppLauncher] DesktopEntries available:", typeof DesktopEntries !== 'undefined') if (typeof DesktopEntries !== 'undefined') { console.log("[AppLauncher] DesktopEntries.entries:", DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') } // Start clipboard refresh immediately on open updateClipboardHistory() } function isMathExpression(str) { // Allow more characters for enhanced math functions return /^[-+*/().0-9\s\w]+$/.test(str) } function safeEval(expr) { return MathHelper.MathHelper.evaluate(expr) } // Main content container Rectangle { anchors.centerIn: parent width: Math.min(700 * scaling, parent.width * 0.75) height: Math.min(550 * scaling, parent.height * 0.8) radius: 32 * scaling color: Colors.backgroundPrimary border.color: Colors.outline border.width: Style.borderThin * scaling // Subtle gradient background gradient: Gradient { GradientStop { position: 0.0 color: Qt.lighter(Colors.backgroundPrimary, 1.02) } GradientStop { position: 1.0 color: Qt.darker(Colors.backgroundPrimary, 1.1) } } ColumnLayout { anchors.fill: parent anchors.margins: Style.marginLarge * scaling spacing: Style.marginMedium * scaling // Search bar Rectangle { Layout.fillWidth: true Layout.preferredHeight: 40 * scaling Layout.bottomMargin: Style.marginMedium * scaling radius: 20 * scaling color: Colors.backgroundSecondary border.color: searchInput.activeFocus ? Colors.accentPrimary : Colors.outline border.width: searchInput.activeFocus ? 2 : 1 Row { anchors.fill: parent anchors.margins: 12 * scaling spacing: 10 * scaling Text { text: "search" font.family: "Material Symbols Outlined" font.pointSize: 16 * scaling color: searchInput.activeFocus ? Colors.accentPrimary : Colors.textSecondary } TextField { id: searchInput placeholderText: "Search applications..." color: Colors.textPrimary placeholderTextColor: Colors.textSecondary background: null font.pointSize: 13 * scaling Layout.fillWidth: true onTextChanged: { searchText = text selectedIndex = 0 // Reset selection when search changes } selectedTextColor: Colors.textPrimary selectionColor: Colors.accentPrimary padding: 0 verticalAlignment: TextInput.AlignVCenter leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: 0 font.bold: true Component.onCompleted: { contentItem.cursorColor = Colors.textPrimary contentItem.verticalAlignment = TextInput.AlignVCenter // Focus the search bar by default Qt.callLater(() => { searchInput.forceActiveFocus() }) } onActiveFocusChanged: contentItem.cursorColor = Colors.textPrimary Keys.onDownPressed: selectNext() Keys.onUpPressed: selectPrev() Keys.onEnterPressed: activateSelected() Keys.onReturnPressed: activateSelected() Keys.onEscapePressed: appLauncherPanel.hide() } } Behavior on border.color { ColorAnimation { duration: 120 } } Behavior on border.width { NumberAnimation { duration: 120 } } } // Applications list ScrollView { Layout.fillWidth: true Layout.fillHeight: true clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded ListView { id: appsList anchors.fill: parent spacing: 4 * scaling model: filteredEntries currentIndex: selectedIndex delegate: Rectangle { width: appsList.width - Style.marginSmall * scaling height: 56 * scaling radius: 16 * scaling property bool isSelected: index === selectedIndex color: (appCardArea.containsMouse || isSelected) ? Qt.darker( Colors.accentPrimary, 1.1) : Colors.backgroundSecondary border.color: (appCardArea.containsMouse || isSelected) ? Colors.accentPrimary : "transparent" border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0 Behavior on color { ColorAnimation { duration: 150 } } Behavior on border.color { ColorAnimation { duration: 150 } } Behavior on border.width { NumberAnimation { duration: 150 } } RowLayout { anchors.fill: parent anchors.margins: Style.marginMedium * scaling spacing: Style.marginMedium * scaling // App icon with background Rectangle { Layout.preferredWidth: 40 * scaling Layout.preferredHeight: 40 * scaling radius: 14 * scaling color: appCardArea.containsMouse ? Qt.darker(Colors.accentPrimary, 1.1) : Colors.backgroundTertiary property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand) || (iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error && iconImg.source !== "") visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode // Clipboard image display Image { id: clipboardImage anchors.fill: parent anchors.margins: 6 * scaling visible: modelData.type === 'image' source: modelData.data || "" fillMode: Image.PreserveAspectCrop asynchronous: true cache: true } IconImage { id: iconImg anchors.fill: parent anchors.margins: 6 * scaling asynchronous: true source: modelData.isCalculator ? "calculate" : modelData.isClipboard ? (modelData.type === 'image' ? "" : "content_paste") : modelData.isCommand ? modelData.icon : (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "") visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded) && modelData.type !== 'image' } // Fallback icon container Rectangle { anchors.fill: parent anchors.margins: 6 * scaling radius: 10 * scaling color: Colors.accentPrimary opacity: 0.3 visible: !parent.iconLoaded } Text { anchors.centerIn: parent visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard || modelData.isCommand) text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" font.pointSize: 18 * scaling font.weight: Font.Bold color: Colors.accentPrimary } Behavior on color { ColorAnimation { duration: 150 } } } // App info ColumnLayout { Layout.fillWidth: true spacing: 2 * scaling NText { text: modelData.name || "Unknown" font.pointSize: 14 * scaling font.weight: Font.Bold color: Colors.textPrimary elide: Text.ElideRight Layout.fillWidth: true } NText { text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "") font.pointSize: 11 * scaling color: (appCardArea.containsMouse || isSelected) ? Colors.textPrimary : Colors.textSecondary elide: Text.ElideRight Layout.fillWidth: true visible: text !== "" } } } MouseArea { id: appCardArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { selectedIndex = index activateSelected() } } } } } // No results message NText { text: searchText.trim() !== "" ? "No applications found" : "No applications available" font.pointSize: Style.fontSizeLarge * scaling color: Colors.textSecondary horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true visible: filteredEntries.length === 0 } // Results count NText { text: searchText.startsWith( ">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length !== 1 ? 's' : ''}` : searchText.startsWith( ">calc") ? `${filteredEntries.length} result${filteredEntries.length !== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}` font.pointSize: Style.fontSizeSmall * scaling color: Colors.textSecondary horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true visible: searchText.trim() !== "" } } } } } }