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

341 lines
12 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.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()
}