import QtQuick import QtQuick.Layouts import QtQuick.Controls import Quickshell import Quickshell.Wayland import Quickshell.Io import "../theme" import ".." PanelWindow { id: spotWindow visible: GlobalState.spotlightVisible anchors { top: true left: true right: true bottom: true } color: Qt.rgba(0, 0, 0, 0.35) WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "quickshell-spotlight" WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None // Dismiss when clicking outside modal MouseArea { anchors.fill: parent onClicked: GlobalState.spotlightVisible = false } // App Database Loader Process readonly property Process appScanProc: Process { command: ["bash", "-c", "dirs=(\"/run/current-system/sw/share/applications\" \"$HOME/.nix-profile/share/applications\" \"$HOME/.local/share/applications\"); echo '['; first=1; for d in \"${dirs[@]}\"; do [ -d \"$d\" ] && for f in \"$d\"/*.desktop; do [ -e \"$f\" ] || continue; grep -q \"^NoDisplay=true\" \"$f\" 2>/dev/null && continue; n=$(grep -m 1 \"^Name=\" \"$f\" 2>/dev/null | cut -d= -f2-); e=$(grep -m 1 \"^Exec=\" \"$f\" 2>/dev/null | cut -d= -f2- | sed -E 's/%[fFuUdDnNickvm]//g'); if [ -n \"$n\" ] && [ -n \"$e\" ]; then [ $first -eq 0 ] && echo ','; first=0; echo \"{\\\"name\\\":\\\"$n\\\",\\\"exec\\\":\\\"$e\\\"}\"; fi; done; done; echo ']'"] stdout: StdioCollector { onDataChanged: { try { const parsed = JSON.parse(data.trim()); if (Array.isArray(parsed)) { spotWindow.allApps = parsed; } } catch(e) {} } } } property var allApps: [ { name: "Brave Browser", exec: "brave", comment: "Web Browser", icon: "browser" }, { name: "Foot Terminal", exec: "foot", comment: "Terminal Emulator", icon: "terminal" }, { name: "Neovim", exec: "foot -e nvim", comment: "Code Editor", icon: "code" }, { name: "Activity Monitor (Btop)", exec: "foot -e btop", comment: "System Resources", icon: "preferences" }, { name: "Volume & Sound (Pavucontrol)", exec: "pavucontrol", comment: "Audio Settings", icon: "preferences" }, { name: "Lock Screen", exec: "hyprlock", comment: "Lock Session", icon: "preferences" }, { name: "System Reboot", exec: "systemctl reboot", comment: "Reboot NixOS", icon: "preferences" }, { name: "Shut Down", exec: "systemctl poweroff", comment: "Power Off", icon: "preferences" } ] property int selectedIndex: 0 onVisibleChanged: { if (visible) { appScanProc.running = true; searchInput.text = ""; selectedIndex = 0; searchInput.forceActiveFocus(); } } // Centered Spotlight Search Modal Rectangle { id: modalBox anchors.horizontalCenter: parent.horizontalCenter y: 140 width: 640 implicitHeight: searchBarRow.height + (resultsListView.count > 0 || mathResultText !== "" ? 340 : 0) radius: Theme.radiusModal color: Theme.windowBg border.color: Theme.windowBorder border.width: 1 Behavior on implicitHeight { NumberAnimation { duration: 180; easing.type: Easing.OutQuad } } // Prevent clicking inside from closing modal MouseArea { anchors.fill: parent onClicked: {} } ColumnLayout { anchors.fill: parent anchors.margins: 14 spacing: 12 // Top Search Bar RowLayout { id: searchBarRow Layout.fillWidth: true Layout.preferredHeight: 44 spacing: 12 Image { width: 24 height: 24 source: "../assets/spotlight.svg" sourceSize: Qt.size(24, 24) } TextInput { id: searchInput Layout.fillWidth: true font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSpotlight font.weight: Font.Light color: Theme.textPrimary focus: true clip: true Text { anchors.fill: parent visible: searchInput.text === "" text: "Spotlight Search" font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSpotlight font.weight: Font.Light color: Theme.textTertiary } Keys.onEscapePressed: GlobalState.spotlightVisible = false Keys.onDownPressed: { if (resultsListView.count > 0) { spotWindow.selectedIndex = Math.min(resultsListView.count - 1, spotWindow.selectedIndex + 1); } } Keys.onUpPressed: { if (resultsListView.count > 0) { spotWindow.selectedIndex = Math.max(0, spotWindow.selectedIndex - 1); } } Keys.onReturnPressed: { if (filteredModel.count > 0 && spotWindow.selectedIndex < filteredModel.count) { const item = filteredModel.get(spotWindow.selectedIndex); GlobalState.launchApp(item.exec); } } } // Clear button Rectangle { visible: searchInput.text !== "" width: 20 height: 20 radius: 10 color: Qt.rgba(1.0, 1.0, 1.0, 0.2) Text { anchors.centerIn: parent text: "✕" font.pixelSize: 11 color: Theme.textPrimary } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: searchInput.text = "" } } } // Math Calculation Result Banner (if valid formula) Rectangle { visible: mathResultText !== "" Layout.fillWidth: true Layout.preferredHeight: 36 radius: Theme.radiusSmall color: Qt.rgba(0.04, 0.52, 1.0, 0.2) border.color: Theme.accentBlue border.width: 1 RowLayout { anchors.fill: parent anchors.margins: 8 spacing: 8 Text { text: "=" font.family: Theme.fontFamily font.pixelSize: 16 font.weight: Font.Bold color: Theme.accentBlue } Text { text: mathResultText font.family: Theme.fontFamily font.pixelSize: 16 font.weight: Font.DemiBold color: "#FFFFFF" Layout.fillWidth: true } } } // Divider Line Rectangle { visible: resultsListView.count > 0 Layout.fillWidth: true Layout.preferredHeight: 1 color: Qt.rgba(1.0, 1.0, 1.0, 0.12) } // Search Results List View ListView { id: resultsListView visible: count > 0 Layout.fillWidth: true Layout.preferredHeight: Math.min(270, count * 42) clip: true spacing: 2 model: filteredModel delegate: Rectangle { id: itemDelegate required property int index required property string name required property string exec required property string comment width: resultsListView.width height: 40 radius: Theme.radiusSmall color: spotWindow.selectedIndex === index ? Theme.menuHoverBlue : "transparent" RowLayout { anchors.fill: parent anchors.leftMargin: 10 anchors.rightMargin: 10 spacing: 10 Image { width: 22 height: 22 source: "../assets/terminal.svg" sourceSize: Qt.size(22, 22) } ColumnLayout { Layout.fillWidth: true spacing: 1 Text { text: itemDelegate.name font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeNormal font.weight: Font.Medium color: "#FFFFFF" elide: Text.ElideRight Layout.fillWidth: true } Text { text: itemDelegate.comment || itemDelegate.exec font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall color: spotWindow.selectedIndex === itemDelegate.index ? Qt.rgba(1,1,1,0.85) : Theme.textTertiary elide: Text.ElideRight Layout.fillWidth: true } } Text { text: "↩" font.pixelSize: 14 color: spotWindow.selectedIndex === itemDelegate.index ? "#FFFFFF" : "transparent" } } MouseArea { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: spotWindow.selectedIndex = itemDelegate.index onClicked: GlobalState.launchApp(itemDelegate.exec) } } } } } // Math calculation evaluator property readonly property string mathResultText: { const query = searchInput.text.trim(); if (/^[0-9+\-*/^().\s%]+$/.test(query) && /[0-9]/.test(query) && /[+\-*/^%]/.test(query)) { try { // Safe evaluation of arithmetic const res = Function(`"use strict"; return (${query.replace(/\^/g, "**")})`)(); if (typeof res === "number" && !isNaN(res) && isFinite(res)) { return `${res}`; } } catch(e) {} } return ""; } // Filtered ListModel updated on text change ListModel { id: filteredModel } function updateFilter() { filteredModel.clear(); const query = searchInput.text.toLowerCase().trim(); let count = 0; for (let i = 0; i < allApps.length && count < 12; i++) { const app = allApps[i]; if (!app.name) continue; if (query === "" || app.name.toLowerCase().includes(query) || (app.exec && app.exec.toLowerCase().includes(query))) { filteredModel.append({ name: app.name, exec: app.exec || "", comment: app.comment || "" }); count++; } } } Connections { target: searchInput function onTextChanged() { spotWindow.selectedIndex = 0; updateFilter(); } } Component.onCompleted: updateFilter() }