import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Services.Mpris import Quickshell.Services.Pipewire import Quickshell.Services.SystemTray import Quickshell.Wayland import Quickshell.Widgets Item { id: bar property date currentTime: new Date() property var controlCenter property var settings property var notificationCenter property var mediaPopup property var toggleMedia: null property var workspaces: [] property string windowTitle: "Desktop" property string language: "EN" property string weather: "N/A" property string gpu: "GPU N/A" property string gpuTemperature: "--" property string mail: "" property int cpuUsage: 0 property int memoryUsage: 0 property string diskUsage: "--" property string temperature: "--" property var audioSink: Pipewire.defaultAudioSink property var mediaPlayer: Mpris.players.values.find(player => player.playbackState === MprisPlaybackState.Playing) || Mpris.players.values[0] readonly property var defaultBarModules: [ "weather", "mail", "media", "notifications", "gpu", "cpu", "memory", "disk", "keyboard", "audio", "calendar", "settings", "tray" ] property var activeModules: settings ? settings.visibleBarModules() : defaultBarModules function start(process) { if (!process.running) process.running = true; } function focusWorkspace(index) { Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", String(index)]); } function parseJson(collector, fallback) { try { const value = JSON.parse(collector.text); return value === null ? fallback : value; } catch (error) { return fallback; } } function moduleComponent(name) { switch (name) { case "weather": return weatherModule; case "mail": return mailModule; case "media": return mediaModule; case "notifications": return notificationsModule; case "gpu": return gpuModule; case "cpu": return cpuModule; case "memory": return memoryModule; case "disk": return diskModule; case "keyboard": return keyboardModule; case "audio": return audioModule; case "calendar": return calendarModule; case "settings": return settingsModule; case "tray": return trayModule; default: return null; } } PanelWindow { id: barWindow anchors { top: true; left: true; right: true } implicitHeight: 40 exclusiveZone: 40 color: "transparent" WlrLayershell.layer: WlrLayer.Top WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.namespace: "quickshell-bar" Rectangle { anchors.fill: parent anchors.topMargin: 4 anchors.leftMargin: 4 anchors.rightMargin: 4 radius: 14 color: "#1e1e2e" border.color: "#313244" border.width: 1 RowLayout { anchors.fill: parent anchors.leftMargin: 6 anchors.rightMargin: 6 spacing: 6 RowLayout { spacing: 3 Repeater { model: bar.workspaces delegate: Rectangle { required property var modelData implicitWidth: workspaceLabel.implicitWidth + 16 implicitHeight: 26 radius: 10 color: modelData.is_focused ? "#89b4fa" : (modelData.is_active ? "#45475a" : "#313244") Label { id: workspaceLabel anchors.centerIn: parent text: modelData.name || String(modelData.idx) color: modelData.is_focused ? "#11111b" : "#cdd6f4" font.pixelSize: 15 font.bold: true } MouseArea { anchors.fill: parent; onClicked: bar.focusWorkspace(modelData.idx) } } } } Item { Layout.fillWidth: true } Label { text: bar.windowTitle color: "#cdd6f4" font.pixelSize: 15 elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter Layout.maximumWidth: 420 Layout.fillWidth: true } Item { Layout.fillWidth: true } RowLayout { spacing: 4 Repeater { model: bar.activeModules delegate: Rectangle { required property string modelData property bool cardStyle: !bar.settings || bar.settings.panelStyle !== "indicators" implicitWidth: moduleLoader.implicitWidth + (cardStyle ? 10 : 0) implicitHeight: 28 radius: 8 color: cardStyle ? "#24243a" : "transparent" border.color: cardStyle ? "#313244" : "transparent" border.width: cardStyle ? 1 : 0 Layout.preferredWidth: implicitWidth Layout.preferredHeight: implicitHeight Loader { id: moduleLoader anchors.centerIn: parent sourceComponent: bar.moduleComponent(modelData) } } } } } } } Component { id: weatherModule Label { text: bar.weather color: "#89dceb" font.pixelSize: 15 ToolTip.visible: mouse.containsMouse ToolTip.text: "Weather" MouseArea { id: mouse; anchors.fill: parent; hoverEnabled: true } } } Component { id: mailModule Label { text: bar.mail visible: bar.mail.length > 0 color: "#b4befe" font.pixelSize: 15 leftPadding: 19 BarIcon { name: "mail"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter } MouseArea { anchors.fill: parent; onClicked: Quickshell.execDetached(["thunderbird"]) } } } Component { id: mediaModule Label { text: bar.mediaPlayer ? (bar.mediaPlayer.trackTitle || bar.mediaPlayer.identity) : "" visible: bar.mediaPlayer !== null color: "#cba6f7" font.pixelSize: 15 leftPadding: 19 elide: Text.ElideRight Layout.maximumWidth: 190 BarIcon { name: "audio"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter } MouseArea { anchors.fill: parent; onClicked: if (bar.toggleMedia) bar.toggleMedia() } } } Component { id: notificationsModule Label { text: bar.notificationCenter ? String(bar.notificationCenter.count) : "" visible: !!bar.settings && bar.settings.notificationBadge && !!bar.notificationCenter && bar.notificationCenter.count > 0 color: "#f38ba8" font.pixelSize: 15 leftPadding: 19 BarIcon { name: "mail"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter } MouseArea { anchors.fill: parent onClicked: if (bar.notificationCenter) bar.notificationCenter.open = !bar.notificationCenter.open } } } Component { id: gpuModule Label { text: bar.gpu color: "#a6e3a1" font.pixelSize: 15 leftPadding: 19 BarIcon { name: "gpu"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter } } } Component { id: cpuModule Label { text: bar.cpuUsage + "%" color: "#a6e3a1" font.pixelSize: 15 leftPadding: 19 BarIcon { name: "cpu"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter } } } Component { id: memoryModule Label { text: bar.memoryUsage + "%" color: "#cba6f7" font.pixelSize: 15 leftPadding: 19 BarIcon { name: "memory"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter } } } Component { id: diskModule Label { text: bar.diskUsage color: "#f38ba8" font.pixelSize: 15 leftPadding: 19 BarIcon { name: "disk"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter } } } Component { id: keyboardModule Label { text: bar.language color: "#94e2d5" font.pixelSize: 15 leftPadding: 19 BarIcon { name: "language"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter } } } Component { id: audioModule Label { text: bar.audioSink && bar.audioSink.audio ? Math.round(bar.audioSink.audio.volume * 100) + "%" : "--" color: "#f9e2af" font.pixelSize: 15 leftPadding: 19 BarIcon { name: "audio"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter } MouseArea { anchors.fill: parent onClicked: if (bar.audioSink && bar.audioSink.audio) bar.audioSink.audio.muted = !bar.audioSink.audio.muted } } } Component { id: calendarModule Label { text: Qt.formatDateTime(bar.currentTime, "dd.MM HH:mm") color: "#cdd6f4" font.pixelSize: 15 MouseArea { anchors.fill: parent onClicked: Quickshell.execDetached(["sh", "-c", "alacritty --title khal-popup -e bash -c 'vdirsyncer sync 2>/dev/null; ikhal'"]) } } } Component { id: settingsModule Label { text: "⚙" color: "#89b4fa" font.pixelSize: 17 MouseArea { anchors.fill: parent; onClicked: if (bar.controlCenter) bar.controlCenter.togglePanel() } } } Component { id: trayModule Item { implicitWidth: trayRow.implicitWidth implicitHeight: 22 Row { id: trayRow spacing: 4 Repeater { model: SystemTray.items delegate: Item { required property var modelData property bool hiddenTrayItem: !modelData || /wayland to x11 video bridge|screen sharing|screen share/i.test(String(modelData.title || "")) || /video-display|screen-share|screen-sharing/i.test(String(modelData.icon || "")) visible: !hiddenTrayItem width: hiddenTrayItem ? 0 : 20 height: 22 Loader { id: trayIcon anchors.fill: parent active: !hiddenTrayItem && modelData sourceComponent: Component { IconImage { anchors.fill: parent implicitSize: 18 source: modelData && modelData.icon && modelData.icon.startsWith("image://") ? modelData.icon : (modelData ? Quickshell.iconPath(modelData.icon) : "") } } } BarIcon { visible: !hiddenTrayItem && !trayIcon.active anchors.fill: parent name: "video" size: 15 anchors.margins: 2 } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: function(mouse) { if (!modelData) return; if (mouse.button === Qt.RightButton && modelData.hasMenu) modelData.display(barWindow, mouse.x, mouse.y + 20); else modelData.activate(); } } } } } } } PwObjectTracker { objects: [bar.audioSink] } Process { id: workspaceReader command: ["niri", "msg", "-j", "workspaces"] stdout: StdioCollector { onStreamFinished: { const value = bar.parseJson(this, []); if (Array.isArray(value)) { value.sort((left, right) => left.idx - right.idx); bar.workspaces = value; } } } } Process { id: windowReader command: ["niri", "msg", "-j", "focused-window"] stdout: StdioCollector { onStreamFinished: bar.windowTitle = bar.parseJson(this, {}).title || "Desktop" } } Process { id: languageReader command: ["niri", "msg", "-j", "keyboard-layouts"] stdout: StdioCollector { onStreamFinished: { const value = bar.parseJson(this, {}); const index = Number(value.current_idx); const name = Array.isArray(value.names) && index >= 0 ? String(value.names[index] || "") : ""; const normalized = name.toLowerCase(); if (normalized.includes("russian") || normalized.includes("рус")) bar.language = "RU"; else if (normalized.includes("english") || normalized.includes("англ")) bar.language = "EN"; else if (name.length > 0) bar.language = name.slice(0, 2).toUpperCase(); } } } Process { id: cpuReader command: ["sh", "-c", "awk '/^cpu / { idle=$5; total=0; for (i=2; i<=NF; i++) total += $i; print int((total-idle)*100/total) }' /proc/stat"] stdout: StdioCollector { onStreamFinished: bar.cpuUsage = parseInt(text.trim()) || 0 } } Process { id: memoryReader command: ["sh", "-c", "free | awk '/^Mem:/ { print int($3*100/$2) }'"] stdout: StdioCollector { onStreamFinished: bar.memoryUsage = parseInt(text.trim()) || 0 } } Process { id: diskReader command: ["sh", "-c", "df -P / | awk 'NR==2 { print $5 }'"] stdout: StdioCollector { onStreamFinished: bar.diskUsage = text.trim() || "--" } } Process { id: temperatureReader command: ["sh", "-c", "max=0; for file in /sys/class/thermal/thermal_zone*/temp; do value=$(cat $file 2>/dev/null); [ $value -gt $max ] 2>/dev/null && max=$value; done; if [ $max -gt 0 ]; then echo $((max / 1000))C; else echo --; fi"] stdout: StdioCollector { onStreamFinished: bar.temperature = text.trim() || "--" } } Process { id: gpuReader command: ["sh", "-c", "nvidia-smi --query-gpu=utilization.gpu,temperature.gpu --format=csv,noheader,nounits 2>/dev/null | head -1"] stdout: StdioCollector { onStreamFinished: { const parts = text.trim().split(","); const usage = parseInt((parts[0] || "").trim()); const temperature = parseInt((parts[1] || "").trim()); bar.gpu = isNaN(usage) ? "N/A" : usage + "%"; bar.gpuTemperature = isNaN(temperature) ? "--" : temperature + "C"; } } } Process { id: weatherReader command: ["bash", Quickshell.shellPath("../scripts/weather.sh")] stdout: StdioCollector { onStreamFinished: bar.weather = bar.parseJson(this, {}).text || "N/A" } } Process { id: mailReader command: ["sh", "-c", "if [ -x $HOME/.local/bin/mail-counter ]; then $HOME/.local/bin/mail-counter; else printf '{}'; fi"] stdout: StdioCollector { onStreamFinished: bar.mail = bar.parseJson(this, {}).text || "" } } Timer { id: refreshTimer interval: bar.settings ? bar.settings.temperatureInterval : 2000 running: true repeat: true onTriggered: { bar.currentTime = new Date(); bar.start(windowReader); bar.start(cpuReader); bar.start(memoryReader); bar.start(diskReader); bar.start(temperatureReader); bar.start(gpuReader); bar.start(mailReader); } } Timer { id: weatherTimer; interval: 900000; running: true; repeat: true; onTriggered: bar.start(weatherReader) } Timer { id: languageTimer; interval: 200; running: true; repeat: true; onTriggered: bar.start(languageReader) } Timer { id: workspaceTimer; interval: 100; running: true; repeat: true; onTriggered: bar.start(workspaceReader) } Component.onCompleted: { bar.start(workspaceReader); bar.start(windowReader); bar.start(languageReader); bar.start(cpuReader); bar.start(memoryReader); bar.start(diskReader); bar.start(temperatureReader); bar.start(gpuReader); bar.start(weatherReader); bar.start(mailReader); } }