469 lines
18 KiB
QML
469 lines
18 KiB
QML
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);
|
||
}
|
||
}
|