Krolyxon
Commit 8ab0f85

Branches

Refs →

aurora.configquickshellauroraBar.qml

293 lines · 12.7 KB · QML

1import QtQuick
2import Quickshell
3import Quickshell.Wayland
4import Quickshell.Bluetooth
5import Quickshell.Hyprland
6import Quickshell.Services.Pipewire
7import Quickshell.Services.UPower
8
9// The bar: a floating pill at the top of every monitor.
10// Left: the mark (a dotted ring: click = runner; its centre turns the live colour while something records or listens,
11// the ring lights up in a wave on a new notification), this monitor's workspaces as dots, the cat.
12// Centre: the active window (focused monitor only). Right: tray, recording, privacy, caffeine/airplane, headset,
13// volume, net + bt, battery, clock (click = calendar). Clicks open the same panel tabs as always.
14// After glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), bar/Bar.qml.
15PanelWindow {
16 id: bar
17 required property var modelData
18 screen: modelData
19 signal togglePanel(string name, var scr)
20 signal toggleOverlay(string name, var scr)
21
22 anchors { top: true; left: true; right: true }
23 implicitHeight: Theme.barHeight // the pill + the gap above it; also the exclusive zone
24 color: "transparent"
25 mask: Region { item: pill }
26 WlrLayershell.namespace: "aurora-bar"
27
28 SystemClock { id: clock; precision: SystemClock.Minutes }
29
30 // caffeine: one inhibitor is enough, keep it on the first screen's bar
31 IdleInhibitor {
32 window: bar
33 enabled: Toggles.caffeine && bar.modelData === Quickshell.screens[0]
34 }
35
36 // ---- state ----
37 readonly property var audio: Pipewire.defaultAudioSink ? Pipewire.defaultAudioSink.audio : null
38 readonly property var mic: Pipewire.defaultAudioSource ? Pipewire.defaultAudioSource.audio : null
39 readonly property var bat: UPower.displayDevice
40 readonly property bool hasBat: bat !== null && bat.isPresent
41 readonly property real batPct: hasBat ? (bat.percentage > 1 ? bat.percentage / 100 : bat.percentage) : 0
42 readonly property bool charging: hasBat && bat.state === UPowerDeviceState.Charging
43 readonly property bool batLow: hasBat && batPct <= Settings.batLow / 100 && !charging
44 readonly property var adapter: Bluetooth.defaultAdapter
45
46 // which Hyprland monitor this bar belongs to, and whether it has focus
47 readonly property var hmon: Hyprland.monitorFor(modelData)
48 readonly property bool monFocused: hmon !== null && hmon.focused
49
50 function findHeadset() {
51 var l = Bluetooth.devices.values
52 for (var i = 0; i < l.length; i++) {
53 var d = l[i]
54 if (!d.connected || !d.batteryAvailable) continue
55 var ic = String(d.icon)
56 if (ic.indexOf("head") >= 0 || ic.indexOf("audio") >= 0) return d
57 }
58 return null
59 }
60 function btConnected() {
61 var l = Bluetooth.devices.values
62 for (var i = 0; i < l.length; i++) if (l[i].connected) return true
63 return false
64 }
65 readonly property var headset: findHeadset()
66 readonly property bool btOn: adapter !== null && adapter.enabled
67 readonly property bool btLink: btOn && btConnected()
68 // something records or listens: the mark's centre goes live
69 readonly property bool live: Rec.busy || Privacy.mic || Privacy.cam || Privacy.screen
70
71 // a new toast: the mark's ring lights up for 3s (error colour if critical)
72 property bool flash: false
73 property bool flashCrit: false
74 Timer { id: flashT; interval: 3000; onTriggered: bar.flash = false }
75 Connections {
76 target: Notifs
77 function onArrived(n, critical) { bar.flashCrit = critical; bar.flash = true; flashT.restart() }
78 }
79
80 Rectangle {
81 id: pill
82 x: Theme.barGap
83 y: Theme.barGap
84 width: parent.width - 2 * Theme.barGap
85 height: Theme.barSize
86 radius: height / 2
87 color: Qt.rgba(Theme.s1.r, Theme.s1.g, Theme.s1.b, Theme.barAlpha)
88 border.width: 1
89 border.color: Theme.s2
90
91 // ---------------- left ----------------
92 Row {
93 anchors.left: parent.left
94 anchors.leftMargin: Math.round(Theme.barSize * 0.25)
95 height: parent.height
96 spacing: 14
97
98 Item {
99 id: mark
100 readonly property real size: Math.round(Theme.barSize * 0.55)
101 width: size
102 height: size
103 anchors.verticalCenter: parent.verticalCenter
104 Ring {
105 anchors.fill: parent
106 anchors.margins: 1.5
107 n: 16
108 dot: 2.6
109 on: bar.flash ? (bar.flashCrit ? Theme.err : Theme.fill) : Theme.fg
110 off: Theme.s3
111 }
112 Rectangle {
113 anchors.centerIn: parent
114 width: Math.round(mark.size * 0.32)
115 height: width
116 radius: width / 2
117 color: bar.live ? Theme.live : Theme.fg
118 }
119 MouseArea {
120 anchors.fill: parent
121 anchors.margins: -5
122 cursorShape: Qt.PointingHandCursor
123 onClicked: bar.toggleOverlay("runner", bar.modelData)
124 }
125 }
126 Workspaces { height: parent.height; monitor: bar.hmon }
127 CatLoop { height: parent.height }
128 }
129
130 // ---------------- center ----------------
131 Lb {
132 anchors.centerIn: parent
133 width: Math.min(implicitWidth, parent.width * 0.32)
134 horizontalAlignment: Text.AlignHCenter
135 elide: Text.ElideRight
136 readonly property var active: ToplevelManager.activeToplevel
137 readonly property string app: active && active.appId ? String(active.appId).split(".").pop() : ""
138 visible: bar.monFocused && active !== null && active.title !== "" // the active window is global: focused monitor only
139 text: active ? (app !== "" ? app + " · " + active.title : active.title) : ""
140 px: 11
141 color: Theme.fgDim
142 }
143
144 // ---------------- right ----------------
145 Row {
146 anchors.right: parent.right
147 anchors.rightMargin: 6
148 height: parent.height
149 spacing: 0
150
151 Tray { show: bar.monFocused }
152
153 BarPill { // click to stop recording / cancel the area selection
154 visible: Rec.busy
155 icon: Theme.iStop
156 text: Rec.running ? "rec " + Theme.fmtClock(Rec.elapsed) : "select area"
157 tint: Theme.live
158 onActivated: Rec.stop()
159 }
160
161 // privacy: global, so only on the focused monitor's bar. Click the mic one to mute the mic.
162 BarPill {
163 visible: bar.monFocused && Privacy.mic
164 icon: bar.mic && bar.mic.muted ? Theme.iMicOff : Theme.iMic
165 text: Privacy.micApps.join(", ")
166 tint: bar.mic && bar.mic.muted ? Theme.grey : Theme.live
167 onActivated: if (bar.mic) bar.mic.muted = !bar.mic.muted
168 }
169 BarPill {
170 visible: bar.monFocused && Privacy.cam
171 icon: Theme.iWebcam
172 text: Privacy.camApps.join(", ")
173 tint: Theme.live
174 }
175 BarPill {
176 visible: bar.monFocused && Privacy.screen
177 icon: Theme.iCast
178 text: Privacy.shareApps.join(", ")
179 tint: Theme.live
180 }
181
182 BarPill {
183 visible: Toggles.caffeine || Toggles.airplane
184 icon: (Toggles.caffeine ? Theme.iCoffee : "") + (Toggles.caffeine && Toggles.airplane ? " " : "")
185 + (Toggles.airplane ? Theme.iPlane : "")
186 onActivated: bar.togglePanel("home", bar.modelData)
187 }
188
189 BarPill {
190 visible: bar.headset !== null
191 icon: Theme.iHeadphones
192 text: bar.headset ? Math.round(bar.headset.battery * 100) + "%" : ""
193 onActivated: bar.togglePanel("bt", bar.modelData)
194 }
195
196 BarPill {
197 visible: bar.audio !== null
198 icon: Theme.volIcon(bar.audio ? bar.audio.volume : 0, bar.audio ? bar.audio.muted : true)
199 text: (bar.audio ? Math.round(bar.audio.volume * 100) : 0) + "%"
200 tint: bar.audio && bar.audio.muted ? Theme.grey : Theme.fg
201 icon2: bar.mic && bar.mic.muted ? Theme.iMicOff : ""
202 tint2: Theme.grey
203 onActivated: button => {
204 if (button === Qt.RightButton && bar.audio) bar.audio.muted = !bar.audio.muted
205 else if (button === Qt.MiddleButton && bar.mic) bar.mic.muted = !bar.mic.muted
206 else bar.togglePanel("audio", bar.modelData)
207 }
208 onActivated2: button => { if (bar.mic) bar.mic.muted = !bar.mic.muted }
209 onScrolled: dir => {
210 if (bar.audio) bar.audio.volume = Math.max(0, Math.min(1, bar.audio.volume + dir * 0.02))
211 }
212 }
213
214 // net + bt share one block: click the left icon for the net tab, the right one for bt
215 BarPill {
216 icon: Net.wired && !(Net.wifiEnabled && Net.connected !== "") ? Net.wiredIcon
217 : (Net.wifiEnabled ? Theme.wifiIcon(Net.connected !== "" ? Net.activeSignal : 0) : Theme.iWifiOff)
218 tint: Net.portal ? Theme.live : ((Net.wired || (Net.wifiEnabled && Net.connected !== "")) ? Theme.fg : Theme.grey)
219 icon2: bar.adapter === null ? "" : (!bar.btOn ? Theme.iBtOff : (bar.btLink ? Theme.iBtConn : Theme.iBt))
220 tint2: bar.btLink ? Theme.fg : Theme.grey
221 onActivated: bar.togglePanel("net", bar.modelData)
222 onActivated2: bar.togglePanel("bt", bar.modelData)
223 }
224
225 // battery: a capsule that fills up, the error colour when low, a bolt + breathing fill while charging
226 BarPill {
227 id: batt
228 visible: bar.hasBat
229 icon: bar.charging ? Theme.iBolt : ""
230 onActivated: bar.togglePanel("sys", bar.modelData)
231 // breathing at 20 steps a second: an Animation would redraw the bar on every vsync while charging
232 property real phase: 0
233 readonly property real pulse: bar.charging ? 0.675 + 0.325 * Math.cos(phase * Math.PI / 1.1) : 1
234 Timer {
235 running: bar.charging && bar.visible
236 interval: 50
237 repeat: true
238 onTriggered: batt.phase = (batt.phase + 0.05) % 2.2
239 onRunningChanged: batt.phase = 0
240 }
241 Rectangle {
242 anchors.verticalCenter: parent.verticalCenter
243 width: 26
244 height: 13
245 radius: height / 2
246 color: "transparent"
247 border.width: 1.5
248 border.color: bar.batLow ? Theme.err : Theme.grey
249 Rectangle {
250 x: 3
251 anchors.verticalCenter: parent.verticalCenter
252 width: Math.max(3, (parent.width - 6) * bar.batPct)
253 height: parent.height - 6
254 radius: height / 2
255 color: bar.batLow ? Theme.err : Theme.fill
256 opacity: batt.pulse
257 }
258 }
259 Lb {
260 anchors.verticalCenter: parent.verticalCenter
261 text: Math.round(bar.batPct * 100)
262 px: 11
263 color: bar.batLow ? Theme.err : Theme.fg
264 }
265 }
266
267 // clock: Doto from bar size 40 up (Doto is unreadable smaller), Space Mono below that
268 BarPill {
269 onActivated: bar.toggleOverlay("cal", bar.modelData)
270 Num {
271 visible: Theme.barSize >= 40
272 anchors.verticalCenter: parent.verticalCenter
273 text: Theme.hh(clock.date) + ":" + Qt.formatDateTime(clock.date, "mm")
274 font.pixelSize: 26
275 }
276 Text {
277 visible: Theme.barSize < 40
278 anchors.verticalCenter: parent.verticalCenter
279 text: Theme.hh(clock.date) + ":" + Qt.formatDateTime(clock.date, "mm")
280 font.family: Theme.mono
281 font.pixelSize: 14
282 font.bold: true
283 color: Theme.fg
284 }
285 Lb {
286 anchors.verticalCenter: parent.verticalCenter
287 text: Qt.locale("en_US").toString(clock.date, "ddd d")
288 px: 10
289 }
290 }
291 }
292 }
293}