Krolyxon
Commit 5515e5c

Branches

Refs →

aurora5515e5c

aurora: add multi-monitor setup for workspaces

Author
krolyxon
Date
2026-10-13 21:15 +0530
Parent
644c846
Commit
5515e5cdf78820748b5911376f5ba8d9848b675b

+246 −97

11 files changed

  1. M .config/quickshell/aurora/Bar.qml +17 −12
  2. M .config/quickshell/aurora/ControlPanel.qml +1 −1
  3. M .config/quickshell/aurora/HomePage.qml +39 −1
  4. A .config/quickshell/aurora/Shot.qml +33 −0
  5. M .config/quickshell/aurora/StatBar.qml +2 −1
  6. M .config/quickshell/aurora/SysPage.qml +49 −40
  7. M .config/quickshell/aurora/SysStats.qml +36 −13
  8. M .config/quickshell/aurora/Theme.qml +9 −2
  9. M .config/quickshell/aurora/Tray.qml +2 −1
  10. M .config/quickshell/aurora/Workspaces.qml +32 −19
  11. M .config/quickshell/aurora/shell.qml +26 −7
M .config/quickshell/aurora/Bar.qml View
@@ -2,6 +2,7 @@ import QtQuick
22 import Quickshell
33 import Quickshell.Wayland
44 import Quickshell.Bluetooth
5+import Quickshell.Hyprland
56 import Quickshell.Services.Pipewire
67 import Quickshell.Services.UPower
78
@@ -9,7 +10,7 @@ PanelWindow {
910 id: bar
1011 required property var modelData
1112 screen: modelData
12− signal togglePanel(string name)
13+ signal togglePanel(string name, var scr)
1314
1415 anchors { top: true; left: true; right: true }
1516 implicitHeight: Theme.barHeight
@@ -29,6 +30,10 @@ PanelWindow {
2930 readonly property bool batLow: hasBat && batPct <= 0.15 && !charging
3031 readonly property var adapter: Bluetooth.defaultAdapter
3132
33+ // which Hyprland monitor this bar belongs to, and whether it has focus
34+ readonly property var hmon: Hyprland.monitorFor(modelData)
35+ readonly property bool monFocused: hmon !== null && hmon.focused
36+
3237 function findHeadset() {
3338 var l = Bluetooth.devices.values
3439 for (var i = 0; i < l.length; i++) {
@@ -56,7 +61,7 @@ PanelWindow {
5661 anchors.left: parent.left
5762 height: parent.height
5863 spacing: 10
59− Workspaces { height: parent.height }
64+ Workspaces { height: parent.height; monitor: bar.hmon }
6065 CatLoop { height: parent.height }
6166 }
6267
@@ -67,7 +72,7 @@ PanelWindow {
6772 elide: Text.ElideRight
6873 horizontalAlignment: Text.AlignHCenter
6974 readonly property string title: ToplevelManager.activeToplevel ? ToplevelManager.activeToplevel.title : ""
70− visible: title !== ""
75+ visible: bar.monFocused && title !== "" // active window is global: show it on the focused monitor only
7176 text: Theme.iArch + " " + title
7277 color: Theme.fg
7378 font.family: Theme.font
@@ -81,13 +86,13 @@ PanelWindow {
8186 height: parent.height
8287 spacing: 10
8388
84− Tray {}
89+ Tray { show: bar.monFocused }
8590
8691 BarPill {
8792 visible: bar.headset !== null
8893 icon: Theme.iHeadphones
8994 text: bar.headset ? Math.round(bar.headset.battery * 100) + "%" : ""
90− onActivated: bar.togglePanel("bt")
95+ onActivated: bar.togglePanel("bt", bar.modelData)
9196 }
9297
9398 BarPill {
@@ -98,7 +103,7 @@ PanelWindow {
98103 onActivated: button => {
99104 if (button === Qt.RightButton && bar.audio) bar.audio.muted = !bar.audio.muted
100105 else if (button === Qt.MiddleButton && bar.mic) bar.mic.muted = !bar.mic.muted
101− else bar.togglePanel("audio")
106+ else bar.togglePanel("audio", bar.modelData)
102107 }
103108 onScrolled: dir => {
104109 if (bar.audio) bar.audio.volume = Math.max(0, Math.min(1, bar.audio.volume + dir * 0.02))
@@ -109,14 +114,14 @@ PanelWindow {
109114 icon: Net.wired && !(Net.wifiEnabled && Net.connected !== "") ? Theme.iEthernet
110115 : (Net.wifiEnabled ? Theme.wifiIcon(Net.connected !== "" ? Net.activeSignal : 0) : Theme.iWifiOff)
111116 tint: (Net.wired || (Net.wifiEnabled && Net.connected !== "")) ? Theme.fg : Theme.dim
112− onActivated: bar.togglePanel("net")
117+ onActivated: bar.togglePanel("net", bar.modelData)
113118 }
114119
115120 BarPill {
116121 visible: bar.adapter !== null
117122 icon: !bar.btOn ? Theme.iBtOff : (bar.btLink ? Theme.iBtConn : Theme.iBt)
118123 tint: bar.btLink ? Theme.fg : Theme.dim
119− onActivated: bar.togglePanel("bt")
124+ onActivated: bar.togglePanel("bt", bar.modelData)
120125 }
121126
122127 BarPill {
@@ -125,7 +130,7 @@ PanelWindow {
125130 text: Math.round(bar.batPct * 100) + "%"
126131 tint: bar.batLow ? Theme.err : Theme.fg
127132 underline: bar.batLow ? Theme.err : Theme.accent
128− onActivated: bar.togglePanel("sys")
133+ onActivated: bar.togglePanel("sys", bar.modelData)
129134 }
130135
131136 BarPill {
@@ -134,19 +139,19 @@ PanelWindow {
134139 tint: Notifs.dnd ? Theme.dim : Theme.fg
135140 onActivated: button => {
136141 if (button === Qt.RightButton) Notifs.dnd = !Notifs.dnd
137− else bar.togglePanel("notif")
142+ else bar.togglePanel("notif", bar.modelData)
138143 }
139144 }
140145
141146 BarPill {
142147 icon: Theme.iCal
143148 text: Qt.formatDateTime(clock.date, "dd MMM yyyy")
144− onActivated: bar.togglePanel("home")
149+ onActivated: bar.togglePanel("home", bar.modelData)
145150 }
146151 BarPill {
147152 icon: Theme.iClock
148153 text: Qt.formatDateTime(clock.date, "HH:mm")
149− onActivated: bar.togglePanel("home")
154+ onActivated: bar.togglePanel("home", bar.modelData)
150155 }
151156 }
152157 }
M .config/quickshell/aurora/ControlPanel.qml View
@@ -15,7 +15,7 @@ PanelWindow {
1515 anchors { top: true; right: true }
1616 margins { top: 6; right: 8 } // sits just under the bar (bar's exclusive zone is respected)
1717 implicitWidth: 440
18− implicitHeight: 610
18+ implicitHeight: 640
1919 exclusiveZone: 0
2020 color: "transparent"
2121 WlrLayershell.layer: WlrLayer.Overlay
M .config/quickshell/aurora/HomePage.qml View
@@ -6,7 +6,7 @@ import Quickshell.Services.Pipewire
66
77 ColumnLayout {
88 id: page
9− spacing: 12
9+ spacing: 10
1010 signal closeRequested()
1111
1212 property string armed: ""
@@ -95,6 +95,44 @@ ColumnLayout {
9595 }
9696 }
9797
98+ SectionLabel { text: "capture" }
99+
100+ RowLayout {
101+ Layout.fillWidth: true
102+ spacing: 6
103+ IconBtn { // where the shot goes: clipboard or ~/pix/ss
104+ Layout.fillWidth: true
105+ icon: Shot.save ? Theme.iSave : Theme.iClipboard
106+ label: Shot.save ? "save" : "copy"
107+ active: true
108+ onClicked: Shot.save = !Shot.save
109+ }
110+ IconBtn {
111+ Layout.fillWidth: true
112+ icon: Theme.iCrop
113+ label: "area"
114+ onClicked: { page.closeRequested(); Shot.take("region") }
115+ }
116+ IconBtn {
117+ Layout.fillWidth: true
118+ icon: Theme.iWindow
119+ label: "window"
120+ onClicked: { page.closeRequested(); Shot.take("window") }
121+ }
122+ IconBtn {
123+ Layout.fillWidth: true
124+ icon: Theme.iMonitor
125+ label: "screen"
126+ onClicked: { page.closeRequested(); Shot.take("output") }
127+ }
128+ IconBtn {
129+ Layout.fillWidth: true
130+ icon: Theme.iSearch
131+ label: "ocr"
132+ onClicked: { page.closeRequested(); Shot.take("ocr") }
133+ }
134+ }
135+
98136 Item { Layout.fillHeight: true }
99137
100138 SectionLabel { text: "session" }
A .config/quickshell/aurora/Shot.qml View
@@ -0,0 +1,33 @@
1+pragma Singleton
2+import QtQuick
3+import Quickshell
4+
5+// Screenshots, ported from your fuzzel script. Needs: hyprshot (+ grim, slurp), and for
6+// OCR: tesseract + wl-clipboard. Used by the Home tab and by `ipc call shell shot <mode>`.
7+Singleton {
8+ id: root
9+
10+ property bool save: false // false = clipboard, true = file
11+ property string dir: Quickshell.env("HOME") + "/pix/ss" // where "save" puts files
12+ property string ocrLang: "eng"
13+
14+ readonly property var names: ({ region: "selected", window: "window", output: "full" })
15+
16+ // mode: "region" | "window" | "output" | "ocr"
17+ function take(mode) {
18+ var script
19+ if (mode === "ocr") {
20+ script = 'slurp | grim -g - - | tesseract stdin stdout -l "$2" | wl-copy'
21+ + ' && notify-send -a screenshot "OCR" "text copied to clipboard"'
22+ } else if (names[mode] === undefined) {
23+ return
24+ } else if (!save) {
25+ script = 'hyprshot -m ' + mode + ' --clipboard-only'
26+ } else {
27+ script = 'mkdir -p "$1"; hyprshot -m ' + mode + ' -o "$1"'
28+ + ' -f "pic-' + names[mode] + '-$(date +%y-%m-%d_%H%M%S).png"'
29+ }
30+ // short delay so the panel is gone before the capture starts
31+ Quickshell.execDetached(["sh", "-c", "sleep 0.35; " + script, "sh", dir, ocrLang])
32+ }
33+}
M .config/quickshell/aurora/StatBar.qml View
@@ -12,7 +12,8 @@ RowLayout {
1212
1313 Text {
1414 text: r.label
15− Layout.preferredWidth: 64
15+ Layout.preferredWidth: 72
16+ elide: Text.ElideRight
1617 color: Theme.dim
1718 font.family: Theme.font
1819 font.pixelSize: Theme.fontSize - 1
M .config/quickshell/aurora/SysPage.qml View
@@ -32,7 +32,11 @@ Flickable {
3232 default: return "idle"
3333 }
3434 }
35− function g(v) { return v.toFixed(1) + "G" }
35+ // GiB -> "812G" / "1.8T"
36+ function size(v) { return v >= 1000 ? (v / 1024).toFixed(1) + "T" : (v >= 100 ? Math.round(v) : v.toFixed(1)) + "G" }
37+
38+ // pick up drives plugged in since the last poll
39+ onVisibleChanged: if (visible) SysStats.refreshDisks()
3640
3741 ColumnLayout {
3842 id: col
@@ -54,16 +58,21 @@ Flickable {
5458 Spark { Layout.fillWidth: true; values: SysStats.history }
5559 }
5660
57− SectionLabel { text: "memory & disk"; Layout.topMargin: 6 }
61+ SectionLabel { text: "memory & storage"; Layout.topMargin: 6 }
5862 StatBar {
5963 label: "memory"
6064 frac: SysStats.memTotal > 0 ? SysStats.memUsed / SysStats.memTotal : 0
61− valueText: flick.g(SysStats.memUsed) + " / " + flick.g(SysStats.memTotal)
65+ valueText: flick.size(SysStats.memUsed) + " / " + flick.size(SysStats.memTotal)
6266 }
63− StatBar {
64− label: "disk /"
65− frac: SysStats.diskTotal > 0 ? SysStats.diskUsed / SysStats.diskTotal : 0
66− valueText: Math.round(SysStats.diskUsed) + "G / " + Math.round(SysStats.diskTotal) + "G"
67+ Repeater {
68+ model: SysStats.disks
69+ delegate: StatBar {
70+ required property var modelData
71+ label: modelData.label
72+ frac: modelData.used / modelData.size
73+ fill: frac > 0.9 ? Theme.err : Theme.accent
74+ valueText: flick.size(modelData.used) + " / " + flick.size(modelData.size)
75+ }
6776 }
6877
6978 ColumnLayout {
@@ -99,39 +108,39 @@ Flickable {
99108 }
100109 }
101110
102− // ColumnLayout {
103− // visible: PowerProfiles.hasPerformanceProfile !== undefined
104− // Layout.fillWidth: true
105− // Layout.topMargin: 6
106− // spacing: 8
107− // SectionLabel { text: "power profile" }
108− // RowLayout {
109− // Layout.fillWidth: true
110− // spacing: 6
111− // IconBtn {
112− // Layout.fillWidth: true
113− // icon: Theme.iLeaf
114− // label: "saver"
115− // active: PowerProfiles.profile === PowerProfile.PowerSaver
116− // onClicked: PowerProfiles.profile = PowerProfile.PowerSaver
117− // }
118− // IconBtn {
119− // Layout.fillWidth: true
120− // icon: Theme.iBalance
121− // label: "balanced"
122− // active: PowerProfiles.profile === PowerProfile.Balanced
123− // onClicked: PowerProfiles.profile = PowerProfile.Balanced
124− // }
125− // IconBtn {
126− // Layout.fillWidth: true
127− // visible: PowerProfiles.hasPerformanceProfile
128− // icon: Theme.iSpeed
129− // label: "perf"
130− // active: PowerProfiles.profile === PowerProfile.Performance
131− // onClicked: PowerProfiles.profile = PowerProfile.Performance
132− // }
133− // }
134− // }
111+ ColumnLayout {
112+ visible: PowerProfiles.hasPerformanceProfile !== undefined
113+ Layout.fillWidth: true
114+ Layout.topMargin: 6
115+ spacing: 8
116+ SectionLabel { text: "power profile" }
117+ RowLayout {
118+ Layout.fillWidth: true
119+ spacing: 6
120+ IconBtn {
121+ Layout.fillWidth: true
122+ icon: Theme.iLeaf
123+ label: "saver"
124+ active: PowerProfiles.profile === PowerProfile.PowerSaver
125+ onClicked: PowerProfiles.profile = PowerProfile.PowerSaver
126+ }
127+ IconBtn {
128+ Layout.fillWidth: true
129+ icon: Theme.iBalance
130+ label: "balanced"
131+ active: PowerProfiles.profile === PowerProfile.Balanced
132+ onClicked: PowerProfiles.profile = PowerProfile.Balanced
133+ }
134+ IconBtn {
135+ Layout.fillWidth: true
136+ visible: PowerProfiles.hasPerformanceProfile
137+ icon: Theme.iSpeed
138+ label: "perf"
139+ active: PowerProfiles.profile === PowerProfile.Performance
140+ onClicked: PowerProfiles.profile = PowerProfile.Performance
141+ }
142+ }
143+ }
135144
136145 ColumnLayout {
137146 visible: Gpu.mode !== ""
M .config/quickshell/aurora/SysStats.qml View
@@ -9,8 +9,7 @@ Singleton {
99 property real cpu: 0 // 0..1
1010 property real memUsed: 0 // GiB
1111 property real memTotal: 0
12− property real diskUsed: 0
13− property real diskTotal: 0
12+ property var disks: [] // [{source, target, label, used, size}] in GiB, "/" first
1413 property real uptime: 0 // seconds
1514 property var history: [] // last 40 cpu samples
1615 property real prevIdle: -1
@@ -52,18 +51,42 @@ Singleton {
5251
5352 Timer { interval: 1000; running: true; repeat: true; triggeredOnStart: true; onTriggered: root.sample() }
5453
54+ // every mounted *local* block device (internal nvme, external drives, usb sticks...).
55+ // pseudo filesystems and network mounts are skipped; one entry per physical source
56+ // (so btrfs subvolumes / bind mounts of the same partition don't repeat).
57+ function refreshDisks() { dfProc.running = true }
58+
59+ function parseDf(text) {
60+ var lines = text.split("\n"), seen = {}, out = []
61+ for (var i = 1; i < lines.length; i++) {
62+ var m = lines[i].match(/^(\S+)\s+(\d+)\s+(\d+)\s+(.*)$/)
63+ if (!m) continue
64+ var src = m[1], target = m[4].trim()
65+ if (src.indexOf("/dev/") !== 0 || src.indexOf("/dev/loop") === 0) continue
66+ if (target.indexOf("/boot") === 0 || target.indexOf("/efi") === 0 || target.indexOf("/var/lib") === 0) continue
67+ if (seen[src]) continue
68+ seen[src] = true
69+ var size = parseFloat(m[3])
70+ if (size <= 0) continue
71+ var parts = target.split("/").filter(function (x) { return x !== "" })
72+ out.push({
73+ source: src, target: target,
74+ label: parts.length ? parts[parts.length - 1] : "/",
75+ used: parseFloat(m[2]) / 1073741824, size: size / 1073741824
76+ })
77+ }
78+ out.sort(function (a, b) {
79+ if (a.target === "/") return -1
80+ if (b.target === "/") return 1
81+ return a.target.localeCompare(b.target)
82+ })
83+ return out
84+ }
85+
5586 Process {
5687 id: dfProc
57− command: ["df", "-B1", "--output=used,size", "/"]
58− stdout: StdioCollector {
59− onStreamFinished: {
60− var l = text.trim().split("\n")
61− if (l.length < 2) return
62− var p = l[1].trim().split(/\s+/)
63− root.diskUsed = parseFloat(p[0]) / 1073741824
64− root.diskTotal = parseFloat(p[1]) / 1073741824
65− }
66− }
88+ command: ["df", "-l", "-B1", "--output=source,used,size,target"]
89+ stdout: StdioCollector { onStreamFinished: root.disks = root.parseDf(text) }
6790 }
68− Timer { interval: 30000; running: true; repeat: true; triggeredOnStart: true; onTriggered: dfProc.running = true }
91+ Timer { interval: 30000; running: true; repeat: true; triggeredOnStart: true; onTriggered: root.refreshDisks() }
6992 }
M .config/quickshell/aurora/Theme.qml View
@@ -10,11 +10,12 @@ Singleton {
1010 // ---- knobs --------------------------------------------------------
1111 property string font: "JetBrainsMono Nerd Font" // any Nerd Font mono
1212 property int fontSize: 12
13+ property int wsFontSize: 11 // workspace icons only; everything else uses fontSize
1314 property real panelAlpha: 0.88
1415 // your waybar uses @color14 for underlines/active workspace, so that is the default.
1516 // set -1 to auto-pick the most vivid of color1..color6 instead
1617 property int accentIndex: 14
17− property int barHeight: 18
18+ property int barHeight: 22
1819 property real barAlpha: 1.0
1920
2021 readonly property string walFile: Quickshell.env("HOME") + "/.cache/wal/colors.json"
@@ -127,6 +128,12 @@ Singleton {
127128 readonly property string iSearch: cp(0xF0349)
128129
129130 readonly property string iHeadset: cp(0xF02CE)
131+ readonly property string iCamera: cp(0xF0100)
132+ readonly property string iCrop: cp(0xF019E)
133+ readonly property string iWindow: cp(0xF05B0)
134+ readonly property string iMonitor: cp(0xF0379)
135+ readonly property string iSave: cp(0xF0193)
136+ readonly property string iClipboard: cp(0xF0147)
130137 readonly property string iBell: cp(0xF009A)
131138 readonly property string iBellOff: cp(0xF009B)
132139 readonly property string iChip: cp(0xF061A)
@@ -152,7 +159,7 @@ Singleton {
152159 String.fromCharCode(0xf07c), String.fromCharCode(0xf120), String.fromCharCode(0xf0c3),
153160 String.fromCharCode(0xf03e)
154161 ]
155− function wsIcon(id) { return (id >= 1 && id <= 20) ? wsIcons[(id - 1) % 10] : String(id) }
162+ function wsIcon(id) { return id >= 1 ? wsIcons[(id - 1) % 10] : String(id) }
156163
157164 readonly property var batLevels: [0xF007A, 0xF007B, 0xF007C, 0xF007D, 0xF007E,
158165 0xF007F, 0xF0080, 0xF0081, 0xF0082, 0xF0079]
M .config/quickshell/aurora/Tray.qml View
@@ -4,7 +4,8 @@ import Quickshell.Services.SystemTray
44
55 Item {
66 id: tray
7− visible: SystemTray.items.values.length > 0
7+ property bool show: true
8+ visible: show && SystemTray.items.values.length > 0
89 implicitWidth: row.implicitWidth + 24
910 implicitHeight: parent ? parent.height : Theme.barHeight
1011
M .config/quickshell/aurora/Workspaces.qml View
@@ -1,24 +1,35 @@
11 import QtQuick
22 import Quickshell.Hyprland
33
4−// Workspaces 1-10 always shown (like a persistent waybar setup), extras appear when used.
4+// Per-monitor workspaces, dwm / split-monitor-workspaces style.
5+// Each monitor owns a block of 10 ids (monitor 1: 1-10, monitor 2: 11-20, ...).
6+// The block is derived from the monitor's own active workspace, so it works with or
7+// without the plugin. Icons repeat every 10, like your waybar format-icons.
58 Row {
69 id: ws
710 spacing: 0
811
12+ property var monitor: null // HyprlandMonitor of the bar this sits in
13+
14+ readonly property var activeWs: monitor ? monitor.activeWorkspace : null
15+ readonly property int offset: activeWs && activeWs.id > 0 ? Math.floor((activeWs.id - 1) / 10) * 10 : 0
16+ readonly property bool monFocused: monitor ? monitor.focused : true
17+ readonly property var list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (i) { return offset + i })
18+
919 function find(id) {
1020 var l = Hyprland.workspaces.values
1121 for (var i = 0; i < l.length; i++) if (l[i].id === id) return l[i]
1222 return null
1323 }
14− function ids() {
15− var out = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
16− var l = Hyprland.workspaces.values
17− for (var i = 0; i < l.length; i++) if (l[i].id > 10) out.push(l[i].id)
18− out.sort(function (a, b) { return a - b })
19− return out
24+
25+ // scroll: next/previous *existing* workspace inside this monitor's block (like e+1 / e-1)
26+ function step(dir) {
27+ var cur = activeWs ? activeWs.id : offset + 1
28+ for (var id = cur + dir; id >= offset + 1 && id <= offset + 10; id += dir) {
29+ var o = find(id)
30+ if (o !== null && o.toplevels.values.length > 0) { Hypr.workspace(id); return }
31+ }
2032 }
21− readonly property var list: ids()
2233
2334 Repeater {
2435 model: ws.list
@@ -26,33 +37,35 @@ Row {
2637 id: b
2738 required property var modelData
2839 readonly property var obj: ws.find(modelData)
29− readonly property bool active: Hyprland.focusedWorkspace !== null && Hyprland.focusedWorkspace.id === modelData
40+ readonly property bool active: ws.activeWs !== null && ws.activeWs.id === modelData
3041 readonly property bool occupied: obj !== null && obj.toplevels.values.length > 0
3142 readonly property bool urgent: obj !== null && obj.urgent
32− readonly property bool lit: active || m.containsMouse
43+ // active on the focused monitor = filled; active on another monitor = accent text + thick underline
44+ readonly property bool lit: (active && ws.monFocused) || m.containsMouse
45+ readonly property bool dimActive: active && !ws.monFocused && !m.containsMouse
3346
34− width: 32
47+ width: 30
3548 height: ws.height
3649
3750 Rectangle {
3851 anchors.fill: parent
3952 color: b.lit ? Theme.accent : "transparent"
4053 }
41− Rectangle { // occupied marker, like the inset underline in your css
42− visible: b.occupied && !b.lit
54+ Rectangle { // occupied marker (1px), or active-on-unfocused-monitor marker (2px)
55+ visible: (b.occupied && !b.lit) || b.dimActive
4356 anchors.left: parent.left
4457 anchors.right: parent.right
4558 anchors.bottom: parent.bottom
46− height: 1
47− color: Theme.cursorC
59+ height: b.dimActive ? 2 : 1
60+ color: b.dimActive ? Theme.accent : Theme.cursorC
4861 }
4962 Text {
5063 anchors.centerIn: parent
5164 text: Theme.wsIcon(b.modelData)
52− color: b.lit ? Theme.bg : (b.urgent ? Theme.err : Theme.fg)
53− opacity: (b.occupied || b.lit || b.urgent) ? 1 : 0.4
65+ color: b.lit ? Theme.bg : (b.dimActive ? Theme.accent : (b.urgent ? Theme.err : Theme.fg))
66+ opacity: (b.occupied || b.active || b.lit || b.urgent) ? 1 : 0.4
5467 font.family: Theme.font
55− font.pixelSize: Theme.fontSize
68+ font.pixelSize: Theme.wsFontSize
5669 }
5770 MouseArea {
5871 id: m
@@ -60,7 +73,7 @@ Row {
6073 hoverEnabled: true
6174 cursorShape: Qt.PointingHandCursor
6275 onClicked: Hypr.workspace(b.modelData)
63− onWheel: w => Hypr.workspace(w.angleDelta.y > 0 ? "e-1" : "e+1")
76+ onWheel: w => ws.step(w.angleDelta.y > 0 ? -1 : 1)
6477 }
6578 }
6679 }
M .config/quickshell/aurora/shell.qml View
@@ -1,6 +1,7 @@
11 import QtQuick
22 import Quickshell
33 import Quickshell.Io
4+import Quickshell.Hyprland
45 import Quickshell.Services.Pipewire
56
67 // aurora: bar + control panel + notifications, themed from pywal16.
@@ -11,26 +12,41 @@ ShellRoot {
1112 property int tab: 0
1213 property double closedAt: 0
1314 property string lastTab: ""
15+ property string lastScreen: ""
16+ property var panelScreen: focusedScreen()
17+ readonly property var focusedScr: focusedScreen()
1418 readonly property var tabNames: ["home", "audio", "net", "bt", "sys", "notif"]
1519
20+ // screen of the monitor that currently has focus (falls back to the first screen)
21+ function focusedScreen() {
22+ var m = Hyprland.focusedMonitor
23+ var l = Quickshell.screens
24+ if (m) for (var i = 0; i < l.length; i++) if (l[i].name === m.name) return l[i]
25+ return l.length ? l[0] : null
26+ }
1627 function close() {
1728 if (!opened) return
1829 opened = false
1930 closedAt = Date.now()
2031 lastTab = tabNames[tab]
32+ lastScreen = panelScreen ? panelScreen.name : ""
2133 }
2234 function openTab(name) {
2335 var i = tabNames.indexOf(name)
2436 if (i >= 0) tab = i
37+ panelScreen = focusedScreen()
2538 opened = true
2639 }
2740 // bar buttons: click once to open that tab, again to close.
2841 // (the focus grab already closed the panel on the press, so guard against instant re-open)
29− function togglePanel(name) {
42+ // scr = the screen whose bar was clicked, so the panel opens on that monitor
43+ function togglePanel(name, scr) {
3044 var i = tabNames.indexOf(name)
31− if (opened && tab === i) { close(); return }
32− if (!opened && lastTab === name && Date.now() - closedAt < 250) return
45+ var sn = scr ? scr.name : ""
46+ if (opened && tab === i && panelScreen && panelScreen.name === sn) { close(); return }
47+ if (!opened && lastTab === name && lastScreen === sn && Date.now() - closedAt < 250) return
3348 tab = i
49+ panelScreen = scr
3450 opened = true
3551 }
3652
@@ -39,24 +55,27 @@ ShellRoot {
3955
4056 Variants {
4157 model: Quickshell.screens
42− Bar { onTogglePanel: name => root.togglePanel(name) }
58+ Bar { onTogglePanel: (name, scr) => root.togglePanel(name, scr) }
4359 }
4460
4561 ControlPanel {
62+ screen: root.panelScreen
4663 opened: root.opened
4764 tab: root.tab
4865 onTabRequested: i => root.tab = i
4966 onCloseRequested: root.close()
5067 }
5168
52− NotifPopups { suppressed: root.opened }
69+ NotifPopups { screen: root.focusedScr; suppressed: root.opened }
5370
5471 IpcHandler {
5572 target: "shell"
56− function toggle(): void { if (root.opened) root.close(); else root.opened = true }
57− function open(): void { root.opened = true }
73+ function toggle(): void { if (root.opened) root.close(); else root.openTab(root.tabNames[root.tab]) }
74+ function open(): void { root.openTab(root.tabNames[root.tab]) }
5875 function close(): void { root.close() }
5976 function show(name: string): void { root.openTab(name) }
6077 function dnd(): void { Notifs.dnd = !Notifs.dnd }
78+ function shot(mode: string): void { root.close(); Shot.take(mode) } // region | window | output | ocr
79+ function shotsave(): void { Shot.save = !Shot.save }
6180 }
6281 }