Redesign Quickshell control center

This commit is contained in:
Mikhail Kilin
2026-07-26 16:51:25 +03:00
parent 6cb1d53a70
commit 4b3109a2dd
17 changed files with 1390 additions and 904 deletions

View File

@@ -14,7 +14,6 @@ Item {
id: bar
property date currentTime: new Date()
property var controlCenter
property var settings
property var notificationCenter
@@ -34,6 +33,11 @@ Item {
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;
@@ -52,13 +56,28 @@ Item {
}
}
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
}
anchors { top: true; left: true; right: true }
implicitHeight: 40
exclusiveZone: 40
color: "transparent"
@@ -71,7 +90,6 @@ Item {
anchors.topMargin: 4
anchors.leftMargin: 4
anchors.rightMargin: 4
anchors.bottomMargin: 0
radius: 14
color: "#1e1e2e"
border.color: "#313244"
@@ -84,9 +102,7 @@ Item {
spacing: 6
RowLayout {
id: workspaceRow
spacing: 3
Repeater {
model: bar.workspaces
delegate: Rectangle {
@@ -95,26 +111,20 @@ Item {
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 ? "#1e1e2e" : "#cdd6f4"
color: modelData.is_focused ? "#11111b" : "#cdd6f4"
font.pixelSize: 15
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: bar.focusWorkspace(modelData.idx)
}
MouseArea { anchors.fill: parent; onClicked: bar.focusWorkspace(modelData.idx) }
}
}
}
Item { Layout.fillWidth: true }
Label {
text: bar.windowTitle
color: "#cdd6f4"
@@ -124,230 +134,27 @@ Item {
Layout.maximumWidth: 420
Layout.fillWidth: true
}
Item { Layout.fillWidth: true }
RowLayout {
spacing: 4
Label {
text: bar.weather
color: "#89dceb"
font.pixelSize: 15
ToolTip.visible: weatherMouse.containsMouse
ToolTip.text: "Weather"
MouseArea {
id: weatherMouse
anchors.fill: parent
}
}
Label {
visible: bar.mail.length > 0
text: bar.mail
color: "#b4befe"
font.pixelSize: 15
leftPadding: 19
BarIcon { name: "mail"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter }
ToolTip.visible: mailMouse.containsMouse
ToolTip.text: "Unread mail"
MouseArea {
id: mailMouse
anchors.fill: parent
onClicked: Quickshell.execDetached(["thunderbird"])
}
}
Label {
visible: bar.mediaPlayer !== null
text: bar.mediaPlayer ? (bar.mediaPlayer.trackTitle || bar.mediaPlayer.identity) : ""
color: "#f5c2e7"
leftPadding: 19
elide: Text.ElideRight
Layout.maximumWidth: 190
BarIcon { name: "audio"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter }
ToolTip.visible: mediaMouse.containsMouse
ToolTip.text: "Open media controls"
MouseArea {
id: mediaMouse
anchors.fill: parent
onClicked: {
if (bar.toggleMedia) bar.toggleMedia();
}
}
}
Label {
visible: !!bar.notificationCenter && bar.notificationCenter.count > 0
text: bar.notificationCenter ? String(bar.notificationCenter.count) : ""
color: "#f38ba8"
leftPadding: 19
BarIcon { name: "mail"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter }
ToolTip.visible: notificationMouse.containsMouse
ToolTip.text: "Open notifications"
MouseArea {
id: notificationMouse
anchors.fill: parent
onClicked: {
if (bar.notificationCenter) bar.notificationCenter.open = !bar.notificationCenter.open;
}
}
}
Label {
text: bar.gpu
color: "#a6e3a1"
font.pixelSize: 15
leftPadding: 19
BarIcon { name: "gpu"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter }
ToolTip.visible: gpuMouse.containsMouse
ToolTip.text: "GPU usage"
MouseArea {
id: gpuMouse
anchors.fill: parent
}
}
Label {
text: bar.cpuUsage + "%"
color: "#a6e3a1"
font.pixelSize: 15
leftPadding: 19
BarIcon { name: "cpu"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter }
ToolTip.visible: cpuMouse.containsMouse
ToolTip.text: "CPU usage"
MouseArea { id: cpuMouse; anchors.fill: parent }
}
Label {
text: bar.memoryUsage + "%"
color: "#cba6f7"
font.pixelSize: 15
leftPadding: 19
BarIcon { name: "memory"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter }
ToolTip.visible: memoryMouse.containsMouse
ToolTip.text: "Memory usage"
MouseArea { id: memoryMouse; anchors.fill: parent }
}
Label {
text: bar.diskUsage
color: "#f38ba8"
font.pixelSize: 15
leftPadding: 19
BarIcon { name: "disk"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter }
ToolTip.visible: diskMouse.containsMouse
ToolTip.text: "Root filesystem"
MouseArea { id: diskMouse; anchors.fill: parent }
}
Label {
text: bar.language
color: "#94e2d5"
font.pixelSize: 15
leftPadding: 19
BarIcon { name: "language"; anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter }
ToolTip.visible: languageMouse.containsMouse
ToolTip.text: "Keyboard layout"
MouseArea { id: languageMouse; anchors.fill: parent }
}
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 }
ToolTip.visible: audioMouse.containsMouse
ToolTip.text: "Audio output"
MouseArea {
id: audioMouse
anchors.fill: parent
onClicked: {
if (bar.audioSink && bar.audioSink.audio) bar.audioSink.audio.muted = !bar.audioSink.audio.muted;
}
}
}
Label {
text: Qt.formatDateTime(bar.currentTime, "dd.MM HH:mm")
color: "#cdd6f4"
font.pixelSize: 15
ToolTip.visible: clockMouse.containsMouse
ToolTip.text: "Open calendar"
MouseArea {
id: clockMouse
anchors.fill: parent
onClicked: Quickshell.execDetached(["sh", "-c", "alacritty --title khal-popup -e bash -c 'vdirsyncer sync 2>/dev/null; ikhal'"])
}
}
Label {
text: ""
color: "#89b4fa"
Layout.preferredWidth: 18
Layout.minimumWidth: 18
Layout.maximumWidth: 18
BarIcon { name: "gear"; size: 18; anchors.centerIn: parent }
ToolTip.visible: settingsMouse.containsMouse
ToolTip.text: "Quick settings"
MouseArea {
id: settingsMouse
anchors.fill: parent
onClicked: {
if (bar.controlCenter) bar.controlCenter.togglePanel();
}
}
}
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
implicitWidth: hiddenTrayItem ? 0 : 20
implicitHeight: 22
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: 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
enabled: !hiddenTrayItem
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();
}
}
id: moduleLoader
anchors.centerIn: parent
sourceComponent: bar.moduleComponent(modelData)
}
}
}
@@ -356,10 +163,211 @@ Item {
}
}
PwObjectTracker {
objects: [bar.audioSink]
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"]
@@ -373,18 +381,11 @@ Item {
}
}
}
Process {
id: windowReader
command: ["niri", "msg", "-j", "focused-window"]
stdout: StdioCollector {
onStreamFinished: {
const value = bar.parseJson(this, {});
bar.windowTitle = value.title || "Desktop";
}
}
stdout: StdioCollector { onStreamFinished: bar.windowTitle = bar.parseJson(this, {}).title || "Desktop" }
}
Process {
id: languageReader
command: ["niri", "msg", "-j", "keyboard-layouts"]
@@ -400,31 +401,26 @@ Item {
}
}
}
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"]
@@ -438,27 +434,15 @@ Item {
}
}
}
Process {
id: weatherReader
command: ["bash", Quickshell.shellPath("../scripts/weather.sh")]
stdout: StdioCollector {
onStreamFinished: {
const value = bar.parseJson(this, {});
bar.weather = value.text || "N/A";
}
}
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: {
const value = bar.parseJson(this, {});
bar.mail = value.text || "";
}
}
stdout: StdioCollector { onStreamFinished: bar.mail = bar.parseJson(this, {}).text || "" }
}
Timer {
@@ -468,50 +452,17 @@ Item {
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);
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)
}
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);
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);
}
}