├── screenshots
├── bands.jpeg
├── hero.jpeg
├── spiral.jpeg
├── vortex.jpeg
├── masonry.jpeg
├── satellite.jpeg
├── smartgrid.jpeg
└── staggered.jpeg
├── modules
├── qmldir
├── SearchBox.qml
├── WindowThumbnail.qml
└── Hyprview.qml
├── shell.qml
├── layouts
├── qmldir
├── LayoutsManager.qml
├── StarggeredLayout.qml
├── ColumnarLayout.qml
├── VortexLayout.qml
├── SatelliteLayout.qml
├── JustifiedLayout.qml
├── SmartGridLayout.qml
├── MasonryLayout.qml
├── HeroLayout.qml
├── SpiralLayout.qml
└── BandsLayout.qml
├── README.md
└── LICENSE
/screenshots/bands.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dom0/qs-hyprview/HEAD/screenshots/bands.jpeg
--------------------------------------------------------------------------------
/screenshots/hero.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dom0/qs-hyprview/HEAD/screenshots/hero.jpeg
--------------------------------------------------------------------------------
/screenshots/spiral.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dom0/qs-hyprview/HEAD/screenshots/spiral.jpeg
--------------------------------------------------------------------------------
/screenshots/vortex.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dom0/qs-hyprview/HEAD/screenshots/vortex.jpeg
--------------------------------------------------------------------------------
/screenshots/masonry.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dom0/qs-hyprview/HEAD/screenshots/masonry.jpeg
--------------------------------------------------------------------------------
/screenshots/satellite.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dom0/qs-hyprview/HEAD/screenshots/satellite.jpeg
--------------------------------------------------------------------------------
/screenshots/smartgrid.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dom0/qs-hyprview/HEAD/screenshots/smartgrid.jpeg
--------------------------------------------------------------------------------
/screenshots/staggered.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dom0/qs-hyprview/HEAD/screenshots/staggered.jpeg
--------------------------------------------------------------------------------
/modules/qmldir:
--------------------------------------------------------------------------------
1 | Hyprview 1.0 Hyprview.qml
2 | WindowThumbnail 1.0 WindowThumbnail.qml
3 | SearchBox 1.0 SearchBox.qml
4 |
--------------------------------------------------------------------------------
/shell.qml:
--------------------------------------------------------------------------------
1 | import QtQuick
2 | import Quickshell
3 | import "./modules"
4 |
5 | ShellRoot {
6 | Hyprview {
7 | liveCapture: false
8 | moveCursorToActiveWindow: false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/layouts/qmldir:
--------------------------------------------------------------------------------
1 | singleton HeroLayout 1.0 HeroLayout.qml
2 | singleton JustifiedLayout 1.0 JustifiedLayout.qml
3 | singleton MasonryLayout 1.0 MasonryLayout.qml
4 | singleton SmartGridLayout 1.0 SmartGridLayout.qml
5 | singleton SpiralLayout 1.0 SpiralLayout.qml
6 | singleton BandsLayout 1.0 BandsLayout.qml
7 | singleton SatelliteLayout 1.0 SatelliteLayout.qml
8 | singleton StarggeredLayout 1.0 StarggeredLayout.qml
9 | singleton ColumnarLayout 1.0 ColumnarLayout.qml
10 | singleton VortexLayout 1.0 VortexLayout.qml
11 |
12 | singleton LayoutsManager 1.0 LayoutsManager.qml
13 |
--------------------------------------------------------------------------------
/modules/SearchBox.qml:
--------------------------------------------------------------------------------
1 | import QtQuick
2 | import Quickshell
3 |
4 | Rectangle {
5 | id: searchBar
6 | width: Math.min(parent.width * 0.6, 480)
7 | height: 40
8 | radius: 20
9 | color: "#66000000"
10 | border.width: 1
11 | border.color: "#33ffffff"
12 | anchors.horizontalCenter: parent.horizontalCenter
13 |
14 | property var onTextChanged: null
15 |
16 | function reset() {
17 | searchInput.text = ""
18 | }
19 |
20 | TextInput {
21 | id: searchInput
22 | anchors.fill: parent
23 | anchors.leftMargin: 16
24 | anchors.rightMargin: 16
25 | verticalAlignment: TextInput.AlignVCenter
26 | color: "white"
27 | font.pixelSize: 16
28 | activeFocusOnTab: false
29 | selectByMouse: true
30 | focus: true
31 |
32 | onTextChanged: {
33 | searchBar.onTextChanged(text)
34 | }
35 |
36 | Text {
37 | anchors.fill: parent
38 | verticalAlignment: Text.AlignVCenter
39 | color: "#88ffffff"
40 | font.pixelSize: 14
41 | text: "Type to filter windows..."
42 | visible: !searchInput.text || searchInput.text.length === 0
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/layouts/LayoutsManager.qml:
--------------------------------------------------------------------------------
1 | pragma Singleton
2 | import Quickshell
3 | import "."
4 |
5 | Singleton {
6 | id: root
7 |
8 | function doLayout( layoutAlgorithm, windowList, width, height) {
9 | var doLayout = null
10 | switch (layoutAlgorithm) {
11 | case 'smartgrid':
12 | doLayout = SmartGridLayout.doLayout
13 | break
14 | case 'justified':
15 | doLayout = JustifiedLayout.doLayout
16 | break
17 | case 'bands':
18 | doLayout = BandsLayout.doLayout
19 | break
20 | case 'masonry':
21 | doLayout = MasonryLayout.doLayout
22 | break
23 | case 'hero':
24 | doLayout = HeroLayout.doLayout
25 | break
26 | case 'spiral':
27 | doLayout = SpiralLayout.doLayout
28 | break
29 | case 'satellite':
30 | doLayout = SatelliteLayout.doLayout
31 | break
32 | case 'staggered':
33 | doLayout = StarggeredLayout.doLayout
34 | break
35 | case 'columnar':
36 | doLayout = ColumnarLayout.doLayout
37 | break
38 | case 'vortex':
39 | doLayout = VortexLayout.doLayout
40 | break
41 | default:
42 | doLayout = SmartGridLayout.doLayout
43 | }
44 |
45 | return doLayout( windowList, width, height)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/layouts/StarggeredLayout.qml:
--------------------------------------------------------------------------------
1 | pragma Singleton
2 | import Quickshell
3 |
4 | Singleton {
5 | id: root
6 |
7 | function doLayout(windowList, outerWidth, outerHeight) {
8 | var N = windowList.length
9 | if (N === 0) return []
10 |
11 | var gap = Math.max(10, outerWidth * 0.01)
12 |
13 | // Safe Area
14 | var useW = outerWidth * 0.9
15 | var useH = outerHeight * 0.9
16 | var offX = (outerWidth - useW) / 2
17 | var offY = (outerHeight - useH) / 2
18 |
19 | // Heuristic: roughly sqrt(N), but slightly weighted towards columns
20 | // to accommodate 16:9 screens better.
21 | var cols = Math.ceil(Math.sqrt(N * 1.5))
22 | var rows = Math.ceil(N / cols)
23 |
24 | // Calculate cell width.
25 | // Note: In a staggered layout, the effective width needed is (cols + 0.5)
26 | // because alternate rows are shifted by half a cell.
27 | var cellW = (useW - (cols * gap)) / (cols + 0.5)
28 | var cellH = (useH - (rows * gap)) / rows
29 |
30 | // Vertical centering of the whole block
31 | var contentH = rows * cellH + (rows - 1) * gap
32 | var startY = offY + (useH - contentH) / 2
33 |
34 | var result = []
35 |
36 | for (var i = 0; i < N; i++) {
37 | var item = windowList[i]
38 |
39 | var r = Math.floor(i / cols)
40 | var c = i % cols
41 |
42 | // Stagger offset: if row is odd, shift right by half cell width
43 | var staggerOffset = (r % 2 === 1) ? (cellW / 2) : 0
44 |
45 | var cellX = staggerOffset + c * (cellW + gap)
46 | var cellY = r * (cellH + gap)
47 |
48 | // Aspect Fit
49 | var w0 = (item.width > 0) ? item.width : 100
50 | var h0 = (item.height > 0) ? item.height : 100
51 | var sc = Math.min(cellW / w0, cellH / h0)
52 |
53 | // Center the thumbnail inside the calculated cell
54 | result.push({
55 | win: item.win,
56 | x: offX + cellX + (cellW - w0 * sc)/2,
57 | y: startY + cellY + (cellH - h0 * sc)/2,
58 | width: w0 * sc,
59 | height: h0 * sc
60 | })
61 | }
62 | return result
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/layouts/ColumnarLayout.qml:
--------------------------------------------------------------------------------
1 | pragma Singleton
2 | import Quickshell
3 |
4 | Singleton {
5 | id: root
6 |
7 | function doLayout(windowList, outerWidth, outerHeight) {
8 | var N = windowList.length
9 | if (N === 0) return []
10 |
11 | var gap = Math.max(8, outerWidth * 0.005)
12 |
13 | // Safe Area: We use slightly more width here (95%)
14 | // as vertical strips look better when filling the screen horizontally.
15 | var useW = outerWidth * 0.95
16 | var useH = outerHeight * 0.90
17 | var offX = (outerWidth - useW) / 2
18 | var offY = (outerHeight - useH) / 2
19 |
20 | // Calculate width of a single column
21 | var colW = (useW - (gap * (N - 1))) / N
22 |
23 | // Safety: If columns become too narrow (e.g. < 200px),
24 | // we clamp the width to keep them readable.
25 | var minColW = 200
26 | if (colW < minColW) colW = minColW
27 |
28 | // Calculate the actual total width used
29 | var totalW = N * colW + (N - 1) * gap
30 |
31 | // Center the group horizontally.
32 | // If N is small, it centers. If N is large (clamped), it starts from left.
33 | var startX = offX
34 | if (totalW < useW) {
35 | startX = offX + (useW - totalW) / 2
36 | }
37 |
38 | var result = []
39 |
40 | for (var i = 0; i < N; i++) {
41 | var item = windowList[i]
42 |
43 | var w0 = (item.width > 0) ? item.width : 100
44 | var h0 = (item.height > 0) ? item.height : 100
45 |
46 | // In this layout, vertical space is abundant (useH).
47 | // The constraining factor is usually the column width.
48 | var sc = Math.min(colW / w0, useH / h0)
49 |
50 | var thumbW = w0 * sc
51 | var thumbH = h0 * sc
52 |
53 | var xPos = startX + i * (colW + gap)
54 |
55 | // Center horizontally within the strip
56 | var xCentered = xPos + (colW - thumbW) / 2
57 |
58 | // Center vertically on screen
59 | var yCentered = offY + (useH - thumbH) / 2
60 |
61 | result.push({
62 | win: item.win,
63 | x: xCentered,
64 | y: yCentered,
65 | width: thumbW,
66 | height: thumbH
67 | })
68 | }
69 |
70 | return result
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/layouts/VortexLayout.qml:
--------------------------------------------------------------------------------
1 | pragma Singleton
2 | import Quickshell
3 |
4 | Singleton {
5 | id: root
6 |
7 | function doLayout(windowList, outerWidth, outerHeight) {
8 | var N = windowList.length
9 | if (N === 0) return []
10 |
11 | // Safe Area (90%)
12 | var contentScale = 0.90
13 | var useW = outerWidth * contentScale
14 | var useH = outerHeight * contentScale
15 | var offX = (outerWidth - useW) / 2
16 | var offY = (outerHeight - useH) / 2
17 |
18 | var centerX = offX + useW / 2
19 | var centerY = offY + useH / 2
20 |
21 | // Maximum radius (distance from center to the furthest edge of safe area)
22 | var maxRadius = Math.min(useW, useH) / 2
23 |
24 | var result = []
25 |
26 | // --- THE VORTEX CONFIGURATION ---
27 |
28 | var goldenAngle = Math.PI * (3 - Math.sqrt(5))
29 |
30 | // PARAMETER TWEAK 1: from 0.3 to 0.5 to keep distant windows readable
31 | var minScale = 0.4
32 |
33 | // PARAMETER TWEAK 2: from 0.4 to 0.6 (60% of screen height)
34 | var baseSizeFactor = 0.5
35 |
36 | for (var i = 0; i < N; i++) {
37 | var item = windowList[i]
38 |
39 | var t = i / Math.max(1, N - 1)
40 | if (N === 1) t = 0
41 |
42 | // PARAMETER TWEAK 3: from 0.9 instead of 0.8 to accommodate larger thumbs
43 | var currentRadius = (maxRadius * 0.85) * Math.sqrt(t)
44 | var currentAngle = i * goldenAngle
45 | var scale = 1.0 - (t * (1.0 - minScale))
46 | var tilt = (Math.cos(currentAngle) * 8)
47 |
48 | // Coordinates (Polar to Cartesian)
49 | var cx = centerX + currentRadius * Math.cos(currentAngle)
50 | var cy = centerY + currentRadius * Math.sin(currentAngle)
51 |
52 | // Dimensions (Aspect Fit)
53 | var w0 = (item.width > 0) ? item.width : 100
54 | var h0 = (item.height > 0) ? item.height : 100
55 |
56 | var baseBoxSize = Math.min(useW, useH) * baseSizeFactor
57 |
58 | var aspect = w0 / h0
59 | var thumbW, thumbH
60 |
61 | if (aspect > 1) {
62 | thumbW = baseBoxSize * scale
63 | thumbH = thumbW / aspect
64 | } else {
65 | thumbH = baseBoxSize * scale
66 | thumbW = thumbH * aspect
67 | }
68 |
69 | result.push({
70 | win: item.win,
71 | x: cx - (thumbW / 2),
72 | y: cy - (thumbH / 2),
73 | width: thumbW,
74 | height: thumbH,
75 | rotation: tilt,
76 | zIndex: N - i
77 | })
78 | }
79 |
80 | return result
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/layouts/SatelliteLayout.qml:
--------------------------------------------------------------------------------
1 | pragma Singleton
2 | import Quickshell
3 | import Quickshell.Hyprland
4 |
5 | Singleton {
6 | id: root
7 |
8 | function doLayout(windowList, outerWidth, outerHeight) {
9 | var N = windowList.length
10 | if (N === 0) return []
11 |
12 | // Move active window to the start of the list (Center Item)
13 | var activeAddr = Hyprland.activeToplevel?.lastIpcObject?.address
14 | if (activeAddr) {
15 | var activeIdx = windowList.findIndex(it => it.lastIpcObject.address === activeAddr)
16 | if (activeIdx !== -1) {
17 | windowList = [windowList[activeIdx], ...windowList.filter(it => it !== windowList[activeIdx])]
18 | }
19 | }
20 |
21 | // Safe Area definition (90%)
22 | var useW = outerWidth * 0.90
23 | var useH = outerHeight * 0.90
24 | var offX = (outerWidth - useW) / 2
25 | var offY = (outerHeight - useH) / 2
26 |
27 | var result = []
28 |
29 | // Center item (hero)
30 | var centerItem = windowList[0]
31 |
32 | // The center item takes up roughly 35% of the screen dimensions
33 | var centerW = useW * 0.35
34 | var centerH = useH * 0.35
35 |
36 | // Aspect Fit for the center item
37 | var w0 = (centerItem.width > 0) ? centerItem.width : 100
38 | var h0 = (centerItem.height > 0) ? centerItem.height : 100
39 | var sc0 = Math.min(centerW / w0, centerH / h0)
40 | var finalCenterW = w0 * sc0
41 | var finalCenterH = h0 * sc0
42 |
43 | result.push({
44 | win: centerItem.win,
45 | x: offX + (useW - finalCenterW) / 2,
46 | y: offY + (useH - finalCenterH) / 2,
47 | width: finalCenterW,
48 | height: finalCenterH,
49 | isSatellite: false
50 | })
51 |
52 | // Orbit items (satellites)
53 | var satellites = windowList.slice(1)
54 | var numSat = satellites.length
55 |
56 | if (numSat > 0) {
57 | // Orbit Radius (distance from center)
58 | var radiusX = useW * 0.4
59 | var radiusY = useH * 0.4
60 |
61 | // Max size for satellites.
62 | // As the number of satellites increases, we shrink them to avoid overlap.
63 | var maxSatW = (useW * 0.25) / Math.max(1, (numSat / 6))
64 | var maxSatH = (useH * 0.25) / Math.max(1, (numSat / 6))
65 |
66 | // Start angle (-90 degrees = Top)
67 | var startAngle = -Math.PI / 2
68 | var stepAngle = (2 * Math.PI) / numSat
69 |
70 | for (var i = 0; i < numSat; i++) {
71 | var item = satellites[i]
72 | var angle = startAngle + (i * stepAngle)
73 |
74 | // Calculate satellite center coordinates
75 | var cx = (useW / 2) + radiusX * Math.cos(angle)
76 | var cy = (useH / 2) + radiusY * Math.sin(angle)
77 |
78 | // Aspect Fit satellite
79 | var ws = (item.width > 0) ? item.width : 100
80 | var hs = (item.height > 0) ? item.height : 100
81 | var scS = Math.min(maxSatW / ws, maxSatH / hs)
82 | var finalSatW = ws * scS
83 | var finalSatH = hs * scS
84 |
85 | result.push({
86 | win: item.win,
87 | x: offX + cx - (finalSatW / 2),
88 | y: offY + cy - (finalSatH / 2),
89 | width: finalSatW,
90 | height: finalSatH,
91 | isSatellite: true
92 | })
93 | }
94 | }
95 |
96 | return result
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/layouts/JustifiedLayout.qml:
--------------------------------------------------------------------------------
1 | pragma Singleton
2 | import Quickshell
3 |
4 | Singleton {
5 | id: root
6 |
7 | function doLayout(windowList, outerWidth, outerHeight) {
8 | var N = windowList.length
9 | if (N === 0)
10 | return []
11 |
12 | var containerWidth = outerWidth * 0.9
13 | var containerHeight = outerHeight * 0.9
14 |
15 | // Gap: 0.8% of screen, clamped between 12px and 32px
16 | var rawGap = Math.min(outerWidth * 0.08, outerHeight * 0.08)
17 | var gap = Math.max(12, Math.min(32, rawGap))
18 |
19 | var maxThumbHeight = outerHeight * 0.3
20 |
21 | if (containerWidth <= 0 || containerHeight <= 0) {
22 | return windowList.map(function(item) {
23 | return {
24 | win: item.win,
25 | x: 0,
26 | y: 0,
27 | width: 0,
28 | height: 0
29 | }
30 | })
31 | }
32 |
33 | var targetRowH = maxThumbHeight
34 | var rows = []
35 | var currentRow = []
36 | var sumAspect = 0
37 |
38 | function flushRow() {
39 | if (currentRow.length === 0)
40 | return
41 |
42 | var n = currentRow.length
43 | var rowHeight = maxThumbHeight
44 | if (sumAspect > 0) {
45 | var totalGapWidth = gap * (n - 1)
46 | var hFit = (containerWidth - totalGapWidth) / sumAspect
47 | if (hFit < rowHeight)
48 | rowHeight = hFit
49 | }
50 |
51 | if (rowHeight > maxThumbHeight)
52 | rowHeight = maxThumbHeight
53 | if (rowHeight <= 0)
54 | rowHeight = 1
55 |
56 | rows.push({
57 | items: currentRow.slice(),
58 | height: rowHeight,
59 | sumAspect: sumAspect
60 | })
61 |
62 | currentRow = []
63 | sumAspect = 0
64 | }
65 |
66 | for (var i = 0; i < N; ++i) {
67 | var item = windowList[i]
68 | var w0 = item.width > 0 ? item.width : 1
69 | var h0 = item.height > 0 ? item.height : 1
70 | var a = w0 / h0
71 | item.aspect = a
72 |
73 | if (currentRow.length > 0 &&
74 | ((sumAspect + a) * targetRowH + gap * currentRow.length) > containerWidth) {
75 | flushRow()
76 | }
77 |
78 | currentRow.push(item)
79 | sumAspect += a
80 | }
81 |
82 | if (currentRow.length > 0) {
83 | flushRow()
84 | }
85 |
86 | var totalRawHeight = 0
87 | for (var r = 0; r < rows.length; ++r) {
88 | totalRawHeight += rows[r].height
89 | }
90 | if (rows.length > 1) {
91 | totalRawHeight += gap * (rows.length - 1)
92 | }
93 |
94 | var sV = 1.0
95 | var availH = containerHeight
96 | if (totalRawHeight > 0 && totalRawHeight > availH) {
97 | sV = availH / totalRawHeight
98 | }
99 | if (sV <= 0)
100 | sV = 0.1
101 | if (sV > 1.0)
102 | sV = 1.0
103 |
104 | var gridTotalHeightScaled = totalRawHeight * sV
105 | var yAcc = (outerHeight - gridTotalHeightScaled) / 2
106 | if (!isFinite(yAcc) || yAcc < 0)
107 | yAcc = 0
108 |
109 | var result = []
110 |
111 | for (var r2 = 0; r2 < rows.length; ++r2) {
112 | var row = rows[r2]
113 | var rowHeightScaled = row.height * sV
114 |
115 | var rowWidthNoGapsScaled = 0
116 | for (var j = 0; j < row.items.length; ++j) {
117 | rowWidthNoGapsScaled += row.items[j].aspect * rowHeightScaled
118 | }
119 | var totalRowWidthScaled = rowWidthNoGapsScaled + gap * (row.items.length - 1)
120 |
121 | var xAcc = (outerWidth - totalRowWidthScaled) / 2
122 | if (!isFinite(xAcc))
123 | xAcc = 0
124 |
125 | for (var j2 = 0; j2 < row.items.length; ++j2) {
126 | var it2 = row.items[j2]
127 | var wScaled = it2.aspect * rowHeightScaled
128 | var hScaled = rowHeightScaled
129 |
130 | result.push({
131 | win: it2.win,
132 | x: xAcc,
133 | y: yAcc,
134 | width: wScaled,
135 | height: hScaled
136 | })
137 |
138 | xAcc += wScaled + gap
139 | }
140 |
141 | yAcc += rowHeightScaled
142 | if (r2 < rows.length - 1) {
143 | yAcc += gap * sV
144 | }
145 | }
146 |
147 | return result
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/layouts/SmartGridLayout.qml:
--------------------------------------------------------------------------------
1 | pragma Singleton
2 | import Quickshell
3 |
4 | Singleton {
5 | id: root
6 |
7 | function doLayout(windowList, outerWidth, outerHeight) {
8 | var N = windowList.length
9 | if (N === 0) return []
10 | if (outerWidth <= 0 || outerHeight <= 0) return []
11 |
12 | var gap = Math.min(outerWidth * 0.03, outerHeight * 0.03)
13 |
14 | // --- 0. DEFINIZIONE AREA SICURA (SCALATA) ---
15 | // Riduciamo l'area di calcolo al 90% per lasciare spazio alle animazioni hover
16 | var contentScale = 0.9
17 | var usableW = outerWidth * contentScale
18 | var usableH = outerHeight * contentScale
19 |
20 | // --- 1. TROVARE LA SCALA OTTIMALE ---
21 | // Usiamo usableW/H per decidere la dimensione delle finestre
22 | var TARGET_ASPECT = 16.0 / 9.0
23 | var bestCols = 1
24 | var bestRows = 1
25 | var bestScale = 0
26 |
27 | for (var cols = 1; cols <= N; cols++) {
28 | var rows = Math.ceil(N / cols)
29 |
30 | // Calcoliamo lo spazio basandoci sull'area ridotta
31 | var availW = usableW - gap * (cols - 1)
32 | var availH = usableH - gap * (rows - 1)
33 |
34 | if (availW <= 0 || availH <= 0) continue
35 |
36 | var cellW = availW / cols
37 | var cellH = availH / rows
38 |
39 | var scaleW = cellW / TARGET_ASPECT
40 | var scaleH = cellH / 1.0
41 | var currentScale = Math.min(scaleW, scaleH)
42 |
43 | if (currentScale > bestScale) {
44 | bestScale = currentScale
45 | bestCols = cols
46 | bestRows = rows
47 | }
48 | }
49 |
50 | // --- 2. CALCOLO DIMENSIONI REALI ---
51 |
52 | // Ricalcoliamo i limiti cella basati sull'area ridotta
53 | var finalAvailW = usableW - gap * (bestCols - 1)
54 | var finalAvailH = usableH - gap * (bestRows - 1)
55 | var maxCellW = finalAvailW / bestCols
56 | var maxCellH = finalAvailH / bestRows
57 |
58 | // --- 3. POSIZIONAMENTO (CENTRATO NELL'AREA TOTALE) ---
59 |
60 | // Calcoliamo l'altezza totale del blocco di contenuto
61 | var totalGridContentH = bestRows * maxCellH + (bestRows - 1) * gap
62 |
63 | // Per centrare verticalmente, usiamo l'outerHeight REALE (al 100%)
64 | // In questo modo il blocco ridotto (90%) finisce esattamente al centro dello schermo fisico
65 | var startOffsetY = (outerHeight - totalGridContentH) / 2
66 |
67 | var result = []
68 |
69 | // Iteriamo per RIGA
70 | for (var r = 0; r < bestRows; r++) {
71 | var rowItems = []
72 | var startIndex = r * bestCols
73 | var endIndex = Math.min(startIndex + bestCols, N)
74 |
75 | if (startIndex >= N) break
76 |
77 | var totalRowContentWidth = 0
78 |
79 | // Fase 3a: Calcolo dimensioni miniature (Packed)
80 | for (var i = startIndex; i < endIndex; i++) {
81 | var item = windowList[i]
82 | var w0 = (item.width && item.width > 0) ? item.width : 100
83 | var h0 = (item.height && item.height > 0) ? item.height : 100
84 |
85 | // Scala calcolata sui limiti "sicuri" (90%)
86 | var scale = Math.min(maxCellW / w0, maxCellH / h0)
87 |
88 | var thumbW = w0 * scale
89 | var thumbH = h0 * scale
90 |
91 | rowItems.push({
92 | originalItem: item,
93 | width: thumbW,
94 | height: thumbH,
95 | index: i,
96 | col: i - startIndex
97 | })
98 |
99 | totalRowContentWidth += thumbW
100 | }
101 |
102 | // Aggiungiamo i gap totali della riga
103 | if (rowItems.length > 1) {
104 | totalRowContentWidth += (rowItems.length - 1) * gap
105 | }
106 |
107 | // Fase 3b: Posizionamento X
108 | // Anche qui, usiamo outerWidth REALE per centrare il blocco riga nello schermo intero
109 | var currentX = (outerWidth - totalRowContentWidth) / 2
110 | var cellAbsY = startOffsetY + r * (maxCellH + gap)
111 |
112 | for (var k = 0; k < rowItems.length; k++) {
113 | var rItem = rowItems[k]
114 |
115 | // Centratura verticale nella fascia
116 | var currentY = cellAbsY + (maxCellH - rItem.height) / 2
117 |
118 | result.push({
119 | win: rItem.originalItem.win,
120 | x: currentX,
121 | y: currentY,
122 | width: rItem.width,
123 | height: rItem.height,
124 | rowIndex: r,
125 | colIndex: rItem.col
126 | })
127 |
128 | currentX += rItem.width + gap
129 | }
130 | }
131 |
132 | return result
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/layouts/MasonryLayout.qml:
--------------------------------------------------------------------------------
1 | pragma Singleton
2 | import Quickshell
3 |
4 | Singleton {
5 | id: root
6 |
7 | function doLayout(windowList, outerWidth, outerHeight) {
8 | var N = windowList.length
9 | if (N === 0) return []
10 |
11 | // Gap: 0.8% of screen, clamped between 12px and 32px
12 | var rawGap = Math.min(outerWidth * 0.08, outerHeight * 0.08)
13 | var gap = Math.max(12, Math.min(32, rawGap))
14 |
15 | // Safe Area (90%)
16 | // Define the bounding box for the content.
17 | var contentScale = 0.90
18 | var useW = outerWidth * contentScale
19 | var useH = outerHeight * contentScale
20 |
21 | // Find Best Column Count
22 | // Standard logic: try to fit content in 1 col, then 2, etc.
23 | var bestCols = N
24 |
25 | for (var cols = 1; cols <= N; cols++) {
26 | var tryColWidth = (useW - (cols - 1) * gap) / cols
27 | var tryColHeights = new Array(cols).fill(0)
28 |
29 | for (var i = 0; i < N; i++) {
30 | var item = windowList[i]
31 | var minH = Math.min.apply(null, tryColHeights)
32 | var colIdx = tryColHeights.indexOf(minH)
33 |
34 | var w0 = (item.width && item.width > 0) ? item.width : 100
35 | var h0 = (item.height && item.height > 0) ? item.height : 100
36 | var scale = tryColWidth / w0
37 |
38 | tryColHeights[colIdx] += (h0 * scale) + gap
39 | }
40 |
41 | var currentMaxH = Math.max.apply(null, tryColHeights)
42 | if (currentMaxH > 0) currentMaxH -= gap
43 |
44 | // If it fits vertically, we stop.
45 | if (currentMaxH <= useH) {
46 | bestCols = cols
47 | break
48 | }
49 | }
50 |
51 | // Rigorous clamping
52 | // We have chosen 'bestCols'. Now we calculate the theoretical column width.
53 | // BUT, if N is small (e.g. 1), this width might produce a height > useH.
54 | // We must calculate a "Global Downscale Factor" to ensure NO item exceeds useH.
55 |
56 | var rawColWidth = (useW - (bestCols - 1) * gap) / bestCols
57 | var maxOverflowRatio = 1.0 // 1.0 means "fits perfectly"
58 |
59 | // Simulate again to find the worst offender (tallest item/column relative to screen)
60 | // Note: In masonry, we care about the total column height, not just single item.
61 | var clampHeights = new Array(bestCols).fill(0)
62 |
63 | for (var j = 0; j < N; j++) {
64 | var it = windowList[j]
65 |
66 | // Standard masonry placement logic
67 | var mH = Math.min.apply(null, clampHeights)
68 | var cId = clampHeights.indexOf(mH)
69 |
70 | var wRaw = (it.width && it.width > 0) ? it.width : 100
71 | var hRaw = (it.height && it.height > 0) ? it.height : 100
72 | var sc = rawColWidth / wRaw
73 |
74 | clampHeights[cId] += (hRaw * sc) + gap
75 | }
76 |
77 | // Find the tallest column produced by the raw width
78 | var tallestCol = Math.max.apply(null, clampHeights)
79 | if (tallestCol > 0) tallestCol -= gap
80 |
81 | // If the tallest column is taller than Safe Area, calculate reduction factor
82 | if (tallestCol > useH) {
83 | maxOverflowRatio = useH / tallestCol
84 | }
85 |
86 | // Apply the reduction factor to the column width.
87 | var finalColWidth = rawColWidth * maxOverflowRatio
88 |
89 | // Re-centering x
90 | var finalGridW = (finalColWidth * bestCols) + (gap * (bestCols - 1))
91 | var finalOffX = (outerWidth - finalGridW) / 2
92 |
93 |
94 | // Final rendering
95 | var colHeights = new Array(bestCols).fill(0)
96 | var result = []
97 |
98 | for (var k = 0; k < N; k++) {
99 | var itemK = windowList[k]
100 |
101 | // 1. Find shortest column
102 | var minH = Math.min.apply(null, colHeights)
103 | var cIdx = colHeights.indexOf(minH)
104 |
105 | // 2. Dimensions
106 | var wOrig = (itemK.width && itemK.width > 0) ? itemK.width : 100
107 | var hOrig = (itemK.height && itemK.height > 0) ? itemK.height : 100
108 | var s = finalColWidth / wOrig
109 | var tH = hOrig * s
110 |
111 | // 3. Position (using Recalculated OffX)
112 | var xPos = finalOffX + cIdx * (finalColWidth + gap)
113 | var yPos = colHeights[cIdx]
114 |
115 | result.push({
116 | win: itemK.win,
117 | x: xPos,
118 | y: yPos,
119 | width: finalColWidth,
120 | height: tH,
121 | colIndex: cIdx
122 | })
123 |
124 | colHeights[cIdx] += tH + gap
125 | }
126 |
127 | // Vertical centering
128 | var realGridH = Math.max.apply(null, colHeights)
129 | if (realGridH > 0) realGridH -= gap
130 |
131 | var finalOffY = (outerHeight - realGridH) / 2
132 |
133 | for (var m = 0; m < result.length; m++) {
134 | result[m].y += finalOffY
135 | }
136 |
137 | return result
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/layouts/HeroLayout.qml:
--------------------------------------------------------------------------------
1 | pragma Singleton
2 | import Quickshell
3 | import Quickshell.Hyprland
4 |
5 | Singleton {
6 | id: root
7 |
8 | function doLayout(windowList, outerWidth, outerHeight) {
9 | if (windowList.length === 0) return []
10 |
11 | // Gap: 0.8% of screen, clamped between 12px and 32px
12 | var rawGap = Math.min(outerWidth * 0.08, outerHeight * 0.08)
13 | var gap = Math.max(12, Math.min(32, rawGap))
14 |
15 | // Move active window to the head of windowList
16 | var activeAddr = Hyprland.activeToplevel?.lastIpcObject?.address
17 | if (activeAddr) {
18 | var activeIdx = windowList.findIndex(it => it.lastIpcObject.address === activeAddr)
19 | if (activeIdx !== -1) {
20 | windowList = [windowList[activeIdx], ...windowList.filter(it => it !== windowList[activeIdx])]
21 | }
22 | }
23 |
24 | // Safe area definition (90%)
25 | var contentScale = 0.90
26 | var useW = outerWidth * contentScale
27 | var useH = outerHeight * contentScale
28 |
29 | // Global offset - center Safe area
30 | var offX = (outerWidth - useW) / 2
31 | var offY = (outerHeight - useH) / 2
32 |
33 | var result = []
34 |
35 | // Screen zones (Hero/Stack)
36 | var heroRatio = 0.40 // 40% Hero
37 | var heroAreaW = useW * heroRatio
38 | var stackAreaW = useW - heroAreaW - gap // 60% Stack
39 |
40 | var heroItem = windowList[0]
41 |
42 | // Aspect Fit
43 | var hScale = Math.min(heroAreaW / heroItem.width, useH / heroItem.height)
44 | var hW = heroItem.width * hScale
45 | var hH = heroItem.height * hScale
46 |
47 | result.push({
48 | win: heroItem.win,
49 | x: offX + (heroAreaW - hW) / 2,
50 | y: offY + (useH - hH) / 2,
51 | width: hW,
52 | height: hH,
53 | isHero: true
54 | })
55 |
56 | var others = windowList.slice(1)
57 | var N = others.length
58 |
59 | if (N > 0) {
60 | var stackStartX = offX + heroAreaW + gap
61 |
62 | // Evaluate col number
63 | var bestCols = 1
64 | var bestRows = N
65 |
66 | // Windows height on a single column
67 | var oneColH = (useH - (gap * (N - 1))) / N
68 |
69 | // TOLERANCE THRESHOLD (0.15 = 15% of screen height)
70 | // If the windows are at least 15% of the screen height, we stay on 1 column.
71 | // With 4 windows we are at ~25% -> OK (1 Column)
72 | // With 7 windows we are at ~14% -> NO (Go to grid calculation)
73 | var useSingleCol = oneColH > (useH * 0.15)
74 |
75 | if (!useSingleCol) {
76 | // If space is limited, we look for the optimal grid starting with 2 columns.
77 | var bestScale = 0
78 | var TARGET_ASPECT = 16.0 / 9.0
79 |
80 | for (var cols = 2; cols <= N; cols++) {
81 | var rows = Math.ceil(N / cols)
82 | var availW = stackAreaW - (gap * (cols - 1))
83 | var availH = useH - (gap * (rows - 1))
84 |
85 | if (availW <= 0 || availH <= 0) continue
86 |
87 | var cellW = availW / cols
88 | var cellH = availH / rows
89 |
90 | // Size score
91 | var sW = cellW / TARGET_ASPECT
92 | var sH = cellH / 1.0
93 | var currentScale = Math.min(sW, sH)
94 |
95 | if (currentScale > bestScale) {
96 | bestScale = currentScale
97 | bestCols = cols
98 | bestRows = rows
99 | }
100 | }
101 | }
102 |
103 | // Evaluation of the final dimensions of the selected grid
104 | var finalAvailW = stackAreaW - (gap * (bestCols - 1))
105 | var finalAvailH = useH - (gap * (bestRows - 1))
106 |
107 | var finalCellW = finalAvailW / bestCols
108 | var finalCellH = finalAvailH / bestRows
109 |
110 | // Vertical centering of the total stack
111 | var totalGridH = bestRows * finalCellH + (bestRows - 1) * gap
112 | var stackStartY = offY + (useH - totalGridH) / 2
113 |
114 | // Items positioning
115 | for (var i = 0; i < N; ++i) {
116 | var item = others[i]
117 |
118 | var row = Math.floor(i / bestCols)
119 | var col = i % bestCols
120 |
121 | // Cell coords (Standard Grid Alignment)
122 | // No “rowOffsetX”, cell 0 always starts on the left
123 | var cellAbsX = stackStartX + col * (finalCellW + gap)
124 | var cellAbsY = stackStartY + row * (finalCellH + gap)
125 |
126 | // Thumb aspect Fit
127 | var sc = Math.min(finalCellW / item.width, finalCellH / item.height)
128 | var w = item.width * sc
129 | var h = item.height * sc
130 |
131 | result.push({
132 | win: item.win,
133 | x: cellAbsX + (finalCellW - w) / 2,
134 | y: cellAbsY + (finalCellH - h) / 2,
135 | width: w,
136 | height: h,
137 | isHero: false
138 | })
139 | }
140 | }
141 |
142 | return result
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/layouts/SpiralLayout.qml:
--------------------------------------------------------------------------------
1 | pragma Singleton
2 | import Quickshell
3 | import Quickshell.Hyprland
4 |
5 | Singleton {
6 | id: root
7 |
8 | function doLayout(windowList, outerWidth, outerHeight, maxSplits) {
9 | var N = windowList.length
10 | if (N === 0) return []
11 |
12 | if (maxSplits === undefined) maxSplits = 3
13 |
14 | // Standard Gap: 0.8% of screen
15 | var rawGap = outerWidth * 0.008
16 | var gap = Math.max(8, Math.min(24, rawGap))
17 |
18 | // Primary Gap: The space between the first Big Window and the rest.
19 | // We make it 3x larger than the standard gap for emphasis.
20 | var primaryGap = gap * 3
21 |
22 | // Safe Area (90%)
23 | var contentScale = 0.90
24 | var useW = outerWidth * contentScale
25 | var useH = outerHeight * contentScale
26 | var offX = (outerWidth - useW) / 2
27 | var offY = (outerHeight - useH) / 2
28 |
29 | // Move Active Window to start
30 | var activeAddr = Hyprland.activeToplevel?.lastIpcObject?.address
31 | if (activeAddr) {
32 | var activeIdx = windowList.findIndex(it => it.lastIpcObject.address === activeAddr)
33 | if (activeIdx !== -1) {
34 | windowList = [windowList[activeIdx], ...windowList.filter(it => it !== windowList[activeIdx])]
35 | }
36 | }
37 |
38 | var result = []
39 |
40 | // Working area cursor
41 | var curX = offX
42 | var curY = offY
43 | var curW = useW
44 | var curH = useH
45 |
46 | // Items to process in Spiral mode
47 | var spiralCount = Math.min(N - 1, maxSplits)
48 |
49 | // Spiral cuts
50 | for (var k = 0; k < spiralCount; k++) {
51 | var sItem = windowList[k]
52 | var sBoxW, sBoxH
53 | var sBoxX = curX
54 | var sBoxY = curY
55 |
56 | // Logic change: Use 'primaryGap' only for the very first cut (k=0),
57 | // otherwise use standard 'gap'.
58 | var currentGap = (k === 0) ? primaryGap : gap
59 |
60 | if (curW > curH) { // Split Vertical
61 | // Calculate width subtracting the specific gap for this iteration
62 | sBoxW = (curW - currentGap) / 2
63 | sBoxH = curH
64 |
65 | // Shift working area for next items by the specific gap
66 | curX += sBoxW + currentGap
67 | curW -= (sBoxW + currentGap)
68 | } else { // Split Horizontal
69 | sBoxW = curW
70 | sBoxH = (curH - currentGap) / 2
71 |
72 | // Shift working area for next items by the specific gap
73 | curY += sBoxH + currentGap
74 | curH -= (sBoxH + currentGap)
75 | }
76 |
77 | // Aspect Fit
78 | var sw0 = (sItem.width > 0) ? sItem.width : 100
79 | var sh0 = (sItem.height > 0) ? sItem.height : 100
80 | var sScale = Math.min(sBoxW / sw0, sBoxH / sh0)
81 |
82 | result.push({
83 | win: sItem.win,
84 | x: sBoxX + (sBoxW - (sw0 * sScale))/2,
85 | y: sBoxY + (sBoxH - (sh0 * sScale))/2,
86 | width: sw0 * sScale,
87 | height: sh0 * sScale,
88 | isSpiral: true,
89 | index: k
90 | })
91 | }
92 |
93 | // Overflow grid
94 | var remainingItems = windowList.slice(spiralCount)
95 | var remN = remainingItems.length
96 |
97 | if (remN > 0) {
98 | // Standard Grid logic for the remaining box
99 | var bestCols = 1
100 | var bestScale = 0
101 | var TARGET_ASPECT = 16.0/9.0
102 |
103 | for (var c = 1; c <= remN; c++) {
104 | var r = Math.ceil(remN / c)
105 | var avW = curW - gap * (c - 1)
106 | var avH = curH - gap * (r - 1)
107 | if (avW <= 0 || avH <= 0) continue
108 |
109 | var cW = avW / c
110 | var cH = avH / r
111 | var sc = Math.min(cW / TARGET_ASPECT, cH)
112 |
113 | if (sc > bestScale) {
114 | bestScale = sc
115 | bestCols = c
116 | }
117 | }
118 |
119 | var remRows = Math.ceil(remN / bestCols)
120 | var finalCellW = (curW - gap * (bestCols - 1)) / bestCols
121 | var finalCellH = (curH - gap * (remRows - 1)) / remRows
122 |
123 | var gridContentH = remRows * finalCellH + (remRows - 1) * gap
124 | var gridStartY = curY + (curH - gridContentH) / 2
125 |
126 | for (var j = 0; j < remN; j++) {
127 | var rItem = remainingItems[j]
128 | var row = Math.floor(j / bestCols)
129 | var col = j % bestCols
130 |
131 | var itemsInRow = Math.min((row + 1) * bestCols, remN) - (row * bestCols)
132 | var rowW = itemsInRow * finalCellW + (itemsInRow - 1) * gap
133 | var rowStartX = curX + (curW - rowW) / 2
134 |
135 | var cellX = rowStartX + col * (finalCellW + gap)
136 | var cellY = gridStartY + row * (finalCellH + gap)
137 |
138 | var rw0 = (rItem.width > 0) ? rItem.width : 100
139 | var rh0 = (rItem.height > 0) ? rItem.height : 100
140 | var rSc = Math.min(finalCellW / rw0, finalCellH / rh0)
141 |
142 | result.push({
143 | win: rItem.win,
144 | x: cellX + (finalCellW - (rw0 * rSc))/2,
145 | y: cellY + (finalCellH - (rh0 * rSc))/2,
146 | width: rw0 * rSc,
147 | height: rh0 * rSc,
148 | isSpiral: false
149 | })
150 | }
151 | }
152 |
153 | return result
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/layouts/BandsLayout.qml:
--------------------------------------------------------------------------------
1 | pragma Singleton
2 | import Quickshell
3 |
4 | Singleton {
5 | id: root
6 |
7 | function doLayout(windowList, outerWidth, outerHeight) {
8 | var N = windowList.length
9 | if (N === 0) return []
10 |
11 | // Gap: 0.8% of screen, clamped between 12px and 24px
12 | var rawGap = Math.min(outerWidth * 0.08, outerHeight * 0.08)
13 | var gap = Math.max(12, Math.min(24, rawGap))
14 |
15 | // Safe Area: 90% of the screen
16 | var contentScale = 0.90
17 | var useW = outerWidth * contentScale
18 | var useH = outerHeight * contentScale
19 |
20 | // Global offsets to center everything
21 | var offX = (outerWidth - useW) / 2
22 | var offY = (outerHeight - useH) / 2
23 |
24 | // Group by workspace
25 | var groups = {}
26 | var wsOrder = []
27 |
28 | for (var i = 0; i < N; i++) {
29 | var w = windowList[i]
30 | var wsId = w.workspaceId
31 |
32 | if (!groups[wsId]) {
33 | groups[wsId] = []
34 | wsOrder.push(wsId)
35 | }
36 | groups[wsId].push(w)
37 | }
38 |
39 | var bandCount = wsOrder.length
40 | if (bandCount === 0) return []
41 |
42 | // Band height & max thumb height
43 |
44 | // Calculate the height allocated for each workspace band
45 | var totalGapH = gap * (bandCount - 1)
46 | var bandHeight = (useH - totalGapH) / bandCount
47 |
48 | // Aesthetic Cap: Even if we have only 1 workspace,
49 | // windows shouldn't exceed 45% of screen height.
50 | var absoluteMaxH = useH * 0.45
51 |
52 | // The effective max height is the smaller of the two.
53 | // If we have 10 bands, bandHeight will be small (e.g. 100px), so that rules.
54 | // If we have 1 band, bandHeight is huge (1000px), so absoluteMaxH (450px) rules.
55 | var localMaxH = Math.min(bandHeight, absoluteMaxH)
56 |
57 | // Minimum safety height to avoid division by zero errors
58 | if (localMaxH < 10) localMaxH = 10
59 |
60 | var result = []
61 | var currentY = offY
62 |
63 | // Process each band
64 | for (var b = 0; b < bandCount; b++) {
65 | var wsId = wsOrder[b]
66 | var items = groups[wsId]
67 | var itemCount = items.length
68 |
69 | // ROW LAYOUT CALCULATION (Justified)
70 | var rows = []
71 | var currentRow = []
72 | var currentAspectSum = 0
73 |
74 | for (var k = 0; k < itemCount; k++) {
75 | var item = items[k]
76 | var w0 = (item.width > 0) ? item.width : 100
77 | var h0 = (item.height > 0) ? item.height : 100
78 | var aspect = w0 / h0
79 |
80 | var wrapper = { win: item.win, aspect: aspect }
81 |
82 | // Check overflow: (SumAspects * MaxH) + Gaps > Width
83 | var hypotheticalWidth = (currentAspectSum + aspect) * localMaxH + (currentRow.length * gap)
84 |
85 | if (currentRow.length > 0 && hypotheticalWidth > useW) {
86 | rows.push({ items: currentRow, aspectSum: currentAspectSum })
87 | currentRow = []
88 | currentAspectSum = 0
89 | }
90 |
91 | currentRow.push(wrapper)
92 | currentAspectSum += aspect
93 | }
94 | if (currentRow.length > 0) {
95 | rows.push({ items: currentRow, aspectSum: currentAspectSum })
96 | }
97 |
98 | // SCALE & FIT ROWS
99 | // Calculate how tall the content actually is
100 | var totalContentH = 0
101 | var finalRows = []
102 |
103 | for (var r = 0; r < rows.length; r++) {
104 | var rowObj = rows[r]
105 | var rItems = rowObj.items
106 |
107 | // Optimal Height = (Available Width / Sum Aspects)
108 | var availRowW = useW - (gap * (rItems.length - 1))
109 | var optimalH = availRowW / rowObj.aspectSum
110 |
111 | // Clamp to limits
112 | if (optimalH > localMaxH) optimalH = localMaxH
113 |
114 | finalRows.push({ items: rItems, h: optimalH })
115 | totalContentH += optimalH
116 | }
117 |
118 | // Add vertical gaps between rows inside the band
119 | if (finalRows.length > 1) {
120 | totalContentH += gap * (finalRows.length - 1)
121 | }
122 |
123 | // If rows overflow the band height (rare, but possible with many windows), scale down
124 | var scaleFactor = 1.0
125 | if (totalContentH > bandHeight) {
126 | scaleFactor = bandHeight / totalContentH
127 | totalContentH = bandHeight // Cap for centering math
128 | }
129 |
130 | // GENERATE COORDINATES
131 | // Center the content vertically within the band slot
132 | // Note: If bandCount=1, bandHeight is huge (90% screen), but totalContentH is constrained by absoluteMaxH.
133 | // This ensures the single row floats nicely in the middle.
134 | var rowY = currentY + (bandHeight - totalContentH) / 2
135 |
136 | for (var r2 = 0; r2 < finalRows.length; r2++) {
137 | var fRow = finalRows[r2]
138 | var rHeight = fRow.h * scaleFactor
139 | var rItems2 = fRow.items
140 |
141 | // Calculate row width for horizontal centering
142 | var actualRowW = 0
143 | for (var j = 0; j < rItems2.length; j++) {
144 | actualRowW += (rItems2[j].aspect * rHeight)
145 | }
146 | actualRowW += gap * (rItems2.length - 1)
147 |
148 | var rowX = offX + (useW - actualRowW) / 2
149 |
150 | for (var j2 = 0; j2 < rItems2.length; j2++) {
151 | var it = rItems2[j2]
152 | var finalW = it.aspect * rHeight
153 |
154 | result.push({
155 | win: it.win,
156 | x: rowX,
157 | y: rowY,
158 | width: finalW,
159 | height: rHeight
160 | })
161 |
162 | rowX += finalW + gap
163 | }
164 |
165 | rowY += rHeight + (gap * scaleFactor)
166 | }
167 |
168 | // Advance Y to the next band slot
169 | currentY += bandHeight + gap
170 | }
171 |
172 | return result
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Quickshell Window Switcher
2 |
3 | ### The ultimate Hyprland Overview
4 |
5 | A modern, fluid, and highly customizable **Window Switcher (Exposé)** for **Hyprland**, built entirely in QML using the [Quickshell](https://github.com/outfoxxed/quickshell) framework.
6 |
7 | It provides a native Wayland experience similar to macOS Mission Control or GNOME Activities, featuring a suite of advanced mathematical layouts designed to visualize your windows beautifully on any screen size.
8 |
9 | ## 📸 Gallery
10 |
11 | See `qs-hyprview` in action with its different layout algorithms.
12 |
13 | | | | |
14 | | :---: | :---: | :---: |
15 | | 
**Smart Grid** | 
**Bands** | 
**Masonry** |
16 | | 
**Hero** | 
**Spiral** | 
**Satellite** |
17 | | 
**Vortex** | 
**Staggered** | **What's next?** |
18 |
19 | ## ✨ Features
20 |
21 | * **⚡ Native Performance:** Built on Qt6/QML and Wayland Layershell for zero latency and smooth 60fps animations.
22 | * **🧮 10 Layout Algorithms:** A collection of layouts ranging from productive grids to scenic orbital views.
23 | * **🔍 Instant Search:** Filter windows by title, class, or app name immediately upon typing.
24 | * **🎮 Full Navigation:** Supports both Keyboard (Arrows/Tab/Enter) and Mouse (Hover/Click).
25 | * **🎨 Smart Safe Area:** All layouts calculate a 90% "Safe Area" to ensure hover animations never clip against screen edges.
26 | * **⚙️ Live Thumbnails:** Live window contents via Hyprland screencopy.
27 |
28 | ## 🛠️ Dependencies
29 |
30 | * **Hyprland**: The Wayland compositor.
31 | * **Quickshell**: The QML shell framework.
32 | * **Qt6**: Core libraries (usually pulled in by Quickshell).
33 |
34 | ## 🚀 Installation
35 |
36 | 1. Clone this repository:
37 | ```bash
38 | git clone https://github.com/dom0/qs-hyprview.git
39 | ```
40 |
41 | 2. Ensure `quickshell` is installed and in your PATH.
42 |
43 | ## ⚙️ Configuration & Usage
44 |
45 | ### Launching
46 | To start the daemon (add this to your `hyprland.conf` with `exec-once`):
47 |
48 | ```bash
49 | quickshell -p /path/to/cloned/repo
50 |
51 | # Or clone into $XDG_CONFIG_HOME/quickshell (usually ~/.config/quickshell) and run with -c flag:
52 | quickshell -c qs-hyprview
53 | ```
54 |
55 | ### Toggle (Open/Close)
56 | The project exposes an IPC handler named `expose`. You can bind a key in Hyprland to toggle the view.
57 |
58 | **In `hyprland.conf`:**
59 | ```ini
60 | # "smartgrid", "justified", "masonry", "bands", "hero", "spiral"
61 | # "satellite", "staggered", "columnar", "vortex", "random"
62 | $layout = 'masonry'
63 |
64 | # Toggle overview visibility
65 | bind = $mainMod, TAB, exec, quickshell ipc -p /path/to/cloned/repo call expose toggle $layout
66 |
67 | # Open overview
68 | bind = $mainMod, TAB, exec, quickshell ipc -p /path/to/cloned/repo call expose open $layout
69 |
70 | # Close overview
71 | bind = $mainMod, TAB, exec, quickshell ipc -p /path/to/cloned/repo call expose close
72 |
73 |
74 | # Or, using XDG_CONFIG_HOME:
75 | #bind = $mainMod, TAB, exec, quickshell ipc -c qs-hyprview call expose toggle $layout
76 | #bind = $mainMod, TAB, exec, quickshell ipc -c qs-hyprview call expose open $layout
77 | #bind = $mainMod, TAB, exec, quickshell ipc -c qs-hyprview call expose close
78 | ```
79 | ### Visual optimizations
80 |
81 | You can optimize your experience by adding an opaque/blurred background (dimming area) or pop-in animations using native Hyprland features.
82 |
83 | **In `hyprland.conf`:**
84 | ```ini
85 | # dimming area
86 | decoration {
87 | dim_around = 0.8
88 | }
89 |
90 | layerrule = dimaround, quickshell:expose
91 | ```
92 |
93 | ```ini
94 | # blur area
95 | decoration {
96 | blur {
97 | enabled = true
98 | size = 3
99 | passes = 1
100 | }
101 | }
102 |
103 | layerrule = blur, quickshell:expose
104 | ```
105 |
106 | ```ini
107 | # popin animation
108 | animations {
109 | enabled = yes
110 | animation = layersIn, 1, 1.5, default, popin
111 | }
112 | ```
113 |
114 | ### Customization
115 | You can modify the core properties at the top of `shell.qml`:
116 |
117 | ```qml
118 | // Set to true for live window updates (monitor refresh rate, higher CPU usage), false for static snapshots (~8fps)
119 | property bool liveCapture: false
120 |
121 | // Automatically move mouse cursor to the center of selected window
122 | property bool moveCursorToActiveWindow: true
123 | ```
124 |
125 | ## 📐 Layout Algorithms
126 |
127 | This project includes a sophisticated `LayoutsManager` offering **10 distinct algorithms**:
128 |
129 | ### 1. Smart Grid (`smartgrid`)
130 | The default layout. It uses an **Iterative Best-Fit** algorithm. It simulates every possible row/column combination to find the exact grid configuration that results in the largest possible thumbnails without overflowing the screen.
131 |
132 | ### 2. Justified (`justified`)
133 | A **Justified Layout** (similar to Google Images). It places windows in rows, maintaining fixed height and original aspect ratios, and scales the row to fit the screen width perfectly.
134 |
135 | ### 3. Masonry (`masonry`)
136 | A **Waterfall** layout (Pinterest-style). It optimizes vertical space by placing windows in dynamic columns. It automatically calculates the optimal number of columns based on the window count.
137 |
138 | ### 4. Bands (`bands`)
139 | Organizes windows by **Workspace**. Creates a horizontal "Band" for each active workspace, grouping relevant tasks together. Windows are justified within their workspace band.
140 |
141 | ### 5. Hero (`hero`)
142 | A focus-centric layout.
143 | * **Hero Area:** The active window takes up 40% of the screen (left side).
144 | * **Stack:** All other windows share the remaining 60% (right side) in a smart grid or column.
145 |
146 | ### 6. Spiral (`spiral`)
147 | A scenic layout based on the **Golden Ratio (BSP)**.
148 | * Windows split the screen in a spiral pattern (Left half, Top-Right half, etc.).
149 | * The first window is separated by a larger gap to emphasize focus.
150 | * If many windows are open, the spiral stops after 3 cuts and arranges the rest in a grid.
151 |
152 | ### 7. Satellite (`satellite`)
153 | An **Orbital** layout.
154 | * The active window sits in the center of the screen.
155 | * All other windows orbit around it in an ellipse.
156 | * Visually stunning and great for focusing on one task while keeping an eye on the surroundings.
157 |
158 | ### 8. Staggered (`staggered`)
159 | A **Honeycomb/Brick** layout.
160 | * Similar to a grid, but every odd row is shifted horizontally by half a cell width.
161 | * Creates a more organic, less rigid look compared to standard grids.
162 |
163 | ### 9. Columnar (`columnar`)
164 | Divides the screen into vertical strips.
165 | * Ignores rows completely and gives every window maximum vertical space.
166 | * Excellent for **Ultrawide** monitors (21:9 / 32:9).
167 |
168 | ### 10. Vortex (`vortex`)
169 | A depth-based Phyllotaxis layout (Sunflower pattern), designed for a scenographic and immersive experience.
170 | * Center Focus: The active window sits in the absolute center at maximum scale.
171 | * Depth Effect: Subsequent windows spiral outwards, gradually decreasing in size and z-index. This creates a 3D "tunnel" effect where older windows fade into the background.
172 |
173 | ### 🎲 Random (`random`)
174 | Feeling adventurous? This mode selects one of the above algorithms at random every time you open the dashboard.
175 |
176 | ## ⌨️ Controls
177 |
178 | | Input | Action |
179 | | :--- | :--- |
180 | | **Typing** | Instantly filters windows by Title, Class, or App ID |
181 | | **Arrows (↑ ↓ ← →)** | Spatial navigation between thumbnails |
182 | | **Tab / Shift+Tab** | Sequential navigation |
183 | | **Enter** | Activate selected window |
184 | | **Middle Click** | Close hovered window |
185 | | **Esc / Click BG** | Close dashboard |
186 |
187 | ## 🤝 Contributing
188 |
189 | Pull Requests are welcome! If you want to add a new layout algorithm or improve performance, please open an issue or submit a PR.
190 |
191 | ## 📄 License
192 |
193 | Distributed under the GNU General Public License v3.0. See `LICENSE` for more information.
194 |
195 | ---
196 |
197 |