me like nix
0

Configure Feed

Select the types of activity you want to include in your feed.

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