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