me like nix
1import Quickshell
2import Quickshell.Wayland
3import Quickshell.Io
4import Quickshell.Services.SystemTray
5import QtQuick
6import QtQuick.Layouts
7
8ShellRoot {
9 Variants {
10 model: Quickshell.screens
11
12 PanelWindow {
13 id: root
14 screen: modelData
15
16 // Catppuccin Frappe palette
17 readonly property color colBase: "#303446"
18 readonly property color colMantle: "#292c3c"
19 readonly property color colSurface0: "#414559"
20 readonly property color colText: "#c6d0f5"
21 readonly property color colSubtext0: "#a5adce"
22 readonly property color colOverlay0: "#737994"
23 readonly property color colBlue: "#8caaee"
24 readonly property color colGreen: "#a6d189"
25 readonly property color colPeach: "#ef9f76"
26 readonly property color colMauve: "#ca9ee6"
27 readonly property color colRed: "#e78284"
28 readonly property color colYellow: "#e5c890"
29 readonly property color colMaroon: "#ea999c"
30 readonly property color colLavender: "#babbf1"
31 readonly property color colSky: "#99d1db"
32 readonly property color colSapphire: "#85c1dc"
33
34 readonly property string fontFamily: "BerkeleyMono Nerd Font"
35 readonly property int fontSize: 14
36
37 // Nerd Font icons (using Unicode escapes)
38 // Volume
39 readonly property string iconVolHigh: "\uf028"
40 readonly property string iconVolLow: "\uf027"
41 readonly property string iconVolMute: "\uf6a9"
42 // Network
43 readonly property string iconWifi: "\uf1eb"
44 readonly property string iconEthernet: "\udb80\ude00"
45 // Power profiles
46 readonly property string iconBolt: "\uf0e7"
47 readonly property string iconBalance: "\uf24e"
48 readonly property string iconLeaf: "\uf06c"
49 // CPU
50 readonly property string iconCpu: "\uf2db"
51 // Memory
52 readonly property string iconMem: "\uefc5"
53 // Temperature
54 readonly property string iconTempLow: "\uf2cb"
55 readonly property string iconTempMed: "\uf2c9"
56 readonly property string iconTempHigh: "\uf2c7"
57 // Backlight
58 readonly property string iconSun: "\uf185"
59 // Battery
60 readonly property string iconBatEmpty: "\uf244"
61 readonly property string iconBatQuarter: "\uf243"
62 readonly property string iconBatHalf: "\uf242"
63 readonly property string iconBatThreeQ: "\uf241"
64 readonly property string iconBatFull: "\uf240"
65 readonly property string iconBatCharge: "\uf0e7"
66 // Power
67 readonly property string iconPower: "\u23fb"
68 // GitHub
69 readonly property string iconGithub: "\uf09b"
70
71 // System data
72 property int cpuUsage: 0
73 property int memUsage: 0
74 property int temperature: 0
75 property var lastCpuIdle: 0
76 property var lastCpuTotal: 0
77 property string networkName: ""
78 property int networkStrength: 0
79 property bool networkConnected: false
80 property bool networkIsEthernet: false
81 property int volume: 0
82 property bool volumeMuted: false
83 property int brightness: 0
84 property int batteryPercent: 0
85 property string batteryStatus: ""
86 property string powerProfile: ""
87 property string powerBackend: "" // "system76" or "ppd"
88 property string weather: ""
89 property int ghNotifications: 0
90 property int ghLastCount: 0
91
92 anchors {
93 top: true
94 left: true
95 right: true
96 }
97 margins.top: 0
98 margins.bottom: 0
99 margins.left: 0
100 margins.right: 0
101 implicitHeight: 30
102 color: Qt.rgba(0.188, 0.204, 0.275, 0.85)
103
104 // CPU process
105 Process {
106 id: cpuProc
107 command: ["sh", "-c", "head -1 /proc/stat"]
108 stdout: SplitParser {
109 onRead: data => {
110 if (!data) return
111 var p = data.trim().split(/\s+/)
112 var idle = parseInt(p[4]) + parseInt(p[5])
113 var total = p.slice(1, 8).reduce((a, b) => a + parseInt(b), 0)
114 if (root.lastCpuTotal > 0) {
115 root.cpuUsage = Math.round(100 * (1 - (idle - root.lastCpuIdle) / (total - root.lastCpuTotal)))
116 }
117 root.lastCpuTotal = total
118 root.lastCpuIdle = idle
119 }
120 }
121 Component.onCompleted: running = true
122 }
123
124 // Memory process
125 Process {
126 id: memProc
127 command: ["sh", "-c", "free | grep Mem"]
128 stdout: SplitParser {
129 onRead: data => {
130 if (!data) return
131 var parts = data.trim().split(/\s+/)
132 var total = parseInt(parts[1]) || 1
133 var used = parseInt(parts[2]) || 0
134 root.memUsage = Math.round(100 * used / total)
135 }
136 }
137 Component.onCompleted: running = true
138 }
139
140 // Temperature
141 Process {
142 id: tempProc
143 command: ["sh", "-c", "cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0"]
144 stdout: SplitParser {
145 onRead: data => {
146 if (!data) return
147 root.temperature = Math.round(parseInt(data.trim()) / 1000)
148 }
149 }
150 Component.onCompleted: running = true
151 }
152
153 // Network - check ethernet first, then wifi
154 Process {
155 id: netProc
156 command: ["sh", "-c", "eth=$(nmcli -t -f TYPE,STATE,CONNECTION dev | grep '^ethernet:connected:' | head -1); if [ -n \"$eth\" ]; then echo \"ethernet:$(echo $eth | cut -d: -f3)\"; else nmcli -t -f ACTIVE,SSID,SIGNAL dev wifi | grep '^yes' | head -1; fi"]
157 stdout: SplitParser {
158 onRead: data => {
159 if (!data || data.trim() === "") {
160 root.networkConnected = false
161 root.networkIsEthernet = false
162 root.networkName = "Disconnected"
163 root.networkStrength = 0
164 return
165 }
166 var trimmed = data.trim()
167 if (trimmed.startsWith("ethernet:")) {
168 root.networkConnected = true
169 root.networkIsEthernet = true
170 root.networkName = trimmed.split(":")[1] || "Ethernet"
171 root.networkStrength = 100
172 } else {
173 var parts = trimmed.split(":")
174 root.networkConnected = true
175 root.networkIsEthernet = false
176 root.networkName = parts[1] || ""
177 root.networkStrength = parseInt(parts[2]) || 0
178 }
179 }
180 }
181 Component.onCompleted: running = true
182 }
183
184 // Volume
185 Process {
186 id: volProc
187 command: ["sh", "-c", "wpctl get-volume @DEFAULT_AUDIO_SINK@"]
188 stdout: SplitParser {
189 onRead: data => {
190 if (!data) return
191 root.volumeMuted = data.indexOf("[MUTED]") !== -1
192 var match = data.match(/Volume:\s+([\d.]+)/)
193 if (match) {
194 root.volume = Math.round(parseFloat(match[1]) * 100)
195 }
196 }
197 }
198 Component.onCompleted: running = true
199 }
200
201 // Brightness
202 Process {
203 id: brightProc
204 command: ["sh", "-c", "brightnessctl -m | cut -d, -f4 | tr -d '%'"]
205 stdout: SplitParser {
206 onRead: data => {
207 if (!data) return
208 root.brightness = parseInt(data.trim()) || 0
209 }
210 }
211 Component.onCompleted: running = true
212 }
213
214 // Battery
215 Process {
216 id: batProc
217 command: ["sh", "-c", "cat /sys/class/power_supply/BAT1/capacity 2>/dev/null || echo 0"]
218 stdout: SplitParser {
219 onRead: data => {
220 if (!data) return
221 root.batteryPercent = parseInt(data.trim()) || 0
222 }
223 }
224 Component.onCompleted: running = true
225 }
226
227 Process {
228 id: batStatusProc
229 command: ["sh", "-c", "cat /sys/class/power_supply/BAT1/status 2>/dev/null || echo Unknown"]
230 stdout: SplitParser {
231 onRead: data => {
232 if (!data) return
233 root.batteryStatus = data.trim()
234 }
235 }
236 Component.onCompleted: running = true
237 }
238
239 // Detect power backend once at startup
240 Process {
241 id: powerBackendProc
242 command: ["sh", "-c", "command -v system76-power >/dev/null 2>&1 && echo system76 || echo ppd"]
243 stdout: SplitParser {
244 onRead: data => {
245 if (!data) return
246 root.powerBackend = data.trim()
247 }
248 }
249 Component.onCompleted: running = true
250 }
251
252 // Power profile
253 Process {
254 id: powerProc
255 command: ["sh", "-c", "if command -v system76-power >/dev/null 2>&1; then p=$(system76-power profile 2>/dev/null | grep -oiE 'performance|balanced|battery' | tr '[:upper:]' '[:lower:]'); [ \"$p\" = \"battery\" ] && p=\"power-saver\"; echo \"${p:-unknown}\"; else powerprofilesctl get 2>/dev/null || echo unknown; fi"]
256 stdout: SplitParser {
257 onRead: data => {
258 if (!data) return
259 root.powerProfile = data.trim()
260 }
261 }
262 Component.onCompleted: running = true
263 }
264
265 // Weather
266 Process {
267 id: weatherProc
268 command: ["sh", "-c", "curl -sf 'wttr.in/?format=%c+%t' | tr -d '+'"]
269 stdout: SplitParser {
270 onRead: data => {
271 if (!data) return
272 root.weather = data.trim()
273 }
274 }
275 Component.onCompleted: running = true
276 }
277
278 // GitHub notifications
279 Process {
280 id: ghProc
281 command: ["sh", "-c", "gh api notifications --jq '[.[] | select(.unread)] | length' 2>/dev/null || echo 0"]
282 stdout: SplitParser {
283 onRead: data => {
284 if (!data) return
285 var count = parseInt(data.trim()) || 0
286 root.ghNotifications = count
287 // Send desktop notification if count increased
288 if (count > root.ghLastCount && count > 0) {
289 ghNotifyProc.running = true
290 }
291 root.ghLastCount = count
292 }
293 }
294 Component.onCompleted: running = true
295 }
296
297 // Desktop notification for new GitHub notifications
298 Process {
299 id: ghNotifyProc
300 command: ["notify-send", "-a", "GitHub", "GitHub", "You have new notifications"]
301 }
302
303 // Open Steam window via niri focus, fallback to spawning
304 Process {
305 id: steamOpenProc
306 command: ["sh", "-c", "WID=$(niri msg --json windows | jq -r '.[] | select(.app_id | test(\"steam\"; \"i\")) | .id' | head -n1); if [ -n \"$WID\" ]; then niri msg action focus-window --id \"$WID\"; else steam steam://open/games; fi"]
307 }
308
309 // GitHub timer (refresh every 2 minutes)
310 Timer {
311 interval: 120000
312 running: true
313 repeat: true
314 onTriggered: ghProc.running = true
315 }
316
317 // Weather timer (refresh every 15 minutes)
318 Timer {
319 interval: 900000
320 running: true
321 repeat: true
322 onTriggered: weatherProc.running = true
323 }
324
325 // Update timer
326 Timer {
327 interval: 2000
328 running: true
329 repeat: true
330 onTriggered: {
331 cpuProc.running = true
332 memProc.running = true
333 tempProc.running = true
334 netProc.running = true
335 volProc.running = true
336 brightProc.running = true
337 batProc.running = true
338 batStatusProc.running = true
339 powerProc.running = true
340 }
341 }
342
343 // Left section - time, weather, github
344 RowLayout {
345 anchors.left: parent.left
346 anchors.leftMargin: 12
347 anchors.verticalCenter: parent.verticalCenter
348 spacing: 12
349
350 Text {
351 id: clockText
352 color: root.colBlue
353 font { family: root.fontFamily; pixelSize: root.fontSize }
354 text: Qt.formatDateTime(new Date(), "dd-MM-yyyy HH:mm")
355 Timer {
356 interval: 1000
357 running: true
358 repeat: true
359 onTriggered: clockText.text = Qt.formatDateTime(new Date(), "dd-MM-yyyy HH:mm")
360 }
361 }
362
363 Rectangle { width: 1; height: 16; color: root.colOverlay0; visible: root.weather !== "" }
364
365 Text {
366 color: root.colText
367 font { family: root.fontFamily; pixelSize: root.fontSize }
368 text: root.weather
369 visible: root.weather !== ""
370 }
371
372 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
373
374 Text {
375 color: root.ghNotifications > 0 ? root.colBlue : root.colOverlay0
376 font { family: root.fontFamily; pixelSize: root.fontSize }
377 text: root.iconGithub + (root.ghNotifications > 0 ? " " + root.ghNotifications : "")
378 MouseArea {
379 anchors.fill: parent
380 cursorShape: Qt.PointingHandCursor
381 onClicked: ghOpenProc.running = true
382 }
383 }
384
385 Process {
386 id: ghOpenProc
387 command: ["xdg-open", "https://github.com/notifications"]
388 }
389
390 Rectangle { width: 1; height: 16; color: root.colOverlay0; visible: trayRepeater.count > 0 }
391
392 Repeater {
393 id: trayRepeater
394 model: SystemTray.items.values
395 delegate: Text {
396 id: trayDelegate
397 required property var modelData
398 color: root.colLavender
399 font { family: root.fontFamily; pixelSize: root.fontSize }
400 text: trayDelegate.modelData.title || trayDelegate.modelData.id
401 MouseArea {
402 anchors.fill: parent
403 cursorShape: Qt.PointingHandCursor
404 onClicked: {
405 if (trayDelegate.modelData.id.toLowerCase().indexOf("steam") !== -1) {
406 steamOpenProc.running = true
407 } else {
408 trayDelegate.modelData.activate()
409 }
410 }
411 }
412 }
413 }
414 }
415
416 // Right section - anchored to right edge
417 RowLayout {
418 anchors.right: parent.right
419 anchors.rightMargin: 12
420 anchors.verticalCenter: parent.verticalCenter
421 spacing: 12
422
423 // Volume
424 Text {
425 color: root.colMaroon
426 font { family: root.fontFamily; pixelSize: root.fontSize }
427 text: root.volume + "% " + (root.volumeMuted ? root.iconVolMute : (root.volume > 50 ? root.iconVolHigh : root.iconVolLow))
428 MouseArea {
429 anchors.fill: parent
430 cursorShape: Qt.PointingHandCursor
431 onClicked: volClickProc.running = true
432 }
433 }
434
435 Process {
436 id: volClickProc
437 command: ["pavucontrol"]
438 }
439
440 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
441
442 // Network
443 Text {
444 color: root.colGreen
445 font { family: root.fontFamily; pixelSize: root.fontSize }
446 text: {
447 if (!root.networkConnected) return "Disconnected \u26a0"
448 if (root.networkIsEthernet) return root.networkName + " " + root.iconEthernet
449 return root.networkName + " (" + root.networkStrength + "%) " + root.iconWifi
450 }
451 }
452
453 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
454
455 // Power Profile
456 Text {
457 color: root.colText
458 font { family: root.fontFamily; pixelSize: root.fontSize }
459 text: {
460 if (root.powerProfile === "performance") return root.iconBolt
461 if (root.powerProfile === "balanced") return root.iconBalance
462 if (root.powerProfile === "power-saver") return root.iconLeaf
463 return root.iconBalance
464 }
465 MouseArea {
466 anchors.fill: parent
467 cursorShape: Qt.PointingHandCursor
468 onClicked: {
469 var next = "balanced"
470 if (root.powerProfile === "balanced") next = "performance"
471 else if (root.powerProfile === "performance") next = "power-saver"
472 else next = "balanced"
473 if (root.powerBackend === "system76") {
474 var s76profile = next === "power-saver" ? "battery" : next
475 powerSetProc.command = ["system76-power", "profile", s76profile]
476 } else {
477 powerSetProc.command = ["powerprofilesctl", "set", next]
478 }
479 powerSetProc.running = true
480 root.powerProfile = next
481 }
482 }
483 }
484
485 Process {
486 id: powerSetProc
487 }
488
489 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
490
491 // CPU
492 Text {
493 color: root.colPeach
494 font { family: root.fontFamily; pixelSize: root.fontSize }
495 text: root.cpuUsage + "% " + root.iconCpu
496 }
497
498 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
499
500 // Memory
501 Text {
502 color: root.colMauve
503 font { family: root.fontFamily; pixelSize: root.fontSize }
504 text: root.memUsage + "% " + root.iconMem
505 }
506
507 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
508
509 // Temperature
510 Text {
511 color: root.colRed
512 font { family: root.fontFamily; pixelSize: root.fontSize }
513 text: {
514 var icon = root.temperature >= 80 ? root.iconTempHigh : (root.temperature >= 50 ? root.iconTempMed : root.iconTempLow)
515 return root.temperature + "\u00b0C " + icon
516 }
517 }
518
519 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
520
521 // Backlight
522 Text {
523 color: root.colYellow
524 font { family: root.fontFamily; pixelSize: root.fontSize }
525 text: root.brightness + "% " + root.iconSun
526 }
527
528 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
529
530 // Battery
531 Text {
532 color: {
533 if (root.batteryStatus === "Charging") return root.colGreen
534 if (root.batteryPercent <= 15) return root.colRed
535 if (root.batteryPercent <= 30) return root.colRed
536 return root.colGreen
537 }
538 font { family: root.fontFamily; pixelSize: root.fontSize }
539 text: {
540 var icon
541 if (root.batteryStatus === "Charging") {
542 icon = root.iconBatCharge
543 } else {
544 var icons = [root.iconBatEmpty, root.iconBatQuarter, root.iconBatHalf, root.iconBatThreeQ, root.iconBatFull]
545 var idx = Math.min(Math.floor(root.batteryPercent / 25), 4)
546 icon = icons[idx]
547 }
548 return root.batteryPercent + "% " + icon
549 }
550 }
551
552 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
553
554 // Power button
555 Text {
556 color: root.colRed
557 font { family: root.fontFamily; pixelSize: root.fontSize }
558 text: root.iconPower
559 }
560 }
561 }
562 }
563}
564