feat(quickshell): show detailed status popups

This commit is contained in:
2026-09-25 17:20:08 -04:00
parent 56581c54a7
commit c31b2486e3
6 changed files with 556 additions and 7 deletions
+22 -6
View File
@@ -31,6 +31,15 @@ PanelWindow {
precision: SystemClock.Seconds precision: SystemClock.Seconds
} }
DetailPopup {
id: detailsPopup
hardware: bar.hardware
sink: bar.sink
source: bar.source
battery: bar.battery
clock: clock
}
Row { Row {
id: leftModules id: leftModules
anchors { anchors {
@@ -137,7 +146,9 @@ PanelWindow {
: Math.round(bar.source.audio.volume * 100) + "%") : Math.round(bar.source.audio.volume * 100) + "%")
: "" : ""
onClicked: mouse => { onClicked: mouse => {
if (mouse.button === Qt.MiddleButton && bar.sink && bar.sink.audio) if (mouse.button === Qt.LeftButton)
detailsPopup.toggle("audio", audioBlock);
else if (mouse.button === Qt.MiddleButton && bar.sink && bar.sink.audio)
bar.sink.audio.muted = !bar.sink.audio.muted; bar.sink.audio.muted = !bar.sink.audio.muted;
else else
Quickshell.execDetached(["qjackctl"]); Quickshell.execDetached(["qjackctl"]);
@@ -151,13 +162,18 @@ PanelWindow {
} }
} }
Network { service: bar.hardware } Network {
service: bar.hardware
onDetailsRequested: anchorItem => detailsPopup.toggle("network", anchorItem)
}
Backlight { service: bar.hardware } Backlight { service: bar.hardware }
Block { Block {
id: batteryBlock
property int percentage: Math.round(bar.battery.percentage * 100) property int percentage: Math.round(bar.battery.percentage * 100)
visible: bar.battery.ready && bar.battery.isLaptopBattery visible: bar.battery.ready && bar.battery.isLaptopBattery
interactive: true
blockColor: percentage <= 15 && UPower.onBattery blockColor: percentage <= 15 && UPower.onBattery
? "#e91e63" : UPower.onBattery ? "#ffffff" : "#2980b9" ? "#e91e63" : UPower.onBattery ? "#ffffff" : "#2980b9"
textColor: UPower.onBattery && percentage > 15 textColor: UPower.onBattery && percentage > 15
@@ -171,6 +187,7 @@ PanelWindow {
if (percentage <= 85) return "\uf241 " + percentage + "%"; if (percentage <= 85) return "\uf241 " + percentage + "%";
return "\uf240 " + percentage + "%"; return "\uf240 " + percentage + "%";
} }
onClicked: detailsPopup.toggle("battery", batteryBlock)
} }
Rectangle { Rectangle {
@@ -233,12 +250,11 @@ PanelWindow {
} }
Block { Block {
property bool detailed: false id: clockBlock
interactive: true interactive: true
text: Qt.formatDateTime(clock.date, text: Qt.formatDateTime(clock.date, "hh:mm")
detailed ? "yyyy-MM-dd hh:mm:ss" : "hh:mm") onClicked: detailsPopup.toggle("clock", clockBlock)
onClicked: detailed = !detailed
} }
} }
} }
@@ -0,0 +1,385 @@
import QtQuick
import Quickshell
import Quickshell.Services.UPower
PopupWindow {
id: root
property var hardware
property var sink
property var source
property var battery
property var clock
property var anchorItem: null
property string page: ""
function toggle(nextPage, item) {
if (visible && page === nextPage) {
visible = false;
return;
}
page = nextPage;
anchorItem = item;
if (page === "network")
hardware.refreshWifi();
anchor.updateAnchor();
visible = true;
}
function duration(seconds) {
if (seconds <= 0)
return "Calculating";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return hours + "h " + minutes + "m";
}
function batteryState() {
switch (battery.state) {
case UPowerDeviceState.Charging: return "Charging";
case UPowerDeviceState.Discharging: return "Discharging";
case UPowerDeviceState.FullyCharged: return "Fully charged";
case UPowerDeviceState.Empty: return "Empty";
case UPowerDeviceState.PendingCharge: return "Waiting to charge";
case UPowerDeviceState.PendingDischarge: return "Waiting to discharge";
default: return "Unknown";
}
}
visible: false
implicitWidth: 320
implicitHeight: contentLoader.item ? contentLoader.item.implicitHeight + 32 : 100
color: "transparent"
anchor {
item: root.anchorItem
edges: Edges.Bottom | Edges.Right
gravity: Edges.Bottom | Edges.Left
margins.top: 4
}
Rectangle {
anchors.fill: parent
color: "#e0000000"
Loader {
id: contentLoader
anchors {
left: parent.left
right: parent.right
top: parent.top
margins: 12
}
sourceComponent: root.page === "clock" ? clockPage
: root.page === "audio" ? audioPage
: root.page === "network" ? networkPage
: batteryPage
}
Rectangle {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: 3
color: "#b3e5fc"
}
}
Component {
id: clockPage
Item {
id: clockContent
property date now: root.clock.date
property int year: now.getFullYear()
property int month: now.getMonth()
property int firstDay: new Date(year, month, 1).getDay()
property int days: new Date(year, month + 1, 0).getDate()
implicitWidth: 288
implicitHeight: 310
PopupHeader {
width: parent.width
text: Qt.formatDateTime(parent.now, "dddd, MMMM d")
}
Text {
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 40
}
text: Qt.formatDateTime(parent.now, "hh:mm:ss")
color: "#ffffff"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
font.pixelSize: 30
}
Row {
anchors {
left: parent.left
top: parent.top
topMargin: 86
}
spacing: 4
Repeater {
model: ["S", "M", "T", "W", "T", "F", "S"]
Text {
required property string modelData
width: 37
text: modelData
color: "#7aa6da"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
horizontalAlignment: Text.AlignHCenter
font.bold: true
}
}
}
Grid {
anchors {
left: parent.left
top: parent.top
topMargin: 112
}
columns: 7
spacing: 4
Repeater {
model: 42
Rectangle {
required property int index
property int day: index - clockContent.firstDay + 1
property bool today: day === clockContent.now.getDate()
width: 37
height: 29
radius: 0
color: today ? "#0288d1" : "transparent"
Rectangle {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: 3
visible: parent.today
color: "#b3e5fc"
}
Text {
anchors.centerIn: parent
text: parent.day > 0 && parent.day <= clockContent.days
? parent.day : ""
color: parent.today ? "#ffffff" : "#cfd8dc"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
font.pixelSize: 14
}
}
}
}
}
}
Component {
id: audioPage
Column {
width: 288
spacing: 12
PopupHeader {
width: parent.width
text: "Audio"
}
PopupSlider {
width: parent.width
label: "Output"
value: root.sink && root.sink.audio ? root.sink.audio.volume : 0
muted: root.sink && root.sink.audio ? root.sink.audio.muted : false
onValueRequested: value => {
if (root.sink && root.sink.audio)
root.sink.audio.volume = value;
}
onMuteRequested: {
if (root.sink && root.sink.audio)
root.sink.audio.muted = !root.sink.audio.muted;
}
}
PopupSlider {
width: parent.width
label: "Microphone"
value: root.source && root.source.audio ? root.source.audio.volume : 0
muted: root.source && root.source.audio ? root.source.audio.muted : false
onValueRequested: value => {
if (root.source && root.source.audio)
root.source.audio.volume = value;
}
onMuteRequested: {
if (root.source && root.source.audio)
root.source.audio.muted = !root.source.audio.muted;
}
}
}
}
Component {
id: networkPage
Column {
width: 288
spacing: 10
PopupHeader {
width: parent.width
text: "Network"
}
Text {
text: "Connected to " + root.hardware.connection
color: "#ffffff"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
font.pixelSize: 15
}
Column {
id: networks
width: parent.width
spacing: 5
Repeater {
model: root.hardware.wifiNetworks
Rectangle {
required property var modelData
width: networks.width
height: 29
radius: 0
color: modelData.active ? "#0288d1" : "#80000000"
Rectangle {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: 3
visible: parent.modelData.active
color: "#b3e5fc"
}
Text {
anchors {
left: parent.left
leftMargin: 9
verticalCenter: parent.verticalCenter
}
width: parent.width - 70
elide: Text.ElideRight
text: modelData.ssid
color: "#ffffff"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
font.pixelSize: 14
}
Text {
anchors {
right: parent.right
rightMargin: 9
verticalCenter: parent.verticalCenter
}
text: modelData.signal + "%"
color: "#ffffff"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
font.pixelSize: 13
}
}
}
}
Rectangle {
width: parent.width
height: 30
radius: 0
color: "#0288d1"
Text {
anchors.centerIn: parent
text: "Open network settings"
color: "#ffffff"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
font.pixelSize: 13
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Quickshell.execDetached(["nm-connection-editor"])
}
}
}
}
Component {
id: batteryPage
Column {
width: 288
spacing: 12
PopupHeader {
width: parent.width
text: "Battery"
}
Text {
text: Math.round(root.battery.percentage * 100) + "% "
+ root.batteryState()
color: "#ffffff"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
font.pixelSize: 25
}
Rectangle {
width: parent.width
height: 8
radius: 0
color: "#40000000"
Rectangle {
width: parent.width * root.battery.percentage
height: parent.height
radius: 0
color: root.battery.percentage <= 0.15 ? "#e91e63" : "#0288d1"
}
}
Text {
text: UPower.onBattery
? "Remaining: " + root.duration(root.battery.timeToEmpty)
: "Until full: " + root.duration(root.battery.timeToFull)
color: "#b3e5fc"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
font.pixelSize: 15
}
Text {
text: "Rate: " + Math.abs(root.battery.changeRate).toFixed(1) + " W"
+ (root.battery.healthSupported
? " Health: " + Math.round(root.battery.healthPercentage * 100) + "%"
: "")
color: "#ffffff"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
font.pixelSize: 14
}
}
}
}
@@ -7,12 +7,18 @@ Scope {
property string connection: "Disconnected" property string connection: "Disconnected"
property int brightness: -1 property int brightness: -1
property var wifiNetworks: []
function setBrightness(change) { function setBrightness(change) {
Quickshell.execDetached(["brightnessctl", "set", change]); Quickshell.execDetached(["brightnessctl", "set", change]);
brightnessRefresh.restart(); brightnessRefresh.restart();
} }
function refreshWifi() {
if (!wifiQuery.running)
wifiQuery.running = true;
}
Process { Process {
id: networkQuery id: networkQuery
command: ["nmcli", "-t", "-f", "TYPE,NAME", "connection", "show", "--active"] command: ["nmcli", "-t", "-f", "TYPE,NAME", "connection", "show", "--active"]
@@ -76,4 +82,35 @@ Scope {
interval: 250 interval: 250
onTriggered: brightnessQuery.running = true onTriggered: brightnessQuery.running = true
} }
Process {
id: wifiQuery
command: ["nmcli", "-t", "-f", "IN-USE,SSID,SIGNAL", "device", "wifi", "list", "--rescan", "auto"]
stdout: StdioCollector {
onStreamFinished: {
const networks = [];
const seen = {};
for (let line of text.trim().split("\n")) {
const fields = line.split(":");
if (fields.length < 3)
continue;
const active = fields.shift() === "*";
const signal = parseInt(fields.pop());
const ssid = fields.join(":").replace(/\\:/g, ":");
if (!ssid || seen[ssid])
continue;
seen[ssid] = true;
networks.push({
"ssid": ssid,
"signal": signal,
"active": active
});
if (networks.length >= 8)
break;
}
root.wifiNetworks = networks;
}
}
}
} }
+7 -1
View File
@@ -6,9 +6,15 @@ Block {
required property var service required property var service
property bool connected: service.connection !== "Disconnected" property bool connected: service.connection !== "Disconnected"
signal detailsRequested(var anchorItem)
blockColor: connected ? "#2980b9" : "#e91e63" blockColor: connected ? "#2980b9" : "#e91e63"
interactive: true interactive: true
text: connected ? "\uf1eb " + service.connection : "Disconnected !" text: connected ? "\uf1eb " + service.connection : "Disconnected !"
onClicked: Quickshell.execDetached(["nm-connection-editor"]) onClicked: mouse => {
if (mouse.button === Qt.LeftButton)
detailsRequested(root);
else
Quickshell.execDetached(["nm-connection-editor"]);
}
} }
@@ -0,0 +1,23 @@
import QtQuick
Rectangle {
id: root
property string text: ""
implicitHeight: 32
color: "#0288d1"
Text {
anchors {
left: parent.left
leftMargin: 10
verticalCenter: parent.verticalCenter
}
text: root.text
color: "#ffffff"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
font.pixelSize: 18
font.bold: true
}
}
@@ -0,0 +1,82 @@
import QtQuick
Item {
id: root
property string label: ""
property real value: 0
property bool muted: false
signal valueRequested(real value)
signal muteRequested()
implicitWidth: 280
implicitHeight: 58
Text {
anchors.left: parent.left
text: root.label
color: "#ffffff"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
font.pixelSize: 15
}
Rectangle {
anchors {
right: parent.right
top: parent.top
}
width: 58
height: 24
radius: 0
color: root.muted ? "#e91e63" : "#0288d1"
Text {
anchors.centerIn: parent
text: root.muted ? "MUTED" : Math.round(root.value * 100) + "%"
color: "#ffffff"
font.pixelSize: 11
font.bold: true
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.muteRequested()
}
}
Rectangle {
id: track
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: 8
radius: 0
color: "#40000000"
Rectangle {
width: Math.min(parent.width, parent.width * root.value / 1.5)
height: parent.height
radius: 0
color: root.muted ? "#e91e63" : "#0288d1"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
function updateValue(mouse) {
root.valueRequested(Math.max(0,
Math.min(1.5, mouse.x / width * 1.5)));
}
onPressed: mouse => updateValue(mouse)
onPositionChanged: mouse => {
if (pressed)
updateValue(mouse);
}
}
}
}