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