Add tabs, a performance page and a mixer button to Quick Settings

This commit is contained in:
SirBlob committed 2026-09-24 12:37:35 -04:00
1 parent 3164e50b36
commit 0ed5e22fc6
11 files changed
+520 -158

No files matched your search

File renamed without changes.
+120
View File
@@ -0,0 +1,120 @@
import QtQuick
import Quickshell.Io
Item {
id: root
property bool polling: false
property int cpuPercent: 0
property int memoryPercent: 0
property string memoryDetail: ""
property int diskPercent: 0
property string diskDetail: ""
property int temperature: 0
property string loadAverage: ""
property real previousIdle: 0
property real previousTotal: 0
function readCpu(raw) {
var values = String(raw).trim().split(/\s+/).slice(1).map(Number)
var idle = (values[3] || 0) + (values[4] || 0)
var total = 0
for (var i = 0; i < values.length; i++) total += values[i] || 0
var idleDelta = idle - root.previousIdle
var totalDelta = total - root.previousTotal
root.previousIdle = idle
root.previousTotal = total
if (totalDelta <= 0) return
root.cpuPercent = Math.round((1 - idleDelta / totalDelta) * 100)
}
function readMemory(raw) {
var parts = String(raw).trim().split(/\s+/).map(Number)
var used = parts[0] || 0
var total = parts[1] || 0
if (!total) {
root.memoryPercent = 0
root.memoryDetail = ""
return
}
root.memoryPercent = Math.round(used / total * 100)
root.memoryDetail = (used / 1024).toFixed(1) + "G / " + (total / 1024).toFixed(1) + "G"
}
function readDisk(raw) {
var parts = String(raw).trim().split(/\s+/)
root.diskPercent = Number(String(parts[0] || "").replace("%", "")) || 0
root.diskDetail = (parts[1] || "0") + " / " + (parts[2] || "0")
}
function readLoad(raw) {
var parts = String(raw).trim().split(/\s+/)
root.loadAverage = parts.length >= 3 ? parts.slice(0, 3).join(" ") : ""
}
function refreshFast() {
if (!cpuProbe.running) cpuProbe.running = true
if (!memoryProbe.running) memoryProbe.running = true
if (!temperatureProbe.running) temperatureProbe.running = true
if (!loadProbe.running) loadProbe.running = true
}
function refreshSlow() {
if (!diskProbe.running) diskProbe.running = true
}
function refresh() {
refreshFast()
refreshSlow()
}
Process {
id: cpuProbe
command: ["bash", "-c", "grep '^cpu ' /proc/stat"]
stdout: StdioCollector { onStreamFinished: root.readCpu(text) }
}
Process {
id: memoryProbe
command: ["bash", "-c", "free -m | awk '/^Mem:/ {print $3\" \"$2}'"]
stdout: StdioCollector { onStreamFinished: root.readMemory(text) }
}
Process {
id: diskProbe
command: ["bash", "-c", "df -h --output=pcent,used,size / | tail -1"]
stdout: StdioCollector { onStreamFinished: root.readDisk(text) }
}
Process {
id: temperatureProbe
command: ["bash", "-c", "cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0"]
stdout: StdioCollector {
onStreamFinished: root.temperature = Math.round(Number(String(text).trim()) / 1000) || 0
}
}
Process {
id: loadProbe
command: ["bash", "-c", "cat /proc/loadavg"]
stdout: StdioCollector { onStreamFinished: root.readLoad(text) }
}
Timer {
interval: 2000
running: root.polling
repeat: true
onTriggered: root.refreshFast()
}
Timer {
interval: 30000
running: root.polling
repeat: true
onTriggered: root.refreshSlow()
}
onPollingChanged: if (root.polling) root.refresh()
}
+2
View File
@@ -42,3 +42,5 @@ CalendarModel 1.0 CalendarModel.js
CalendarView 1.0 CalendarView.qml
CalendarDays 1.0 CalendarDays.qml
CalendarMonthPicker 1.0 CalendarMonthPicker.qml
MetricBar 1.0 MetricBar.qml
SystemStats 1.0 SystemStats.qml
+20 -2
View File
@@ -454,6 +454,11 @@ Panel {
// The hero switch is the whole panel's on/off, so it carries both channels
// at once. It reads as on while anything is still audible, which keeps
// muting a single channel from the row below flipping the master switch.
function openMixer() {
root.close()
Quickshell.execDetached(["uwsm-app", "--", "pavucontrol"])
}
function toggleAllMuted() {
var mute = anyAudible
if (hasOutput) volumeSink.audio.muted = mute
@@ -669,6 +674,7 @@ Panel {
onCloseRequested: root.close()
onTabRequested: function(direction) { root.switchPanel(direction) }
onTextKey: function(t) {
if (t === "x" || t === "X") { root.openMixer(); return }
// 'm' mutes whatever the cursor is on: focused section's slider
// for output/input, the focused stream for streams.
if (t === "m" || t === "M") {
@@ -741,12 +747,24 @@ Panel {
}
}
PanelActionButton {
id: mixerButton
anchors.right: powerSwitch.left
anchors.rightMargin: Style.space(8)
anchors.verticalCenter: parent.verticalCenter
iconText: ""
tooltipText: "Mixer (x)"
foreground: root.bar.foreground
fontFamily: root.bar.fontFamily
onClicked: root.openMixer()
}
Column {
id: heroLabels
anchors.left: heroIcon.right
anchors.leftMargin: Style.space(14)
anchors.right: parent.right
anchors.rightMargin: powerSwitch.width + Style.space(12)
anchors.right: mixerButton.left
anchors.rightMargin: Style.space(12)
anchors.verticalCenter: parent.verticalCenter
spacing: Style.space(2)
@@ -0,0 +1,137 @@
import QtQuick
import Quickshell.Io
import qs.Commons
import qs.Ui
Column {
id: root
property bool polling: false
signal commandRequested(string command)
signal tuiRequested(string command)
readonly property var profileLabels: ({
"power-saver": "Power saver",
"balanced": "Balanced",
"performance": "Performance"
})
property var profiles: []
property string activeProfile: ""
spacing: Style.spaceReal(12)
function readProfiles(raw) {
var available = []
var active = ""
var lines = String(raw).trim().split("\n")
for (var i = 0; i < lines.length; i++) {
var fields = lines[i].split("\t")
if (!fields[0]) continue
available.push({ value: fields[0], label: root.profileLabels[fields[0]] || fields[0] })
if (fields[1] === "1") active = fields[0]
}
root.profiles = available
root.activeProfile = active
}
function refreshProfiles() {
if (!profileProbe.running) profileProbe.running = true
}
function setProfile(profile) {
root.activeProfile = profile
root.commandRequested("blob-power-set autodetect " + profile)
profileSettleTimer.restart()
}
onPollingChanged: if (root.polling) root.refreshProfiles()
SystemStats {
id: stats
polling: root.polling
}
Process {
id: profileProbe
command: ["blob-power-list", "--active-state"]
stdout: StdioCollector { onStreamFinished: root.readProfiles(text) }
}
Timer {
id: profileSettleTimer
interval: 500
onTriggered: root.refreshProfiles()
}
MetricBar {
width: parent.width
icon: "󰍛"
name: "CPU"
percent: stats.cpuPercent
detail: stats.cpuPercent + "%"
}
MetricBar {
width: parent.width
icon: "󰘚"
name: "Memory"
percent: stats.memoryPercent
detail: stats.memoryDetail
}
MetricBar {
width: parent.width
icon: "󰋊"
name: "Disk"
percent: stats.diskPercent
detail: stats.diskDetail
}
MetricBar {
width: parent.width
icon: ""
name: "Temperature"
percent: stats.temperature
detail: stats.temperature + "°C"
}
Text {
width: parent.width
visible: stats.loadAverage !== ""
text: "Load average " + stats.loadAverage
textFormat: Text.PlainText
color: Util.alpha(Color.foreground, 0.7)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
Row {
width: parent.width
visible: root.profiles.length > 0
spacing: Style.spaceReal(6)
Repeater {
model: root.profiles
Button {
required property var modelData
width: (parent.width - parent.spacing * (root.profiles.length - 1)) / root.profiles.length
text: modelData.label
selected: modelData.value === root.activeProfile
bordered: true
fontSize: Style.font.bodySmall
onClicked: root.setProfile(modelData.value)
}
}
}
Button {
width: parent.width
text: "Open btop"
iconText: "󰍛"
bordered: true
fontSize: Style.font.bodySmall
onClicked: root.tuiRequested("btop")
}
}
@@ -15,6 +15,18 @@ Item {
readonly property string pluginId: (manifest && manifest.id) || "blob.quick-settings"
readonly property var tabs: [
{ id: "general", label: "General", icon: "" },
{ id: "sound", label: "Sound & Music", icon: "" },
{ id: "performance", label: "Performance", icon: "" },
{ id: "quick", label: "Quick Settings", icon: "" }
]
property string currentTab: "general"
readonly property bool generalVisible: opened && currentTab === "general"
readonly property bool soundVisible: opened && currentTab === "sound"
readonly property bool performanceVisible: opened && currentTab === "performance"
readonly property bool quickVisible: opened && currentTab === "quick"
readonly property int barInset: {
var bar = root.shell ? root.shell.bar : null
if (!bar || bar.barHidden === true) return 0
@@ -60,6 +72,8 @@ Item {
recordingFlag.refresh()
volumeValue.refresh()
mutedFlag.refresh()
micVolumeValue.refresh()
micMutedFlag.refresh()
brightnessValue.refresh()
mediaValue.refresh()
}
@@ -77,57 +91,71 @@ Item {
PolledFlag {
id: bluetoothFlag
polling: root.opened
polling: root.quickVisible
command: "bluetoothctl show 2>/dev/null | grep -q 'Powered: yes' && echo on || echo off"
}
PolledFlag {
id: dndFlag
polling: root.opened
polling: root.quickVisible
command: "blob-shell notifications dndState 2>/dev/null"
}
PolledFlag {
id: nightLightFlag
polling: root.opened
polling: root.quickVisible
command: "blob-toggle-nightlight --status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
}
PolledFlag {
id: stayAwakeFlag
polling: root.opened
polling: root.quickVisible
command: "blob-toggle-idle status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
}
PolledFlag {
id: tabletFlag
polling: root.opened
polling: root.quickVisible
command: "blob-toggle-tablet status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
}
PolledFlag {
id: recordingFlag
polling: root.opened
polling: root.quickVisible
command: "pgrep -f '^gpu-screen-recorder' >/dev/null && echo on || echo off"
}
PolledFlag {
id: mutedFlag
polling: root.opened
polling: root.soundVisible
intervalMs: 1000
command: "pamixer --get-mute 2>/dev/null | grep -qx true && echo on || echo off"
}
PolledText {
id: volumeValue
polling: root.opened
polling: root.soundVisible
intervalMs: 1000
command: "pamixer --get-volume 2>/dev/null || echo 0"
}
PolledFlag {
id: micMutedFlag
polling: root.soundVisible
intervalMs: 1000
command: "pamixer --default-source --get-mute 2>/dev/null | grep -qx true && echo on || echo off"
}
PolledText {
id: micVolumeValue
polling: root.soundVisible
intervalMs: 1000
command: "pamixer --default-source --get-volume 2>/dev/null || echo 0"
}
PolledText {
id: brightnessValue
polling: root.opened
polling: root.generalVisible
intervalMs: 2000
command: "brightnessctl -m 2>/dev/null | cut -d, -f4 | tr -d '%' || echo 0"
}
@@ -148,7 +176,7 @@ Item {
PolledText {
id: mediaValue
polling: root.opened
polling: root.soundVisible
intervalMs: 1000
command: "playerctl metadata -f '{{title}}|||{{artist}}|||{{status}}' 2>/dev/null || echo 'No Media||||||Stopped'"
}
@@ -221,13 +249,70 @@ Item {
onCloseRequested: root.dismiss()
}
Calendar {
TabBar {
width: parent.width
polling: root.opened
tabs: root.tabs
currentTab: root.currentTab
onTabRequested: function(tabId) { root.currentTab = tabId }
}
Column {
width: parent.width
visible: root.currentTab === "general"
spacing: Style.spaceReal(12)
Calendar {
width: parent.width
polling: root.generalVisible
}
SliderRow {
width: parent.width
icon: "\uf185"
value: Number(brightnessValue.value) || 0
onValueRequested: function(next) {
root.run("brightnessctl set " + next + "%")
brightnessValue.value = String(next)
}
}
}
SoundTab {
width: parent.width
visible: root.currentTab === "sound"
outputVolume: Number(volumeValue.value) || 0
outputMuted: mutedFlag.value
inputVolume: Number(micVolumeValue.value) || 0
inputMuted: micMutedFlag.value
mediaParts: root.mediaParts
onCommandRequested: function(command) { root.toggleAndRefresh(command) }
onOutputVolumeRequested: function(next) {
root.run("pamixer --set-volume " + next)
volumeValue.value = String(next)
}
onInputVolumeRequested: function(next) {
root.run("pamixer --default-source --set-volume " + next)
micVolumeValue.value = String(next)
}
onMediaControlRequested: function(action) {
root.run("playerctl " + action)
stateSettleTimer.restart()
}
onPanelRequested: function(pluginId) { root.openPanel(pluginId) }
onAppRequested: function(command) { root.runAndDismiss("uwsm-app -- " + command) }
}
PerformanceTab {
width: parent.width
visible: root.currentTab === "performance"
polling: root.performanceVisible
onCommandRequested: function(command) { root.run(command) }
onTuiRequested: function(command) { root.runAndDismiss("blob-launch-or-focus-tui " + command) }
}
Tiles {
width: parent.width
visible: root.currentTab === "quick"
bluetoothOn: bluetoothFlag.value
doNotDisturb: dndFlag.value
nightLight: nightLightFlag.value
@@ -238,39 +323,6 @@ Item {
onPanelRequested: function(pluginId) { root.openPanel(pluginId) }
onToggleRequested: function(command) { root.toggleAndRefresh(command) }
}
SliderRow {
width: parent.width
icon: mutedFlag.value ? "\uf026" : "\uf028"
dimmed: mutedFlag.value
value: Number(volumeValue.value) || 0
onIconActivated: root.toggleAndRefresh("pamixer --toggle-mute")
onValueRequested: function(next) {
root.run("pamixer --set-volume " + next)
volumeValue.value = String(next)
}
}
SliderRow {
width: parent.width
icon: "\uf185"
value: Number(brightnessValue.value) || 0
onValueRequested: function(next) {
root.run("brightnessctl set " + next + "%")
brightnessValue.value = String(next)
}
}
MediaRow {
width: parent.width
title: root.mediaParts.length > 0 ? root.mediaParts[0] : ""
artist: root.mediaParts.length > 1 ? root.mediaParts[1] : ""
status: root.mediaParts.length > 2 ? root.mediaParts[2] : "Stopped"
onControlRequested: function(action) {
root.run("playerctl " + action)
stateSettleTimer.restart()
}
}
}
}
@@ -0,0 +1,83 @@
import QtQuick
import qs.Commons
import qs.Ui
Column {
id: root
property int outputVolume: 0
property bool outputMuted: false
property int inputVolume: 0
property bool inputMuted: false
property var mediaParts: []
signal commandRequested(string command)
signal outputVolumeRequested(int value)
signal inputVolumeRequested(int value)
signal mediaControlRequested(string action)
signal panelRequested(string pluginId)
signal appRequested(string command)
spacing: Style.spaceReal(12)
SliderRow {
width: parent.width
icon: root.outputMuted ? "" : ""
dimmed: root.outputMuted
value: root.outputVolume
onIconActivated: root.commandRequested("pamixer --toggle-mute")
onValueRequested: function(next) { root.outputVolumeRequested(next) }
}
SliderRow {
width: parent.width
icon: root.inputMuted ? "" : ""
dimmed: root.inputMuted
value: root.inputVolume
onIconActivated: root.commandRequested("pamixer --default-source --toggle-mute")
onValueRequested: function(next) { root.inputVolumeRequested(next) }
}
MediaRow {
id: mediaRow
width: parent.width
title: root.mediaParts.length > 0 ? root.mediaParts[0] : ""
artist: root.mediaParts.length > 1 ? root.mediaParts[1] : ""
status: root.mediaParts.length > 2 ? root.mediaParts[2] : "Stopped"
onControlRequested: function(action) { root.mediaControlRequested(action) }
}
Text {
width: parent.width
visible: !mediaRow.visible
horizontalAlignment: Text.AlignHCenter
text: "Nothing playing"
textFormat: Text.PlainText
color: Util.alpha(Color.foreground, 0.6)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
Row {
width: parent.width
spacing: Style.spaceReal(6)
Button {
width: (parent.width - parent.spacing) / 2
text: "Audio devices"
iconText: ""
bordered: true
fontSize: Style.font.bodySmall
onClicked: root.panelRequested("blob.audio")
}
Button {
width: (parent.width - parent.spacing) / 2
text: "Mixer"
iconText: ""
bordered: true
fontSize: Style.font.bodySmall
onClicked: root.appRequested("pavucontrol")
}
}
}
@@ -0,0 +1,28 @@
import QtQuick
import qs.Commons
import qs.Ui
Row {
id: root
property var tabs: []
property string currentTab: ""
signal tabRequested(string tabId)
spacing: Style.spaceReal(6)
Repeater {
model: root.tabs
Button {
required property var modelData
width: (root.width - root.spacing * (root.tabs.length - 1)) / root.tabs.length
text: modelData.label
iconText: modelData.icon
selected: modelData.id === root.currentTab
bordered: true
fontSize: Style.font.bodySmall
onClicked: root.tabRequested(modelData.id)
}
}
}
+15 -108
View File
@@ -14,16 +14,6 @@ Item {
readonly property string pluginId: (manifest && manifest.id) || "blob.sysmon"
property int cpuPercent: 0
property int memoryPercent: 0
property string memoryDetail: ""
property int diskPercent: 0
property string diskDetail: ""
property int temperature: 0
property real previousIdle: 0
property real previousTotal: 0
function open(payloadJson) {
root.opened = true
}
@@ -38,92 +28,9 @@ Item {
root.shell.hide(root.pluginId)
}
function readCpu(raw) {
var values = String(raw).trim().split(/\s+/).slice(1).map(Number)
var idle = (values[3] || 0) + (values[4] || 0)
var total = 0
for (var i = 0; i < values.length; i++) total += values[i] || 0
var idleDelta = idle - root.previousIdle
var totalDelta = total - root.previousTotal
root.previousIdle = idle
root.previousTotal = total
if (totalDelta <= 0) return
root.cpuPercent = Math.round((1 - idleDelta / totalDelta) * 100)
}
function readMemory(raw) {
var parts = String(raw).trim().split(/\s+/).map(Number)
var used = parts[0] || 0
var total = parts[1] || 0
if (!total) {
root.memoryPercent = 0
root.memoryDetail = ""
return
}
root.memoryPercent = Math.round(used / total * 100)
root.memoryDetail = (used / 1024).toFixed(1) + "G / " + (total / 1024).toFixed(1) + "G"
}
function readDisk(raw) {
var parts = String(raw).trim().split(/\s+/)
root.diskPercent = Number(String(parts[0] || "").replace("%", "")) || 0
root.diskDetail = (parts[1] || "0") + " / " + (parts[2] || "0")
}
Process {
id: cpuProbe
command: ["bash", "-c", "grep '^cpu ' /proc/stat"]
stdout: StdioCollector { onStreamFinished: root.readCpu(text) }
}
Process {
id: memoryProbe
command: ["bash", "-c", "free -m | awk '/^Mem:/ {print $3\" \"$2}'"]
stdout: StdioCollector { onStreamFinished: root.readMemory(text) }
}
Process {
id: diskProbe
command: ["bash", "-c", "df -h --output=pcent,used,size / | tail -1"]
stdout: StdioCollector { onStreamFinished: root.readDisk(text) }
}
Process {
id: temperatureProbe
command: ["bash", "-c", "cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0"]
stdout: StdioCollector {
onStreamFinished: root.temperature = Math.round(Number(String(text).trim()) / 1000) || 0
}
}
function refreshFast() {
if (!cpuProbe.running) cpuProbe.running = true
if (!memoryProbe.running) memoryProbe.running = true
if (!temperatureProbe.running) temperatureProbe.running = true
}
function refreshSlow() {
if (!diskProbe.running) diskProbe.running = true
}
Timer {
interval: 2000
running: root.opened
repeat: true
onTriggered: root.refreshFast()
}
Timer {
interval: 30000
running: root.opened
repeat: true
onTriggered: root.refreshSlow()
}
onOpenedChanged: {
if (!root.opened) return
root.refreshFast()
root.refreshSlow()
SystemStats {
id: stats
polling: root.opened
}
PanelWindow {
@@ -158,36 +65,36 @@ Item {
font.bold: true
}
Metric {
MetricBar {
width: parent.width
icon: "󰍛"
name: "CPU"
percent: root.cpuPercent
detail: root.cpuPercent + "%"
percent: stats.cpuPercent
detail: stats.cpuPercent + "%"
}
Metric {
MetricBar {
width: parent.width
icon: "󰘚"
name: "Memory"
percent: root.memoryPercent
detail: root.memoryDetail
percent: stats.memoryPercent
detail: stats.memoryDetail
}
Metric {
MetricBar {
width: parent.width
icon: "󰋊"
name: "Disk"
percent: root.diskPercent
detail: root.diskDetail
percent: stats.diskPercent
detail: stats.diskDetail
}
Metric {
MetricBar {
width: parent.width
icon: ""
name: "Temperature"
percent: root.temperature
detail: root.temperature + "°C"
percent: stats.temperature
detail: stats.temperature + "°C"
}
}
}