Files
NixOS-Configs/home/hypr/quickshell/components/Spotlight.qml

364 lines
14 KiB
QML

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.45)
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(String(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
Item {
id: modalContainer
anchors.horizontalCenter: parent.horizontalCenter
y: 140
width: 640
height: modalBox.implicitHeight
scale: spotWindow.visible ? 1.0 : 0.96
opacity: spotWindow.visible ? 1.0 : 0.0
Behavior on scale {
NumberAnimation { duration: Theme.animSmooth; easing.type: Easing.OutQuint }
}
Behavior on opacity {
NumberAnimation { duration: Theme.animSmooth; easing.type: Easing.OutQuint }
}
Rectangle {
id: modalBox
anchors.fill: parent
implicitHeight: searchBarRowContainer.height + (resultsListView.count > 0 || mathResultText !== "" ? 340 : 0)
radius: 0
color: Theme.bgPrimary
border.color: Theme.glassBorder
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
spacing: 0
// Top Search Bar Area
Item {
id: searchBarRowContainer
Layout.fillWidth: true
Layout.preferredHeight: 64
RowLayout {
id: searchBarRow
anchors.fill: parent
anchors.margins: 14
spacing: 12
Text {
text: "⌕"
font.family: Theme.fontSans
font.pixelSize: 24
color: Theme.accentVolt
Layout.alignment: Qt.AlignVCenter
}
TextInput {
id: searchInput
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
font.family: Theme.fontSans
font.pixelSize: 20
color: Theme.textPrimary
focus: true
clip: true
Text {
anchors.fill: parent
visible: searchInput.text === ""
text: "Spotlight Search"
font.family: Theme.fontSans
font.pixelSize: 20
color: Theme.textMuted
}
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: 24
height: 24
radius: 0
color: "transparent"
Text {
anchors.centerIn: parent
text: "✕"
font.pixelSize: 16
color: clearMouseArea.containsMouse ? Theme.textPrimary : Theme.textMuted
}
MouseArea {
id: clearMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: searchInput.text = ""
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 1
color: Theme.glassBorder
}
}
// Math Calculation Result Banner (if valid formula)
Rectangle {
visible: mathResultText !== ""
Layout.fillWidth: true
Layout.preferredHeight: 36
Layout.margins: 14
radius: 0
color: "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 8
Text {
text: "="
font.family: Theme.fontMono
font.pixelSize: 16
font.weight: Font.Bold
color: Theme.accentVolt
}
Text {
text: mathResultText
font.family: Theme.fontMono
font.pixelSize: 16
color: Theme.accentVolt
Layout.fillWidth: true
}
}
}
// Search Results List View
ListView {
id: resultsListView
visible: count > 0
Layout.fillWidth: true
Layout.preferredHeight: Math.min(270, count * 40)
Layout.topMargin: 8
Layout.bottomMargin: 8
clip: true
spacing: 0
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: 0
color: spotWindow.selectedIndex === index ? Qt.rgba(0.812, 0.988, 0.0, 0.08) : "transparent"
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 2
color: Theme.accentVolt
visible: spotWindow.selectedIndex === index
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 14
anchors.rightMargin: 14
spacing: 12
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Text {
text: itemDelegate.name
font.family: Theme.fontSans
font.pixelSize: 14
color: Theme.textPrimary
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: itemDelegate.comment || itemDelegate.exec
font.family: Theme.fontMono
font.pixelSize: 11
color: Theme.textMuted
elide: Text.ElideRight
Layout.fillWidth: true
}
}
Text {
text: "↩"
font.pixelSize: 14
color: spotWindow.selectedIndex === itemDelegate.index
? Theme.textPrimary : "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()
}