aboutsummaryrefslogtreecommitdiffstats
path: root/config/quickshell/qs-hyprview/modules
diff options
context:
space:
mode:
Diffstat (limited to 'config/quickshell/qs-hyprview/modules')
-rw-r--r--config/quickshell/qs-hyprview/modules/Hyprview.qml400
-rw-r--r--config/quickshell/qs-hyprview/modules/SearchBox.qml53
-rw-r--r--config/quickshell/qs-hyprview/modules/WindowThumbnail.qml278
-rw-r--r--config/quickshell/qs-hyprview/modules/qmldir3
4 files changed, 734 insertions, 0 deletions
diff --git a/config/quickshell/qs-hyprview/modules/Hyprview.qml b/config/quickshell/qs-hyprview/modules/Hyprview.qml
new file mode 100644
index 00000000..3eafc5e3
--- /dev/null
+++ b/config/quickshell/qs-hyprview/modules/Hyprview.qml
@@ -0,0 +1,400 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import Qt5Compat.GraphicalEffects
+import "../layouts"
+import "."
+
+PanelWindow {
+ id: root
+
+ // --- SETTINGS ---
+ property string layoutAlgorithm: ""
+ property string lastLayoutAlgorithm: ""
+ property bool liveCapture: false
+ property bool moveCursorToActiveWindow: false
+
+ // --- INTERNAL STATE ---
+ property bool isActive: false
+ property bool specialActive: false
+ property bool animateWindows: false
+ property var lastPositions: {}
+
+ anchors { top: true; bottom: true; left: true; right: true }
+ color: "transparent"
+ visible: isActive
+
+ // LayerShell Configs
+ WlrLayershell.layer: WlrLayer.Overlay
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: isActive ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
+ WlrLayershell.namespace: "quickshell:expose"
+
+ Timer {
+ id: searchFocusTimer
+ interval: 25
+ repeat: false
+ onTriggered: {
+ if (root.isActive) {
+ searchBox.focusInput()
+ }
+ }
+ }
+
+ // --- IPC & EVENTS ---
+ IpcHandler {
+ target: "expose"
+ function toggle(layout: string) {
+ root.layoutAlgorithm = layout
+ root.toggleExpose()
+ }
+
+ function open(layout: string) {
+ root.layoutAlgorithm = layout
+ if (root.isActive) return
+ root.toggleExpose()
+ }
+
+ function close() {
+ if (!root.isActive) return
+ root.toggleExpose()
+ }
+ }
+
+ Connections {
+ target: Hyprland
+ function onRawEvent(ev) {
+ if (!root.isActive && ev.name !== "activespecial") return
+
+ switch (ev.name) {
+ case "openwindow":
+ case "closewindow":
+ case "changefloatingmode":
+ case "movewindow":
+ Hyprland.refreshToplevels()
+ refreshThumbs()
+ return
+
+ case "activespecial":
+ var dataStr = String(ev.data)
+ var namePart = dataStr.split(",")[0]
+ root.specialActive = (namePart.length > 0)
+ return
+
+ default:
+ return
+ }
+ }
+ }
+
+ // Update thumbs every 125ms if liveCapture = false
+ Timer {
+ id: screencopyTimer
+ interval: 125
+ repeat: true
+ running: !root.liveCapture && root.isActive
+ onTriggered: root.refreshThumbs()
+ }
+
+
+ function toggleExpose() {
+ root.isActive = !root.isActive
+ if (root.isActive) {
+ if (root.layoutAlgorithm === 'random') {
+ var layouts = [
+ 'smartgrid',
+ 'justified',
+ 'bands',
+ 'masonry',
+ 'hero',
+ 'spiral',
+ 'satellite',
+ 'staggered',
+ 'columnar',
+ 'vortex',
+ ].filter((l) => l !== root.lastLayoutAlgorithm)
+ var randomLayout = layouts[Math.floor(Math.random() * layouts.length)]
+ root.lastLayoutAlgorithm = randomLayout
+ } else {
+ root.lastLayoutAlgorithm = root.layoutAlgorithm
+ }
+
+ exposeArea.currentIndex = -1
+ searchBox.reset()
+ Hyprland.refreshToplevels()
+ refreshThumbs()
+ searchFocusTimer.restart()
+ } else {
+ root.animateWindows = false
+ root.lastPositions = {}
+ searchFocusTimer.stop()
+ searchBox.releaseInputFocus()
+ }
+ }
+
+ function refreshThumbs() {
+ if (!root.isActive) return
+ for (var i = 0; i < winRepeater.count; ++i) {
+ var it = winRepeater.itemAt(i)
+ if (it && it.visible && it.refreshThumb) {
+ it.refreshThumb()
+ }
+ }
+ }
+
+ // --- USER INTERFACE ---
+ FocusScope {
+ id: mainScope
+ anchors.fill: parent
+ focus: true
+
+ Keys.onPressed: (event) => {
+ if (!root.isActive) return
+
+ if (event.key === Qt.Key_Escape) {
+ root.toggleExpose()
+ event.accepted = true
+ return
+ }
+
+ const total = winRepeater.count
+ if (total <= 0) return
+
+ // Helper for horizontal navigation
+ function moveSelectionHorizontal(delta) {
+ var start = exposeArea.currentIndex
+ for (var step = 1; step <= total; ++step) {
+ var candidate = (start + delta * step + total) % total
+ var it = winRepeater.itemAt(candidate)
+ if (it && it.visible) {
+ exposeArea.currentIndex = candidate
+ return
+ }
+ }
+ }
+
+ // Helper for vertical navigation
+ function moveSelectionVertical(dir) {
+ var startIndex = exposeArea.currentIndex
+ var currentItem = winRepeater.itemAt(startIndex)
+
+ if (!currentItem || !currentItem.visible) {
+ moveSelectionHorizontal(dir > 0 ? 1 : -1)
+ return
+ }
+
+ var curCx = currentItem.x + currentItem.width / 2
+ var curCy = currentItem.y + currentItem.height / 2
+
+ var bestIndex = -1
+ var bestDy = 99999999
+ var bestDx = 99999999
+
+ for (var i = 0; i < total; ++i) {
+ var it = winRepeater.itemAt(i)
+ if (!it || !it.visible || i === startIndex) continue
+
+ var cx = it.x + it.width / 2
+ var cy = it.y + it.height / 2
+ var dy = cy - curCy
+
+ // Direction filtering
+ if (dir > 0 && dy <= 0) continue
+ if (dir < 0 && dy >= 0) continue
+
+ var absDy = Math.abs(dy)
+ var absDx = Math.abs(cx - curCx)
+
+ // Search for nearest thumb (first in vertical, then horizontal distance)
+ if (absDy < bestDy || (absDy === bestDy && absDx < bestDx)) {
+ bestDy = absDy
+ bestDx = absDx
+ bestIndex = i
+ }
+ }
+
+ if (bestIndex >= 0) {
+ exposeArea.currentIndex = bestIndex
+ }
+ }
+
+ // --- NVIM-style navigation with Ctrl ---
+ const ctrl = event.modifiers & Qt.ControlModifier
+
+ if (ctrl) {
+ if (event.key === Qt.Key_L) {
+ moveSelectionHorizontal(1)
+ event.accepted = true
+ } else if (event.key === Qt.Key_H) {
+ moveSelectionHorizontal(-1)
+ event.accepted = true
+ } else if (event.key === Qt.Key_J) {
+ moveSelectionVertical(1)
+ event.accepted = true
+ } else if (event.key === Qt.Key_K) {
+ moveSelectionVertical(-1)
+ event.accepted = true
+ }
+ return
+ }
+
+ if (event.key === Qt.Key_Right || event.key === Qt.Key_Tab) {
+ moveSelectionHorizontal(1)
+ event.accepted = true
+ } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Backtab) {
+ moveSelectionHorizontal(-1)
+ event.accepted = true
+ } else if (event.key === Qt.Key_Down) {
+ moveSelectionVertical(1)
+ event.accepted = true
+ } else if (event.key === Qt.Key_Up) {
+ moveSelectionVertical(-1)
+ event.accepted = true
+ } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ var item = winRepeater.itemAt(exposeArea.currentIndex)
+ if (item && item.activateWindow) {
+ item.activateWindow()
+ event.accepted = true
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: false
+ z: -1
+ onClicked: root.toggleExpose()
+ }
+
+ Item {
+ id: layoutContainer
+ anchors.fill: parent
+ anchors.margins: 32
+
+ Column {
+ id: layoutRoot
+ anchors.fill: parent
+ anchors.margins: 48
+ spacing: 20
+
+ // thumbs area
+ Item {
+ id: exposeArea
+ width: layoutRoot.width
+ height: layoutRoot.height - searchBox.implicitHeight - layoutRoot.spacing
+
+ property int currentIndex: 0
+ property string searchText: ""
+
+ // Reset active thumb on searchText change
+ onSearchTextChanged: {
+ currentIndex = (windowLayoutModel.count > 0) ? 0 : -1
+ }
+
+ ScriptModel {
+ id: windowLayoutModel
+
+ property int areaW: exposeArea.width
+ property int areaH: exposeArea.height
+ property string query: exposeArea.searchText
+ property string algo: root.lastLayoutAlgorithm
+ property var rawToplevels: Hyprland.toplevels.values
+
+ values: {
+ // Bailout on wrong screen size
+ if (areaW <= 0 || areaH <= 0) return []
+
+ var q = (query || "").toLowerCase()
+ var windowList = []
+ var idx = 0
+
+ if (!rawToplevels) return []
+
+ for (var it of rawToplevels) {
+ var w = it
+ var clientInfo = w && w.lastIpcObject ? w.lastIpcObject : {}
+ var workspace = clientInfo && clientInfo.workspace ? clientInfo.workspace : null
+ var workspaceId = workspace && workspace.id !== undefined ? workspace.id : undefined
+
+ // Filter invalid workspace or offscreen windows
+ if (workspaceId === undefined || workspaceId === null) continue
+ var size = clientInfo && clientInfo.size ? clientInfo.size : [0, 0]
+ var at = clientInfo && clientInfo.at ? clientInfo.at : [-1000, -1000]
+ if (at[1] + size[1] <= 0) continue
+
+ // Text filtering
+ var title = (w.title || clientInfo.title || "").toLowerCase()
+ var clazz = (clientInfo["class"] || "").toLowerCase()
+ var ic = (clientInfo.initialClass || "").toLowerCase()
+ var app = (w.appId || clientInfo.initialClass || "").toLowerCase()
+
+ if (q.length > 0) {
+ var match = title.indexOf(q) !== -1 || clazz.indexOf(q) !== -1 ||
+ ic.indexOf(q) !== -1 || app.indexOf(q) !== -1
+ if (!match) continue
+ }
+
+ windowList.push({
+ win: w,
+ clientInfo: clientInfo,
+ workspaceId: workspaceId,
+ width: size[0],
+ height: size[1],
+ originalIndex: idx++,
+ lastIpcObject: w.lastIpcObject
+ })
+ }
+
+ // Sort by workspaceId, then originalIndex
+ windowList.sort(function(a, b) {
+ if (a.workspaceId < b.workspaceId) return -1
+ if (a.workspaceId > b.workspaceId) return 1
+ if (a.originalIndex < b.originalIndex) return -1
+ if (a.originalIndex > b.originalIndex) return 1
+ return 0
+ })
+
+ return LayoutsManager.doLayout(algo, windowList, areaW, areaH)
+ }
+ }
+
+ Repeater {
+ id: winRepeater
+ model: windowLayoutModel
+
+ delegate: WindowThumbnail {
+ // Model data
+ hWin: modelData.win
+ wHandle: hWin.wayland
+ winKey: String(hWin.address)
+ thumbW: modelData.width
+ thumbH: modelData.height
+ clientInfo: hWin.lastIpcObject
+
+ // Layout-generated coordinates
+ targetX: modelData.x
+ targetY: modelData.y
+ targetZ: (visible && (exposeArea.currentIndex === index)) ? 1000: modelData.zIndex || 0
+ targetRotation: modelData.rotation || 0
+
+ hovered: visible && (exposeArea.currentIndex === index)
+ moveCursorToActiveWindow: root.moveCursorToActiveWindow
+ }
+ }
+ }
+
+ SearchBox {
+ id: searchBox
+ onTextChanged: function(text) {
+ root.animateWindows = true
+ exposeArea.searchText = text
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/config/quickshell/qs-hyprview/modules/SearchBox.qml b/config/quickshell/qs-hyprview/modules/SearchBox.qml
new file mode 100644
index 00000000..42971bd1
--- /dev/null
+++ b/config/quickshell/qs-hyprview/modules/SearchBox.qml
@@ -0,0 +1,53 @@
+import QtQuick
+import Quickshell
+
+Rectangle {
+ id: searchBar
+ width: Math.min(parent.width * 0.6, 480)
+ height: 40
+ radius: 20
+ color: "#66000000"
+ border.width: 1
+ border.color: m3.m3Primary
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ property var onTextChanged: null
+
+ function reset() {
+ searchInput.text = ""
+ }
+
+ function focusInput() {
+ searchInput.forceActiveFocus(Qt.ActiveWindowFocusReason)
+ }
+
+ function releaseInputFocus() {
+ searchInput.focus = false
+ }
+
+ TextInput {
+ id: searchInput
+ anchors.fill: parent
+ anchors.leftMargin: 16
+ anchors.rightMargin: 16
+ verticalAlignment: TextInput.AlignVCenter
+ color: "white"
+ font.pixelSize: 16
+ activeFocusOnTab: false
+ selectByMouse: true
+ focus: false
+
+ onTextChanged: {
+ searchBar.onTextChanged(text)
+ }
+
+ Text {
+ anchors.fill: parent
+ verticalAlignment: Text.AlignVCenter
+ color: "#88ffffff"
+ font.pixelSize: 14
+ text: "Type to filter windows..."
+ visible: !searchInput.text || searchInput.text.length === 0
+ }
+ }
+}
diff --git a/config/quickshell/qs-hyprview/modules/WindowThumbnail.qml b/config/quickshell/qs-hyprview/modules/WindowThumbnail.qml
new file mode 100644
index 00000000..89383291
--- /dev/null
+++ b/config/quickshell/qs-hyprview/modules/WindowThumbnail.qml
@@ -0,0 +1,278 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import Qt5Compat.GraphicalEffects
+
+Item {
+ id: thumbContainer
+
+ property var hWin: null
+ property var wHandle:null
+
+ property string winKey: ''
+
+ property real thumbW: -1
+ property real thumbH: -1
+
+ property var clientInfo: {}
+ property bool hovered: false
+
+ property real targetX: -1000
+ property real targetY: -1000
+ property real targetZ: 0
+ property real targetRotation: 0
+
+ property bool moveCursorToActiveWindow: false
+
+ width: thumbW
+ height: thumbH
+
+ x: 0
+ y: 0
+ z: targetZ
+ rotation: 0
+
+ visible: !!wHandle
+
+ NumberAnimation {
+ id: animX
+ target: thumbContainer
+ property: "x"
+ duration: root.animateWindows ? 100 : 0
+ easing.type: Easing.OutQuad
+ }
+ NumberAnimation {
+ id: animY
+ target: thumbContainer
+ property: "y"
+ duration: root.animateWindows ? 100 : 0
+ easing.type: Easing.OutQuad
+ }
+ NumberAnimation {
+ id: animRotation
+ target: thumbContainer
+ property: "rotation"
+ duration: 400
+ easing.type: Easing.OutBack // Effetto rimbalzo/inerzia
+ easing.overshoot: 1.2
+ }
+
+ function updateLastPos() {
+ var lp = root.lastPositions || ({})
+ var prev = lp[winKey] || ({})
+ prev.x = x
+ prev.y = y
+ lp[winKey] = prev
+ root.lastPositions = lp
+ }
+
+ onTargetXChanged: {
+ if (!root.animateWindows) {
+ x = targetX
+ updateLastPos()
+ return
+ }
+
+ var lp = root.lastPositions || ({})
+ var prev = lp[winKey]
+ var startX = (prev && prev.x !== undefined) ? prev.x : targetX
+
+ if (startX === targetX) {
+ x = targetX
+ updateLastPos()
+ return
+ }
+
+ animX.stop()
+ animX.from = startX
+ animX.to = targetX
+ animX.start()
+ }
+
+ onTargetYChanged: {
+ if (!root.animateWindows) {
+ y = targetY
+ updateLastPos()
+ return
+ }
+
+ var lp = root.lastPositions || ({})
+ var prev = lp[winKey]
+ var startY = (prev && prev.y !== undefined) ? prev.y : targetY
+
+ if (startY === targetY) {
+ y = targetY
+ updateLastPos()
+ return
+ }
+
+ animY.stop()
+ animY.from = startY
+ animY.to = targetY
+ animY.start()
+ }
+
+ onTargetRotationChanged: {
+ rotation = targetRotation
+ animRotation.stop()
+ animRotation.from = 0
+ animRotation.to = targetRotation
+ animRotation.start()
+ }
+
+ onXChanged: updateLastPos()
+ onYChanged: updateLastPos()
+
+ Component.onCompleted: {
+ rotation = targetRotation
+ if (!root.animateWindows) {
+ x = targetX
+ y = targetY
+ updateLastPos()
+ }
+ }
+
+ function activateWindow() {
+ if (!hWin) return
+
+ var targetIsSpecial = (hWin?.workspace ?? 0) < 0 || (hWin?.workspace?.name ?? "").startsWith("special")
+
+ if (root.specialActive && !targetIsSpecial) {
+ Hyprland.dispatch("togglespecialworkspace")
+ }
+
+ if (hWin.workspace) {
+ hWin.workspace.activate()
+ }
+
+ root.toggleExpose()
+ Hyprland.dispatch("focuswindow address:0x" + hWin.address)
+ Hyprland.dispatch("alterzorder top")
+ if (thumbContainer.moveCursorToActiveWindow) {
+ var cx = clientInfo.at[0] + (clientInfo.size[0]/2)
+ var cy = clientInfo.at[1] + (clientInfo.size[1]/2)
+ Hyprland.dispatch("movecursor " + cx + " " + cy)
+
+ }
+ }
+
+ function closeWindow() {
+ if (!hWin) return
+ Hyprland.dispatch("closewindow address:0x" + hWin.address)
+ }
+
+ function refreshThumb() {
+ if (thumbLoader.item) {
+ thumbLoader.item.captureFrame()
+ }
+ }
+
+ Item {
+ id: card
+ anchors.fill: parent
+
+ scale: thumbContainer.hovered ? 1.05 : 0.95
+ transformOrigin: Item.Center
+
+ Behavior on scale {
+ NumberAnimation { duration: 100; easing.type: Easing.OutQuad }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton
+
+ onEntered: {
+ exposeArea.currentIndex = index
+ }
+ onClicked: event => {
+ exposeArea.currentIndex = index
+
+ if (event.button === Qt.LeftButton) {
+ thumbContainer.activateWindow()
+ }
+ if (event.button === Qt.MiddleButton) {
+ thumbContainer.closeWindow()
+ }
+ }
+ onExited: {
+ if (exposeArea.currentIndex === index) {
+ exposeArea.currentIndex = -1
+ }
+ }
+ }
+
+ RectangularShadow {
+ anchors.fill: parent
+ radius: 16
+ blur: 24
+ spread: 10
+ color: "#55000000"
+ cached: true
+ }
+
+ Loader {
+ id: thumbLoader
+ anchors.fill: parent
+ active: root.isActive && !!thumbContainer.wHandle
+ sourceComponent: ScreencopyView {
+ id: thumb
+ anchors.fill: parent
+ captureSource: thumbContainer.wHandle
+ live: root.liveCapture && root.isActive
+ paintCursor: false
+ visible: root.isActive && thumbContainer.wHandle && hasContent
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: thumb.width
+ height: thumb.height
+ radius: 16
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: thumbContainer.hovered ? "transparent": "#33000000"
+ border.width : thumbContainer.hovered ? 3 : 1
+ border.color : thumbContainer.hovered ? m3.m3Primary : m3.m3Secondary
+ radius: 16
+ }
+ }
+ }
+
+ Rectangle {
+ id: badge
+ z: 100
+ width: Math.min(titleText.implicitWidth + 24, thumbContainer.thumbW * 0.75)
+ height: titleText.implicitHeight + 12
+
+ x: (card.width - width) / 2
+ y: card.height - height - (card.height * 0.08)
+
+ radius: 12
+ color: thumbContainer.hovered ? "#FF000000" : "#CC000000"
+ border.width : 1
+ border.color : "#ff464646"
+
+ Text {
+ id: titleText
+ anchors.centerIn: parent
+ width: parent.width - 16
+ text: hWin.title
+ color: "white"
+ font.pixelSize: thumbContainer.hovered ? 13 : 12
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+ }
+}
diff --git a/config/quickshell/qs-hyprview/modules/qmldir b/config/quickshell/qs-hyprview/modules/qmldir
new file mode 100644
index 00000000..097b9ca9
--- /dev/null
+++ b/config/quickshell/qs-hyprview/modules/qmldir
@@ -0,0 +1,3 @@
+Hyprview 1.0 Hyprview.qml
+WindowThumbnail 1.0 WindowThumbnail.qml
+SearchBox 1.0 SearchBox.qml
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage