From 2b39cbfe01c66baf324a8350bc2ac4d93265771a Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 18:44:04 +0200 Subject: [PATCH] Add AppLauncher --- Helpers/MathHelper.js | 120 +++++++ Modules/AppLauncher/AppLauncher.qml | 519 ++++++++++++++++++++++++++++ Modules/LockScreen/LockScreen.qml | 19 +- Services/Clipboard.qml | 139 ++++++++ Services/IPCManager.qml | 3 +- shell.qml | 7 + 6 files changed, 788 insertions(+), 19 deletions(-) create mode 100644 Helpers/MathHelper.js create mode 100644 Modules/AppLauncher/AppLauncher.qml create mode 100644 Services/Clipboard.qml diff --git a/Helpers/MathHelper.js b/Helpers/MathHelper.js new file mode 100644 index 0000000..cc86775 --- /dev/null +++ b/Helpers/MathHelper.js @@ -0,0 +1,120 @@ +// Math helper functions for calculator functionality +var MathHelper = { + // Basic arithmetic operations + add: (a, b) => a + b, + subtract: (a, b) => a - b, + multiply: (a, b) => a * b, + divide: (a, b) => b !== 0 ? a / b : NaN, + + // Power and roots + pow: (base, exponent) => Math.pow(base, exponent), + sqrt: (x) => x >= 0 ? Math.sqrt(x) : NaN, + cbrt: (x) => Math.cbrt(x), + + // Trigonometric functions (in radians) + sin: (x) => Math.sin(x), + cos: (x) => Math.cos(x), + tan: (x) => Math.tan(x), + asin: (x) => Math.asin(x), + acos: (x) => Math.acos(x), + atan: (x) => Math.atan(x), + + // Logarithmic functions + log: (x) => x > 0 ? Math.log(x) : NaN, + log10: (x) => x > 0 ? Math.log10(x) : NaN, + log2: (x) => x > 0 ? Math.log2(x) : NaN, + + // Other mathematical functions + abs: (x) => Math.abs(x), + floor: (x) => Math.floor(x), + ceil: (x) => Math.ceil(x), + round: (x) => Math.round(x), + min: (...args) => Math.min(...args), + max: (...args) => Math.max(...args), + + // Constants + PI: Math.PI, + E: Math.E, + + // Factorial + factorial: (n) => { + if (n < 0 || n !== Math.floor(n)) return NaN; + if (n === 0 || n === 1) return 1; + let result = 1; + for (let i = 2; i <= n; i++) { + result *= i; + } + return result; + }, + + // Percentage + percent: (value, total) => (value / total) * 100, + + // Degrees to radians and vice versa + toRadians: (degrees) => degrees * (Math.PI / 180), + toDegrees: (radians) => radians * (180 / Math.PI), + + // Safe evaluation with math functions + evaluate: (expression) => { + try { + // Replace common math functions with MathHelper equivalents + let processedExpr = expression + .replace(/\bpi\b/gi, 'MathHelper.PI') + .replace(/\be\b/gi, 'MathHelper.E') + .replace(/\bsin\b/gi, 'MathHelper.sin') + .replace(/\bcos\b/gi, 'MathHelper.cos') + .replace(/\btan\b/gi, 'MathHelper.tan') + .replace(/\basin\b/gi, 'MathHelper.asin') + .replace(/\bacos\b/gi, 'MathHelper.acos') + .replace(/\batan\b/gi, 'MathHelper.atan') + .replace(/\blog\b/gi, 'MathHelper.log') + .replace(/\blog10\b/gi, 'MathHelper.log10') + .replace(/\blog2\b/gi, 'MathHelper.log2') + .replace(/\bsqrt\b/gi, 'MathHelper.sqrt') + .replace(/\bcbrt\b/gi, 'MathHelper.cbrt') + .replace(/\bpow\b/gi, 'MathHelper.pow') + .replace(/\babs\b/gi, 'MathHelper.abs') + .replace(/\bfloor\b/gi, 'MathHelper.floor') + .replace(/\bceil\b/gi, 'MathHelper.ceil') + .replace(/\bround\b/gi, 'MathHelper.round') + .replace(/\bmin\b/gi, 'MathHelper.min') + .replace(/\bmax\b/gi, 'MathHelper.max') + .replace(/\bfactorial\b/gi, 'MathHelper.factorial') + .replace(/\bpercent\b/gi, 'MathHelper.percent') + .replace(/\btoRadians\b/gi, 'MathHelper.toRadians') + .replace(/\btoDegrees\b/gi, 'MathHelper.toDegrees'); + + // Evaluate the expression + const result = Function('MathHelper', 'return ' + processedExpr)(MathHelper); + + // Check if result is valid + if (isNaN(result) || !isFinite(result)) { + return null; + } + + return result; + } catch (error) { + return null; + } + }, + + // Format result for display + formatResult: (result) => { + if (result === null || isNaN(result) || !isFinite(result)) { + return "Error"; + } + + // For very large or small numbers, use scientific notation + if (Math.abs(result) >= 1e10 || (Math.abs(result) < 1e-10 && result !== 0)) { + return result.toExponential(6); + } + + // For integers, don't show decimal places + if (Number.isInteger(result)) { + return result.toString(); + } + + // For decimals, limit to 8 significant digits + return parseFloat(result.toPrecision(8)).toString(); + } +}; \ No newline at end of file diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml new file mode 100644 index 0000000..f7f19b2 --- /dev/null +++ b/Modules/AppLauncher/AppLauncher.qml @@ -0,0 +1,519 @@ +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: (appCardArea.containsMouse || isSelected) ? Colors.backgroundPrimary : 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.backgroundPrimary : 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() !== "" + } + } + } + } + } +} \ No newline at end of file diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 97e135c..31f2fc9 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -342,24 +342,6 @@ WlSessionLock { anchors.margins: 12 * Scaling.scale(screen) spacing: 12 * Scaling.scale(screen) - Text { - text: "●" - color: Colors.error - font.pixelSize: 16 * Scaling.scale(screen) - } - - Text { - text: "●" - color: Colors.warning - font.pixelSize: 16 * Scaling.scale(screen) - } - - Text { - text: "●" - color: Colors.accentPrimary - font.pixelSize: 16 * Scaling.scale(screen) - } - Text { text: "SECURE TERMINAL" color: Colors.textPrimary @@ -554,6 +536,7 @@ WlSessionLock { border.width: 1 enabled: !lock.authenticating Layout.alignment: Qt.AlignRight + Layout.bottomMargin: -12 * Scaling.scale(screen) Text { anchors.centerIn: parent diff --git a/Services/Clipboard.qml b/Services/Clipboard.qml new file mode 100644 index 0000000..0d82e37 --- /dev/null +++ b/Services/Clipboard.qml @@ -0,0 +1,139 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Services + +Singleton { + id: root + + property var history: [] + property bool initialized: false + + // Internal state + property bool _enabled: true + + Timer { + interval: 1000 + repeat: true + running: root._enabled + onTriggered: root.refresh() + } + + // Detect current clipboard types (text/image) + Process { + id: typeProcess + property bool isLoading: false + property var currentTypes: [] + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + currentTypes = String(stdout.text).trim().split('\n').filter(t => t) + + const imageType = currentTypes.find(t => t.startsWith('image/')) + if (imageType) { + imageProcess.mimeType = imageType + imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`] + imageProcess.running = true + } else { + textProcess.command = ["wl-paste", "-n", "--type", "text/plain"] + textProcess.running = true + } + } else { + typeProcess.isLoading = false + } + } + + stdout: StdioCollector {} + } + + // Read image data + Process { + id: imageProcess + property string mimeType: "" + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const base64 = stdout.text.trim() + if (base64) { + const entry = { + type: 'image', + mimeType: mimeType, + data: `data:${mimeType};base64,${base64}`, + timestamp: new Date().getTime() + } + + const exists = root.history.find(item => item.type === 'image' && item.data === entry.data) + if (!exists) { + root.history = [entry, ...root.history].slice(0, 20) + } + } + } + + if (!textProcess.isLoading) { + root.initialized = true + } + typeProcess.isLoading = false + } + + stdout: StdioCollector {} + } + + // Read text data + Process { + id: textProcess + property bool isLoading: false + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const content = String(stdout.text).trim() + if (content) { + const entry = { + type: 'text', + content: content, + timestamp: new Date().getTime() + } + + const exists = root.history.find(item => { + if (item.type === 'text') { + return item.content === content + } + return item === content + }) + + if (!exists) { + const newHistory = root.history.map(item => { + if (typeof item === 'string') { + return { + type: 'text', + content: item, + timestamp: new Date().getTime() + } + } + return item + }) + + root.history = [entry, ...newHistory].slice(0, 20) + } + } + } else { + textProcess.isLoading = false + } + + root.initialized = true + typeProcess.isLoading = false + } + + stdout: StdioCollector {} + } + + function refresh() { + if (!typeProcess.isLoading && !textProcess.isLoading) { + typeProcess.isLoading = true + typeProcess.command = ["wl-paste", "-l"] + typeProcess.running = true + } + } +} + diff --git a/Services/IPCManager.qml b/Services/IPCManager.qml index 992b7c9..998635b 100644 --- a/Services/IPCManager.qml +++ b/Services/IPCManager.qml @@ -37,7 +37,8 @@ Item { IpcHandler { target: "appLauncher" - function toggle() {// TODO + function toggle() { + appLauncherPanel.isLoaded = !appLauncherPanel.isLoaded } } diff --git a/shell.qml b/shell.qml index 81b5e7d..b68c05f 100644 --- a/shell.qml +++ b/shell.qml @@ -10,6 +10,7 @@ import qs.Modules.Calendar import qs.Modules.Demo import qs.Modules.Background import qs.Modules.SidePanel +import qs.Modules.AppLauncher import qs.Modules.Notification import qs.Modules.Settings import qs.Services @@ -24,6 +25,12 @@ ShellRoot { Bar {} Dock {} + AppLauncher { + id: appLauncherPanel + } + + + DemoPanel { id: demoPanel }