diff --git a/.config/quickshell/modules/common/Appearance.qml b/.config/quickshell/modules/common/Appearance.qml index 32133ed7a..fecdfb8e7 100644 --- a/.config/quickshell/modules/common/Appearance.qml +++ b/.config/quickshell/modules/common/Appearance.qml @@ -197,6 +197,7 @@ Singleton { property int barPreferredSideSectionWidth: 400 property int sidebarWidth: 450 property int notificationPopupWidth: 410 + property int searchWidthCollapsed: 260 property int searchWidth: 450 property int hyprlandGapsOut: 5 property int elevationMargin: 7 diff --git a/.config/quickshell/modules/common/ConfigOptions.qml b/.config/quickshell/modules/common/ConfigOptions.qml index b75bb7a0c..858d211f7 100644 --- a/.config/quickshell/modules/common/ConfigOptions.qml +++ b/.config/quickshell/modules/common/ConfigOptions.qml @@ -41,6 +41,12 @@ Singleton { property int updateInterval: 3000 } + property QtObject search: QtObject { + property int nonAppResultDelay: 30 // This prevents lagging when typing + property string engineBaseUrl: "https://www.google.com/search?q=" + property list excludedSites: [ "quora.com" ] + } + property QtObject hacks: QtObject { property int arbitraryRaceConditionDelay: 10 // milliseconds } diff --git a/.config/quickshell/modules/overview/SearchItem.qml b/.config/quickshell/modules/overview/SearchItem.qml index e024fae9e..1f874752b 100644 --- a/.config/quickshell/modules/overview/SearchItem.qml +++ b/.config/quickshell/modules/overview/SearchItem.qml @@ -11,12 +11,17 @@ import Quickshell.Widgets Button { id: root - property DesktopEntry desktopEntry - property string itemName: desktopEntry?.name - property string itemIcon: desktopEntry?.icon - property var itemExecute: desktopEntry?.execute - property string itemClickActionName: desktopEntry?.clickActionName + property var entry + property bool entryShown: entry?.shown ?? true + property string itemType: entry?.type + property string itemName: entry?.name + property string itemIcon: entry?.icon ?? "" + property var itemExecute: entry?.execute + property string fontType: entry?.fontType ?? "main" + property string itemClickActionName: entry?.clickActionName + property string materialSymbol: entry?.materialSymbol ?? "" + visible: root.entryShown property int horizontalMargin: 10 property int buttonHorizontalPadding: 10 property int buttonVerticalPadding: 5 @@ -61,20 +66,46 @@ Button { anchors.leftMargin: root.horizontalMargin + root.buttonHorizontalPadding anchors.rightMargin: root.horizontalMargin + root.buttonHorizontalPadding + // Icon IconImage { + visible: root.materialSymbol == "" source: Quickshell.iconPath(root.itemIcon); width: 35 height: 35 } - StyledText { - Layout.fillWidth: true - id: nameText - font.pixelSize: Appearance.font.pixelSize.normal + MaterialSymbol { + visible: root.materialSymbol != "" + text: root.materialSymbol + font.pixelSize: 30 color: Appearance.m3colors.m3onSurface - horizontalAlignment: Text.AlignLeft - elide: Text.ElideRight - text: root.itemName + // width: 35 + // height: 35 } + + // Main text + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 0 + StyledText { + font.pixelSize: Appearance.font.pixelSize.small + color: Appearance.colors.colSubtext + visible: root.itemType && root.itemType != "App" + text: root.itemType + } + StyledText { + Layout.fillWidth: true + id: nameText + font.pixelSize: Appearance.font.pixelSize.normal + font.family: Appearance.font.family[root.fontType] + color: Appearance.m3colors.m3onSurface + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + text: root.itemName + } + } + + // Action text StyledText { Layout.fillWidth: false visible: (root.hovered || root.focus) diff --git a/.config/quickshell/modules/overview/SearchWidget.qml b/.config/quickshell/modules/overview/SearchWidget.qml index d7d7c25a9..801a9208e 100644 --- a/.config/quickshell/modules/overview/SearchWidget.qml +++ b/.config/quickshell/modules/overview/SearchWidget.qml @@ -17,15 +17,106 @@ Item { // Wrapper implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2 implicitHeight: searchWidgetContent.implicitHeight + Appearance.sizes.elevationMargin * 2 - Keys.onPressed: { - // Only handle printable characters (ignore modifiers, arrows, etc.) - if (event.text && event.text.length === 1 && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return) { + property string mathResult: "" + + Timer { + id: nonAppResultsTimer + interval: ConfigOptions.search.nonAppResultDelay + onTriggered: { + mathProcess.calculateExpression(root.searchingText); + } + } + + Process { + id: mathProcess + property list baseCommand: ["qalc", "-t"] + function calculateExpression(expression) { + // mathProcess.running = false + mathProcess.command = baseCommand.concat(expression) + mathProcess.running = true + } + stdout: SplitParser { + onRead: data => { + root.mathResult = data + if (searchInput.focus) appResults.currentIndex = 0; // Focus the first item + } + } + } + + Process { + id: copyText + property list baseCommand: ["wl-copy"] + function copyTextToClipboard(text) { + copyText.running = false + copyText.command = baseCommand.concat(text) + copyText.running = true + } + } + + Process { + id: webSearch + property list 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 + } + } + + 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); + event.text + + searchInput.text.slice(searchInput.cursorPosition); searchInput.cursorPosition += 1; event.accepted = true; } @@ -57,7 +148,12 @@ Item { // Wrapper RowLayout { id: searchBar spacing: 5 - KeyNavigation.down: appResults + KeyNavigation.down: { + if (appResults.count > 1) { + appResults.currentIndex = 1; + appResults.forceActiveFocus(); + } + } MaterialSymbol { id: searchIcon Layout.leftMargin: 15 @@ -75,8 +171,14 @@ Item { // Wrapper selectedTextColor: Appearance.m3colors.m3onSurface placeholderText: qsTr("Search") placeholderTextColor: Appearance.m3colors.m3outline - implicitWidth: Appearance.sizes.searchWidth + 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 { @@ -125,20 +227,65 @@ Item { // Wrapper spacing: 0 KeyNavigation.up: searchBar + Connections { + target: root + function onSearchingTextChanged() { + if (appResults.count > 0) + appResults.currentIndex = 0; + } + } + model: ScriptModel { id: model - values: DesktopEntries.applications.values - .filter((entry) => { - if (root.searchingText == "") return false - return entry.name.toLowerCase().includes(root.searchingText.toLowerCase()) - }) - .map((entry) => { - entry.clickActionName = "Launch"; - return entry; - }) + 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( + DesktopEntries.applications.values + .filter((entry) => { + if (root.searchingText == "") return false + return entry.name.toLowerCase().includes(root.searchingText.toLowerCase()) + }) + .map((entry) => { + entry.clickActionName = "Launch"; + entry.type = "App" + return entry; + }) + ); + + // Add non-app results + result.push({ + name: root.mathResult, + clickActionName: "Copy", + type: qsTr("Math result"), + fontType: "monospace", + materialSymbol: 'calculate', + execute: () => { + copyText.copyTextToClipboard(root.mathResult); + } + }); + result.push({ + name: root.searchingText, + clickActionName: "Search", + type: "Search the web", + materialSymbol: 'travel_explore', + execute: () => { + webSearch.search(root.searchingText); + } + }); + + return result; + } } delegate: SearchItem { - desktopEntry: modelData + entry: modelData // itemName: modelData.name // itemIcon: modelData.icon }