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,184 +134,192 @@ Item {
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: weatherMouse.containsMouse
ToolTip.visible: mouse.containsMouse
ToolTip.text: "Weather"
MouseArea {
id: weatherMouse
anchors.fill: parent
MouseArea { id: mouse; anchors.fill: parent; hoverEnabled: true }
}
}
Component {
id: mailModule
Label {
visible: bar.mail.length > 0
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 }
ToolTip.visible: mailMouse.containsMouse
ToolTip.text: "Unread mail"
MouseArea {
id: mailMouse
anchors.fill: parent
onClicked: Quickshell.execDetached(["thunderbird"])
MouseArea { anchors.fill: parent; onClicked: Quickshell.execDetached(["thunderbird"]) }
}
}
Component {
id: mediaModule
Label {
visible: bar.mediaPlayer !== null
text: bar.mediaPlayer ? (bar.mediaPlayer.trackTitle || bar.mediaPlayer.identity) : ""
color: "#f5c2e7"
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 }
ToolTip.visible: mediaMouse.containsMouse
ToolTip.text: "Open media controls"
MouseArea {
id: mediaMouse
anchors.fill: parent
onClicked: {
if (bar.toggleMedia) bar.toggleMedia();
}
MouseArea { anchors.fill: parent; onClicked: if (bar.toggleMedia) bar.toggleMedia() }
}
}
Component {
id: notificationsModule
Label {
visible: !!bar.notificationCenter && bar.notificationCenter.count > 0
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 }
ToolTip.visible: notificationMouse.containsMouse
ToolTip.text: "Open notifications"
MouseArea {
id: notificationMouse
anchors.fill: parent
onClicked: {
if (bar.notificationCenter) bar.notificationCenter.open = !bar.notificationCenter.open;
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 }
ToolTip.visible: gpuMouse.containsMouse
ToolTip.text: "GPU usage"
MouseArea {
id: gpuMouse
anchors.fill: parent
}
}
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 }
ToolTip.visible: cpuMouse.containsMouse
ToolTip.text: "CPU usage"
MouseArea { id: cpuMouse; anchors.fill: parent }
}
}
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 }
ToolTip.visible: memoryMouse.containsMouse
ToolTip.text: "Memory usage"
MouseArea { id: memoryMouse; anchors.fill: parent }
}
}
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 }
ToolTip.visible: diskMouse.containsMouse
ToolTip.text: "Root filesystem"
MouseArea { id: diskMouse; anchors.fill: parent }
}
}
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 }
ToolTip.visible: languageMouse.containsMouse
ToolTip.text: "Keyboard layout"
MouseArea { id: languageMouse; anchors.fill: parent }
}
}
Component {
id: audioModule
Label {
text: bar.audioSink && bar.audioSink.audio
? Math.round(bar.audioSink.audio.volume * 100) + "%"
: "--"
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;
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
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'"])
}
}
}
Component {
id: settingsModule
Label {
text: ""
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();
}
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 {
@@ -310,9 +328,8 @@ Item {
|| /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
width: hiddenTrayItem ? 0 : 20
height: 22
Loader {
id: trayIcon
anchors.fill: parent
@@ -322,32 +339,25 @@ Item {
anchors.fill: parent
implicitSize: 18
source: modelData && modelData.icon && modelData.icon.startsWith("image://")
? modelData.icon
: (modelData ? Quickshell.iconPath(modelData.icon) : "")
? 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) {
if (mouse.button === Qt.RightButton && modelData.hasMenu)
modelData.display(barWindow, mouse.x, mouse.y + 20);
} else {
modelData.activate();
}
}
else modelData.activate();
}
}
}
@@ -356,9 +366,7 @@ Item {
}
}
PwObjectTracker {
objects: [bar.audioSink]
}
PwObjectTracker { objects: [bar.audioSink] }
Process {
id: workspaceReader
@@ -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);
}
}

View File

