Add tabs, a performance page and a mixer button to Quick Settings
This commit is contained in:
1 parent
3164e50b36
commit
0ed5e22fc6
11 files changed
+520
-158
No files matched your search
@@ -4,7 +4,7 @@ o.window({ tag = "floating-window" }, { center = true })
|
|||||||
o.window({ tag = "floating-window" }, { size = { 875, 600 } })
|
o.window({ tag = "floating-window" }, { size = { 875, 600 } })
|
||||||
|
|
||||||
o.window(
|
o.window(
|
||||||
"(org.blob.btop|org.blob.terminal|org.blob.bash|org.codeberg.dnkl.foot|org.gnome.NautilusPreviewer|org.gnome.Evince|Blob|About|TUI.float|imv|mpv)",
|
"(org.blob.btop|org.blob.terminal|org.blob.bash|org.codeberg.dnkl.foot|org.gnome.NautilusPreviewer|org.gnome.Evince|Blob|About|TUI.float|imv|mpv|org.pulseaudio.pavucontrol)",
|
||||||
{
|
{
|
||||||
tag = "+floating-window",
|
tag = "+floating-window",
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-2
@@ -9,8 +9,8 @@ autostarted.
|
|||||||
|
|
||||||
| Was | Is | Note |
|
| Was | Is | Note |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `QuickSettings.tsx` (523 lines) | `shell/plugins/panels/quick-settings/` | split across 9 files, none over 250 lines |
|
| `QuickSettings.tsx` (523 lines) | `shell/plugins/panels/quick-settings/` | split across 13 files, none over 350 lines, with four tabs |
|
||||||
| `SysMonitor.tsx` (142) | `shell/plugins/panels/sysmon/` | CPU, memory, disk, temperature |
|
| `SysMonitor.tsx` (142) | `shell/plugins/panels/sysmon/` | CPU, memory, disk, temperature; the probes and bars are `Ui/SystemStats.qml` and `Ui/MetricBar.qml`, shared with the Performance tab |
|
||||||
| `Notifications.tsx` (160) | `shell/plugins/notification-center/` | reads the notification service's own history files |
|
| `Notifications.tsx` (160) | `shell/plugins/notification-center/` | reads the notification service's own history files |
|
||||||
| `WallPicker.tsx` (106) | the existing image picker | `blob-wallpaper-set --menu` now calls `blob-menu-images`, so wallpapers get the image grid instead of a text list |
|
| `WallPicker.tsx` (106) | the existing image picker | `blob-wallpaper-set --menu` now calls `blob-menu-images`, so wallpapers get the image grid instead of a text list |
|
||||||
| `ThemePicker.tsx` (178) | the menu and `blob-theme-switcher` | the shell already had a theme switcher; a second picker was duplicate surface |
|
| `ThemePicker.tsx` (178) | the menu and `blob-theme-switcher` | the shell already had a theme switcher; a second picker was duplicate surface |
|
||||||
@@ -33,6 +33,21 @@ The CSS was written against pywal's `@color0`, `@color4` and `@color5` aliases.
|
|||||||
falling back to `accent` for themes that define neither. `color0` is
|
falling back to `accent` for themes that define neither. `color0` is
|
||||||
`background`, which Color already had.
|
`background`, which Color already had.
|
||||||
|
|
||||||
|
## Quick settings tabs
|
||||||
|
|
||||||
|
Under the header, a tab bar switches the card between four pages. Each page
|
||||||
|
only polls the state it shows, so an open card on the General tab is not
|
||||||
|
running `pamixer` and `playerctl` every second.
|
||||||
|
|
||||||
|
| Tab | Holds |
|
||||||
|
| --- | --- |
|
||||||
|
| General | the calendar and the brightness slider |
|
||||||
|
| Sound & Music | output and microphone volume (click the icon to mute), the media row with its controls, and buttons for the audio panel (picking devices) and the pavucontrol mixer |
|
||||||
|
| Performance | CPU, memory, disk and temperature bars, the load average, the power profile (Power saver, Balanced, Performance, whichever the machine offers), and a button that opens btop |
|
||||||
|
| Quick Settings | the toggle tiles: Wi-Fi, Bluetooth, Wallpaper, Silence, Night Light, Record, Stay Awake, Pick Color, Theme, Tablet Lock |
|
||||||
|
|
||||||
|
The weather card beside it is not part of the tabs and stays visible.
|
||||||
|
|
||||||
## Opening them
|
## Opening them
|
||||||
|
|
||||||
| Panel | Opened by |
|
| Panel | Opened by |
|
||||||
|
|||||||
File renamed without changes.
@@ -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()
|
||||||
|
}
|
||||||
@@ -42,3 +42,5 @@ CalendarModel 1.0 CalendarModel.js
|
|||||||
CalendarView 1.0 CalendarView.qml
|
CalendarView 1.0 CalendarView.qml
|
||||||
CalendarDays 1.0 CalendarDays.qml
|
CalendarDays 1.0 CalendarDays.qml
|
||||||
CalendarMonthPicker 1.0 CalendarMonthPicker.qml
|
CalendarMonthPicker 1.0 CalendarMonthPicker.qml
|
||||||
|
MetricBar 1.0 MetricBar.qml
|
||||||
|
SystemStats 1.0 SystemStats.qml
|
||||||
@@ -454,6 +454,11 @@ Panel {
|
|||||||
// The hero switch is the whole panel's on/off, so it carries both channels
|
// 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
|
// 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.
|
// muting a single channel from the row below flipping the master switch.
|
||||||
|
function openMixer() {
|
||||||
|
root.close()
|
||||||
|
Quickshell.execDetached(["uwsm-app", "--", "pavucontrol"])
|
||||||
|
}
|
||||||
|
|
||||||
function toggleAllMuted() {
|
function toggleAllMuted() {
|
||||||
var mute = anyAudible
|
var mute = anyAudible
|
||||||
if (hasOutput) volumeSink.audio.muted = mute
|
if (hasOutput) volumeSink.audio.muted = mute
|
||||||
@@ -669,6 +674,7 @@ Panel {
|
|||||||
onCloseRequested: root.close()
|
onCloseRequested: root.close()
|
||||||
onTabRequested: function(direction) { root.switchPanel(direction) }
|
onTabRequested: function(direction) { root.switchPanel(direction) }
|
||||||
onTextKey: function(t) {
|
onTextKey: function(t) {
|
||||||
|
if (t === "x" || t === "X") { root.openMixer(); return }
|
||||||
// 'm' mutes whatever the cursor is on: focused section's slider
|
// 'm' mutes whatever the cursor is on: focused section's slider
|
||||||
// for output/input, the focused stream for streams.
|
// for output/input, the focused stream for streams.
|
||||||
if (t === "m" || t === "M") {
|
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 {
|
Column {
|
||||||
id: heroLabels
|
id: heroLabels
|
||||||
anchors.left: heroIcon.right
|
anchors.left: heroIcon.right
|
||||||
anchors.leftMargin: Style.space(14)
|
anchors.leftMargin: Style.space(14)
|
||||||
anchors.right: parent.right
|
anchors.right: mixerButton.left
|
||||||
anchors.rightMargin: powerSwitch.width + Style.space(12)
|
anchors.rightMargin: Style.space(12)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Style.space(2)
|
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 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: {
|
readonly property int barInset: {
|
||||||
var bar = root.shell ? root.shell.bar : null
|
var bar = root.shell ? root.shell.bar : null
|
||||||
if (!bar || bar.barHidden === true) return 0
|
if (!bar || bar.barHidden === true) return 0
|
||||||
@@ -60,6 +72,8 @@ Item {
|
|||||||
recordingFlag.refresh()
|
recordingFlag.refresh()
|
||||||
volumeValue.refresh()
|
volumeValue.refresh()
|
||||||
mutedFlag.refresh()
|
mutedFlag.refresh()
|
||||||
|
micVolumeValue.refresh()
|
||||||
|
micMutedFlag.refresh()
|
||||||
brightnessValue.refresh()
|
brightnessValue.refresh()
|
||||||
mediaValue.refresh()
|
mediaValue.refresh()
|
||||||
}
|
}
|
||||||
@@ -77,57 +91,71 @@ Item {
|
|||||||
|
|
||||||
PolledFlag {
|
PolledFlag {
|
||||||
id: bluetoothFlag
|
id: bluetoothFlag
|
||||||
polling: root.opened
|
polling: root.quickVisible
|
||||||
command: "bluetoothctl show 2>/dev/null | grep -q 'Powered: yes' && echo on || echo off"
|
command: "bluetoothctl show 2>/dev/null | grep -q 'Powered: yes' && echo on || echo off"
|
||||||
}
|
}
|
||||||
|
|
||||||
PolledFlag {
|
PolledFlag {
|
||||||
id: dndFlag
|
id: dndFlag
|
||||||
polling: root.opened
|
polling: root.quickVisible
|
||||||
command: "blob-shell notifications dndState 2>/dev/null"
|
command: "blob-shell notifications dndState 2>/dev/null"
|
||||||
}
|
}
|
||||||
|
|
||||||
PolledFlag {
|
PolledFlag {
|
||||||
id: nightLightFlag
|
id: nightLightFlag
|
||||||
polling: root.opened
|
polling: root.quickVisible
|
||||||
command: "blob-toggle-nightlight --status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
|
command: "blob-toggle-nightlight --status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
|
||||||
}
|
}
|
||||||
|
|
||||||
PolledFlag {
|
PolledFlag {
|
||||||
id: stayAwakeFlag
|
id: stayAwakeFlag
|
||||||
polling: root.opened
|
polling: root.quickVisible
|
||||||
command: "blob-toggle-idle status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
|
command: "blob-toggle-idle status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
|
||||||
}
|
}
|
||||||
|
|
||||||
PolledFlag {
|
PolledFlag {
|
||||||
id: tabletFlag
|
id: tabletFlag
|
||||||
polling: root.opened
|
polling: root.quickVisible
|
||||||
command: "blob-toggle-tablet status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
|
command: "blob-toggle-tablet status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
|
||||||
}
|
}
|
||||||
|
|
||||||
PolledFlag {
|
PolledFlag {
|
||||||
id: recordingFlag
|
id: recordingFlag
|
||||||
polling: root.opened
|
polling: root.quickVisible
|
||||||
command: "pgrep -f '^gpu-screen-recorder' >/dev/null && echo on || echo off"
|
command: "pgrep -f '^gpu-screen-recorder' >/dev/null && echo on || echo off"
|
||||||
}
|
}
|
||||||
|
|
||||||
PolledFlag {
|
PolledFlag {
|
||||||
id: mutedFlag
|
id: mutedFlag
|
||||||
polling: root.opened
|
polling: root.soundVisible
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
command: "pamixer --get-mute 2>/dev/null | grep -qx true && echo on || echo off"
|
command: "pamixer --get-mute 2>/dev/null | grep -qx true && echo on || echo off"
|
||||||
}
|
}
|
||||||
|
|
||||||
PolledText {
|
PolledText {
|
||||||
id: volumeValue
|
id: volumeValue
|
||||||
polling: root.opened
|
polling: root.soundVisible
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
command: "pamixer --get-volume 2>/dev/null || echo 0"
|
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 {
|
PolledText {
|
||||||
id: brightnessValue
|
id: brightnessValue
|
||||||
polling: root.opened
|
polling: root.generalVisible
|
||||||
intervalMs: 2000
|
intervalMs: 2000
|
||||||
command: "brightnessctl -m 2>/dev/null | cut -d, -f4 | tr -d '%' || echo 0"
|
command: "brightnessctl -m 2>/dev/null | cut -d, -f4 | tr -d '%' || echo 0"
|
||||||
}
|
}
|
||||||
@@ -148,7 +176,7 @@ Item {
|
|||||||
|
|
||||||
PolledText {
|
PolledText {
|
||||||
id: mediaValue
|
id: mediaValue
|
||||||
polling: root.opened
|
polling: root.soundVisible
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
command: "playerctl metadata -f '{{title}}|||{{artist}}|||{{status}}' 2>/dev/null || echo 'No Media||||||Stopped'"
|
command: "playerctl metadata -f '{{title}}|||{{artist}}|||{{status}}' 2>/dev/null || echo 'No Media||||||Stopped'"
|
||||||
}
|
}
|
||||||
@@ -221,13 +249,70 @@ Item {
|
|||||||
onCloseRequested: root.dismiss()
|
onCloseRequested: root.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
Calendar {
|
TabBar {
|
||||||
width: parent.width
|
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 {
|
Tiles {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
visible: root.currentTab === "quick"
|
||||||
bluetoothOn: bluetoothFlag.value
|
bluetoothOn: bluetoothFlag.value
|
||||||
doNotDisturb: dndFlag.value
|
doNotDisturb: dndFlag.value
|
||||||
nightLight: nightLightFlag.value
|
nightLight: nightLightFlag.value
|
||||||
@@ -238,39 +323,6 @@ Item {
|
|||||||
onPanelRequested: function(pluginId) { root.openPanel(pluginId) }
|
onPanelRequested: function(pluginId) { root.openPanel(pluginId) }
|
||||||
onToggleRequested: function(command) { root.toggleAndRefresh(command) }
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,16 +14,6 @@ Item {
|
|||||||
|
|
||||||
readonly property string pluginId: (manifest && manifest.id) || "blob.sysmon"
|
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) {
|
function open(payloadJson) {
|
||||||
root.opened = true
|
root.opened = true
|
||||||
}
|
}
|
||||||
@@ -38,92 +28,9 @@ Item {
|
|||||||
root.shell.hide(root.pluginId)
|
root.shell.hide(root.pluginId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function readCpu(raw) {
|
SystemStats {
|
||||||
var values = String(raw).trim().split(/\s+/).slice(1).map(Number)
|
id: stats
|
||||||
var idle = (values[3] || 0) + (values[4] || 0)
|
polling: root.opened
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
@@ -158,36 +65,36 @@ Item {
|
|||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Metric {
|
MetricBar {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
icon: ""
|
icon: ""
|
||||||
name: "CPU"
|
name: "CPU"
|
||||||
percent: root.cpuPercent
|
percent: stats.cpuPercent
|
||||||
detail: root.cpuPercent + "%"
|
detail: stats.cpuPercent + "%"
|
||||||
}
|
}
|
||||||
|
|
||||||
Metric {
|
MetricBar {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
icon: ""
|
icon: ""
|
||||||
name: "Memory"
|
name: "Memory"
|
||||||
percent: root.memoryPercent
|
percent: stats.memoryPercent
|
||||||
detail: root.memoryDetail
|
detail: stats.memoryDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
Metric {
|
MetricBar {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
icon: ""
|
icon: ""
|
||||||
name: "Disk"
|
name: "Disk"
|
||||||
percent: root.diskPercent
|
percent: stats.diskPercent
|
||||||
detail: root.diskDetail
|
detail: stats.diskDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
Metric {
|
MetricBar {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
icon: ""
|
icon: ""
|
||||||
name: "Temperature"
|
name: "Temperature"
|
||||||
percent: root.temperature
|
percent: stats.temperature
|
||||||
detail: root.temperature + "°C"
|
detail: stats.temperature + "°C"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user