aboutsummaryrefslogtreecommitdiffstats
path: root/config/quickshell/modules/overview/SearchWidget.qml
diff options
context:
space:
mode:
authorKiran George <kirangeorge1995@gmail.com>2025-06-09 11:30:08 +0530
committerKiran George <kirangeorge1995@gmail.com>2025-06-09 11:30:08 +0530
commit952aa63147c9fb28f6ace6f0bc7ccf45ced1299a (patch)
tree306e6d86603a162c00bc5113b56baac0fe7bec7c /config/quickshell/modules/overview/SearchWidget.qml
parent4cf0d0bd5930da76e60f6770de3ee97c10ca7024 (diff)
Overview v2
Diffstat (limited to 'config/quickshell/modules/overview/SearchWidget.qml')
-rw-r--r--config/quickshell/modules/overview/SearchWidget.qml425
1 files changed, 425 insertions, 0 deletions
diff --git a/config/quickshell/modules/overview/SearchWidget.qml b/config/quickshell/modules/overview/SearchWidget.qml
new file mode 100644
index 00000000..fed710ec
--- /dev/null
+++ b/config/quickshell/modules/overview/SearchWidget.qml
@@ -0,0 +1,425 @@
+import "root:/"
+import "root:/services/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import Qt5Compat.GraphicalEffects
+import Qt.labs.platform
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+Item { // Wrapper
+ id: root
+ readonly property string xdgConfigHome: Directories.config
+ 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: ""
+
+ function disableExpandAnimation() {
+ searchWidthBehavior.enabled = false;
+ }
+
+ function cancelSearch() {
+ searchInput.selectAll()
+ root.searchingText = ""
+ searchWidthBehavior.enabled = true;
+ }
+
+ function setSearchingText(text) {
+ searchInput.text = text;
+ root.searchingText = text;
+ }
+
+ property var searchActions: [
+ {
+ action: "img",
+ execute: () => {
+ executor.executeCommand(Directories.wallpaperSwitchScriptPath)
+ }
+ },
+ {
+ action: "dark",
+ execute: () => {
+ executor.executeCommand(`${Directories.wallpaperSwitchScriptPath} --mode dark --noswitch`)
+ }
+ },
+ {
+ action: "light",
+ execute: () => {
+ executor.executeCommand(`${Directories.wallpaperSwitchScriptPath} --mode light --noswitch`)
+ }
+ },
+ {
+ action: "accentcolor",
+ execute: (args) => {
+ executor.executeCommand(
+ `${Directories.wallpaperSwitchScriptPath} --noswitch --color ${args != '' ? ("'"+args+"'") : ""}`
+ )
+ }
+ },
+ {
+ 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: executor
+ property list<string> baseCommand: ["bash", "-c"]
+ function executeCommand(command) {
+ executor.command = baseCommand.concat(
+ `${command} || ${ConfigOptions.apps.terminal} fish -C 'echo "${qsTr("Searching for package with that command")}..." && pacman -F ${command}'`
+ )
+ executor.startDetached()
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+
+ StyledRectangularShadow {
+ target: searchWidgetContent
+ }
+ 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
+ iconSize: Appearance.font.pixelSize.huge
+ color: Appearance.m3colors.m3onSurface
+ text: root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard) ? 'content_paste_search' : ''
+ }
+ TextField { // Search box
+ id: searchInput
+
+ focus: GlobalStates.overviewOpen
+ Layout.rightMargin: 15
+ padding: 15
+ renderType: Text.NativeRendering
+ font {
+ family: Appearance?.font.family.main ?? "sans-serif"
+ pixelSize: Appearance?.font.pixelSize.small ?? 15
+ hintingPreference: Font.PreferFullHinting
+ }
+ color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
+ selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
+ selectionColor: Appearance.m3colors.m3secondaryContainer
+ placeholderText: qsTr("Search, calculate or run")
+ placeholderTextColor: Appearance.m3colors.m3outline
+ implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth
+
+ Behavior on implicitWidth {
+ id: searchWidthBehavior
+ enabled: false
+ NumberAnimation {
+ duration: 300
+ easing.type: Appearance.animation.elementMove.type
+ easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
+ }
+ }
+
+ onTextChanged: root.searchingText = text
+
+ 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: null
+
+ cursorDelegate: Rectangle {
+ width: 1
+ color: searchInput.activeFocus ? Appearance.colors.colPrimary : "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: 2
+ KeyNavigation.up: searchBar
+ highlightMoveDuration : 100
+
+ onFocusChanged: {
+ if(focus) appResults.currentIndex = 1;
+ }
+
+ Connections {
+ target: root
+ function onSearchingTextChanged() {
+ if (appResults.count > 0)
+ appResults.currentIndex = 0;
+ }
+ }
+
+ model: ScriptModel {
+ id: model
+ values: { // Search results are handled here
+ ////////////////// Skip? //////////////////
+ if(root.searchingText == "") return [];
+
+ ///////////// Special cases ///////////////
+ if (root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard)) { // Clipboard
+ const searchString = root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length);
+ return Cliphist.fuzzyQuery(searchString).map(entry => {
+ return {
+ cliphistRawString: entry,
+ name: entry.replace(/^\s*\S+\s+/, ""),
+ clickActionName: "",
+ type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`,
+ execute: () => {
+ Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(entry)}' | cliphist decode | wl-copy`);
+ }
+ };
+ }).filter(Boolean);
+ }
+ if (root.searchingText.startsWith(ConfigOptions.search.prefix.emojis)) { // Clipboard
+ const searchString = root.searchingText.slice(ConfigOptions.search.prefix.emojis.length);
+ return Emojis.fuzzyQuery(searchString).map(entry => {
+ return {
+ cliphistRawString: entry,
+ bigText: entry.match(/^\s*(\S+)/)?.[1] || "",
+ name: entry.replace(/^\s*\S+\s+/, ""),
+ clickActionName: "",
+ type: "Emoji",
+ execute: () => {
+ Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(entry.match(/^\s*(\S+)/)?.[1])}'`);
+ }
+ };
+ }).filter(Boolean);
+ }
+
+
+ ////////////////// Init ///////////////////
+ nonAppResultsTimer.restart();
+ const mathResultObject = {
+ name: root.mathResult,
+ clickActionName: qsTr("Copy"),
+ type: qsTr("Math result"),
+ fontType: "monospace",
+ materialSymbol: 'calculate',
+ execute: () => {
+ Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(root.mathResult)}'`)
+ }
+ }
+ const commandResultObject = {
+ name: searchingText.replace("file://", ""),
+ clickActionName: qsTr("Run"),
+ type: qsTr("Run command"),
+ fontType: "monospace",
+ materialSymbol: 'terminal',
+ execute: () => {
+ executor.executeCommand(searchingText.startsWith('sudo') ? `${ConfigOptions.apps.terminal} fish -C '${root.searchingText.replace("file://", "")}'` : root.searchingText.replace("file://", ""));
+ }
+ }
+ const launcherActionObjects = root.searchActions
+ .map(action => {
+ const actionString = `${ConfigOptions.search.prefix.action}${action.action}`;
+ if (actionString.startsWith(root.searchingText) || root.searchingText.startsWith(actionString)) {
+ return {
+ name: root.searchingText.startsWith(actionString) ? root.searchingText : actionString,
+ clickActionName: qsTr("Run"),
+ type: qsTr("Action"),
+ materialSymbol: 'settings_suggest',
+ execute: () => {
+ action.execute(root.searchingText.split(" ").slice(1).join(" "))
+ },
+ };
+ }
+ return null;
+ })
+ .filter(Boolean);
+
+ let result = [];
+
+ //////////////// Apps //////////////////
+ result = result.concat(
+ AppSearch.fuzzyQuery(root.searchingText)
+ .map((entry) => {
+ entry.clickActionName = qsTr("Launch");
+ entry.type = qsTr("App");
+ return entry;
+ })
+ );
+
+ ////////// Launcher actions ////////////
+ result = result.concat(launcherActionObjects);
+
+ /////////// Math result & command //////////
+ const startsWithNumber = /^\d/.test(root.searchingText);
+ if (startsWithNumber) {
+ result.push(mathResultObject);
+ result.push(commandResultObject);
+ } else {
+ result.push(commandResultObject);
+ result.push(mathResultObject);
+ }
+
+ ///////////////// Web search ////////////////
+ result.push({
+ name: root.searchingText,
+ clickActionName: qsTr("Search"),
+ type: qsTr("Search the web"),
+ materialSymbol: 'travel_explore',
+ execute: () => {
+ let url = ConfigOptions.search.engineBaseUrl + root.searchingText
+ for (let site of ConfigOptions.search.excludedSites) {
+ url += ` -site:${site}`;
+ }
+ Qt.openUrlExternally(url);
+ }
+ });
+
+ return result;
+ }
+ }
+
+ delegate: SearchItem { // The selectable item for each search result
+ required property var modelData
+ anchors.left: parent?.left
+ anchors.right: parent?.right
+ entry: modelData
+ query: root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard) ?
+ root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length) :
+ root.searchingText;
+ }
+ }
+
+ }
+ }
+} \ No newline at end of file
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage