Krolyxon
Commit 229376d

Branches

Refs →

aurora229376d

aurora: add window overview (live previews, drag to move)

Author
krolyxon
Date
2026-10-17 15:04 +0530
Parent
4ad2e15
Commit
229376d3084e242bae800945439a1910beafe8eb

+276 −0

4 files changed

  1. M .config/quickshell/aurora/HomePage.qml +6 −0
  2. M .config/quickshell/aurora/Hypr.qml +18 −0
  3. A .config/quickshell/aurora/Overview.qml +245 −0
  4. M .config/quickshell/aurora/shell.qml +7 −0
M .config/quickshell/aurora/HomePage.qml View
@@ -113,6 +113,12 @@ ColumnLayout {
113113 label: "random"
114114 onClicked: Wall.random()
115115 }
116+ IconBtn {
117+ Layout.fillWidth: true
118+ icon: Theme.iWindow
119+ label: "overview"
120+ onClicked: page.overlayRequested("overview")
121+ }
116122 IconBtn {
117123 Layout.fillWidth: true
118124 visible: Clip.available
M .config/quickshell/aurora/Hypr.qml View
@@ -14,6 +14,24 @@ Singleton {
1414 Quickshell.execDetached(["hyprctl", "dispatch", "workspace", String(arg)])
1515 }
1616
17+ function addr(a) { a = String(a); return a.indexOf("0x") === 0 ? a : "0x" + a }
18+
19+ function focusWindow(a) {
20+ if (luaSyntax)
21+ Quickshell.execDetached(["hyprctl", "dispatch", 'hl.dsp.focus({window="address:' + addr(a) + '"})'])
22+ else
23+ Quickshell.execDetached(["hyprctl", "dispatch", "focuswindow", "address:" + addr(a)])
24+ }
25+ // window.move ignores an unknown `window=` key (returns ok and moves the active window instead),
26+ // so focus the target first and then move the active one
27+ function moveWindow(a, ws) {
28+ if (luaSyntax)
29+ Quickshell.execDetached(["hyprctl", "--batch",
30+ 'dispatch hl.dsp.focus({window="address:' + addr(a) + '"}); dispatch hl.dsp.window.move({workspace=' + Number(ws) + '})'])
31+ else
32+ Quickshell.execDetached(["hyprctl", "dispatch", "movetoworkspacesilent", Number(ws) + ",address:" + addr(a)])
33+ }
34+
1735 // compositor-agnostic logout
1836 function logout() { Quickshell.execDetached(["sh", "-c", 'loginctl terminate-user "$USER"']) }
1937 }
A .config/quickshell/aurora/Overview.qml View
@@ -0,0 +1,245 @@
1+import QtQuick
2+import QtQuick.Layouts
3+import Quickshell
4+import Quickshell.Wayland
5+import Quickshell.Hyprland
6+
7+// Window overview for one monitor: its 10 workspaces as scaled boxes with live window previews.
8+// Click a window = focus, click a workspace = switch, drag a window onto another box = move.
9+// Type to filter by title/class, Enter focuses the first match, Esc closes.
10+PanelWindow {
11+ id: win
12+ property bool opened: false
13+ signal closeRequested()
14+
15+ visible: opened
16+ anchors { top: true; bottom: true; left: true; right: true }
17+ exclusionMode: ExclusionMode.Ignore
18+ color: "transparent"
19+ WlrLayershell.layer: WlrLayer.Overlay
20+ WlrLayershell.namespace: "aurora-overview"
21+ WlrLayershell.keyboardFocus: opened ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
22+
23+ readonly property var mon: screen ? Hyprland.monitorFor(screen) : null
24+ readonly property var activeWs: mon ? mon.activeWorkspace : null
25+ // same 10-per-monitor block as Workspaces.qml (split-monitor-workspaces)
26+ readonly property int offset: activeWs && activeWs.id > 0 ? Math.floor((activeWs.id - 1) / 10) * 10 : 0
27+ readonly property real monW: mon ? mon.width / mon.scale : 1920
28+ readonly property real monH: mon ? mon.height / mon.scale : 1080
29+
30+ onOpenedChanged: {
31+ if (!opened) return
32+ filter.text = ""
33+ Hyprland.refreshToplevels()
34+ Hyprland.refreshMonitors()
35+ filter.forceActiveFocus()
36+ }
37+
38+ function windowsOn(id) {
39+ var l = Hyprland.toplevels.values, out = []
40+ for (var i = 0; i < l.length; i++) if (l[i].workspace && l[i].workspace.id === id) out.push(l[i])
41+ return out
42+ }
43+ function cls(t) { return t.lastIpcObject && t.lastIpcObject.class ? t.lastIpcObject.class : "" }
44+ function matches(t) {
45+ var q = filter.text.toLowerCase()
46+ return q === "" || String(t.title).toLowerCase().indexOf(q) >= 0 || cls(t).toLowerCase().indexOf(q) >= 0
47+ }
48+ function firstMatch() {
49+ for (var i = 1; i <= 10; i++) {
50+ var l = windowsOn(offset + i)
51+ for (var j = 0; j < l.length; j++) if (matches(l[j])) return l[j]
52+ }
53+ return null
54+ }
55+ function iconFor(t) {
56+ var e = DesktopEntries.heuristicLookup(cls(t))
57+ return Quickshell.iconPath(e ? e.icon : cls(t).toLowerCase(), true)
58+ }
59+ property int dragWs: -1 // box whose window is being dragged (raised above the others)
60+ Timer { id: resync; interval: 300; onTriggered: Hyprland.refreshToplevels() }
61+
62+ function focus(t) { Hypr.focusWindow(t.address); closeRequested() }
63+
64+ // box under a point in grid coordinates (for drops)
65+ function wsAt(px, py) {
66+ for (var i = 0; i < boxes.count; i++) {
67+ var b = boxes.itemAt(i)
68+ var p = b.mapFromItem(grid, px, py)
69+ if (p.x >= 0 && p.y >= 0 && p.x <= b.width && p.y <= b.height) return b.wsId
70+ }
71+ return -1
72+ }
73+
74+ Rectangle {
75+ anchors.fill: parent
76+ color: Theme.bgA
77+ MouseArea { anchors.fill: parent; onClicked: win.closeRequested() }
78+ }
79+
80+ ColumnLayout {
81+ anchors.fill: parent
82+ anchors.margins: 40
83+ anchors.topMargin: Theme.barHeight + 24
84+ spacing: 16
85+
86+ RowLayout {
87+ Layout.alignment: Qt.AlignHCenter
88+ spacing: 8
89+ Rectangle {
90+ implicitWidth: 420
91+ implicitHeight: 32
92+ color: Theme.surface
93+ border.width: 1
94+ border.color: Theme.accent
95+ Text {
96+ anchors.left: parent.left
97+ anchors.leftMargin: 10
98+ anchors.verticalCenter: parent.verticalCenter
99+ text: Theme.iSearch
100+ color: Theme.dim
101+ font.family: Theme.font
102+ font.pixelSize: Theme.fontSize
103+ }
104+ TextInput {
105+ id: filter
106+ anchors.fill: parent
107+ anchors.leftMargin: 32
108+ anchors.rightMargin: 10
109+ verticalAlignment: TextInput.AlignVCenter
110+ color: Theme.fg
111+ selectionColor: Theme.accent
112+ selectedTextColor: Theme.bg
113+ font.family: Theme.font
114+ font.pixelSize: Theme.fontSize
115+ clip: true
116+ Keys.onEscapePressed: win.closeRequested()
117+ Keys.onReturnPressed: { var t = win.firstMatch(); if (t) win.focus(t) }
118+ Keys.onEnterPressed: { var t = win.firstMatch(); if (t) win.focus(t) }
119+ }
120+ }
121+ Text {
122+ text: "click focus drag move enter first match esc close"
123+ color: Theme.dim
124+ font.family: Theme.font
125+ font.pixelSize: Theme.fontSize - 2
126+ }
127+ }
128+
129+ Item {
130+ id: grid
131+ Layout.fillWidth: true
132+ Layout.fillHeight: true
133+
134+ readonly property int cols: 5
135+ readonly property int gap: 16
136+ // largest box with the monitor's aspect ratio that fits 5 x 2
137+ readonly property real boxW: Math.min((width - gap * (cols - 1)) / cols,
138+ ((height - gap - 2 * 22) / 2) * win.monW / win.monH)
139+ readonly property real boxH: boxW * win.monH / win.monW
140+ readonly property real sc: boxW / win.monW
141+
142+ Repeater {
143+ id: boxes
144+ model: 10
145+ delegate: Item {
146+ id: box
147+ required property int index
148+ readonly property int wsId: win.offset + index + 1
149+ readonly property bool current: win.activeWs !== null && win.activeWs.id === wsId
150+ readonly property real originX: (grid.width - (grid.boxW * grid.cols + grid.gap * (grid.cols - 1))) / 2
151+ x: originX + (index % grid.cols) * (grid.boxW + grid.gap)
152+ y: Math.floor(index / grid.cols) * (grid.boxH + grid.gap + 22)
153+ width: grid.boxW
154+ height: grid.boxH + 22
155+ z: win.dragWs === wsId ? 10 : 0
156+
157+ Text {
158+ text: Theme.wsIcon(box.wsId) + " " + box.wsId
159+ color: box.current ? Theme.accent : Theme.dim
160+ font.family: Theme.font
161+ font.pixelSize: Theme.fontSize
162+ font.bold: box.current
163+ }
164+
165+ Rectangle {
166+ id: frame
167+ y: 20
168+ width: grid.boxW
169+ height: grid.boxH
170+ color: Theme.surface
171+ border.width: box.current ? 2 : 1
172+ border.color: box.current ? Theme.accent : (wm.containsMouse ? Theme.accentDim : Theme.line)
173+
174+ MouseArea {
175+ id: wm
176+ anchors.fill: parent
177+ hoverEnabled: true
178+ onClicked: { Hypr.workspace(box.wsId); win.closeRequested() }
179+ }
180+
181+ Repeater {
182+ model: win.windowsOn(box.wsId)
183+ delegate: Rectangle {
184+ id: tile
185+ required property var modelData
186+ readonly property var ipc: modelData.lastIpcObject || {}
187+ readonly property var at: ipc.at || [0, 0]
188+ readonly property var size: ipc.size || [win.monW / 2, win.monH / 2]
189+ readonly property bool hit: win.matches(modelData)
190+ readonly property real homeX: (at[0] - (win.mon ? win.mon.x : 0)) * grid.sc
191+ readonly property real homeY: (at[1] - (win.mon ? win.mon.y : 0)) * grid.sc
192+ x: homeX
193+ y: homeY
194+ width: Math.max(12, size[0] * grid.sc)
195+ height: Math.max(12, size[1] * grid.sc)
196+ z: dm.drag.active ? 10 : (ipc.floating ? 2 : 1)
197+ color: Theme.surfaceHi
198+ border.width: dm.containsMouse || dm.drag.active ? 2 : 1
199+ border.color: dm.containsMouse || dm.drag.active ? Theme.accent : Theme.line
200+ opacity: hit ? 1 : 0.25
201+
202+ ScreencopyView {
203+ id: shot
204+ anchors.fill: parent
205+ anchors.margins: 1
206+ captureSource: win.opened && tile.modelData.wayland ? tile.modelData.wayland : null
207+ live: true
208+ }
209+ Image { // big centred icon until the preview arrives, then a corner badge
210+ x: shot.hasContent ? 4 : (parent.width - width) / 2
211+ y: shot.hasContent ? parent.height - height - 4 : (parent.height - height) / 2
212+ width: shot.hasContent ? 18 : Math.min(32, parent.width / 2)
213+ height: width
214+ sourceSize.width: 64
215+ sourceSize.height: 64
216+ source: win.iconFor(tile.modelData)
217+ asynchronous: true
218+ }
219+ MouseArea {
220+ id: dm
221+ anchors.fill: parent
222+ hoverEnabled: true
223+ cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.PointingHandCursor
224+ drag.target: tile
225+ drag.threshold: 6
226+ drag.onActiveChanged: if (drag.active) win.dragWs = box.wsId
227+ onReleased: {
228+ win.dragWs = -1
229+ if (!drag.active) return
230+ var c = tile.mapToItem(grid, tile.width / 2, tile.height / 2)
231+ var ws = win.wsAt(c.x, c.y)
232+ if (ws > 0 && ws !== box.wsId) { Hypr.moveWindow(tile.modelData.address, ws); resync.restart() }
233+ tile.x = Qt.binding(() => tile.homeX)
234+ tile.y = Qt.binding(() => tile.homeY)
235+ }
236+ onClicked: win.focus(tile.modelData)
237+ }
238+ }
239+ }
240+ }
241+ }
242+ }
243+ }
244+ }
245+}
M .config/quickshell/aurora/shell.qml View
@@ -94,6 +94,12 @@ ShellRoot {
9494 onOverlayRequested: name => { root.close(); root.toggleOverlay(name) }
9595 }
9696
97+ Overview {
98+ screen: root.overlayScreen
99+ opened: root.overlay === "overview"
100+ onCloseRequested: root.closeOverlay()
101+ }
102+
97103 CalPopup {
98104 screen: root.overlayScreen
99105 opened: root.overlay === "cal"
@@ -134,6 +140,7 @@ ShellRoot {
134140 function dnd(): void { Notifs.dnd = !Notifs.dnd }
135141 function shot(mode: string): void { root.close(); Shot.take(mode) } // region | window | output | ocr
136142 function shotsave(): void { Shot.save = !Shot.save }
143+ function overview(): void { root.toggleOverlay("overview") }
137144 function cal(): void { root.toggleOverlay("cal") }
138145 function clip(): void { root.toggleOverlay("clip") }
139146 function wall(): void { root.toggleOverlay("wall") }