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 weather: ""
87 property int ghNotifications: 0
88 property int ghLastCount: 0
89
90 anchors {
91 top: true
92 left: true
93 right: true
94 }
95 margins.top: 0
96 margins.bottom: 0
97 margins.left: 0
98 margins.right: 0
99 implicitHeight: 30
100 color: Qt.rgba(0.188, 0.204, 0.275, 0.85)
101
102 // CPU process
103 Process {
104 id: cpuProc
105 command: ["sh", "-c", "head -1 /proc/stat"]
106 stdout: SplitParser {
107 onRead: data => {
108 if (!data) return
109 var p = data.trim().split(/\s+/)
110 var idle = parseInt(p[4]) + parseInt(p[5])
111 var total = p.slice(1, 8).reduce((a, b) => a + parseInt(b), 0)
112 if (root.lastCpuTotal > 0) {
113 root.cpuUsage = Math.round(100 * (1 - (idle - root.lastCpuIdle) / (total - root.lastCpuTotal)))
114 }
115 root.lastCpuTotal = total
116 root.lastCpuIdle = idle
117 }
118 }
119 Component.onCompleted: running = true
120 }
121
122 // Memory process
123 Process {
124 id: memProc
125 command: ["sh", "-c", "free | grep Mem"]
126 stdout: SplitParser {
127 onRead: data => {
128 if (!data) return
129 var parts = data.trim().split(/\s+/)
130 var total = parseInt(parts[1]) || 1
131 var used = parseInt(parts[2]) || 0
132 root.memUsage = Math.round(100 * used / total)
133 }
134 }
135 Component.onCompleted: running = true
136 }
137
138 // Temperature
139 Process {
140 id: tempProc
141 command: ["sh", "-c", "cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0"]
142 stdout: SplitParser {
143 onRead: data => {
144 if (!data) return
145 root.temperature = Math.round(parseInt(data.trim()) / 1000)
146 }
147 }
148 Component.onCompleted: running = true
149 }
150
151 // Network - check ethernet first, then wifi
152 Process {
153 id: netProc
154 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"]
155 stdout: SplitParser {
156 onRead: data => {
157 if (!data || data.trim() === "") {
158 root.networkConnected = false
159 root.networkIsEthernet = false
160 root.networkName = "Disconnected"
161 root.networkStrength = 0
162 return
163 }
164 var trimmed = data.trim()
165 if (trimmed.startsWith("ethernet:")) {
166 root.networkConnected = true
167 root.networkIsEthernet = true
168 root.networkName = trimmed.split(":")[1] || "Ethernet"
169 root.networkStrength = 100
170 } else {
171 var parts = trimmed.split(":")
172 root.networkConnected = true
173 root.networkIsEthernet = false
174 root.networkName = parts[1] || ""
175 root.networkStrength = parseInt(parts[2]) || 0
176 }
177 }
178 }
179 Component.onCompleted: running = true
180 }
181
182 // Volume
183 Process {
184 id: volProc
185 command: ["sh", "-c", "wpctl get-volume @DEFAULT_AUDIO_SINK@"]
186 stdout: SplitParser {
187 onRead: data => {
188 if (!data) return
189 root.volumeMuted = data.indexOf("[MUTED]") !== -1
190 var match = data.match(/Volume:\s+([\d.]+)/)
191 if (match) {
192 root.volume = Math.round(parseFloat(match[1]) * 100)
193 }
194 }
195 }
196 Component.onCompleted: running = true
197 }
198
199 // Brightness
200 Process {
201 id: brightProc
202 command: ["sh", "-c", "brightnessctl -m | cut -d, -f4 | tr -d '%'"]
203 stdout: SplitParser {
204 onRead: data => {
205 if (!data) return
206 root.brightness = parseInt(data.trim()) || 0
207 }
208 }
209 Component.onCompleted: running = true
210 }
211
212 // Battery
213 Process {
214 id: batProc
215 command: ["sh", "-c", "cat /sys/class/power_supply/BAT1/capacity 2>/dev/null || echo 0"]
216 stdout: SplitParser {
217 onRead: data => {
218 if (!data) return
219 root.batteryPercent = parseInt(data.trim()) || 0
220 }
221 }
222 Component.onCompleted: running = true
223 }
224
225 Process {
226 id: batStatusProc
227 command: ["sh", "-c", "cat /sys/class/power_supply/BAT1/status 2>/dev/null || echo Unknown"]
228 stdout: SplitParser {
229 onRead: data => {
230 if (!data) return
231 root.batteryStatus = data.trim()
232 }
233 }
234 Component.onCompleted: running = true
235 }
236
237 // Power profile
238 Process {
239 id: powerProc
240 command: ["sh", "-c", "powerprofilesctl get 2>/dev/null || echo unknown"]
241 stdout: SplitParser {
242 onRead: data => {
243 if (!data) return
244 root.powerProfile = data.trim()
245 }
246 }
247 Component.onCompleted: running = true
248 }
249
250 // Weather
251 Process {
252 id: weatherProc
253 command: ["sh", "-c", "curl -sf 'wttr.in/?format=%c+%t' | tr -d '+'"]
254 stdout: SplitParser {
255 onRead: data => {
256 if (!data) return
257 root.weather = data.trim()
258 }
259 }
260 Component.onCompleted: running = true
261 }
262
263 // GitHub notifications
264 Process {
265 id: ghProc
266 command: ["sh", "-c", "gh api notifications --jq '[.[] | select(.unread)] | length' 2>/dev/null || echo 0"]
267 stdout: SplitParser {
268 onRead: data => {
269 if (!data) return
270 var count = parseInt(data.trim()) || 0
271 root.ghNotifications = count
272 // Send desktop notification if count increased
273 if (count > root.ghLastCount && count > 0) {
274 ghNotifyProc.running = true
275 }
276 root.ghLastCount = count
277 }
278 }
279 Component.onCompleted: running = true
280 }
281
282 // Desktop notification for new GitHub notifications
283 Process {
284 id: ghNotifyProc
285 command: ["notify-send", "-a", "GitHub", "GitHub", "You have new notifications"]
286 }
287
288 // GitHub timer (refresh every 2 minutes)
289 Timer {
290 interval: 120000
291 running: true
292 repeat: true
293 onTriggered: ghProc.running = true
294 }
295
296 // Weather timer (refresh every 15 minutes)
297 Timer {
298 interval: 900000
299 running: true
300 repeat: true
301 onTriggered: weatherProc.running = true
302 }
303
304 // Update timer
305 Timer {
306 interval: 2000
307 running: true
308 repeat: true
309 onTriggered: {
310 cpuProc.running = true
311 memProc.running = true
312 tempProc.running = true
313 netProc.running = true
314 volProc.running = true
315 brightProc.running = true
316 batProc.running = true
317 batStatusProc.running = true
318 powerProc.running = true
319 }
320 }
321
322 // Left section - time, weather, github
323 RowLayout {
324 anchors.left: parent.left
325 anchors.leftMargin: 12
326 anchors.verticalCenter: parent.verticalCenter
327 spacing: 12
328
329 Text {
330 id: clockText
331 color: root.colBlue
332 font { family: root.fontFamily; pixelSize: root.fontSize }
333 text: Qt.formatDateTime(new Date(), "dd-MM-yyyy HH:mm")
334 Timer {
335 interval: 1000
336 running: true
337 repeat: true
338 onTriggered: clockText.text = Qt.formatDateTime(new Date(), "dd-MM-yyyy HH:mm")
339 }
340 }
341
342 Rectangle { width: 1; height: 16; color: root.colOverlay0; visible: root.weather !== "" }
343
344 Text {
345 color: root.colText
346 font { family: root.fontFamily; pixelSize: root.fontSize }
347 text: root.weather
348 visible: root.weather !== ""
349 }
350
351 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
352
353 Text {
354 color: root.ghNotifications > 0 ? root.colBlue : root.colOverlay0
355 font { family: root.fontFamily; pixelSize: root.fontSize }
356 text: root.iconGithub + (root.ghNotifications > 0 ? " " + root.ghNotifications : "")
357 MouseArea {
358 anchors.fill: parent
359 cursorShape: Qt.PointingHandCursor
360 onClicked: ghOpenProc.running = true
361 }
362 }
363
364 Process {
365 id: ghOpenProc
366 command: ["xdg-open", "https://github.com/notifications"]
367 }
368 }
369
370 // Right section - anchored to right edge
371 RowLayout {
372 anchors.right: parent.right
373 anchors.rightMargin: 12
374 anchors.verticalCenter: parent.verticalCenter
375 spacing: 12
376
377 // Volume
378 Text {
379 color: root.colMaroon
380 font { family: root.fontFamily; pixelSize: root.fontSize }
381 text: root.volume + "% " + (root.volumeMuted ? root.iconVolMute : (root.volume > 50 ? root.iconVolHigh : root.iconVolLow))
382 MouseArea {
383 anchors.fill: parent
384 cursorShape: Qt.PointingHandCursor
385 onClicked: volClickProc.running = true
386 }
387 }
388
389 Process {
390 id: volClickProc
391 command: ["pavucontrol"]
392 }
393
394 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
395
396 // Network
397 Text {
398 color: root.colGreen
399 font { family: root.fontFamily; pixelSize: root.fontSize }
400 text: {
401 if (!root.networkConnected) return "Disconnected \u26a0"
402 if (root.networkIsEthernet) return root.networkName + " " + root.iconEthernet
403 return root.networkName + " (" + root.networkStrength + "%) " + root.iconWifi
404 }
405 }
406
407 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
408
409 // Power Profile
410 Text {
411 color: root.colText
412 font { family: root.fontFamily; pixelSize: root.fontSize }
413 text: {
414 if (root.powerProfile === "performance") return root.iconBolt
415 if (root.powerProfile === "balanced") return root.iconBalance
416 if (root.powerProfile === "power-saver") return root.iconLeaf
417 return root.iconBalance
418 }
419 }
420
421 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
422
423 // CPU
424 Text {
425 color: root.colPeach
426 font { family: root.fontFamily; pixelSize: root.fontSize }
427 text: root.cpuUsage + "% " + root.iconCpu
428 }
429
430 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
431
432 // Memory
433 Text {
434 color: root.colMauve
435 font { family: root.fontFamily; pixelSize: root.fontSize }
436 text: root.memUsage + "% " + root.iconMem
437 }
438
439 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
440
441 // Temperature
442 Text {
443 color: root.colRed
444 font { family: root.fontFamily; pixelSize: root.fontSize }
445 text: {
446 var icon = root.temperature >= 80 ? root.iconTempHigh : (root.temperature >= 50 ? root.iconTempMed : root.iconTempLow)
447 return root.temperature + "\u00b0C " + icon
448 }
449 }
450
451 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
452
453 // Backlight
454 Text {
455 color: root.colYellow
456 font { family: root.fontFamily; pixelSize: root.fontSize }
457 text: root.brightness + "% " + root.iconSun
458 }
459
460 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
461
462 // Battery
463 Text {
464 color: {
465 if (root.batteryStatus === "Charging") return root.colGreen
466 if (root.batteryPercent <= 15) return root.colRed
467 if (root.batteryPercent <= 30) return root.colRed
468 return root.colGreen
469 }
470 font { family: root.fontFamily; pixelSize: root.fontSize }
471 text: {
472 var icon
473 if (root.batteryStatus === "Charging") {
474 icon = root.iconBatCharge
475 } else {
476 var icons = [root.iconBatEmpty, root.iconBatQuarter, root.iconBatHalf, root.iconBatThreeQ, root.iconBatFull]
477 var idx = Math.min(Math.floor(root.batteryPercent / 25), 4)
478 icon = icons[idx]
479 }
480 return root.batteryPercent + "% " + icon
481 }
482 }
483
484 Rectangle { width: 1; height: 16; color: root.colOverlay0 }
485
486 // Power button
487 Text {
488 color: root.colRed
489 font { family: root.fontFamily; pixelSize: root.fontSize }
490 text: root.iconPower
491 }
492 }
493 }
494 }
495}
496