qs: move panels into modules/ii

This commit is contained in:
end-4
2025-11-09 08:58:56 +01:00
parent f678a55e6a
commit 53768a6885
194 changed files with 68 additions and 73 deletions
@@ -0,0 +1,241 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import Qt.labs.synchronizer
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
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
// WlrLayershell.keyboardFocus: GlobalStates.overviewOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
color: "transparent"
mask: Region {
item: GlobalStates.overviewOpen ? columnLayout : null
}
anchors {
top: true
bottom: true
left: true
right: 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) {
searchWidget.disableExpandAnimation();
overviewScope.dontAutoCancelSearch = false;
} else {
if (!overviewScope.dontAutoCancelSearch) {
searchWidget.cancelSearch();
}
delayedGrabTimer.start();
}
}
}
Timer {
id: delayedGrabTimer
interval: Config.options.hacks.arbitraryRaceConditionDelay
repeat: false
onTriggered: {
if (!grab.canBeActive)
return;
grab.active = GlobalStates.overviewOpen;
}
}
implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight
function setSearchingText(text) {
searchWidget.setSearchingText(text);
searchWidget.focusFirstItem();
}
Column {
id: columnLayout
visible: GlobalStates.overviewOpen
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
}
spacing: -8
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
GlobalStates.overviewOpen = false;
} else if (event.key === Qt.Key_Left) {
if (!root.searchingText)
Hyprland.dispatch("workspace r-1");
} else if (event.key === Qt.Key_Right) {
if (!root.searchingText)
Hyprland.dispatch("workspace r+1");
}
}
SearchWidget {
id: searchWidget
anchors.horizontalCenter: parent.horizontalCenter
Synchronizer on searchingText {
property alias source: root.searchingText
}
}
Loader {
id: overviewLoader
anchors.horizontalCenter: parent.horizontalCenter
active: GlobalStates.overviewOpen && (Config?.options.overview.enable ?? true)
sourceComponent: OverviewWidget {
panelWindow: root
visible: (root.searchingText == "")
}
}
}
}
}
function toggleClipboard() {
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(Config.options.search.prefix.clipboard);
GlobalStates.overviewOpen = true;
return;
}
}
}
function toggleEmojis() {
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(Config.options.search.prefix.emojis);
GlobalStates.overviewOpen = true;
return;
}
}
}
IpcHandler {
target: "overview"
function toggle() {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
function close() {
GlobalStates.overviewOpen = false;
}
function open() {
GlobalStates.overviewOpen = true;
}
function toggleReleaseInterrupt() {
GlobalStates.superReleaseMightTrigger = false;
}
function clipboardToggle() {
overviewScope.toggleClipboard();
}
}
GlobalShortcut {
name: "overviewToggle"
description: "Toggles overview on press"
onPressed: {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "overviewClose"
description: "Closes overview"
onPressed: {
GlobalStates.overviewOpen = false;
}
}
GlobalShortcut {
name: "overviewToggleRelease"
description: "Toggles overview on release"
onPressed: {
GlobalStates.superReleaseMightTrigger = true;
}
onReleased: {
if (!GlobalStates.superReleaseMightTrigger) {
GlobalStates.superReleaseMightTrigger = true;
return;
}
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "overviewToggleReleaseInterrupt"
description: "Interrupts possibility of overview being toggled on release. " + "This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. " + "To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything."
onPressed: {
GlobalStates.superReleaseMightTrigger = false;
}
}
GlobalShortcut {
name: "overviewClipboardToggle"
description: "Toggle clipboard query on overview widget"
onPressed: {
overviewScope.toggleClipboard();
}
}
GlobalShortcut {
name: "overviewEmojiToggle"
description: "Toggle emoji query on overview widget"
onPressed: {
overviewScope.toggleEmojis();
}
}
}
@@ -0,0 +1,329 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Layouts
import Quickshell
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: Config.options.overview.rows * Config.options.overview.columns
readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown)
property bool monitorIsFocused: (Hyprland.focusedMonitor?.name == monitor.name)
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: Config.options.overview.scale
property color activeBorderColor: Appearance.colors.colSecondary
property real workspaceImplicitWidth: (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: (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 largeWorkspaceRadius: Appearance.rounding.large
property real smallWorkspaceRadius: Appearance.rounding.verysmall
property real workspaceNumberMargin: 80
property real workspaceNumberSize: 250 * 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: []
StyledRectangularShadow {
target: overviewBackground
}
Rectangle { // Background
id: overviewBackground
property real padding: 10
anchors.fill: parent
anchors.margins: Appearance.sizes.elevationMargin
implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2
implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2
radius: root.largeWorkspaceRadius + padding
color: Appearance.colors.colBackgroundSurfaceContainer
Column { // Workspaces
id: workspaceColumnLayout
z: root.workspaceZ
anchors.centerIn: parent
spacing: workspaceSpacing
Repeater {
model: Config.options.overview.rows
delegate: Row {
id: row
required property int index
spacing: workspaceSpacing
Repeater { // Workspace repeater
model: Config.options.overview.columns
Rectangle { // Workspace
id: workspace
required property int index
property int colIndex: index
property int workspaceValue: root.workspaceGroup * root.workspacesShown + row.index * Config.options.overview.columns + colIndex + 1
property color defaultWorkspaceColor: ColorUtils.mix(Appearance.colors.colBackgroundSurfaceContainer, Appearance.colors.colSurfaceContainerHigh, 0.8)
property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1)
property color hoveredBorderColor: Appearance.colors.colLayer2Hover
property bool hoveredWhileDragging: false
implicitWidth: root.workspaceImplicitWidth
implicitHeight: root.workspaceImplicitHeight
color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor
property bool workspaceAtLeft: colIndex === 0
property bool workspaceAtRight: colIndex === Config.options.overview.columns - 1
property bool workspaceAtTop: row.index === 0
property bool workspaceAtBottom: row.index === Config.options.overview.rows - 1
topLeftRadius: (workspaceAtLeft && workspaceAtTop) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
topRightRadius: (workspaceAtRight && workspaceAtTop) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
bottomLeftRadius: (workspaceAtLeft && workspaceAtBottom) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
bottomRightRadius: (workspaceAtRight && workspaceAtBottom) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
border.width: 2
border.color: hoveredWhileDragging ? hoveredBorderColor : "transparent"
StyledText {
anchors.centerIn: parent
text: workspace.workspaceValue
font {
pixelSize: root.workspaceNumberSize * root.scale
weight: Font.DemiBold
family: Appearance.font.family.expressive
}
color: ColorUtils.transparentize(Appearance.colors.colOnLayer1, 0.8)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
MouseArea {
id: workspaceArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onPressed: {
if (root.draggingTargetWorkspace === -1) {
GlobalStates.overviewOpen = false
Hyprland.dispatch(`workspace ${workspace.workspaceValue}`)
}
}
}
DropArea {
anchors.fill: parent
onEntered: {
root.draggingTargetWorkspace = workspace.workspaceValue
if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return;
hoveredWhileDragging = true
}
onExited: {
hoveredWhileDragging = false
if (root.draggingTargetWorkspace == workspace.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: {
// console.log(JSON.stringify(ToplevelManager.toplevels.values.map(t => t), null, 2))
return [...ToplevelManager.toplevels.values.filter((toplevel) => {
const address = `0x${toplevel.HyprlandToplevel?.address}`
var win = windowByAddress[address]
const inWorkspaceGroup = (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown)
return inWorkspaceGroup;
})].reverse()
}
}
delegate: OverviewWindow {
id: window
required property var modelData
property int monitorId: windowData?.monitor
property var monitor: HyprlandData.monitors.find(m => m.id == monitorId)
property var address: `0x${modelData.HyprlandToplevel.address}`
toplevel: modelData
monitorData: this.monitor
scale: root.scale
widgetMonitor: HyprlandData.monitors.find(m => m.id == root.monitor.id)
windowData: windowByAddress[address]
property bool atInitPosition: (initX == x && initY == y)
// Offset on the canvas
property int workspaceColIndex: (windowData?.workspace.id - 1) % Config.options.overview.columns
property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / Config.options.overview.columns)
xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex
yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex
property real xWithinWorkspaceWidget: Math.max((windowData?.at[0] - (monitor?.x ?? 0) - monitorData?.reserved[0]) * root.scale, 0)
property real yWithinWorkspaceWidget: Math.max((windowData?.at[1] - (monitor?.y ?? 0) - monitorData?.reserved[1]) * root.scale, 0)
// Radius
property real minRadius: Appearance.rounding.small
property bool workspaceAtLeft: workspaceColIndex === 0
property bool workspaceAtRight: workspaceColIndex === Config.options.overview.columns - 1
property bool workspaceAtTop: workspaceRowIndex === 0
property bool workspaceAtBottom: workspaceRowIndex === Config.options.overview.rows - 1
property bool workspaceAtTopLeft: (workspaceAtLeft && workspaceAtTop)
property bool workspaceAtTopRight: (workspaceAtRight && workspaceAtTop)
property bool workspaceAtBottomLeft: (workspaceAtLeft && workspaceAtBottom)
property bool workspaceAtBottomRight: (workspaceAtRight && workspaceAtBottom)
property real distanceFromLeftEdge: xWithinWorkspaceWidget
property real distanceFromRightEdge: root.workspaceImplicitWidth - (xWithinWorkspaceWidget + targetWindowWidth)
property real distanceFromTopEdge: yWithinWorkspaceWidget
property real distanceFromBottomEdge: root.workspaceImplicitHeight - (yWithinWorkspaceWidget + targetWindowHeight)
property real distanceFromTopLeftCorner: Math.max(distanceFromLeftEdge, distanceFromTopEdge)
property real distanceFromTopRightCorner: Math.max(distanceFromRightEdge, distanceFromTopEdge)
property real distanceFromBottomLeftCorner: Math.max(distanceFromLeftEdge, distanceFromBottomEdge)
property real distanceFromBottomRightCorner: Math.max(distanceFromRightEdge, distanceFromBottomEdge)
topLeftRadius: Math.max((workspaceAtTopLeft ? root.largeWorkspaceRadius : root.smallWorkspaceRadius) - distanceFromTopLeftCorner, minRadius)
topRightRadius: Math.max((workspaceAtTopRight ? root.largeWorkspaceRadius : root.smallWorkspaceRadius) - distanceFromTopRightCorner, minRadius)
bottomLeftRadius: Math.max((workspaceAtBottomLeft ? root.largeWorkspaceRadius : root.smallWorkspaceRadius) - distanceFromBottomLeftCorner, minRadius)
bottomRightRadius: Math.max((workspaceAtBottomRight ? root.largeWorkspaceRadius : root.smallWorkspaceRadius) - distanceFromBottomRightCorner, minRadius)
Timer {
id: updateWindowPosition
interval: Config.options.hacks.arbitraryRaceConditionDelay
repeat: false
running: false
onTriggered: {
window.x = Math.round(xWithinWorkspaceWidget + xOffset)
window.y = Math.round(yWithinWorkspaceWidget + yOffset)
}
}
z: Drag.active ? root.windowDraggingZ : (root.windowZ + windowData?.floating)
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
MouseArea {
id: dragArea
anchors.fill: parent
hoverEnabled: true
onEntered: hovered = true // For hover color change
onExited: hovered = false // For hover color change
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
drag.target: parent
onPressed: (mouse) => {
root.draggingFromWorkspace = windowData?.workspace.id
window.pressed = true
window.Drag.active = true
window.Drag.source = window
window.Drag.hotSpot.x = mouse.x
window.Drag.hotSpot.y = mouse.y
// console.log(`[OverviewWindow] Dragging window ${windowData?.address} from position (${window.x}, ${window.y})`)
}
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 {
if (!window.windowData.floating) {
updateWindowPosition.restart()
return
}
const percentageX = Math.round((window.x - xOffset) / root.workspaceImplicitWidth * 100)
const percentageY = Math.round((window.y - yOffset) / root.workspaceImplicitHeight * 100)
Hyprland.dispatch(`movewindowpixel exact ${percentageX}% ${percentageY}%, address:${window.windowData?.address}`)
}
}
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
text: `${windowData.title}\n[${windowData.class}] ${windowData.xwayland ? "[XWayland] " : ""}`
}
}
}
}
Rectangle { // Focused workspace indicator
id: focusedWorkspaceIndicator
property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown)
property int rowIndex: Math.floor((activeWorkspaceInGroup - 1) / Config.options.overview.columns)
property int colIndex: (activeWorkspaceInGroup - 1) % Config.options.overview.columns
x: (root.workspaceImplicitWidth + workspaceSpacing) * colIndex
y: (root.workspaceImplicitHeight + workspaceSpacing) * rowIndex
z: root.windowZ
width: root.workspaceImplicitWidth
height: root.workspaceImplicitHeight
color: "transparent"
property bool workspaceAtLeft: colIndex === 0
property bool workspaceAtRight: colIndex === Config.options.overview.columns - 1
property bool workspaceAtTop: rowIndex === 0
property bool workspaceAtBottom: rowIndex === Config.options.overview.rows - 1
topLeftRadius: (workspaceAtLeft && workspaceAtTop) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
topRightRadius: (workspaceAtRight && workspaceAtTop) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
bottomLeftRadius: (workspaceAtLeft && workspaceAtBottom) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
bottomRightRadius: (workspaceAtRight && workspaceAtBottom) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
border.width: 2
border.color: root.activeBorderColor
Behavior on x {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on y {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on topLeftRadius {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Behavior on topRightRadius {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Behavior on bottomLeftRadius {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Behavior on bottomRightRadius {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
}
}
}
}
@@ -0,0 +1,144 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
Item { // Window
id: root
property var toplevel
property var windowData
property var monitorData
property var scale
property bool restrictToWorkspace: true
property real widthRatio: {
const widgetWidth = widgetMonitor.transform & 1 ? widgetMonitor.height : widgetMonitor.width;
const monitorWidth = monitorData.transform & 1 ? monitorData.height : monitorData.width;
return (widgetWidth * monitorData.scale) / (monitorWidth * widgetMonitor.scale);
}
property real heightRatio: {
const widgetHeight = widgetMonitor.transform & 1 ? widgetMonitor.width : widgetMonitor.height;
const monitorHeight = monitorData.transform & 1 ? monitorData.width : monitorData.height;
return (widgetHeight * monitorData.scale) / (monitorHeight * widgetMonitor.scale);
}
property real initX: {
return Math.max((windowData?.at[0] - (monitorData?.x ?? 0) - monitorData?.reserved[0]) * widthRatio * root.scale, 0) + xOffset;
}
property real initY: {
return Math.max((windowData?.at[1] - (monitorData?.y ?? 0) - monitorData?.reserved[1]) * heightRatio * root.scale, 0) + yOffset;
}
property real xOffset: 0
property real yOffset: 0
property var widgetMonitor
property int widgetMonitorId: widgetMonitor.id
property var targetWindowWidth: windowData?.size[0] * scale * widthRatio
property var targetWindowHeight: windowData?.size[1] * scale * heightRatio
property bool hovered: false
property bool pressed: false
property bool centerIcons: Config.options.overview.centerIcons
property real iconGapRatio: 0.06
property real iconToWindowRatio: centerIcons ? 0.35 : 0.15
property real xwaylandIndicatorToIconRatio: 0.35
property real iconToWindowRatioCompact: 0.6
property string 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: windowData?.xwayland ?? false
x: initX
y: initY
width: targetWindowWidth
height: targetWindowHeight
opacity: windowData.monitor == widgetMonitorId ? 1 : 0.4
property real topLeftRadius
property real topRightRadius
property real bottomLeftRadius
property real bottomRightRadius
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: root.width
height: root.height
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomRightRadius: root.bottomRightRadius
bottomLeftRadius: root.bottomLeftRadius
}
}
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)
}
ScreencopyView {
id: windowPreview
anchors.fill: parent
captureSource: GlobalStates.overviewOpen ? root.toplevel : null
live: true
// Color overlay for interactions
Rectangle {
anchors.fill: parent
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomRightRadius: root.bottomRightRadius
bottomLeftRadius: root.bottomLeftRadius
color: pressed ? ColorUtils.transparentize(Appearance.colors.colLayer2Active, 0.5) :
hovered ? ColorUtils.transparentize(Appearance.colors.colLayer2Hover, 0.7) :
ColorUtils.transparentize(Appearance.colors.colLayer2)
border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.88)
border.width : 1
}
Image {
id: windowIcon
property real baseSize: Math.min(root.targetWindowWidth, root.targetWindowHeight)
anchors {
top: root.centerIcons ? undefined : parent.top
left: root.centerIcons ? undefined : parent.left
centerIn: root.centerIcons ? parent : undefined
margins: baseSize * root.iconGapRatio
}
property var iconSize: {
// console.log("-=-=-", root.toplevel.title, "-=-=-")
// console.log("Target window size:", targetWindowWidth, targetWindowHeight)
// console.log("Icon ratio:", root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio)
// console.log("Scale:", root.monitorData.scale)
// console.log("Final:", Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / root.monitorData.scale)
return baseSize * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio);
}
// mipmap: true
Layout.alignment: Qt.AlignHCenter
source: root.iconPath
width: iconSize
height: iconSize
sourceSize: Qt.size(iconSize, iconSize)
Behavior on width {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Behavior on height {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
}
}
}
@@ -0,0 +1,152 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
RowLayout {
id: root
spacing: 6
property bool animateWidth: false
property alias searchInput: searchInput
property string searchingText
function forceFocus() {
searchInput.forceActiveFocus();
}
enum SearchPrefixType { Action, App, Clipboard, Emojis, Math, ShellCommand, WebSearch, DefaultSearch }
property var searchPrefixType: {
if (root.searchingText.startsWith(Config.options.search.prefix.action)) return SearchBar.SearchPrefixType.Action;
if (root.searchingText.startsWith(Config.options.search.prefix.app)) return SearchBar.SearchPrefixType.App;
if (root.searchingText.startsWith(Config.options.search.prefix.clipboard)) return SearchBar.SearchPrefixType.Clipboard;
if (root.searchingText.startsWith(Config.options.search.prefix.emojis)) return SearchBar.SearchPrefixType.Emojis;
if (root.searchingText.startsWith(Config.options.search.prefix.math)) return SearchBar.SearchPrefixType.Math;
if (root.searchingText.startsWith(Config.options.search.prefix.shellCommand)) return SearchBar.SearchPrefixType.ShellCommand;
if (root.searchingText.startsWith(Config.options.search.prefix.webSearch)) return SearchBar.SearchPrefixType.WebSearch;
return SearchBar.SearchPrefixType.DefaultSearch;
}
MaterialShapeWrappedMaterialSymbol {
id: searchIcon
Layout.alignment: Qt.AlignVCenter
iconSize: Appearance.font.pixelSize.huge
shape: switch(root.searchPrefixType) {
case SearchBar.SearchPrefixType.Action: return MaterialShape.Shape.Pill;
case SearchBar.SearchPrefixType.App: return MaterialShape.Shape.Clover4Leaf;
case SearchBar.SearchPrefixType.Clipboard: return MaterialShape.Shape.Gem;
case SearchBar.SearchPrefixType.Emojis: return MaterialShape.Shape.Sunny;
case SearchBar.SearchPrefixType.Math: return MaterialShape.Shape.PuffyDiamond;
case SearchBar.SearchPrefixType.ShellCommand: return MaterialShape.Shape.PixelCircle;
case SearchBar.SearchPrefixType.WebSearch: return MaterialShape.Shape.SoftBurst;
default: return MaterialShape.Shape.Cookie7Sided;
}
text: switch (root.searchPrefixType) {
case SearchBar.SearchPrefixType.Action: return "settings_suggest";
case SearchBar.SearchPrefixType.App: return "apps";
case SearchBar.SearchPrefixType.Clipboard: return "content_paste_search";
case SearchBar.SearchPrefixType.Emojis: return "add_reaction";
case SearchBar.SearchPrefixType.Math: return "calculate";
case SearchBar.SearchPrefixType.ShellCommand: return "terminal";
case SearchBar.SearchPrefixType.WebSearch: return "travel_explore";
case SearchBar.SearchPrefixType.DefaultSearch: return "search";
default: return "search";
}
}
ToolbarTextField { // Search box
id: searchInput
Layout.topMargin: 4
Layout.bottomMargin: 4
implicitHeight: 40
focus: GlobalStates.overviewOpen
font.pixelSize: Appearance.font.pixelSize.small
placeholderText: Translation.tr("Search, calculate or run")
implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth
Behavior on implicitWidth {
id: searchWidthBehavior
enabled: root.animateWidth
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();
}
}
}
}
IconToolbarButton {
Layout.topMargin: 4
Layout.bottomMargin: 4
onClicked: {
GlobalStates.overviewOpen = false;
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "search"]);
}
text: "image_search"
StyledToolTip {
text: Translation.tr("Google Lens")
}
}
IconToolbarButton {
id: songRecButton
Layout.topMargin: 4
Layout.bottomMargin: 4
Layout.rightMargin: 4
toggled: SongRec.running
onClicked: SongRec.toggleRunning()
text: "music_cast"
StyledToolTip {
text: Translation.tr("Recognize music")
}
colText: toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnSurfaceVariant
background: MaterialShape {
RotationAnimation on rotation {
running: songRecButton.toggled
duration: 12000
easing.type: Easing.Linear
loops: Animation.Infinite
from: 0
to: 360
}
shape: {
if (songRecButton.down) {
return songRecButton.toggled ? MaterialShape.Shape.Circle : MaterialShape.Shape.Square
} else {
return songRecButton.toggled ? MaterialShape.Shape.SoftBurst : MaterialShape.Shape.Circle
}
}
color: {
if (songRecButton.toggled) {
return songRecButton.hovered ? Appearance.colors.colPrimaryHover : Appearance.colors.colPrimary
} else {
return songRecButton.hovered ? Appearance.colors.colSurfaceContainerHigh : ColorUtils.transparentize(Appearance.colors.colSurfaceContainerHigh)
}
}
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}
}
@@ -0,0 +1,286 @@
// pragma NativeMethodBehavior: AcceptThisObject
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Layouts
import Quickshell
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 ?? Translation.tr("App")
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 ?? "Open"
property string bigText: entry?.bigText ?? ""
property string materialSymbol: entry?.materialSymbol ?? ""
property string cliphistRawString: entry?.cliphistRawString ?? ""
property bool blurImage: entry?.blurImage ?? false
property string blurImageText: entry?.blurImageText ?? "Image hidden"
visible: root.entryShown
property int horizontalMargin: 10
property int buttonHorizontalPadding: 10
property int buttonVerticalPadding: 6
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.colPrimaryContainerActive :
((root.hovered || root.focus) ? Appearance.colors.colPrimaryContainer :
ColorUtils.transparentize(Appearance.colors.colPrimaryContainer, 1))
colBackgroundHover: Appearance.colors.colPrimaryContainer
colRipple: Appearance.colors.colPrimaryContainerActive
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 : [];
}
PointingHandInteraction {}
background {
anchors.fill: root
anchors.leftMargin: root.horizontalMargin
anchors.rightMargin: root.horizontalMargin
}
onClicked: {
GlobalStates.overviewOpen = false
root.itemExecute()
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Delete && event.modifiers === Qt.ShiftModifier) {
const deleteAction = root.entry.actions.find(action => action.name == "Delete");
if (deleteAction) {
deleteAction.execute()
}
} else 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 != Translation.tr("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
}
}
}
Repeater { // Favicons for links
model: root.query == root.itemName ? [] : root.urls
Favicon {
required property var modelData
size: parent.height
url: modelData
}
}
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 && Cliphist.entryIsImage(root.cliphistRawString)
sourceComponent: CliphistImage {
Layout.fillWidth: true
entry: root.cliphistRawString
maxWidth: contentColumn.width
maxHeight: 140
blur: root.blurImage
blurText: root.blurImageText
}
}
}
// Action text
StyledText {
Layout.fillWidth: false
visible: (root.hovered || root.focus)
id: clickAction
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnPrimaryContainer
horizontalAlignment: Text.AlignRight
text: root.itemClickActionName
}
RowLayout {
Layout.alignment: Qt.AlignTop
Layout.topMargin: root.buttonVerticalPadding
Layout.bottomMargin: -root.buttonVerticalPadding // Why is this necessary? Good question.
spacing: 4
Repeater {
model: (root.entry.actions ?? []).slice(0, 4)
delegate: RippleButton {
id: actionButton
required property var modelData
property string iconName: modelData.icon ?? ""
property string materialIconName: modelData.materialIcon ?? ""
implicitHeight: 34
implicitWidth: 34
colBackgroundHover: Appearance.colors.colSecondaryContainerHover
colRipple: Appearance.colors.colSecondaryContainerActive
contentItem: Item {
id: actionContentItem
anchors.centerIn: parent
Loader {
anchors.centerIn: parent
active: !(actionButton.iconName !== "") || actionButton.materialIconName
sourceComponent: MaterialSymbol {
text: actionButton.materialIconName || "video_settings"
font.pixelSize: Appearance.font.pixelSize.hugeass
color: Appearance.m3colors.m3onSurface
}
}
Loader {
anchors.centerIn: parent
active: actionButton.materialIconName.length == 0 && actionButton.iconName && actionButton.iconName !== ""
sourceComponent: IconImage {
source: Quickshell.iconPath(actionButton.iconName)
implicitSize: 20
}
}
}
onClicked: modelData.execute()
StyledToolTip {
text: modelData.name
}
}
}
}
}
}
@@ -0,0 +1,471 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import Qt.labs.synchronizer
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
Item { // Wrapper
id: root
readonly property string xdgConfigHome: Directories.config
property string searchingText: ""
property bool showResults: searchingText != ""
implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2
implicitHeight: searchBar.implicitHeight + searchBar.verticalPadding * 2 + Appearance.sizes.elevationMargin * 2
property string mathResult: ""
property bool clipboardWorkSafetyActive: {
const enabled = Config.options.workSafety.enable.clipboard;
const sensitiveNetwork = (StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords))
return enabled && sensitiveNetwork;
}
property var searchActions: [
{
action: "accentcolor",
execute: args => {
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--noswitch", "--color", ...(args != '' ? [`${args}`] : [])]);
}
},
{
action: "dark",
execute: () => {
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "dark", "--noswitch"]);
}
},
{
action: "konachanwallpaper",
execute: () => {
Quickshell.execDetached([Quickshell.shellPath("scripts/colors/random/random_konachan_wall.sh")]);
}
},
{
action: "light",
execute: () => {
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "light", "--noswitch"]);
}
},
{
action: "superpaste",
execute: args => {
if (!/^(\d+)/.test(args.trim())) { // Invalid if doesn't start with numbers
Quickshell.execDetached([
"notify-send",
Translation.tr("Superpaste"),
Translation.tr("Usage: <tt>%1superpaste NUM_OF_ENTRIES[i]</tt>\nSupply <tt>i</tt> when you want images\nExamples:\n<tt>%1superpaste 4i</tt> for the last 4 images\n<tt>%1superpaste 7</tt> for the last 7 entries").arg(Config.options.search.prefix.action),
"-a", "Shell"
]);
return;
}
const syntaxMatch = /^(?:(\d+)(i)?)/.exec(args.trim());
const count = syntaxMatch[1] ? parseInt(syntaxMatch[1]) : 1;
const isImage = !!syntaxMatch[2];
Cliphist.superpaste(count, isImage);
}
},
{
action: "todo",
execute: args => {
Todo.addTask(args);
}
},
{
action: "wallpaper",
execute: () => {
GlobalStates.wallpaperSelectorOpen = true;
}
},
{
action: "wipeclipboard",
execute: () => {
Cliphist.wipe();
}
},
]
function focusFirstItem() {
appResults.currentIndex = 0;
}
function focusSearchInput() {
searchBar.forceFocus();
}
function disableExpandAnimation() {
searchBar.animateWidth = false;
}
function cancelSearch() {
searchBar.searchInput.selectAll();
root.searchingText = "";
searchBar.animateWidth = true;
}
function setSearchingText(text) {
searchBar.searchInput.text = text;
root.searchingText = text;
}
function containsUnsafeLink(entry) {
if (entry == undefined) return false;
const unsafeKeywords = Config.options.workSafety.triggerCondition.linkKeywords;
return StringUtils.stringListContainsSubstring(entry.toLowerCase(), unsafeKeywords);
}
Timer {
id: nonAppResultsTimer
interval: Config.options.search.nonAppResultDelay
onTriggered: {
let expr = root.searchingText;
if (expr.startsWith(Config.options.search.prefix.math)) {
expr = expr.slice(Config.options.search.prefix.math.length);
}
mathProcess.calculateExpression(expr);
}
}
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.focusFirstItem();
}
}
}
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 (!searchBar.searchInput.activeFocus) {
root.focusSearchInput();
if (event.modifiers & Qt.ControlModifier) {
// Delete word before cursor
let text = searchBar.searchInput.text;
let pos = searchBar.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;
searchBar.searchInput.text = text.slice(0, pos - deleteLen) + text.slice(pos);
searchBar.searchInput.cursorPosition = pos - deleteLen;
}
} else {
// Delete character before cursor if any
if (searchBar.searchInput.cursorPosition > 0) {
searchBar.searchInput.text = searchBar.searchInput.text.slice(0, searchBar.searchInput.cursorPosition - 1) + searchBar.searchInput.text.slice(searchBar.searchInput.cursorPosition);
searchBar.searchInput.cursorPosition -= 1;
}
}
// Always move cursor to end after programmatic edit
searchBar.searchInput.cursorPosition = searchBar.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.key !== Qt.Key_Delete && event.text.charCodeAt(0) >= 0x20) // ignore control chars like Backspace, Tab, etc.
{
if (!searchBar.searchInput.activeFocus) {
root.focusSearchInput();
// Insert the character at the cursor position
searchBar.searchInput.text = searchBar.searchInput.text.slice(0, searchBar.searchInput.cursorPosition) + event.text + searchBar.searchInput.text.slice(searchBar.searchInput.cursorPosition);
searchBar.searchInput.cursorPosition += 1;
event.accepted = true;
root.focusFirstItem();
}
}
}
StyledRectangularShadow {
target: searchWidgetContent
}
Rectangle { // Background
id: searchWidgetContent
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: Appearance.sizes.elevationMargin
}
clip: true
implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight
radius: searchBar.height / 2 + searchBar.verticalPadding
color: Appearance.colors.colBackgroundSurfaceContainer
Behavior on implicitHeight {
id: searchHeightBehavior
enabled: GlobalStates.overviewOpen && root.showResults
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
ColumnLayout {
id: columnLayout
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
}
spacing: 0
// clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: searchWidgetContent.width
height: searchWidgetContent.width
radius: searchWidgetContent.radius
}
}
SearchBar {
id: searchBar
property real verticalPadding: 4
Layout.fillWidth: true
Layout.leftMargin: 10
Layout.rightMargin: 4
Layout.topMargin: verticalPadding
Layout.bottomMargin: verticalPadding
Synchronizer on searchingText {
property alias source: root.searchingText
}
}
Rectangle {
// Separator
visible: root.showResults
Layout.fillWidth: true
height: 1
color: Appearance.colors.colOutlineVariant
}
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
objectProp: "key"
values: {
// Search results are handled here
////////////////// Skip? //////////////////
if (root.searchingText == "")
return [];
///////////// Special cases ///////////////
if (root.searchingText.startsWith(Config.options.search.prefix.clipboard)) {
// Clipboard
const searchString = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.clipboard);
return Cliphist.fuzzyQuery(searchString).map((entry, index, array) => {
const mightBlurImage = Cliphist.entryIsImage(entry) && root.clipboardWorkSafetyActive;
let shouldBlurImage = mightBlurImage;
if (mightBlurImage) {
shouldBlurImage = shouldBlurImage && (containsUnsafeLink(array[index - 1]) || containsUnsafeLink(array[index + 1]));
}
const type = `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`
return {
key: type,
cliphistRawString: entry,
name: StringUtils.cleanCliphistEntry(entry),
clickActionName: "",
type: type,
execute: () => {
Cliphist.copy(entry)
},
actions: [
{
name: "Copy",
materialIcon: "content_copy",
execute: () => {
Cliphist.copy(entry);
}
},
{
name: "Delete",
materialIcon: "delete",
execute: () => {
Cliphist.deleteEntry(entry);
}
}
],
blurImage: shouldBlurImage,
blurImageText: Translation.tr("Work safety")
};
}).filter(Boolean);
}
else if (root.searchingText.startsWith(Config.options.search.prefix.emojis)) {
// Clipboard
const searchString = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.emojis);
return Emojis.fuzzyQuery(searchString).map(entry => {
const emoji = entry.match(/^\s*(\S+)/)?.[1] || ""
return {
key: emoji,
cliphistRawString: entry,
bigText: emoji,
name: entry.replace(/^\s*\S+\s+/, ""),
clickActionName: "",
type: "Emoji",
execute: () => {
Quickshell.clipboardText = entry.match(/^\s*(\S+)/)?.[1];
}
};
}).filter(Boolean);
}
////////////////// Init ///////////////////
nonAppResultsTimer.restart();
const mathResultObject = {
key: `Math result: ${root.mathResult}`,
name: root.mathResult,
clickActionName: Translation.tr("Copy"),
type: Translation.tr("Math result"),
fontType: "monospace",
materialSymbol: 'calculate',
execute: () => {
Quickshell.clipboardText = root.mathResult;
}
};
const appResultObjects = AppSearch.fuzzyQuery(StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.app)).map(entry => {
entry.clickActionName = Translation.tr("Launch");
entry.type = Translation.tr("App");
entry.key = entry.execute
return entry;
})
const commandResultObject = {
key: `cmd ${root.searchingText}`,
name: StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.shellCommand).replace("file://", ""),
clickActionName: Translation.tr("Run"),
type: Translation.tr("Run command"),
fontType: "monospace",
materialSymbol: 'terminal',
execute: () => {
let cleanedCommand = root.searchingText.replace("file://", "");
cleanedCommand = StringUtils.cleanPrefix(cleanedCommand, Config.options.search.prefix.shellCommand);
if (cleanedCommand.startsWith(Config.options.search.prefix.shellCommand)) {
cleanedCommand = cleanedCommand.slice(Config.options.search.prefix.shellCommand.length);
}
Quickshell.execDetached(["bash", "-c", searchingText.startsWith('sudo') ? `${Config.options.apps.terminal} fish -C '${cleanedCommand}'` : cleanedCommand]);
}
};
const webSearchResultObject = {
key: `website ${root.searchingText}`,
name: StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.webSearch),
clickActionName: Translation.tr("Search"),
type: Translation.tr("Search the web"),
materialSymbol: 'travel_explore',
execute: () => {
let query = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.webSearch);
let url = Config.options.search.engineBaseUrl + query;
for (let site of Config.options.search.excludedSites) {
url += ` -site:${site}`;
}
Qt.openUrlExternally(url);
}
}
const launcherActionObjects = root.searchActions.map(action => {
const actionString = `${Config.options.search.prefix.action}${action.action}`;
if (actionString.startsWith(root.searchingText) || root.searchingText.startsWith(actionString)) {
return {
key: `Action ${actionString}`,
name: root.searchingText.startsWith(actionString) ? root.searchingText : actionString,
clickActionName: Translation.tr("Run"),
type: Translation.tr("Action"),
materialSymbol: 'settings_suggest',
execute: () => {
action.execute(root.searchingText.split(" ").slice(1).join(" "));
}
};
}
return null;
}).filter(Boolean);
//////// Prioritized by prefix /////////
let result = [];
const startsWithNumber = /^\d/.test(root.searchingText);
const startsWithMathPrefix = root.searchingText.startsWith(Config.options.search.prefix.math);
const startsWithShellCommandPrefix = root.searchingText.startsWith(Config.options.search.prefix.shellCommand);
const startsWithWebSearchPrefix = root.searchingText.startsWith(Config.options.search.prefix.webSearch);
if (startsWithNumber || startsWithMathPrefix) {
result.push(mathResultObject);
} else if (startsWithShellCommandPrefix) {
result.push(commandResultObject);
} else if (startsWithWebSearchPrefix) {
result.push(webSearchResultObject);
}
//////////////// Apps //////////////////
result = result.concat(appResultObjects);
////////// Launcher actions ////////////
result = result.concat(launcherActionObjects);
/// Math result, command, web search ///
if (Config.options.search.prefix.showDefaultActionsWithoutPrefix) {
if (!startsWithShellCommandPrefix) result.push(commandResultObject);
if (!startsWithNumber && !startsWithMathPrefix) result.push(mathResultObject);
if (!startsWithWebSearchPrefix) result.push(webSearchResultObject);
}
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: StringUtils.cleanOnePrefix(root.searchingText, [
Config.options.search.prefix.action,
Config.options.search.prefix.app,
Config.options.search.prefix.clipboard,
Config.options.search.prefix.emojis,
Config.options.search.prefix.math,
Config.options.search.prefix.shellCommand,
Config.options.search.prefix.webSearch
])
}
}
}
}
}