Rearrange for tidier structure (#2212)

This commit is contained in:
clsty
2025-10-16 07:19:55 +08:00
parent 13065d7e5a
commit 8b493e091d
529 changed files with 165 additions and 138 deletions
@@ -0,0 +1,241 @@
import qs
import qs.services
import qs.modules.common
import qs.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
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
}
// HyprlandWindow.visibleMask: Region { // Buggy with scaled monitors
// item: GlobalStates.overviewOpen ? columnLayout : null
// }
anchors {
top: true
bottom: true
left: !(Config?.options.overview.enable ?? true)
right: !(Config?.options.overview.enable ?? 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();
}
ColumnLayout {
id: columnLayout
visible: GlobalStates.overviewOpen
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
}
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
Layout.alignment: Qt.AlignHCenter
onSearchingTextChanged: text => {
root.searchingText = searchingText;
}
}
Loader {
id: overviewLoader
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,275 @@
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.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 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: Appearance.rounding.screenRounding * root.scale + padding
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.colors.colLayer0Border
Column { // Workspaces
id: workspaceColumnLayout
z: root.workspaceZ
anchors.centerIn: parent
spacing: workspaceSpacing
Repeater {
model: Config.options.overview.rows
delegate: Row {
id: row
property int rowIndex: index
spacing: workspaceSpacing
Repeater { // Workspace repeater
model: Config.options.overview.columns
Rectangle { // Workspace
id: workspace
property int colIndex: index
property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * Config.options.overview.columns + colIndex + 1
property color defaultWorkspaceColor: Appearance.colors.colLayer1 // TODO: reconsider this color for a cleaner look
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
radius: Appearance.rounding.screenRounding * root.scale
border.width: 2
border.color: hoveredWhileDragging ? hoveredBorderColor : "transparent"
StyledText {
anchors.centerIn: parent
text: 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 ${workspaceValue}`)
}
}
}
DropArea {
anchors.fill: parent
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: {
// 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
availableWorkspaceWidth: root.workspaceImplicitWidth
availableWorkspaceHeight: root.workspaceImplicitHeight
widgetMonitorId: root.monitor.id
windowData: windowByAddress[address]
property bool atInitPosition: (initX == x && initY == y)
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
Timer {
id: updateWindowPosition
interval: Config.options.hacks.arbitraryRaceConditionDelay
repeat: false
running: false
onTriggered: {
window.x = Math.round(Math.max((windowData?.at[0] - (monitor?.x ?? 0) - monitorData?.reserved[0]) * root.scale, 0) + xOffset)
window.y = Math.round(Math.max((windowData?.at[1] - (monitor?.y ?? 0) - monitorData?.reserved[1]) * root.scale, 0) + yOffset)
}
}
z: Drag.active ? root.windowDraggingZ : (root.windowZ + windowData?.floating)
Drag.hotSpot.x: targetWindowWidth / 2
Drag.hotSpot.y: targetWindowHeight / 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 activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / Config.options.overview.columns)
property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % Config.options.overview.columns
x: (root.workspaceImplicitWidth + workspaceSpacing) * activeWorkspaceColIndex
y: (root.workspaceImplicitHeight + workspaceSpacing) * activeWorkspaceRowIndex
z: root.windowZ
width: root.workspaceImplicitWidth
height: root.workspaceImplicitHeight
color: "transparent"
radius: Appearance.rounding.screenRounding * root.scale
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)
}
}
}
}
}
@@ -0,0 +1,110 @@
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 var availableWorkspaceWidth
property var availableWorkspaceHeight
property bool restrictToWorkspace: true
property real initX: Math.max((windowData?.at[0] - (monitorData?.x ?? 0) - monitorData?.reserved[0]) * root.scale, 0) + xOffset
property real initY: Math.max((windowData?.at[1] - (monitorData?.y ?? 0) - monitorData?.reserved[1]) * root.scale, 0) + yOffset
property real xOffset: 0
property real yOffset: 0
property int widgetMonitorId: 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: windowData?.xwayland ?? false
x: initX
y: initY
width: windowData?.size[0] * root.scale
height: windowData?.size[1] * root.scale
opacity: windowData.monitor == widgetMonitorId ? 1 : 0.4
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: root.width
height: root.height
radius: Appearance.rounding.windowRounding * root.scale
}
}
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
radius: Appearance.rounding.windowRounding * root.scale
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.7)
border.width : 1
}
Image {
id: windowIcon
anchors.centerIn: parent
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 Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / root.monitorData.scale;
}
// 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,280 @@
// 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.colSecondaryContainerActive :
((root.hovered || root.focus) ? Appearance.colors.colSecondaryContainer :
ColorUtils.transparentize(Appearance.colors.colSecondaryContainer, 1))
colBackgroundHover: Appearance.colors.colSecondaryContainer
colRipple: Appearance.colors.colSecondaryContainerActive
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_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.colSubtext
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,503 @@
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
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: ""
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 disableExpandAnimation() {
searchWidthBehavior.enabled = false;
}
function cancelSearch() {
searchInput.selectAll();
root.searchingText = "";
searchWidthBehavior.enabled = true;
}
function setSearchingText(text) {
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 (!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;
root.focusFirstItem();
}
}
}
StyledRectangularShadow {
target: searchWidgetContent
}
Rectangle { // Background
id: searchWidgetContent
anchors.centerIn: parent
implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight
radius: Appearance.rounding.large
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.colors.colLayer0Border
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(Config.options.search.prefix.clipboard) ? 'content_paste_search' : '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.colors.colSecondaryContainer
placeholderText: Translation.tr("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.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
])
}
}
}
}
}