@@ -0,0 +1,97 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
property var settings
property var theme
property var availableModules: []
implicitHeight: pageColumn.implicitHeight
function moveModule(module, delta) {
let modules = settings.barModules.slice();
const index = modules.indexOf(module);
const next = index + delta;
if (index < 0 || next < 0 || next >= modules.length) return;
const swap = modules[next];
modules[next] = module;
modules[index] = swap;
settings.setBarModules(modules);
}
ColumnLayout {
id: pageColumn
width: parent.width
spacing: 10
SettingsCard {
theme: root.theme
title: "Bar modules"
description: "Weather stays first; workspace buttons remain on the left."
Layout.fillWidth: true
Repeater {
model: root.settings && Array.isArray(root.settings.barModules)
? root.settings.barModules : root.availableModules
delegate: SettingsRow {
required property string modelData
required property int index
theme: root.theme
label: modelData
description: index + 1 + " / " + (root.settings && Array.isArray(root.settings.barModules)
? root.settings.barModules.length : root.availableModules.length)
CCSwitch {
theme: root.theme
checked: root.settings.visibleBarModules().indexOf(modelData) >= 0
onToggled: root.settings.setModuleVisible(modelData, checked)
}
CCButton {
theme: root.theme
text: "↑"
enabled: index > 0
onClicked: root.moveModule(modelData, -1)
}
CCButton {
theme: root.theme
text: "↓"
enabled: index < (root.settings && Array.isArray(root.settings.barModules)
? root.settings.barModules.length : root.availableModules.length) - 1
onClicked: root.moveModule(modelData, 1)
}
}
}
}
SettingsCard {
theme: root.theme
title: "Indicator style"
description: "Keep the bar compact and consistent with the cards."
Layout.fillWidth: true
SettingsRow {
theme: root.theme
label: "Card style"
ComboBox {
palette.base: root.theme.surfaceVariant
palette.button: root.theme.surfaceVariant
palette.text: root.theme.text
palette.buttonText: root.theme.text
palette.highlight: root.theme.primary
model: ["Cards", "Indicators"]
currentIndex: root.settings.panelStyle === "indicators" ? 1 : 0
onActivated: root.settings.panelStyle = currentIndex === 1 ? "indicators" : "cards"
}
}
SettingsRow {
theme: root.theme
label: "Notification badge"
CCSwitch {
theme: root.theme
checked: root.settings.notificationBadge
onToggled: root.settings.notificationBadge = checked
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
import QtQuick
import QtQuick.Controls
Button {
id: root
property var theme
property bool accent: false
implicitHeight: 32
leftPadding: 11
rightPadding: 11
topPadding: 5
bottomPadding: 5
contentItem: Label {
text: root.text
color: root.enabled
? (root.accent && root.theme ? root.theme.background : (root.theme ? root.theme.text : "#cdd6f4"))
: (root.theme ? root.theme.dimText : "#6c7086")
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 12
elide: Text.ElideRight
}
background: Rectangle {
radius: root.theme ? root.theme.controlRadius : 9
color: !root.enabled
? (root.theme ? root.theme.surface : "#181825")
: (root.accent
? (root.theme ? root.theme.primary : "#89b4fa")
: (root.hovered ? (root.theme ? root.theme.surfaceRaised : "#24243a")
: (root.theme ? root.theme.surfaceVariant : "#1e1e2e")))
border.color: root.theme ? root.theme.border : "#313244"
border.width: 1
}
}

View File

@@ -0,0 +1,36 @@
import QtQuick
import QtQuick.Controls
Slider {
id: root
property var theme
implicitHeight: 24
background: Rectangle {
x: root.leftPadding
y: root.topPadding + root.availableHeight / 2 - height / 2
width: root.availableWidth
height: 4
radius: 2
color: root.theme ? root.theme.surfaceRaised : "#24243a"
Rectangle {
width: root.visualPosition * parent.width
height: parent.height
radius: 2
color: root.theme ? root.theme.primary : "#89b4fa"
}
}
handle: Rectangle {
x: root.leftPadding + root.visualPosition * (root.availableWidth - width)
y: root.topPadding + root.availableHeight / 2 - height / 2
width: 14
height: 14
radius: 7
color: root.theme ? root.theme.primary : "#89b4fa"
border.color: root.theme ? root.theme.background : "#11111b"
border.width: 2
}
}

View File

@@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Controls
Switch {
id: root
property var theme
implicitWidth: 42
implicitHeight: 24
indicator: Rectangle {
x: root.leftPadding
y: (root.height - height) / 2
width: 40
height: 22
radius: 11
color: root.checked && root.enabled
? (root.theme ? root.theme.primary : "#89b4fa")
: (root.theme ? root.theme.surfaceRaised : "#24243a")
border.color: root.theme ? root.theme.border : "#313244"
Rectangle {
x: root.checked ? parent.width - width - 3 : 3
anchors.verticalCenter: parent.verticalCenter
width: 16
height: 16
radius: 8
color: root.checked ? (root.theme ? root.theme.background : "#11111b") : (root.theme ? root.theme.mutedText : "#9399b2")
Behavior on x { NumberAnimation { duration: 120 } }
}
}
}

View File

@@ -9,6 +9,8 @@ import Quickshell.Wayland
Item {
id: root
UiTokens { id: theme }
property bool open: false
property var selectedPlayer: null
property var player: selectedPlayer
@@ -51,8 +53,8 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 52
radius: 20
color: "#f5f2ea"
border.color: "#d8d0c2"
color: "#11111b"
border.color: "#313244"
border.width: 1
z: 1
@@ -70,32 +72,22 @@ Item {
Layout.fillWidth: true
Label {
text: "Media"
color: "#252321"
color: "#cdd6f4"
font.pixelSize: 20
font.bold: true
Layout.fillWidth: true
}
Label {
text: root.player ? root.player.identity : "No player"
color: "#7c756c"
color: "#9399b2"
elide: Text.ElideRight
}
Rectangle {
width: 26
height: 26
radius: 13
color: "#e5e0d7"
Label {
anchors.centerIn: parent
text: "x"
color: "#6e675f"
}
MouseArea {
anchors.fill: parent
CCButton {
theme: theme
text: "Close"
onClicked: root.dismissed()
}
}
}
RowLayout {
visible: root.player !== null
@@ -107,7 +99,7 @@ Item {
Layout.preferredWidth: 132
Layout.preferredHeight: 132
radius: 14
color: "#e5e0d7"
color: "#181825"
clip: true
Image {
@@ -132,7 +124,7 @@ Item {
Label {
text: root.player ? (root.player.trackTitle || "Unknown track") : ""
color: "#252321"
color: "#cdd6f4"
font.pixelSize: 17
font.bold: true
elide: Text.ElideRight
@@ -140,20 +132,21 @@ Item {
}
Label {
text: root.player ? (root.player.trackArtist || root.player.trackAlbum || "Unknown artist") : ""
color: "#7c756c"
color: "#9399b2"
elide: Text.ElideRight
Layout.fillWidth: true
}
Item { Layout.fillHeight: true }
Label {
text: root.player ? root.formatTime(root.player.position) + " / " + root.formatTime(root.player.length) : ""
color: "#7c756c"
color: "#9399b2"
font.pixelSize: 11
}
}
}
Slider {
CCSlider {
theme: theme
visible: root.player && root.player.length > 0
Layout.fillWidth: true
from: 0
@@ -169,17 +162,20 @@ Item {
Layout.alignment: Qt.AlignHCenter
spacing: 14
Button {
CCButton {
theme: theme
text: "Prev"
enabled: !!(root.player && root.player.canGoPrevious)
onClicked: root.player.previous()
}
Button {
CCButton {
theme: theme
text: root.player && root.player.playbackState === MprisPlaybackState.Playing ? "Pause" : "Play"
enabled: !!(root.player && root.player.canTogglePlaying)
onClicked: root.player.togglePlaying()
}
Button {
CCButton {
theme: theme
text: "Next"
enabled: !!(root.player && root.player.canGoNext)
onClicked: root.player.next()
@@ -191,13 +187,14 @@ Item {
Layout.fillWidth: true
Label {
text: "Players"
color: "#7c756c"
color: "#9399b2"
Layout.fillWidth: true
}
Repeater {
model: Mpris.players
delegate: Button {
delegate: CCButton {
required property var modelData
theme: theme
text: modelData.identity
onClicked: root.selectedPlayer = modelData
}

View File

@@ -0,0 +1,88 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
property var theme
property int currentIndex: 0
signal selected(int index)
readonly property var names: ["Quick", "Bar", "Panel", "Notifications", "Status"]
implicitWidth: 78
Rectangle {
anchors.fill: parent
radius: root.theme ? root.theme.cardRadius : 14
color: root.theme ? root.theme.surface : "#181825"
border.color: root.theme ? root.theme.border : "#313244"
border.width: 1
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 7
spacing: 5
Label {
text: "iN"
color: root.theme ? root.theme.primary : "#89b4fa"
font.pixelSize: 17
font.bold: true
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
Layout.bottomMargin: 5
}
Repeater {
model: root.names
delegate: Rectangle {
required property int index
required property string modelData
Layout.fillWidth: true
Layout.preferredHeight: 48
radius: 11
color: index === root.currentIndex
? (root.theme ? root.theme.primary : "#89b4fa")
: (navMouse.containsMouse ? (root.theme ? root.theme.surfaceRaised : "#24243a") : "transparent")
Column {
anchors.centerIn: parent
spacing: 2
Label {
text: ["⌂", "≡", "▣", "●", "⌁"][index]
color: index === root.currentIndex ? (root.theme ? root.theme.background : "#11111b") : (root.theme ? root.theme.mutedText : "#9399b2")
font.pixelSize: 17
horizontalAlignment: Text.AlignHCenter
width: 58
}
Label {
text: modelData
color: index === root.currentIndex ? (root.theme ? root.theme.background : "#11111b") : (root.theme ? root.theme.mutedText : "#9399b2")
font.pixelSize: 9
horizontalAlignment: Text.AlignHCenter
width: 58
}
}
MouseArea {
id: navMouse
anchors.fill: parent
hoverEnabled: true
onClicked: root.selected(index)
}
}
}
Item { Layout.fillHeight: true }
Label {
text: "Niri"
color: root.theme ? root.theme.dimText : "#6c7086"
font.pixelSize: 9
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
}
}
}

View File

@@ -9,10 +9,13 @@ import Quickshell.Wayland
Item {
id: root
UiTokens { id: theme }
property bool open: false
property bool dnd: false
property bool toastOpen: false
property var latest: null
property var settings
readonly property int count: server.trackedNotifications ? server.trackedNotifications.values.length : 0
signal dismissed()
@@ -32,7 +35,7 @@ Item {
Timer {
id: toastTimer
interval: 5000
interval: root.settings ? root.settings.notificationPopupDuration : 5000
repeat: false
onTriggered: root.toastOpen = false
}
@@ -48,13 +51,12 @@ Item {
Rectangle {
width: Math.min(390, parent.width - 32)
height: 100
height: 112
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: 16
x: root.settings && root.settings.notificationPosition === "top-left" ? 16 : parent.width - width - 16
radius: 16
color: "#f5f2ea"
border.color: "#d8d0c2"
color: "#181825"
border.color: "#313244"
border.width: 1
MouseArea {
@@ -69,16 +71,26 @@ Item {
anchors.fill: parent
anchors.margins: 14
spacing: 3
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: root.latest ? root.latest.appName : ""
color: "#7c756c"
color: "#9399b2"
font.pixelSize: 11
Layout.fillWidth: true
elide: Text.ElideRight
}
CCButton {
theme: theme
text: "Close"
onClicked: root.toastOpen = false
}
}
Label {
text: root.latest ? root.latest.summary : ""
color: "#252321"
color: "#cdd6f4"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
@@ -86,7 +98,7 @@ Item {
}
Label {
text: root.latest ? root.latest.body : ""
color: "#5f5952"
color: "#9399b2"
Layout.fillWidth: true
elide: Text.ElideRight
}
@@ -115,8 +127,8 @@ Item {
anchors.right: parent.right
anchors.margins: 16
radius: 20
color: "#f5f2ea"
border.color: "#d8d0c2"
color: "#11111b"
border.color: "#313244"
border.width: 1
z: 1
@@ -134,16 +146,18 @@ Item {
Layout.fillWidth: true
Label {
text: "Notifications"
color: "#252321"
color: "#cdd6f4"
font.pixelSize: 22
font.bold: true
Layout.fillWidth: true
}
Button {
CCButton {
theme: theme
text: root.dnd ? "DND on" : "DND off"
onClicked: root.dnd = !root.dnd
}
Button {
CCButton {
theme: theme
text: "Close"
onClicked: root.dismissed()
}
@@ -152,7 +166,7 @@ Item {
Label {
visible: root.count === 0
text: "No notifications"
color: "#7c756c"
color: "#9399b2"
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 18
}
@@ -173,7 +187,7 @@ Item {
Layout.fillWidth: true
implicitHeight: notificationColumn.implicitHeight + 22
radius: 14
color: "#e8e3da"
color: "#181825"
ColumnLayout {
id: notificationColumn
@@ -185,19 +199,20 @@ Item {
Layout.fillWidth: true
Label {
text: modelData.appName
color: "#7c756c"
color: "#9399b2"
font.pixelSize: 11
Layout.fillWidth: true
elide: Text.ElideRight
}
Button {
CCButton {
theme: theme
text: "x"
onClicked: modelData.dismiss()
}
}
Label {
text: modelData.summary
color: "#252321"
color: "#cdd6f4"
font.pixelSize: 15
font.bold: true
Layout.fillWidth: true
@@ -205,7 +220,7 @@ Item {
}
Label {
text: modelData.body
color: "#5f5952"
color: "#9399b2"
Layout.fillWidth: true
wrapMode: Text.Wrap
}

View File

@@ -0,0 +1,78 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
property var settings
property var theme
property var notificationCenter
implicitHeight: pageColumn.implicitHeight
ColumnLayout {
id: pageColumn
width: parent.width
spacing: 10
SettingsCard {
theme: root.theme
title: "Notifications"
description: root.notificationCenter ? root.notificationCenter.count + " tracked notifications" : "Notification service"
Layout.fillWidth: true
SettingsRow {
theme: root.theme
label: "Bar badge"
CCSwitch {
theme: root.theme
checked: root.settings.notificationBadge
onToggled: root.settings.notificationBadge = checked
}
}
SettingsRow {
theme: root.theme
label: "Popup position"
ComboBox {
palette.base: root.theme.surfaceVariant
palette.button: root.theme.surfaceVariant
palette.text: root.theme.text
palette.buttonText: root.theme.text
palette.highlight: root.theme.primary
model: ["Top right", "Top left"]
currentIndex: root.settings.notificationPosition === "top-left" ? 1 : 0
onActivated: root.settings.notificationPosition = currentIndex === 1 ? "top-left" : "top-right"
}
}
SettingsRow {
theme: root.theme
label: "Popup duration"
SpinBox {
palette.base: root.theme.surfaceVariant
palette.text: root.theme.text
palette.button: root.theme.surfaceVariant
palette.buttonText: root.theme.text
from: 1000
to: 15000
stepSize: 500
value: root.settings.notificationPopupDuration
editable: true
onValueModified: root.settings.notificationPopupDuration = value
}
Label { text: "ms"; color: root.theme.mutedText }
}
}
SettingsCard {
theme: root.theme
title: "Current queue"
description: "Click the bar badge to open the existing notification center."
Layout.fillWidth: true
CCButton {
theme: root.theme
text: "Open notifications"
accent: true
onClicked: if (root.notificationCenter) root.notificationCenter.open = !root.notificationCenter.open
}
}
}
}

View File

@@ -0,0 +1,93 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
property var settings
property var theme
implicitHeight: pageColumn.implicitHeight
ColumnLayout {
id: pageColumn
width: parent.width
spacing: 10
SettingsCard {
theme: root.theme
title: "Panel"
description: "The right-side full-height slide-out panel."
Layout.fillWidth: true
SettingsRow {
theme: root.theme
label: "Width"
SpinBox {
palette.base: root.theme.surfaceVariant
palette.text: root.theme.text
palette.button: root.theme.surfaceVariant
palette.buttonText: root.theme.text
from: 360
to: 720
value: root.settings.panelWidth
editable: true
onValueModified: root.settings.panelWidth = value
}
}
SettingsRow {
theme: root.theme
label: "Corner radius"
SpinBox {
palette.base: root.theme.surfaceVariant
palette.text: root.theme.text
palette.button: root.theme.surfaceVariant
palette.buttonText: root.theme.text
from: 8
to: 40
value: root.settings.panelRadius
editable: true
onValueModified: root.settings.panelRadius = value
}
}
SettingsRow {
theme: root.theme
label: "Opacity"
CCSlider {
theme: root.theme
implicitWidth: 130
from: 0.75
to: 1.0
value: root.settings.panelOpacity
onMoved: root.settings.panelOpacity = value
}
Label {
text: Math.round(root.settings.panelOpacity * 100) + "%"
color: root.theme.text
}
}
SettingsRow {
theme: root.theme
label: "Open from corner"
description: "A small top-right hot corner."
CCSwitch {
theme: root.theme
checked: root.settings.panelOpenOnCorner
onToggled: root.settings.panelOpenOnCorner = checked
}
}
}
SettingsCard {
theme: root.theme
title: "Motion"
description: "Content appears after the slide completes."
Layout.fillWidth: true
Label {
text: "The panel keeps its existing open/close animation and overlay dismissal."
color: root.theme.mutedText
wrapMode: Text.Wrap
Layout.fillWidth: true
}
}
}
}

View File

@@ -0,0 +1,219 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Bluetooth
import Quickshell.Networking
import Quickshell.Services.Pipewire
import Quickshell.Services.UPower
Item {
id: root
property var controlCenter
property var settings
property var theme
implicitHeight: pageColumn.implicitHeight
ColumnLayout {
id: pageColumn
width: parent.width
spacing: 10
SettingsCard {
theme: root.theme
title: "Wi-Fi"
description: root.controlCenter && root.controlCenter.wifiDevice
? (root.controlCenter.wifiDevice.name || "Available networks")
: "Wireless networking unavailable"
Layout.fillWidth: true
SettingsRow {
theme: root.theme
label: "Wireless"
CCSwitch {
theme: root.theme
checked: Networking.wifiEnabled
enabled: Networking.wifiHardwareEnabled
onToggled: Networking.wifiEnabled = checked
}
}
SettingsRow {
theme: root.theme
label: root.controlCenter && root.controlCenter.wifiDevice
&& root.controlCenter.wifiDevice.networks.values.find(network => network.connected)
? root.controlCenter.wifiDevice.networks.values.find(network => network.connected).name
: "Not connected"
CCButton {
theme: root.theme
text: "Scan"
onClicked: {
if (root.controlCenter && root.controlCenter.wifiDevice)
root.controlCenter.wifiDevice.scannerEnabled = true;
}
}
}
Repeater {
model: root.controlCenter && root.controlCenter.wifiDevice ? root.controlCenter.wifiDevice.networks : null
delegate: SettingsRow {
required property var modelData
theme: root.theme
label: modelData.name
CCButton {
theme: root.theme
text: modelData.connected ? "Disconnect" : "Connect"
enabled: !modelData.stateChanging
onClicked: modelData.connected ? modelData.disconnect() : root.controlCenter.connectWifi(modelData)
}
}
}
SettingsRow {
visible: root.controlCenter && root.controlCenter.pendingNetwork !== null
theme: root.theme
label: "Password"
TextField {
implicitWidth: 150
palette.base: root.theme.surfaceVariant
palette.text: root.theme.text
palette.placeholderText: root.theme.mutedText
palette.highlight: root.theme.primary
placeholderText: "Network password"
echoMode: TextInput.Password
text: root.controlCenter ? root.controlCenter.wifiPassword : ""
onTextChanged: if (root.controlCenter) root.controlCenter.wifiPassword = text
onAccepted: if (root.controlCenter) root.controlCenter.submitWifiPassword()
}
CCButton {
theme: root.theme
text: "Join"
enabled: root.controlCenter && root.controlCenter.wifiPassword.length > 0
onClicked: root.controlCenter.submitWifiPassword()
}
}
}
SettingsCard {
theme: root.theme
title: "Bluetooth"
description: root.controlCenter && root.controlCenter.bluetoothAdapter ? "Nearby devices" : "Bluetooth unavailable"
Layout.fillWidth: true
SettingsRow {
theme: root.theme
label: "Bluetooth"
CCSwitch {
theme: root.theme
checked: root.controlCenter && root.controlCenter.bluetoothAdapter
? root.controlCenter.bluetoothAdapter.enabled : false
enabled: root.controlCenter && root.controlCenter.bluetoothAdapter !== null
onToggled: if (root.controlCenter.bluetoothAdapter) root.controlCenter.bluetoothAdapter.enabled = checked
}
CCButton {
theme: root.theme
text: root.controlCenter && root.controlCenter.bluetoothAdapter && root.controlCenter.bluetoothAdapter.discovering ? "Stop" : "Scan"
enabled: root.controlCenter && root.controlCenter.bluetoothAdapter && root.controlCenter.bluetoothAdapter.enabled
onClicked: if (root.controlCenter.bluetoothAdapter) root.controlCenter.bluetoothAdapter.discovering = !root.controlCenter.bluetoothAdapter.discovering
}
}
Repeater {
model: root.controlCenter && root.controlCenter.bluetoothAdapter ? root.controlCenter.bluetoothAdapter.devices : null
delegate: SettingsRow {
required property var modelData
theme: root.theme
label: modelData.name || modelData.deviceName || "Unknown device"
description: modelData.connected ? "Connected" : (modelData.paired ? "Paired" : "Not paired")
Label {
visible: modelData.batteryAvailable
text: Math.round(modelData.battery * 100) + "%"
color: root.theme.mutedText
}
CCButton {
theme: root.theme
text: modelData.connected ? "Disconnect" : (modelData.paired ? "Connect" : "Pair")
onClicked: {
if (modelData.connected) modelData.disconnect();
else if (modelData.paired) modelData.connect();
else modelData.pair();
}
}
}
}
}
SettingsCard {
theme: root.theme
title: "Audio"
description: root.controlCenter && root.controlCenter.audioSink
? (root.controlCenter.audioSink.description || "Default output") : "Audio unavailable"
Layout.fillWidth: true
SettingsRow {
theme: root.theme
label: "Output volume"
CCButton {
theme: root.theme
text: root.controlCenter && root.controlCenter.audioSink && root.controlCenter.audioSink.audio && root.controlCenter.audioSink.audio.muted ? "Unmute" : "Mute"
enabled: !!(root.controlCenter && root.controlCenter.audioSink && root.controlCenter.audioSink.audio)
onClicked: root.controlCenter.audioSink.audio.muted = !root.controlCenter.audioSink.audio.muted
}
Label {
text: root.controlCenter && root.controlCenter.audioSink && root.controlCenter.audioSink.audio ? Math.round(root.controlCenter.audioSink.audio.volume * 100) + "%" : "--"
color: root.theme.text
}
}
CCSlider {
theme: root.theme
Layout.fillWidth: true
from: 0
to: 1
value: root.controlCenter && root.controlCenter.audioSink && root.controlCenter.audioSink.audio ? root.controlCenter.audioSink.audio.volume : 0
enabled: !!(root.controlCenter && root.controlCenter.audioSink && root.controlCenter.audioSink.audio)
onMoved: if (root.controlCenter.audioSink && root.controlCenter.audioSink.audio) root.controlCenter.audioSink.audio.volume = value
}
Repeater {
model: Pipewire.nodes
delegate: SettingsRow {
required property var modelData
visible: modelData.isSink && !modelData.isStream
theme: root.theme
label: modelData.description || modelData.name
CCButton {
theme: root.theme
text: modelData === root.controlCenter.audioSink ? "Default" : "Use"
enabled: modelData !== root.controlCenter.audioSink
onClicked: Pipewire.preferredDefaultAudioSink = modelData
}
}
}
}
SettingsCard {
theme: root.theme
title: "Display"
description: "Brightness and power profile"
Layout.fillWidth: true
SettingsRow {
theme: root.theme
label: "Brightness"
description: root.controlCenter && root.controlCenter.brightness > 0 ? root.controlCenter.brightness + "%" : "Unavailable"
}
CCSlider {
theme: root.theme
Layout.fillWidth: true
from: 1
to: 100
value: root.controlCenter ? root.controlCenter.brightness : 0
enabled: root.controlCenter && root.controlCenter.brightness > 0
onPressedChanged: if (!pressed && root.controlCenter) root.controlCenter.setBrightness(value)
}
SettingsRow {
theme: root.theme
label: "Power profile"
description: PowerProfile.toString(PowerProfiles.profile)
CCButton { theme: root.theme; text: "Saver"; onClicked: PowerProfiles.profile = PowerProfile.PowerSaver }
CCButton { theme: root.theme; text: "Balanced"; accent: true; onClicked: PowerProfiles.profile = PowerProfile.Balanced }
CCButton { theme: root.theme; text: "Performance"; enabled: PowerProfiles.hasPerformanceProfile; onClicked: PowerProfiles.profile = PowerProfile.Performance }
}
}
}
}

View File

@@ -9,6 +9,39 @@ QtObject {
property alias temperatureInterval: values.temperatureInterval
property alias temperatureWarning: values.temperatureWarning
property alias temperatureCritical: values.temperatureCritical
property alias panelStyle: values.panelStyle
property alias panelWidth: values.panelWidth
property alias panelRadius: values.panelRadius
property alias panelOpacity: values.panelOpacity
property alias panelOpenOnCorner: values.panelOpenOnCorner
property alias notificationBadge: values.notificationBadge
property alias notificationPosition: values.notificationPosition
property alias notificationPopupDuration: values.notificationPopupDuration
property alias barModules: values.barModules
property alias hiddenBarModules: values.hiddenBarModules
readonly property var defaultBarModules: [
"weather", "mail", "media", "notifications", "gpu", "cpu", "memory",
"disk", "keyboard", "audio", "calendar", "settings", "tray"
]
function visibleBarModules() {
const configured = Array.isArray(root.barModules) ? root.barModules : root.defaultBarModules;
const hidden = Array.isArray(root.hiddenBarModules) ? root.hiddenBarModules : [];
return configured.filter(module => root.defaultBarModules.indexOf(module) >= 0 && hidden.indexOf(module) < 0);
}
function setBarModules(modules) {
root.barModules = modules.filter(module => root.defaultBarModules.indexOf(module) >= 0);
}
function setModuleVisible(module, visible) {
let hidden = Array.isArray(root.hiddenBarModules) ? root.hiddenBarModules.slice() : [];
const index = hidden.indexOf(module);
if (visible && index >= 0) hidden.splice(index, 1);
if (!visible && index < 0) hidden.push(module);
root.hiddenBarModules = hidden;
}
function setTemperatureWarning(value) {
root.temperatureWarning = Math.min(value, root.temperatureCritical - 1);
@@ -31,6 +64,19 @@ QtObject {
property int temperatureInterval: 2000
property int temperatureWarning: 75
property int temperatureCritical: 90
property string panelStyle: "cards"
property int panelWidth: 470
property int panelRadius: 24
property real panelOpacity: 1.0
property bool panelOpenOnCorner: false
property bool notificationBadge: true
property string notificationPosition: "top-right"
property int notificationPopupDuration: 5000
property var barModules: [
"weather", "mail", "media", "notifications", "gpu", "cpu", "memory",
"disk", "keyboard", "audio", "calendar", "settings", "tray"
]
property var hiddenBarModules: []
}
}
}

View File

@@ -0,0 +1,58 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
property var theme
property string title: ""
property string description: ""
default property alias contentData: content.data
implicitHeight: cardColumn.implicitHeight + 24
Rectangle {
anchors.fill: parent
radius: root.theme ? root.theme.cardRadius : 14
color: root.theme ? root.theme.surface : "#181825"
border.color: root.theme ? root.theme.border : "#313244"
border.width: 1
}
ColumnLayout {
id: cardColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
ColumnLayout {
visible: root.title.length > 0 || root.description.length > 0
Layout.fillWidth: true
spacing: 1
Label {
text: root.title
color: root.theme ? root.theme.text : "#cdd6f4"
font.pixelSize: 15
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
}
Label {
visible: root.description.length > 0
text: root.description
color: root.theme ? root.theme.mutedText : "#9399b2"
font.pixelSize: 11
Layout.fillWidth: true
wrapMode: Text.Wrap
}
}
ColumnLayout {
id: content
Layout.fillWidth: true
spacing: 7
}
}
}

View File

@@ -0,0 +1,40 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
RowLayout {
id: root
property var theme
property string label: ""
property string description: ""
default property alias controlData: controls.data
Layout.fillWidth: true
spacing: 10
ColumnLayout {
Layout.fillWidth: true
spacing: 1
Label {
text: root.label
color: root.theme ? root.theme.text : "#cdd6f4"
Layout.fillWidth: true
elide: Text.ElideRight
}
Label {
visible: root.description.length > 0
text: root.description
color: root.theme ? root.theme.mutedText : "#9399b2"
font.pixelSize: 10
Layout.fillWidth: true
wrapMode: Text.Wrap
}
}
RowLayout {
id: controls
spacing: 6
}
}

View File

@@ -0,0 +1,91 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
property var settings
property var theme
property string cpuTemperature: "--"
property string gpuTemperature: "--"
property string temperatureColorCpu: "#9399b2"
property string temperatureColorGpu: "#9399b2"
implicitHeight: pageColumn.implicitHeight
ColumnLayout {
id: pageColumn
width: parent.width
spacing: 10
SettingsCard {
theme: root.theme
title: "Temperatures"
description: "The existing CPU and GPU readers remain active in the bar service."
Layout.fillWidth: true
SettingsRow { theme: root.theme; label: "CPU temperature"; Label { text: root.cpuTemperature; color: root.temperatureColorCpu } }
SettingsRow { theme: root.theme; label: "GPU temperature"; Label { text: root.gpuTemperature; color: root.temperatureColorGpu } }
}
SettingsCard {
theme: root.theme
title: "Polling"
description: "Used by the system metrics and temperature readers."
Layout.fillWidth: true
SettingsRow {
theme: root.theme
label: "Refresh interval"
ComboBox {
palette.base: root.theme.surfaceVariant
palette.button: root.theme.surfaceVariant
palette.text: root.theme.text
palette.buttonText: root.theme.text
palette.highlight: root.theme.primary
model: ["1 second", "2 seconds", "5 seconds"]
currentIndex: [1000, 2000, 5000].indexOf(root.settings.temperatureInterval)
onActivated: root.settings.temperatureInterval = [1000, 2000, 5000][currentIndex]
}
}
SettingsRow {
theme: root.theme
label: "Warning / critical"
SpinBox {
palette.base: root.theme.surfaceVariant
palette.text: root.theme.text
palette.button: root.theme.surfaceVariant
palette.buttonText: root.theme.text
from: 40
to: 99
value: root.settings.temperatureWarning
editable: true
onValueModified: root.settings.setTemperatureWarning(value)
}
Label { text: "/"; color: root.theme.mutedText }
SpinBox {
palette.base: root.theme.surfaceVariant
palette.text: root.theme.text
palette.button: root.theme.surfaceVariant
palette.buttonText: root.theme.text
from: 41
to: 110
value: root.settings.temperatureCritical
editable: true
onValueModified: root.settings.setTemperatureCritical(value)
}
Label { text: "°C"; color: root.theme.mutedText }
}
}
SettingsCard {
theme: root.theme
title: "Live status"
description: "Weather, keyboard layouts, workspaces and tray filtering keep their current polling paths."
Layout.fillWidth: true
Label {
text: "Screen-sharing tray items remain hidden."
color: root.theme.success
Layout.fillWidth: true
}
}
}
}

View File

@@ -0,0 +1,19 @@
import QtQuick
QtObject {
readonly property color background: "#11111b"
readonly property color surface: "#181825"
readonly property color surfaceVariant: "#1e1e2e"
readonly property color surfaceRaised: "#24243a"
readonly property color border: "#313244"
readonly property color primary: "#89b4fa"
readonly property color secondary: "#cba6f7"
readonly property color warning: "#f9e2af"
readonly property color critical: "#f38ba8"
readonly property color success: "#a6e3a1"
readonly property color text: "#cdd6f4"
readonly property color mutedText: "#9399b2"
readonly property color dimText: "#6c7086"
readonly property int cardRadius: 14
readonly property int controlRadius: 9
}

View File

@@ -15,10 +15,12 @@ Scope {
property bool open: false
property bool panelVisible: false
property bool contentReady: false
property bool mediaOpen: false
property bool notificationsOpen: false
property bool osdOpen: false
property string osdKind: "volume"
property int pageIndex: 0
property int brightness: 0
property var pendingNetwork: null
property string wifiPassword: ""
@@ -29,9 +31,8 @@ Scope {
property alias cpuTemperature: statusBar.temperature
property alias gpuTemperature: statusBar.gpuTemperature
Settings {
id: statusSettings
}
Settings { id: statusSettings }
UiTokens { id: uiTheme }
Bar {
id: statusBar
@@ -50,6 +51,7 @@ Scope {
Notifications {
id: notificationCenter
settings: root.settings
open: root.notificationsOpen
onDismissed: root.notificationsOpen = false
}
@@ -74,7 +76,6 @@ Scope {
network.connect();
return;
}
root.pendingNetwork = network;
root.wifiPassword = "";
}
@@ -102,20 +103,23 @@ Scope {
function temperatureColor(value) {
const match = String(value).match(/(\d+)C\s*$/);
const temperature = match ? Number(match[1]) : -1;
if (temperature < 0) return "#806d95";
if (temperature >= root.settings.temperatureCritical) return "#c94f6d";
if (temperature >= root.settings.temperatureWarning) return "#996315";
return "#4d7c38";
if (temperature < 0) return uiTheme.mutedText;
if (temperature >= root.settings.temperatureCritical) return uiTheme.critical;
if (temperature >= root.settings.temperatureWarning) return uiTheme.warning;
return uiTheme.success;
}
function openPanel() {
closePanelTimer.stop();
root.panelVisible = true;
root.contentReady = false;
openPanelTimer.restart();
}
function closePanel() {
openPanelTimer.stop();
contentReadyTimer.stop();
root.contentReady = false;
root.open = false;
closePanelTimer.restart();
}
@@ -129,9 +133,17 @@ Scope {
id: openPanelTimer
interval: 1
repeat: false
onTriggered: root.open = true
onTriggered: {
root.open = true;
contentReadyTimer.restart();
}
}
Timer {
id: contentReadyTimer
interval: 220
repeat: false
onTriggered: root.contentReady = true
}
Timer {
id: closePanelTimer
interval: 240
@@ -141,62 +153,22 @@ Scope {
IpcHandler {
target: "controlcenter"
function toggle() {
root.togglePanel();
function toggle() { root.togglePanel(); }
function open() { root.openPanel(); }
function close() { root.closePanel(); }
}
function open() {
root.openPanel();
}
function close() {
root.closePanel();
}
}
IpcHandler {
target: "osd"
function volume_up() { Quickshell.execDetached(["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", "5%+"]); root.showOsd("volume"); }
function volume_down() { Quickshell.execDetached(["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", "5%-"]); root.showOsd("volume"); }
function volume_mute() { Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"]); root.showOsd("volume"); }
function mic_mute() { Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_AUDIO_SOURCE@", "toggle"]); root.showOsd("mic"); }
function brightness_up() { root.adjustBrightness(5); }
function brightness_down() { root.adjustBrightness(-5); }
}
Timer { id: osdTimer; interval: 1400; repeat: false; onTriggered: root.osdOpen = false }
function volume_up() {
Quickshell.execDetached(["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", "5%+"]);
root.showOsd("volume");
}
function volume_down() {
Quickshell.execDetached(["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", "5%-"]);
root.showOsd("volume");
}
function volume_mute() {
Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"]);
root.showOsd("volume");
}
function mic_mute() {
Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_AUDIO_SOURCE@", "toggle"]);
root.showOsd("mic");
}
function brightness_up() {
root.adjustBrightness(5);
}
function brightness_down() {
root.adjustBrightness(-5);
}
}
Timer {
id: osdTimer
interval: 1400
repeat: false
onTriggered: root.osdOpen = false
}
PwObjectTracker {
objects: [root.audioSink]
}
PwObjectTracker { objects: [root.audioSink] }
Process {
id: brightnessReader
@@ -208,26 +180,18 @@ Scope {
}
}
}
Timer {
id: brightnessPoll
interval: 3000
repeat: true
running: root.open
onTriggered: {
if (!brightnessReader.running) brightnessReader.running = true;
}
onTriggered: if (!brightnessReader.running) brightnessReader.running = true
}
PanelWindow {
visible: root.panelVisible
color: "transparent"
anchors {
top: true
bottom: true
left: true
right: true
}
anchors { top: true; bottom: true; left: true; right: true }
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: root.panelVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
@@ -240,237 +204,80 @@ Scope {
Rectangle {
id: card
width: Math.min(470, parent.width - 32)
width: Math.min(root.settings.panelWidth, parent.width - 24)
height: parent.height
anchors.top: parent.top
anchors.topMargin: 0
x: root.open ? parent.width - width - 16 : parent.width + 24
radius: 20
color: "#f5f2ea"
border.color: "#d8d0c2"
radius: root.settings.panelRadius
color: Qt.rgba(uiTheme.background.r, uiTheme.background.g, uiTheme.background.b, root.settings.panelOpacity)
border.color: uiTheme.border
border.width: 1
z: 1
Behavior on x {
NumberAnimation {
duration: 220
easing.type: Easing.OutCubic
}
NumberAnimation { duration: 220; easing.type: Easing.OutCubic }
}
MouseArea {
MouseArea { anchors.fill: parent; onClicked: mouse.accepted = true }
RowLayout {
anchors.fill: parent
onClicked: mouse.accepted = true
anchors.margins: 12
spacing: 12
NavigationRail {
id: navigation
theme: uiTheme
currentIndex: root.pageIndex
Layout.fillHeight: true
onSelected: root.pageIndex = index
}
Rectangle {
Layout.fillHeight: true
Layout.preferredWidth: 1
color: uiTheme.border
}
ColumnLayout {
id: content
anchors.fill: parent
anchors.margins: 18
spacing: 12
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
RowLayout {
Layout.fillWidth: true
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Label {
text: "Control center"
color: "#252321"
text: navigation.names[root.pageIndex]
color: uiTheme.text
font.pixelSize: 22
font.bold: true
}
Label {
text: "Niri / QuickShell"
color: "#7c756c"
font.pixelSize: 12
}
}
Button {
text: "✕"
ToolTip.visible: hovered
ToolTip.text: "Close"
onClicked: root.closePanel()
}
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ColumnLayout {
width: parent.width
spacing: 10
Rectangle {
Layout.fillWidth: true
implicitHeight: wifiColumn.implicitHeight + 24
radius: 14
color: "#e5f0ef"
ColumnLayout {
id: wifiColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
Label {
text: "Wi-Fi"
color: "#1c666d"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
}
Label {
text: root.wifiDevice ? (root.wifiDevice.name || "Available") : "Unavailable"
color: "#52777a"
elide: Text.ElideRight
}
Switch {
checked: Networking.wifiEnabled
enabled: Networking.wifiHardwareEnabled
onToggled: Networking.wifiEnabled = checked
}
}
RowLayout {
visible: root.wifiDevice !== null
Layout.fillWidth: true
Label {
text: root.wifiDevice && root.wifiDevice.networks.values.find(network => network.connected)
? root.wifiDevice.networks.values.find(network => network.connected).name
: "Not connected"
color: "#252321"
elide: Text.ElideRight
Layout.fillWidth: true
}
Button {
text: "Scan"
onClicked: {
if (root.wifiDevice) root.wifiDevice.scannerEnabled = true;
}
}
}
Repeater {
model: root.wifiDevice ? root.wifiDevice.networks : null
delegate: RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: modelData.name
color: "#252321"
elide: Text.ElideRight
Layout.fillWidth: true
}
Label {
text: modelData.connected ? "Connected" : Math.round(modelData.signalStrength) + "%"
color: modelData.connected ? "#1c666d" : "#7c756c"
}
Button {
text: modelData.connected ? "Disconnect" : "Connect"
enabled: !modelData.stateChanging
onClicked: modelData.connected ? modelData.disconnect() : root.connectWifi(modelData)
}
}
}
RowLayout {
visible: root.pendingNetwork !== null
Layout.fillWidth: true
TextField {
Layout.fillWidth: true
placeholderText: "Password for " + (root.pendingNetwork ? root.pendingNetwork.name : "network")
echoMode: TextInput.Password
text: root.wifiPassword
onTextChanged: root.wifiPassword = text
onAccepted: root.submitWifiPassword()
}
Button {
text: "Join"
enabled: root.wifiPassword.length > 0
onClicked: root.submitWifiPassword()
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: bluetoothColumn.implicitHeight + 24
radius: 14
color: "#eceafd"
ColumnLayout {
id: bluetoothColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
Label {
text: "Bluetooth"
color: "#55499c"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
}
Switch {
checked: root.bluetoothAdapter ? root.bluetoothAdapter.enabled : false
enabled: root.bluetoothAdapter !== null
onToggled: {
if (root.bluetoothAdapter) root.bluetoothAdapter.enabled = checked;
}
}
Button {
text: root.bluetoothAdapter && root.bluetoothAdapter.discovering ? "Stop scan" : "Scan"
enabled: root.bluetoothAdapter && root.bluetoothAdapter.enabled
onClicked: {
if (root.bluetoothAdapter) root.bluetoothAdapter.discovering = !root.bluetoothAdapter.discovering;
}
}
}
Repeater {
model: root.bluetoothAdapter ? root.bluetoothAdapter.devices : null
delegate: RowLayout {
Layout.fillWidth: true
spacing: 8
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Label {
text: modelData.name || modelData.deviceName || "Unknown device"
color: "#252321"
elide: Text.ElideRight
Layout.fillWidth: true
}
Label {
text: modelData.connected ? "Connected" : (modelData.paired ? "Paired" : "Not paired")
color: "#7c756c"
text: "Control center / Niri"
color: uiTheme.mutedText
font.pixelSize: 11
}
}
Label {
visible: modelData.batteryAvailable
text: Math.round(modelData.battery * 100) + "%"
color: "#7c756c"
CCButton { theme: uiTheme; text: "Close"; onClicked: root.closePanel() }
}
Button {
text: modelData.connected ? "Disconnect" : (modelData.paired ? "Connect" : "Pair")
onClicked: {
if (modelData.connected) modelData.disconnect();
else if (modelData.paired) modelData.connect();
else modelData.pair();
ScrollView {
id: pageScroll
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
contentWidth: availableWidth
Loader {
id: pageLoader
width: pageScroll.availableWidth
active: root.contentReady
sourceComponent: [quickPage, barPage, panelPage, notificationsPage, statusPage][root.pageIndex]
opacity: root.contentReady ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 140 } }
}
}
}
@@ -478,271 +285,52 @@ Scope {
}
}
PanelWindow {
visible: root.settings.panelOpenOnCorner && !root.panelVisible
implicitWidth: 30
implicitHeight: 30
anchors { top: true; right: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-control-center-hot-corner"
Rectangle {
Layout.fillWidth: true
implicitHeight: audioColumn.implicitHeight + 24
radius: 14
color: "#fff1d7"
ColumnLayout {
id: audioColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
Label {
text: "Audio"
color: "#996315"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
}
Label {
text: root.audioSink ? (root.audioSink.description || "Default output") : "Unavailable"
color: "#806a48"
elide: Text.ElideRight
Layout.maximumWidth: 230
anchors.margins: 4
radius: 8
color: uiTheme.primary
opacity: 0.35
MouseArea { anchors.fill: parent; onClicked: root.openPanel() }
}
}
RowLayout {
Layout.fillWidth: true
Button {
text: root.audioSink && root.audioSink.audio && root.audioSink.audio.muted ? "Unmute" : "Mute"
enabled: root.audioSink && root.audioSink.audio
onClicked: {
if (root.audioSink && root.audioSink.audio) root.audioSink.audio.muted = !root.audioSink.audio.muted;
}
}
Slider {
Layout.fillWidth: true
from: 0
to: 1
value: root.audioSink && root.audioSink.audio ? root.audioSink.audio.volume : 0
enabled: root.audioSink && root.audioSink.audio
onMoved: {
if (root.audioSink && root.audioSink.audio) root.audioSink.audio.volume = value;
}
}
Label {
text: root.audioSink && root.audioSink.audio ? Math.round(root.audioSink.audio.volume * 100) + "%" : "--"
color: "#806a48"
}
}
Repeater {
model: Pipewire.nodes
delegate: RowLayout {
visible: modelData.isSink && !modelData.isStream
Layout.fillWidth: true
Label {
text: modelData.description || modelData.name
color: "#252321"
elide: Text.ElideRight
Layout.fillWidth: true
}
Button {
text: modelData === root.audioSink ? "Default" : "Use"
enabled: modelData !== root.audioSink
onClicked: Pipewire.preferredDefaultAudioSink = modelData
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: displayColumn.implicitHeight + 24
radius: 14
color: "#f8e4dc"
ColumnLayout {
id: displayColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
Label {
text: "Display brightness"
color: "#9e4d39"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
}
Label {
text: root.brightness > 0 ? root.brightness + "%" : "Unavailable"
color: "#8b665b"
}
}
Slider {
Layout.fillWidth: true
from: 1
to: 100
value: root.brightness
enabled: root.brightness > 0
onPressedChanged: {
if (!pressed) root.setBrightness(value);
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: statusColumn.implicitHeight + 24
radius: 14
color: "#eee8f7"
ColumnLayout {
id: statusColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
Label {
text: "Status bar"
color: "#684b8f"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
}
Label {
text: "Temperature"
color: "#806d95"
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "CPU temperature"
color: "#252321"
Layout.fillWidth: true
}
Label {
text: root.cpuTemperature
color: root.temperatureColor(root.cpuTemperature)
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "GPU temperature"
color: "#252321"
Layout.fillWidth: true
}
Label {
text: root.gpuTemperature
color: root.temperatureColor(root.gpuTemperature)
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "Refresh interval"
color: "#252321"
Layout.fillWidth: true
}
ComboBox {
model: ["1 second", "2 seconds", "5 seconds"]
currentIndex: [1000, 2000, 5000].indexOf(root.settings.temperatureInterval)
onActivated: root.settings.temperatureInterval = [1000, 2000, 5000][currentIndex]
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "Warning / critical"
color: "#252321"
Layout.fillWidth: true
}
SpinBox {
from: 40
to: 99
value: root.settings.temperatureWarning
editable: true
onValueModified: root.settings.setTemperatureWarning(value)
}
Label {
text: "/"
color: "#7c756c"
}
SpinBox {
from: 41
to: 110
value: root.settings.temperatureCritical
editable: true
onValueModified: root.settings.setTemperatureCritical(value)
}
Label {
text: "°C"
color: "#7c756c"
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: powerColumn.implicitHeight + 24
radius: 14
color: "#e8f1df"
ColumnLayout {
id: powerColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
Label {
text: "Power profile"
color: "#4d7c38"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
}
Label {
text: PowerProfile.toString(PowerProfiles.profile)
color: "#66825a"
}
}
RowLayout {
Layout.fillWidth: true
Button {
text: "Saver"
Layout.fillWidth: true
onClicked: PowerProfiles.profile = PowerProfile.PowerSaver
}
Button {
text: "Balanced"
Layout.fillWidth: true
onClicked: PowerProfiles.profile = PowerProfile.Balanced
}
Button {
text: "Performance"
enabled: PowerProfiles.hasPerformanceProfile
Layout.fillWidth: true
onClicked: PowerProfiles.profile = PowerProfile.Performance
}
}
}
Component {
id: quickPage
QuickPage { width: pageLoader.width; controlCenter: root; settings: root.settings; theme: uiTheme }
}
Component {
id: barPage
BarPage { width: pageLoader.width; settings: root.settings; theme: uiTheme; availableModules: root.settings.defaultBarModules }
}
Component {
id: panelPage
PanelPage { width: pageLoader.width; settings: root.settings; theme: uiTheme }
}
Component {
id: notificationsPage
NotificationsPage { width: pageLoader.width; settings: root.settings; theme: uiTheme; notificationCenter: notificationCenter }
}
Component {
id: statusPage
StatusPage {
width: pageLoader.width
settings: root.settings
theme: uiTheme
cpuTemperature: root.cpuTemperature
gpuTemperature: root.gpuTemperature
temperatureColorCpu: root.temperatureColor(root.cpuTemperature)
temperatureColorGpu: root.temperatureColor(root.gpuTemperature)
}
}
}