dots-hyprland/.config/quickshell/modules/overview/SearchWidget.qml
2025-05-03 15:49:01 +02:00

377 lines
No EOL
14 KiB
QML

import "root:/"
import "root:/services/"
import "root:/modules/common"
import "root:/modules/common/widgets"
import Qt5Compat.GraphicalEffects
import Qt.labs.platform
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
Item { // Wrapper
id: root
required property var panelWindow
readonly property string xdgConfigHome: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
property string searchingText: ""
property bool showResults: searchingText != ""
property real searchBarHeight: searchBar.height + Appearance.sizes.elevationMargin * 2
implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2
implicitHeight: searchWidgetContent.implicitHeight + Appearance.sizes.elevationMargin * 2
property string mathResult: ""
property var searchActions: [
{
action: "img",
execute: () => {
executor.executeCommand(`${xdgConfigHome}/quickshell/scripts/switchwall.sh`.replace(/file:\/\//, ""))
}
},
{
action: "dark",
execute: () => {
executor.executeCommand(`${xdgConfigHome}/quickshell/scripts/switchwall.sh --mode dark --noswitch`.replace(/file:\/\//, ""))
}
},
{
action: "light",
execute: () => {
executor.executeCommand(`${xdgConfigHome}/quickshell/scripts/switchwall.sh --mode light --noswitch`.replace(/file:\/\//, ""))
}
},
{
action: "accentcolor",
execute: (args) => {
console.log(args)
executor.executeCommand(
`${xdgConfigHome}/quickshell/scripts/switchwall.sh --noswitch --color ${args != '' ? ("'"+args+"'") : ""}`
.replace(/file:\/\//, ""))
}
},
{
action: "todo",
execute: (args) => {
Todo.addTask(args)
}
},
]
function focusFirstItemIfNeeded() {
if (searchInput.focus) appResults.currentIndex = 0; // Focus the first item
}
Timer {
id: nonAppResultsTimer
interval: ConfigOptions.search.nonAppResultDelay
onTriggered: {
mathProcess.calculateExpression(root.searchingText);
}
}
Process {
id: mathProcess
property list<string> baseCommand: ["qalc", "-t"]
function calculateExpression(expression) {
// mathProcess.running = false
mathProcess.command = baseCommand.concat(expression)
mathProcess.running = true
}
stdout: SplitParser {
onRead: data => {
root.mathResult = data
root.focusFirstItemIfNeeded()
}
}
}
Process {
id: webSearch
property list<string> baseCommand: ["xdg-open"]
function search(query) {
webSearch.running = false
let url = ConfigOptions.search.engineBaseUrl + query
for (let site of ConfigOptions.search.excludedSites) {
url += ` -site:${site}`;
}
webSearch.command = baseCommand.concat(url)
webSearch.running = true
}
}
Process {
id: executor
property list<string> baseCommand: ["bash", "-c"]
function executeCommand(command) {
executor.running = false
executor.command = baseCommand.concat(
`${command} || ${ConfigOptions.apps.terminal} fish -C 'echo "${qsTr("Searching for package with that command")}..." && pacman -F ${command}'`
)
executor.running = true
}
}
Keys.onPressed: (event) => {
// Prevent Esc and Backspace from registering
if (event.key === Qt.Key_Escape) return;
// Handle Backspace: focus and delete character if not focused
if (event.key === Qt.Key_Backspace) {
if (!searchInput.activeFocus) {
searchInput.forceActiveFocus();
if (event.modifiers & Qt.ControlModifier) {
// Delete word before cursor
let text = searchInput.text;
let pos = searchInput.cursorPosition;
if (pos > 0) {
// Find the start of the previous word
let left = text.slice(0, pos);
let match = left.match(/(\s*\S+)\s*$/);
let deleteLen = match ? match[0].length : 1;
searchInput.text = text.slice(0, pos - deleteLen) + text.slice(pos);
searchInput.cursorPosition = pos - deleteLen;
}
} else {
// Delete character before cursor if any
if (searchInput.cursorPosition > 0) {
searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition - 1) +
searchInput.text.slice(searchInput.cursorPosition);
searchInput.cursorPosition -= 1;
}
}
// Always move cursor to end after programmatic edit
searchInput.cursorPosition = searchInput.text.length;
event.accepted = true;
}
// If already focused, let TextField handle it
return;
}
// Only handle visible printable characters (ignore control chars, arrows, etc.)
if (
event.text &&
event.text.length === 1 &&
event.key !== Qt.Key_Enter &&
event.key !== Qt.Key_Return &&
event.text.charCodeAt(0) >= 0x20 // ignore control chars like Backspace, Tab, etc.
) {
if (!searchInput.activeFocus) {
searchInput.forceActiveFocus();
// Insert the character at the cursor position
searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition) +
event.text +
searchInput.text.slice(searchInput.cursorPosition);
searchInput.cursorPosition += 1;
event.accepted = true;
}
}
}
Rectangle { // Background
id: searchWidgetContent
anchors.centerIn: parent
implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight
radius: Appearance.rounding.large
color: Appearance.colors.colLayer0
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: 0
clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: searchWidgetContent.width
height: searchWidgetContent.width
radius: searchWidgetContent.radius
}
}
RowLayout {
id: searchBar
spacing: 5
MaterialSymbol {
id: searchIcon
Layout.leftMargin: 15
font.pixelSize: Appearance.font.pixelSize.huge
color: Appearance.m3colors.m3onSurface
text: "search"
}
TextField { // Search box
id: searchInput
focus: root.panelWindow.visible || GlobalStates.overviewOpen
Layout.rightMargin: 15
padding: 15
renderType: Text.NativeRendering
color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
selectedTextColor: Appearance.m3colors.m3onPrimary
selectionColor: Appearance.m3colors.m3primary
placeholderText: qsTr("Search, calculate or run")
placeholderTextColor: Appearance.m3colors.m3outline
implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.animation.elementDecelFast.duration
easing.type: Appearance.animation.elementDecelFast.type
}
}
onTextChanged: root.searchingText = text
Connections {
target: root
function onVisibleChanged() {
searchInput.selectAll()
root.searchingText = ""
}
}
onAccepted: {
if (appResults.count > 0) {
// Get the first visible delegate and trigger its click
let firstItem = appResults.itemAtIndex(0);
if (firstItem && firstItem.clicked) {
firstItem.clicked();
}
}
}
background: Item {}
cursorDelegate: Rectangle {
width: 1
color: searchInput.activeFocus ? Appearance.m3colors.m3primary : "transparent"
radius: 1
}
}
}
Rectangle { // Separator
visible: root.showResults
Layout.fillWidth: true
height: 1
color: Appearance.m3colors.m3outlineVariant
}
ListView { // App results
id: appResults
visible: root.showResults
Layout.fillWidth: true
implicitHeight: Math.min(600, appResults.contentHeight + topMargin + bottomMargin)
clip: true
topMargin: 10
bottomMargin: 10
spacing: 0
KeyNavigation.up: searchBar
onFocusChanged: {
if(focus) appResults.currentIndex = 1;
}
Connections {
target: root
function onSearchingTextChanged() {
if (appResults.count > 0)
appResults.currentIndex = 0;
}
}
model: ScriptModel {
id: model
values: {
if(root.searchingText == "") return [];
// Start math and other non-app stuff
nonAppResultsTimer.restart();
// Init result array
let result = [];
// Add filtered application entries
result = result.concat(
AppSearch.fuzzyQuery(root.searchingText)
.map((entry) => {
entry.clickActionName = "Launch";
entry.type = "App"
return entry;
})
);
// Add non-app results
// Launcher actions
for (let action of root.searchActions) {
const actionString = `${ConfigOptions.search.prefix.action}${action.action}`
if (actionString.startsWith(root.searchingText) || root.searchingText.startsWith(actionString)) {
result.push({
name: root.searchingText.startsWith(actionString) ? root.searchingText : actionString,
clickActionName: "Run",
type: "Action",
materialSymbol: 'settings_suggest',
execute: () => {
action.execute(root.searchingText.split(" ").slice(1).join(" "))
},
});
}
}
// Run command
result.push({
name: searchingText,
clickActionName: "Run",
type: qsTr("Run command"),
fontType: "monospace",
materialSymbol: 'terminal',
execute: () => {
executor.executeCommand(searchingText.startsWith('sudo') ? `${ConfigOptions.apps.terminal} fish -C '${root.searchingText}'` : root.searchingText);
}
});
// Qalc math result
result.push({
name: root.mathResult,
clickActionName: "Copy",
type: qsTr("Math result"),
fontType: "monospace",
materialSymbol: 'calculate',
execute: () => {
Hyprland.dispatch(`exec wl-copy '${root.mathResult}'`)
}
});
// Web search
result.push({
name: root.searchingText,
clickActionName: "Search",
type: "Search the web",
materialSymbol: 'travel_explore',
execute: () => {
webSearch.search(root.searchingText);
}
});
return result;
}
}
delegate: SearchItem {
entry: modelData
// itemName: modelData.name
// itemIcon: modelData.icon
}
}
}
}
DropShadow {
id: searchWidgetShadow
anchors.fill: searchWidgetContent
source: searchWidgetContent
radius: Appearance.sizes.elevationMargin
samples: radius * 2 + 1
color: Appearance.colors.colShadow
verticalOffset: 2
horizontalOffset: 0
}
}