Files
dotfiles/fedora/quickshell/control-center/Bar.qml
2026-07-26 16:51:25 +03:00

469 lines
18 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}