import QtQuick import QtQuick.Layouts import QtQuick.Controls import Quickshell import Quickshell.Wayland import Quickshell.Io import Quickshell.Hyprland import "../config" as Cfg PanelWindow { id: root property bool isTop: true property int barHeight: Cfg.Config.barHeight property int barMargin: Cfg.Config.margin readonly property var t: Cfg.Config.theme readonly property int launcherGap: 12 GlobalShortcut { id: launcherShortcut name: "toggle" onPressed: root.toggle() } property bool _isOpen: false readonly property bool isOpen: _isOpen property string mode: "drun" property bool _keyboardNav: false property var allItems: [] property bool loading: false property bool proxyMode: false readonly property var filteredItems: { const q = searchInput.text.toLowerCase() if (!q) return allItems.slice(0, 80) return allItems.filter(i => i.name.toLowerCase().includes(q)) } function toggle() { _isOpen ? close() : open() } function open() { closeTimer.stop() visible = true _isOpen = true searchInput.text = "" resultsList.currentIndex = -1 loadItems() Qt.callLater(() => searchInput.forceActiveFocus()) } function close() { _isOpen = false closeTimer.restart() } Connections { target: root function onActiveChanged() { if (!root.active && root._isOpen) root.close() } } function loadItems() { loading = true readCache() updateProc.command = [Cfg.Config.launcherScript, "--update-only"] updateProc.running = true } function readCache() { const path = Cfg.Config.launcherCacheDir + "/" + (mode === "drun" ? "drun.txt" : "run.txt") dataProc.rawOutput = "" dataProc.command = ["bash", "-c", "cat " + path] dataProc.running = true } function launch(item) { if (!item || !item.exec) return const cleanExec = item.exec.replace(/%[fFuUik]/g, "").trim() const finalExec = root.proxyMode ? "proxychains4 " + cleanExec : cleanExec const logPath = "/tmp/qml_launch.log" launchProc.command = ["sh", "-c", finalExec + " > " + logPath + " 2>&1 &"] launchProc.running = true console.log("Launching: " + finalExec) if (typeof close === "function") close() } onModeChanged: if (_isOpen) loadItems() visible: false color: "transparent" anchors { top: true; bottom: true; left: true; right: true } WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "main-shell-launcher" WlrLayershell.exclusiveZone: -1 WlrLayershell.keyboardFocus: _isOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None mask: Region { item: clipContainer } Timer { id: closeTimer interval: Cfg.Config.popupAnimDuration + 70 onTriggered: { root.visible = false root.mode = "drun" root.allItems = [] } } Process { id: updateProc onRunningChanged: if (!running) root.readCache() } Process { id: dataProc property string rawOutput: "" stdout: SplitParser { onRead: data => dataProc.rawOutput += data + "\n" } onRunningChanged: { if (!running && rawOutput) { const lines = rawOutput.trim().split("\n") const items = [] for (let i = 0; i < lines.length; i++) { const p = lines[i].split("\t") if (p.length >= 3) items.push({ name: p[0], exec: p[1], id: p[2], icon: p[3] || "" }) } root.allItems = items root.loading = false } } } Process { id: launchProc } Item { id: clipContainer anchors.horizontalCenter: parent.horizontalCenter width: 460 anchors.top: root.isTop ? parent.top : undefined anchors.bottom: root.isTop ? undefined : parent.bottom anchors.topMargin: root.isTop ? (root.barHeight + root.launcherGap) : 0 anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + root.launcherGap) height: parent.height - root.barHeight - root.launcherGap clip: true Rectangle { id: bubble width: parent.width property int targetHeight: Math.min(layout.implicitHeight + 28, 540) height: targetHeight anchors.top: root.isTop ? parent.top : undefined anchors.bottom: root.isTop ? undefined : parent.bottom transform: Translate { y: root._isOpen ? 0 : (root.isTop ? -bubble.height : bubble.height) Behavior on y { NumberAnimation { duration: Cfg.Config.popupAnimDuration; easing.type: Easing.OutExpo } enabled: root.visible } } opacity: root._isOpen ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 200 } } radius: Cfg.Config.popupRadius color: t.launcherBg border.color: t.launcherBorder border.width: Cfg.Config.popupBorderWidth focus: root._isOpen Keys.onEscapePressed: root.close() ColumnLayout { id: layout anchors.fill: parent anchors.margins: 14 spacing: 10 // ── Search input ────────────────────────────────────────── Rectangle { Layout.fillWidth: true height: 44 radius: 10 color: t.launcherInput border.color: searchInput.activeFocus ? t.launcherInputBorderFocus : t.launcherBorder border.width: Cfg.Config.popupBorderWidth Behavior on color { NumberAnimation { duration: 180 } } RowLayout { anchors.fill: parent anchors.leftMargin: 12 anchors.rightMargin: 8 spacing: 10 TextInput { id: searchInput Layout.fillWidth: true color: t.launcherText font.pixelSize: 15 verticalAlignment: TextInput.AlignVCenter selectionColor: t.launcherAccent onTextChanged: resultsList.currentIndex = -1 Keys.onReturnPressed: { if (root.filteredItems.length > 0) { let idx = resultsList.currentIndex; if (idx === -1) idx = 0; root.launch(root.filteredItems[idx]); } } Keys.onDownPressed: { root._keyboardNav = true if (resultsList.currentIndex < 0) { resultsList.currentIndex = 0; } else { resultsList.currentIndex = Math.min(resultsList.currentIndex + 1, resultsList.count - 1) } resultsList.positionViewAtIndex(resultsList.currentIndex, ListView.Contain) } Keys.onUpPressed: { root._keyboardNav = true if (resultsList.currentIndex < 0) { resultsList.currentIndex = resultsList.count - 1; } else { resultsList.currentIndex = Math.max(resultsList.currentIndex - 1, 0) } resultsList.positionViewAtIndex(resultsList.currentIndex, ListView.Contain) } Keys.onTabPressed: root.mode = root.mode === "drun" ? "run" : "drun" } Rectangle { height: 28 width: modeRow.implicitWidth + 10 radius: 8 color: t.launcherPill border.color: t.launcherBorder border.width: Cfg.Config.popupBorderWidth RowLayout { id: modeRow anchors.centerIn: parent spacing: 2 Repeater { model: ["drun", "run"] delegate: Rectangle { height: 22 width: label.implicitWidth + 14 radius: 6 color: root.mode === modelData ? t.launcherModeActiveBg : "transparent" Text { id: label anchors.centerIn: parent text: modelData font.pixelSize: 10 font.bold: root.mode === modelData color: root.mode === modelData ? t.launcherModeActiveText : t.launcherDim } MouseArea { anchors.fill: parent onClicked: root.mode = modelData } } } } } // ── Proxychains toggle ──────────────────────────── Rectangle { id: proxyBtn height: 28 width: 110 radius: 8 color: root.proxyMode ? Qt.rgba(1, 0.35, 0.35, 0.22) : t.launcherPill border.color: root.proxyMode ? Qt.rgba(1, 0.35, 0.35, 0.65) : t.launcherBorder border.width: Cfg.Config.popupBorderWidth Behavior on color { ColorAnimation { duration: 150 } } Behavior on border.color { ColorAnimation { duration: 150 } } RowLayout { id: proxyBtnRow anchors.centerIn: parent spacing: 5 Text { text: "󰒃" font.pixelSize: 11 color: root.proxyMode ? Qt.rgba(1, 0.45, 0.45, 1) : t.launcherDim Behavior on color { ColorAnimation { duration: 150 } } } Text { text: "proxychains4" font.pixelSize: 10 font.bold: root.proxyMode color: root.proxyMode ? Qt.rgba(1, 0.45, 0.45, 1) : t.launcherDim Behavior on color { ColorAnimation { duration: 150 } } } } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: root.proxyMode = !root.proxyMode } } } } ListView { id: resultsList Layout.fillWidth: true implicitHeight: Math.min(contentHeight, 400) clip: true model: root.filteredItems currentIndex: -1 spacing: 2 onModelChanged: currentIndex = -1 ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } delegate: Rectangle { width: resultsList.width height: 38 radius: 8 property bool hovered: false color: hovered ? t.launcherHover : (resultsList.currentIndex === index && index !== -1 ? t.launcherSelected : "transparent") RowLayout { anchors.fill: parent anchors.leftMargin: 12 anchors.rightMargin: 12 spacing: 12 Item { width: 22; height: 22 Layout.alignment: Qt.AlignVCenter Image { id: appIcon anchors.fill: parent source: modelData.icon ? "image://icon/" + modelData.icon : "" fillMode: Image.PreserveAspectFit smooth: true visible: status === Image.Ready } Text { anchors.centerIn: parent visible: !appIcon.visible text: root.mode === "drun" ? "󰣆" : "󰆍" color: resultsList.currentIndex === index && index !== -1 ? t.launcherAccent : t.launcherDim font.pixelSize: 16 } } Text { text: modelData.name color: t.launcherText font.pixelSize: 14 Layout.fillWidth: true elide: Text.ElideRight } } MouseArea { anchors.fill: parent hoverEnabled: true onEntered: parent.hovered = true onExited: parent.hovered = false onClicked: root.launch(modelData) } } } } } } }