aboutsummaryrefslogtreecommitdiffstats
path: root/config/quickshell/modules/overview
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
parent4cf0d0bd5930da76e60f6770de3ee97c10ca7024 (diff)
Overview v2
Diffstat (limited to 'config/quickshell/modules/overview')
-rw-r--r--config/quickshell/modules/overview/Overview.qml279
-rw-r--r--config/quickshell/modules/overview/OverviewWidget.qml341
-rw-r--r--config/quickshell/modules/overview/OverviewWindow.qml94
-rw-r--r--config/quickshell/modules/overview/SearchItem.qml220
-rw-r--r--config/quickshell/modules/overview/SearchWidget.qml425
5 files changed, 1359 insertions, 0 deletions
diff --git a/config/quickshell/modules/overview/Overview.qml b/config/quickshell/modules/overview/Overview.qml
new file mode 100644
index 00000000..ef5a49c3
--- /dev/null
+++ b/config/quickshell/modules/overview/Overview.qml
@@ -0,0 +1,279 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Scope {
+ id: overviewScope
+ property bool dontAutoCancelSearch: false
+ property bool searchEnabled: ConfigOptions.search.searchEnabled
+
+ Variants {
+ id: overviewVariants
+ model: Quickshell.screens
+ PanelWindow {
+ id: root
+ required property var modelData
+ property string searchingText: ""
+ readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
+ property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor.id)
+ screen: modelData
+ visible: GlobalStates.overviewOpen
+
+ WlrLayershell.namespace: "quickshell:overview"
+ WlrLayershell.layer: WlrLayer.Overlay
+ color: "transparent"
+
+ mask: Region {
+ item: GlobalStates.overviewOpen ? columnLayout : null
+ }
+ HyprlandWindow.visibleMask: Region {
+ item: GlobalStates.overviewOpen ? columnLayout : null
+ }
+
+ anchors {
+ top: true
+ left: true
+ right: true
+ bottom: true
+ }
+
+ HyprlandFocusGrab {
+ id: grab
+ windows: [ root ]
+ property bool canBeActive: root.monitorIsFocused
+ active: false
+ onCleared: () => {
+ if (!active) GlobalStates.overviewOpen = false
+ }
+ }
+
+ Connections {
+ target: GlobalStates
+ function onOverviewOpenChanged() {
+ if (!GlobalStates.overviewOpen) {
+ if (overviewScope.searchEnabled && searchWidget) {
+ searchWidget.disableExpandAnimation()
+ }
+ overviewScope.dontAutoCancelSearch = false;
+ } else {
+ if (!overviewScope.dontAutoCancelSearch && overviewScope.searchEnabled && searchWidget) {
+ searchWidget.cancelSearch()
+ }
+ delayedGrabTimer.start()
+ }
+ }
+ }
+
+ Timer {
+ id: delayedGrabTimer
+ interval: ConfigOptions.hacks.arbitraryRaceConditionDelay
+ repeat: false
+ onTriggered: {
+ if (!grab.canBeActive) return
+ grab.active = GlobalStates.overviewOpen
+ }
+ }
+
+ implicitWidth: columnLayout.implicitWidth
+ implicitHeight: columnLayout.implicitHeight
+
+ function setSearchingText(text) {
+ if (overviewScope.searchEnabled && searchWidget) {
+ searchWidget.setSearchingText(text);
+ }
+ }
+
+ ColumnLayout {
+ id: columnLayout
+ visible: GlobalStates.overviewOpen
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ top: ConfigOptions.overview.position === 0 ? parent.top : undefined
+ verticalCenter: ConfigOptions.overview.position === 1 ? parent.verticalCenter : undefined
+ bottom: ConfigOptions.overview.position === 2 ? parent.bottom : undefined
+ }
+
+ Keys.onPressed: (event) => {
+ if (event.key === Qt.Key_Escape) {
+ GlobalStates.overviewOpen = false;
+ }
+ }
+
+ Item {
+ height: 1
+ width: 1
+ }
+
+ // Conditionally render SearchWidget - only exists when searchEnabled is true
+ SearchWidget {
+ id: searchWidget
+ Layout.alignment: Qt.AlignHCenter
+ visible: overviewScope.searchEnabled
+ height: overviewScope.searchEnabled ? implicitHeight : 0
+ Layout.preferredHeight: overviewScope.searchEnabled ? implicitHeight : 0
+ onSearchingTextChanged: (text) => {
+ root.searchingText = searchingText
+ }
+ }
+
+ Item {
+ Layout.preferredHeight: overviewScope.searchEnabled ? 0 : 20
+ Layout.fillWidth: true
+ visible: !overviewScope.searchEnabled
+ }
+
+ Loader {
+ id: overviewLoader
+ active: GlobalStates.overviewOpen
+ sourceComponent: OverviewWidget {
+ panelWindow: root
+ // Show OverviewWidget when search is disabled OR when search text is empty
+ visible: !overviewScope.searchEnabled || (root.searchingText == "")
+ }
+ }
+ }
+ }
+ }
+
+ IpcHandler {
+ target: "overview"
+
+ function toggle() {
+ GlobalStates.overviewOpen = !GlobalStates.overviewOpen
+ }
+ function close() {
+ GlobalStates.overviewOpen = false
+ }
+ function open() {
+ GlobalStates.overviewOpen = true
+ }
+ function toggleReleaseInterrupt() {
+ GlobalStates.superReleaseMightTrigger = false
+ }
+ // Add function to control search
+ function toggleSearch() {
+ overviewScope.searchEnabled = !overviewScope.searchEnabled
+ }
+ function enableSearch() {
+ overviewScope.searchEnabled = true
+ }
+ function disableSearch() {
+ overviewScope.searchEnabled = false
+ }
+ }
+
+ GlobalShortcut {
+ name: "overviewToggle"
+ description: qsTr("Toggles overview on press")
+
+ onPressed: {
+ GlobalStates.overviewOpen = !GlobalStates.overviewOpen
+ }
+ }
+
+ GlobalShortcut {
+ name: "overviewClose"
+ description: qsTr("Closes overview")
+
+ onPressed: {
+ GlobalStates.overviewOpen = false
+ }
+ }
+
+ GlobalShortcut {
+ name: "overviewToggleRelease"
+ description: qsTr("Toggles overview on release")
+
+ onPressed: {
+ GlobalStates.superReleaseMightTrigger = true
+ }
+
+ onReleased: {
+ if (!GlobalStates.superReleaseMightTrigger) {
+ GlobalStates.superReleaseMightTrigger = true
+ return
+ }
+ GlobalStates.overviewOpen = !GlobalStates.overviewOpen
+ }
+ }
+
+ GlobalShortcut {
+ name: "overviewToggleReleaseInterrupt"
+ description: qsTr("Interrupts possibility of overview being toggled on release. ") +
+ qsTr("This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. ") +
+ qsTr("To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything.")
+
+ onPressed: {
+ GlobalStates.superReleaseMightTrigger = false
+ }
+ }
+
+ // Only enable clipboard/emoji shortcuts when search is enabled
+ GlobalShortcut {
+ name: "overviewClipboardToggle"
+ description: qsTr("Toggle clipboard query on overview widget")
+
+ onPressed: {
+ if (!overviewScope.searchEnabled) return; // Skip if search disabled
+
+ if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
+ GlobalStates.overviewOpen = false;
+ return;
+ }
+ for (let i = 0; i < overviewVariants.instances.length; i++) {
+ let panelWindow = overviewVariants.instances[i];
+ if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
+ overviewScope.dontAutoCancelSearch = true;
+ panelWindow.setSearchingText(
+ ConfigOptions.search.prefix.clipboard
+ );
+ GlobalStates.overviewOpen = true;
+ return
+ }
+ }
+ }
+ }
+
+ GlobalShortcut {
+ name: "overviewEmojiToggle"
+ description: qsTr("Toggle emoji query on overview widget")
+
+ onPressed: {
+ if (!overviewScope.searchEnabled) return; // Skip if search disabled
+
+ if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
+ GlobalStates.overviewOpen = false;
+ return;
+ }
+ for (let i = 0; i < overviewVariants.instances.length; i++) {
+ let panelWindow = overviewVariants.instances[i];
+ if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
+ overviewScope.dontAutoCancelSearch = true;
+ panelWindow.setSearchingText(
+ ConfigOptions.search.prefix.emojis
+ );
+ GlobalStates.overviewOpen = true;
+ return
+ }
+ }
+ }
+ }
+
+ // Optional: Add shortcut to toggle search functionality
+ GlobalShortcut {
+ name: "overviewToggleSearch"
+ description: qsTr("Toggle search functionality in overview")
+
+ onPressed: {
+ overviewScope.searchEnabled = !overviewScope.searchEnabled
+ }
+ }
+} \ No newline at end of file
diff --git a/config/quickshell/modules/overview/OverviewWidget.qml b/config/quickshell/modules/overview/OverviewWidget.qml
new file mode 100644
index 00000000..73dbbc26
--- /dev/null
+++ b/config/quickshell/modules/overview/OverviewWidget.qml
@@ -0,0 +1,341 @@
+import "root:/"
+import "root:/services/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Item {
+ id: root
+ required property var panelWindow
+ readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
+ readonly property var toplevels: ToplevelManager.toplevels
+ readonly property int workspacesShown: ConfigOptions.overview.numOfRows * ConfigOptions.overview.numOfCols
+ readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown)
+ property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor.id)
+ property var windows: HyprlandData.windowList
+ property var windowByAddress: HyprlandData.windowByAddress
+ property var windowAddresses: HyprlandData.addresses
+ property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor.id)
+ property real scale: ConfigOptions.overview.scale
+ property color activeBorderColor: Appearance.m3colors.m3secondary
+
+ property real workspaceImplicitWidth: Math.max(100, (monitorData?.transform % 2 === 1) ?
+ ((monitor.height - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale) :
+ ((monitor.width - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale))
+ property real workspaceImplicitHeight: Math.max(60, (monitorData?.transform % 2 === 1) ?
+ ((monitor.width - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale) :
+ ((monitor.height - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale))
+
+ property real workspaceNumberMargin: 80
+ property real workspaceNumberSize: Math.min(workspaceImplicitHeight, workspaceImplicitWidth) * monitor.scale
+ property int workspaceZ: 0
+ property int windowZ: 1
+ property int windowDraggingZ: 99999
+ property real workspaceSpacing: 5
+
+ property int draggingFromWorkspace: -1
+ property int draggingTargetWorkspace: -1
+
+ implicitWidth: overviewBackground.implicitWidth + Appearance.sizes.elevationMargin * 2
+ implicitHeight: overviewBackground.implicitHeight + Appearance.sizes.elevationMargin * 2
+
+ property Component windowComponent: OverviewWindow {}
+ property list<OverviewWindow> windowWidgets: []
+
+ // Shared wallpaper image - loaded once and reused
+ Image {
+ id: sharedWallpaper
+ source: Appearance.background_image || ""
+ visible: false // Hidden as it's only used as a source
+ cache: true
+ asynchronous: true
+ smooth: true
+ opacity: Appearance.workpaceTransparency // Adds slight transparency (0.0 = fully transparent, 1.0 = fully opaque)
+ }
+
+ StyledRectangularShadow {
+ target: overviewBackground
+ }
+ Rectangle { // Background
+ id: overviewBackground
+ property real padding: 10
+ anchors.fill: parent
+ anchors.margins: Appearance.sizes.elevationMargin
+ border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.2)
+ border.width : 2
+
+ implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2
+ implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2
+ radius: Appearance.rounding.screenRounding * root.scale + padding
+ color: Appearance.colors.colLayer0
+
+
+ ColumnLayout { // Workspaces
+ id: workspaceColumnLayout
+
+ z: root.workspaceZ
+ anchors.centerIn: parent
+ spacing: workspaceSpacing
+ Repeater {
+ model: ConfigOptions.overview.numOfRows
+ delegate: RowLayout {
+ id: row
+ property int rowIndex: index
+ spacing: workspaceSpacing
+
+ Repeater { // Workspace repeater
+ model: ConfigOptions.overview.numOfCols
+ Rectangle { // Workspace
+ id: workspace
+ property int colIndex: index
+ property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * ConfigOptions.overview.numOfCols + colIndex + 1
+ property color defaultWorkspaceColor: Appearance.colors.colLayer1
+ property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1)
+ property color hoveredBorderColor: Appearance.colors.colLayer2Hover
+ property bool hoveredWhileDragging: false
+ readonly property int padding: ConfigOptions.overview.windowPadding
+
+ Layout.preferredWidth: root.workspaceImplicitWidth
+ Layout.preferredHeight: root.workspaceImplicitHeight
+ Layout.minimumWidth: 100
+ Layout.minimumHeight: 60
+
+ width: root.workspaceImplicitWidth
+ height: root.workspaceImplicitHeight
+ color: "transparent"
+ radius: Appearance.rounding.screenRounding * root.scale
+ clip: true
+ opacity: Appearance.workpaceTransparency // Adds slight transparency (0.0 = fully transparent, 1.0 = fully opaque)
+
+
+ // Efficient wallpaper using ShaderEffectSource
+ Rectangle {
+ id: wallpaperContainer
+ anchors.fill: parent
+ anchors.margins: 2 // Leave space for border
+ radius: workspace.radius - 2
+ color: workspace.defaultWorkspaceColor // Fallback color
+ clip: true
+
+ ShaderEffectSource {
+ id: wallpaperSource
+ anchors.fill: parent
+ sourceItem: sharedWallpaper
+ visible: sharedWallpaper.status === Image.Ready
+ smooth: true
+
+ // Scale to fill while preserving aspect ratio
+ transform: Scale {
+ property real aspectRatio: sharedWallpaper.implicitWidth / Math.max(1, sharedWallpaper.implicitHeight)
+ property real containerRatio: wallpaperContainer.width / Math.max(1, wallpaperContainer.height)
+
+ xScale: aspectRatio > containerRatio ?
+ wallpaperContainer.height * aspectRatio / wallpaperContainer.width : 1
+ yScale: aspectRatio > containerRatio ?
+ 1 : wallpaperContainer.width / (wallpaperContainer.height * aspectRatio)
+
+ origin.x: wallpaperContainer.width / 2
+ origin.y: wallpaperContainer.height / 2
+ }
+ }
+
+ // Fallback when image fails to load or isn't ready
+ Rectangle {
+ anchors.fill: parent
+ color: workspace.defaultWorkspaceColor
+ visible: sharedWallpaper.status !== Image.Ready
+ }
+
+ // Optional: Add overlay for better text readability and hover effects
+ Rectangle {
+ anchors.fill: parent
+ color: hoveredWhileDragging ? hoveredWorkspaceColor : "black"
+ opacity: hoveredWhileDragging ? 0.3 : 0.1
+ }
+ }
+
+ // Border overlay - on top of wallpaper
+ Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ radius: parent.radius
+ border.width: 1
+ border.color: hoveredWhileDragging ? hoveredBorderColor : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.6)
+ z: 10 // Ensure it's on top
+ }
+
+ StyledText {
+ // Position in top-left corner with padding
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.topMargin: 12 // Padding from top edge
+ anchors.leftMargin: 12 // Padding from left edge
+
+ text: workspaceValue
+ font.pixelSize: root.workspaceNumberSize * root.scale
+ font.weight: Font.DemiBold
+ color: ColorUtils.transparentize(Appearance.colors.colOnLayer1, 0.8)
+ horizontalAlignment: Text.AlignLeft
+ verticalAlignment: Text.AlignTop
+ z: 15 // Above border
+ }
+
+ MouseArea {
+ id: workspaceArea
+ anchors.fill: parent
+ acceptedButtons: Qt.LeftButton
+ z: 20 // Above all visual elements
+ onClicked: {
+ if (root.draggingTargetWorkspace === -1) {
+ GlobalStates.overviewOpen = false
+ Hyprland.dispatch(`workspace ${workspaceValue}`)
+ }
+ }
+ }
+
+ DropArea {
+ anchors.fill: parent
+ z: 20 // Same level as MouseArea
+ onEntered: {
+ root.draggingTargetWorkspace = workspaceValue
+ if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return;
+ hoveredWhileDragging = true
+ }
+ onExited: {
+ hoveredWhileDragging = false
+ if (root.draggingTargetWorkspace == workspaceValue) root.draggingTargetWorkspace = -1
+ }
+ }
+
+ }
+ }
+ }
+ }
+ }
+
+ Item { // Windows & focused workspace indicator
+ id: windowSpace
+ anchors.centerIn: parent
+ implicitWidth: workspaceColumnLayout.implicitWidth
+ implicitHeight: workspaceColumnLayout.implicitHeight
+
+ Repeater { // Window repeater
+ model: ScriptModel {
+ values: windowAddresses.filter((address) => {
+ var win = windowByAddress[address]
+ return (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown)
+ })
+ }
+ delegate: OverviewWindow {
+ id: window
+ windowData: windowByAddress[modelData]
+ monitorData: root.monitorData
+ scale: root.scale
+ availableWorkspaceWidth: root.workspaceImplicitWidth
+ availableWorkspaceHeight: root.workspaceImplicitHeight
+
+ property bool atInitPosition: (initX == x && initY == y)
+ restrictToWorkspace: Drag.active || atInitPosition
+
+ property int workspaceColIndex: (windowData?.workspace.id - 1) % ConfigOptions.overview.numOfCols
+ property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / ConfigOptions.overview.numOfCols)
+ xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex
+ yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex
+
+ Timer {
+ id: updateWindowPosition
+ interval: ConfigOptions.hacks.arbitraryRaceConditionDelay
+ repeat: false
+ running: false
+ onTriggered: {
+ window.x = Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset
+ window.y = Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset
+ }
+ }
+
+ z: atInitPosition ? root.windowZ : root.windowDraggingZ
+ Drag.hotSpot.x: targetWindowWidth / 2
+ Drag.hotSpot.y: targetWindowHeight / 2
+ MouseArea {
+ id: dragArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onEntered: hovered = true
+ onExited: hovered = false
+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton
+ drag.target: parent
+ onPressed: {
+ root.draggingFromWorkspace = windowData?.workspace.id
+ window.pressed = true
+ window.Drag.active = true
+ window.Drag.source = window
+ }
+ onReleased: {
+ const targetWorkspace = root.draggingTargetWorkspace
+ window.pressed = false
+ window.Drag.active = false
+ root.draggingFromWorkspace = -1
+ if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) {
+ Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${window.windowData?.address}`)
+ updateWindowPosition.restart()
+ }
+ else {
+ window.x = window.initX
+ window.y = window.initY
+ }
+ }
+ onClicked: (event) => {
+ if (!windowData) return;
+
+ if (event.button === Qt.LeftButton) {
+ GlobalStates.overviewOpen = false
+ Hyprland.dispatch(`focuswindow address:${windowData.address}`)
+ event.accepted = true
+ } else if (event.button === Qt.MiddleButton) {
+ Hyprland.dispatch(`closewindow address:${windowData.address}`)
+ event.accepted = true
+ }
+ }
+
+ StyledToolTip {
+ extraVisibleCondition: false
+ alternativeVisibleCondition: dragArea.containsMouse && !window.Drag.active
+ content: `${windowData.title}\n[${windowData.class}] ${windowData.xwayland ? "[XWayland] " : ""}\n`
+ }
+ }
+ }
+ }
+
+ Rectangle { // Focused workspace indicator
+ id: focusedWorkspaceIndicator
+ property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown)
+ property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / ConfigOptions.overview.numOfCols)
+ property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % ConfigOptions.overview.numOfCols
+ x: (root.workspaceImplicitWidth + workspaceSpacing) * activeWorkspaceColIndex
+ y: (root.workspaceImplicitHeight + workspaceSpacing) * activeWorkspaceRowIndex
+ z: root.windowZ
+ width: Math.max(100, root.workspaceImplicitWidth)
+ height: Math.max(60, root.workspaceImplicitHeight)
+ color: "transparent"
+ radius: Appearance.rounding.screenRounding * root.scale
+ border.width: 2
+ border.color: root.activeBorderColor
+ visible: width > 0 && height > 0 && activeWorkspaceInGroup > 0
+ Behavior on x {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on y {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/config/quickshell/modules/overview/OverviewWindow.qml b/config/quickshell/modules/overview/OverviewWindow.qml
new file mode 100644
index 00000000..273eff7e
--- /dev/null
+++ b/config/quickshell/modules/overview/OverviewWindow.qml
@@ -0,0 +1,94 @@
+import "root:/services/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Io
+import Quickshell.Hyprland
+
+Rectangle { // Window
+ id: root
+ property var windowData
+ property var monitorData
+ property var scale
+ property var availableWorkspaceWidth
+ property var availableWorkspaceHeight
+ property bool restrictToWorkspace: true
+ property real initX: Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset
+ property real initY: Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset
+ property real xOffset: 0
+ property real yOffset: 0
+
+ property var targetWindowWidth: windowData?.size[0] * scale
+ property var targetWindowHeight: windowData?.size[1] * scale
+ property bool hovered: false
+ property bool pressed: false
+
+ property var iconToWindowRatio: 0.35
+ property var xwaylandIndicatorToIconRatio: 0.35
+ property var iconToWindowRatioCompact: 0.6
+ property var iconPath: Quickshell.iconPath(AppSearch.guessIcon(windowData?.class), "image-missing")
+ property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth
+
+ property bool indicateXWayland: (ConfigOptions.overview.showXwaylandIndicator && windowData?.xwayland) ?? false
+
+ x: initX
+ y: initY
+ width: Math.min(windowData?.size[0] * root.scale, (restrictToWorkspace ? windowData?.size[0] : availableWorkspaceWidth - x + xOffset))
+ height: Math.min(windowData?.size[1] * root.scale, (restrictToWorkspace ? windowData?.size[1] : availableWorkspaceHeight - y + yOffset))
+
+ radius: Appearance.rounding.windowRounding * root.scale
+ color: pressed ? Appearance.colors.colLayer2Active : hovered ? Appearance.colors.colLayer2Hover : Appearance.colors.colLayer2
+ // border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.9)
+ border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.4)
+ border.pixelAligned : false
+ border.width : 2
+
+ Behavior on x {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on y {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on width {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on height {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+
+ ColumnLayout {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.right: parent.right
+ spacing: Appearance.font.pixelSize.smaller * 0.5
+
+ IconImage {
+ id: windowIcon
+ Layout.alignment: Qt.AlignHCenter
+ source: root.iconPath
+ implicitSize: Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio)
+
+ Behavior on implicitSize {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ }
+
+ StyledText {
+ Layout.leftMargin: 10
+ Layout.rightMargin: 10
+ visible: !compactMode
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ horizontalAlignment: Text.AlignHCenter
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ font.italic: indicateXWayland ? true : false
+ elide: Text.ElideRight
+ text: windowData?.title ?? ""
+ }
+ }
+} \ No newline at end of file
diff --git a/config/quickshell/modules/overview/SearchItem.qml b/config/quickshell/modules/overview/SearchItem.qml
new file mode 100644
index 00000000..1363b88d
--- /dev/null
+++ b/config/quickshell/modules/overview/SearchItem.qml
@@ -0,0 +1,220 @@
+// pragma NativeMethodBehavior: AcceptThisObject
+import "root:/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Hyprland
+
+RippleButton {
+ id: root
+ property var entry
+ property string query
+ 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 bigText: entry?.bigText ?? ""
+ property string materialSymbol: entry?.materialSymbol ?? ""
+ property string cliphistRawString: entry?.cliphistRawString ?? ""
+
+ property string highlightPrefix: `<u><font color="${Appearance.colors.colPrimary}">`
+ property string highlightSuffix: `</font></u>`
+ function highlightContent(content, query) {
+ if (!query || query.length === 0 || content == query || fontType === "monospace")
+ return StringUtils.escapeHtml(content);
+
+ let contentLower = content.toLowerCase();
+ let queryLower = query.toLowerCase();
+
+ let result = "";
+ let lastIndex = 0;
+ let qIndex = 0;
+
+ for (let i = 0; i < content.length && qIndex < query.length; i++) {
+ if (contentLower[i] === queryLower[qIndex]) {
+ // Add non-highlighted part (escaped)
+ if (i > lastIndex)
+ result += StringUtils.escapeHtml(content.slice(lastIndex, i));
+ // Add highlighted character (escaped)
+ result += root.highlightPrefix + StringUtils.escapeHtml(content[i]) + root.highlightSuffix;
+ lastIndex = i + 1;
+ qIndex++;
+ }
+ }
+ // Add the rest of the string (escaped)
+ if (lastIndex < content.length)
+ result += StringUtils.escapeHtml(content.slice(lastIndex));
+
+ return result;
+ }
+ property string displayContent: highlightContent(root.itemName, root.query)
+
+ property list<string> urls: {
+ if (!root.itemName) return [];
+ // Regular expression to match URLs
+ const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi;
+ const matches = root.itemName?.match(urlRegex)
+ ?.filter(url => !url.includes("…")) // Elided = invalid
+ return matches ? matches : [];
+ }
+
+ visible: root.entryShown
+ property int horizontalMargin: 10
+ property int buttonHorizontalPadding: 10
+ property int buttonVerticalPadding: 5
+ property bool keyboardDown: false
+
+ implicitHeight: rowLayout.implicitHeight + root.buttonVerticalPadding * 2
+ implicitWidth: rowLayout.implicitWidth + root.buttonHorizontalPadding * 2
+ buttonRadius: Appearance.rounding.normal
+ colBackground: (root.down || root.keyboardDown) ? Appearance.colors.colLayer1Active :
+ ((root.hovered || root.focus) ? Appearance.colors.colLayer1Hover :
+ ColorUtils.transparentize(Appearance.m3colors.m3surfaceContainerHigh, 1))
+ colBackgroundHover: Appearance.colors.colLayer1Hover
+ colRipple: Appearance.colors.colLayer1Active
+
+ background {
+ anchors.fill: root
+ anchors.leftMargin: root.horizontalMargin
+ anchors.rightMargin: root.horizontalMargin
+ }
+
+ PointingHandInteraction {}
+ onClicked: {
+ root.itemExecute()
+ Hyprland.dispatch("global quickshell:overviewClose")
+ }
+ Keys.onPressed: (event) => {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ root.keyboardDown = true
+ root.clicked()
+ event.accepted = true;
+ }
+ }
+ Keys.onReleased: (event) => {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ root.keyboardDown = false
+ event.accepted = true;
+ }
+ }
+
+ RowLayout {
+ id: rowLayout
+ spacing: iconLoader.sourceComponent === null ? 0 : 10
+ anchors.fill: parent
+ anchors.leftMargin: root.horizontalMargin + root.buttonHorizontalPadding
+ anchors.rightMargin: root.horizontalMargin + root.buttonHorizontalPadding
+
+ // Icon
+ Loader {
+ id: iconLoader
+ active: true
+ sourceComponent: root.materialSymbol !== "" ? materialSymbolComponent :
+ root.bigText ? bigTextComponent :
+ root.itemIcon !== "" ? iconImageComponent :
+ null
+ }
+
+ Component {
+ id: iconImageComponent
+ IconImage {
+ source: Quickshell.iconPath(root.itemIcon, "image-missing")
+ width: 35
+ height: 35
+ }
+ }
+
+ Component {
+ id: materialSymbolComponent
+ MaterialSymbol {
+ text: root.materialSymbol
+ iconSize: 30
+ color: Appearance.m3colors.m3onSurface
+ }
+ }
+
+ Component {
+ id: bigTextComponent
+ StyledText {
+ text: root.bigText
+ font.pixelSize: Appearance.font.pixelSize.larger
+ color: Appearance.m3colors.m3onSurface
+ }
+ }
+
+ // Main text
+ ColumnLayout {
+ id: contentColumn
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignVCenter
+ spacing: 0
+ StyledText {
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ color: Appearance.colors.colSubtext
+ visible: root.itemType && root.itemType != qsTr("App")
+ text: root.itemType
+ }
+ RowLayout {
+ Loader { // Checkmark for copied clipboard entry
+ visible: itemName == Quickshell.clipboardText && root.cliphistRawString
+ active: itemName == Quickshell.clipboardText && root.cliphistRawString
+ sourceComponent: Rectangle {
+ implicitWidth: activeText.implicitHeight
+ implicitHeight: activeText.implicitHeight
+ radius: Appearance.rounding.full
+ color: Appearance.colors.colPrimary
+ MaterialSymbol {
+ id: activeText
+ anchors.centerIn: parent
+ text: "check"
+ font.pixelSize: Appearance.font.pixelSize.normal
+ color: Appearance.m3colors.m3onPrimary
+ }
+ }
+ }
+ StyledText { // Item name/content
+ Layout.fillWidth: true
+ id: nameText
+ textFormat: Text.StyledText // RichText also works, but StyledText ensures elide work
+ font.pixelSize: Appearance.font.pixelSize.small
+ font.family: Appearance.font.family[root.fontType]
+ color: Appearance.m3colors.m3onSurface
+ horizontalAlignment: Text.AlignLeft
+ elide: Text.ElideRight
+ text: `${root.displayContent}`
+ }
+ }
+ Loader { // Clipboard image preview
+ active: root.cliphistRawString && /^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(root.cliphistRawString)
+ sourceComponent: CliphistImage {
+ Layout.fillWidth: true
+ entry: root.cliphistRawString
+ maxWidth: contentColumn.width
+ maxHeight: 140
+ }
+ }
+ }
+
+ // Action text
+ StyledText {
+ Layout.fillWidth: false
+ visible: (root.hovered || root.focus)
+ id: clickAction
+ font.pixelSize: Appearance.font.pixelSize.normal
+ color: Appearance.colors.colSubtext
+ horizontalAlignment: Text.AlignRight
+ text: root.itemClickActionName
+ }
+ }
+}
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