added three new modules (Crypto.qml, Battery.qml, Network.qml), improved Stats.qml, improved Tray.qml, optimized code in general, added a polkit agent, configurations change (check configs)

This commit is contained in:
SomeElse
2026-05-14 04:56:33 +00:00
parent 046a36fbec
commit e3bfa6661f
31 changed files with 5857 additions and 353 deletions

View File

@@ -112,6 +112,7 @@ PanelWindow {
required property string modelData
source: Qt.resolvedUrl("modules/" + modelData + ".qml")
onLoaded: barContent.bindModule(item, "left")
visible: !item || item.moduleEnabled !== false
}
}
}
@@ -139,6 +140,7 @@ PanelWindow {
required property string modelData
source: Qt.resolvedUrl("modules/" + modelData + ".qml")
onLoaded: barContent.bindModule(item, "right")
visible: !item || item.moduleEnabled !== false
}
}
}
@@ -170,6 +172,7 @@ PanelWindow {
required property string modelData
source: Qt.resolvedUrl("modules/" + modelData + ".qml")
onLoaded: barContent.bindModule(item, "center")
visible: !item || item.moduleEnabled !== false
}
}
}

589
bar/modules/Battery.qml Normal file
View File

@@ -0,0 +1,589 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.UPower
import "../../config" as Cfg
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
readonly property var bat: UPower.displayDevice
readonly property bool batReady: bat && bat.ready && bat.isLaptopBattery
readonly property bool moduleEnabled: batready
readonly property real pct: batReady ? bat.percentage * 100 : 72
readonly property int state: batReady ? bat.state : UPowerDeviceState.Unknown
readonly property bool charging: state === UPowerDeviceState.Charging
readonly property bool fullCharge: state === UPowerDeviceState.FullyCharged
readonly property string batIcon: {
if (!batReady) return "󱉞"
if (fullCharge) return "󱟢"
if (charging) {
if (pct >= 90) return "󰂋"
if (pct >= 80) return "󰂊"
if (pct >= 70) return "󰢞"
if (pct >= 60) return "󰂉"
if (pct >= 50) return "󰢝"
if (pct >= 40) return "󰂈"
if (pct >= 30) return "󰂇"
if (pct >= 20) return "󰂆"
if (pct >= 10) return "󰢜"
return "󰢟"
}
if (pct >= 90) return "󰂂"
if (pct >= 80) return "󰂁"
if (pct >= 70) return "󰂀"
if (pct >= 60) return "󰁿"
if (pct >= 50) return "󰁾"
if (pct >= 40) return "󰁽"
if (pct >= 30) return "󰁼"
if (pct >= 20) return "󰁻"
if (pct >= 10) return "󰁺"
return "󰂎"
}
readonly property bool isCritical: batReady && !charging && pct <= 15
readonly property bool isLow: batReady && !charging && pct <= 30
readonly property color iconColor: {
if (fullCharge) return t.batteryFull ?? t.accent
if (charging) return t.batteryCharging ?? t.accent
if (isCritical) return t.batteryCritical ?? "#ff5555"
if (isLow) return t.batteryLow ?? "#ffb86c"
return t.batteryIcon ?? t.clockIcon
}
readonly property var up: UPower
readonly property var pp: PowerProfiles
readonly property string typeText: {
if (!batReady) return "—"
switch (bat.type) {
case UPowerDeviceType.Battery: return "Battery"
case UPowerDeviceType.Ups: return "UPS"
case UPowerDeviceType.Mouse: return "Mouse"
case UPowerDeviceType.Keyboard: return "Keyboard"
case UPowerDeviceType.Phone: return "Phone"
case UPowerDeviceType.Tablet: return "Tablet"
default: return "Other"
}
}
readonly property string degradationText: {
if (pp.degradationReason === PerformanceDegradationReason.None) return ""
if (pp.degradationReason === PerformanceDegradationReason.LapDetected) return "Lap detected"
if (pp.degradationReason === PerformanceDegradationReason.HighTemperature) return "High temp"
return ""
}
readonly property var profileHolds: {
let names = []
for (let i = 0; i < pp.holds.length; i++) {
let h = pp.holds[i]
let label = PowerProfile.toString(h.profile)
if (h.applicationId) label += " (" + h.applicationId + ")"
names.push(label)
}
return names
}
width: batLayout.implicitWidth + 24
height: Cfg.Config.moduleHeight
radius: panelRadius
color: hoverHandler.hovered ? t.batBgHover : t.batBg
border.color: hoverHandler.hovered ? t.batBorderHover : t.batBorder
border.width: borderWidth
Behavior on width { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } }
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
HoverHandler { id: hoverHandler; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: batteryPopup.toggle() }
RowLayout {
id: batLayout
anchors.centerIn: parent
spacing: 0
Text {
text: root.batIcon
color: root.iconColor
font.pixelSize: 16
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: 7
SequentialAnimation on opacity {
running: root.isCritical
loops: Animation.Infinite
NumberAnimation { to: 0.3; duration: 700; easing.type: Easing.InOutSine }
NumberAnimation { to: 1.0; duration: 700; easing.type: Easing.InOutSine }
}
opacity: 1.0
}
Text {
text: root.batReady ? Math.round(root.pct) + "%" : "—"
color: root.t.batText
font.pixelSize: 13
font.bold: true
verticalAlignment: Text.AlignVCenter
}
Text {
id: chargingLabel
text: root.charging ? " +" : (root.fullCharge ? " ✓" : "")
color: root.iconColor
font.pixelSize: 13
font.bold: true
verticalAlignment: Text.AlignVCenter
opacity: (hoverHandler.hovered || batteryPopup.popupOpen) ? 1.0 : 0.0
Layout.preferredWidth: (hoverHandler.hovered || batteryPopup.popupOpen)
? implicitWidth : 0
clip: true
Behavior on opacity { NumberAnimation { duration: 200 } }
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
}
}
PanelWindow {
id: batteryPopup
screen: root.targetScreen
visible: false
color: "transparent"
property bool popupOpen: false
function open() { hideTimer.stop(); visible = true; popupOpen = true }
function close() { popupOpen = false; hideTimer.restart() }
function toggle() { popupOpen ? close() : open() }
Timer {
id: hideTimer
interval: Cfg.Config.popupAnimDuration + 70
onTriggered: batteryPopup.visible = false
}
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "main-shell-battery"
WlrLayershell.exclusiveZone: -1
mask: popupOpen ? null : _noInput
Region { id: _noInput }
TapHandler { onTapped: batteryPopup.close() }
Item {
id: popupClipContainer
readonly property int cardW: 260
readonly property int screenPad: root.barMargin + 10
x: {
let centerX = root.mapToItem(null, 0, 0).x + root.width / 2
let desiredX = centerX - cardW / 2
return Math.max(screenPad,
Math.min(desiredX, parent.width - cardW - screenPad))
}
width: cardW
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
height: parent.height - root.barHeight - 10
clip: true
Rectangle {
id: cardShadow
anchors.fill: card
anchors.margins: -3
radius: card.radius + 3
color: "transparent"
border.color: Qt.rgba(0, 0, 0, 0.35)
border.width: 6
visible: batteryPopup.popupOpen
opacity: batteryPopup.popupOpen ? 0.6 : 0
Behavior on opacity { NumberAnimation { duration: 200 } }
}
Rectangle {
id: card
width: parent.width
height: cardCol.implicitHeight + 28
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
transform: Translate {
y: batteryPopup.popupOpen ? 0
: (root.isTop ? -card.height : card.height)
Behavior on y {
NumberAnimation {
duration: Cfg.Config.popupAnimDuration
easing.type: Easing.OutExpo
}
enabled: batteryPopup.visible
}
}
opacity: batteryPopup.popupOpen ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 220 } }
color: root.t.batPopupBg
radius: Cfg.Config.popupRadius
border.color: root.t.batPopupBorder
border.width: Cfg.Config.popupBorderWidth
layer.enabled: true
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
ColumnLayout {
id: cardCol
anchors {
left: parent.left
right: parent.right
top: parent.top
margins: 18
topMargin: 20
}
spacing: 12
RowLayout {
Layout.fillWidth: true
spacing: 14
Item {
width: 52
height: 52
Rectangle {
anchors.centerIn: parent
width: 46
height: 46
radius: 23
color: Qt.rgba(
Qt.darker(root.iconColor, 1.0).r,
Qt.darker(root.iconColor, 1.0).g,
Qt.darker(root.iconColor, 1.0).b,
0.15
)
}
Text {
anchors.centerIn: parent
text: root.batIcon
color: root.iconColor
font.pixelSize: 26
SequentialAnimation on opacity {
running: root.isCritical
loops: Animation.Infinite
NumberAnimation { to: 0.3; duration: 700; easing.type: Easing.InOutSine }
NumberAnimation { to: 1.0; duration: 700; easing.type: Easing.InOutSine }
}
opacity: 1.0
}
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
Text {
text: root.batReady ? Math.round(root.pct) + "%" : "—"
color: root.t.batPopupHeader
font.pixelSize: 30
font.bold: true
}
Text {
text: {
if (!root.batReady) return "No battery"
if (root.fullCharge) return "Fully charged"
if (root.charging) return "Charging"
if (root.isCritical) return "Critical"
if (root.isLow) return "Low"
return "Discharging"
}
color: root.iconColor
font.pixelSize: 11
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.8
font.bold: true
}
}
}
Item {
Layout.fillWidth: true
height: 10
Rectangle {
anchors.fill: parent
radius: 5
color: root.t.batProgressTrack
}
Rectangle {
width: Math.max(height, parent.width * (root.pct / 100))
height: parent.height
radius: 5
clip: false
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0.0
color: Qt.darker(root.iconColor, 1.25)
}
GradientStop {
position: 1.0
color: root.iconColor
}
}
Behavior on width { NumberAnimation { duration: 450; easing.type: Easing.OutCubic } }
}
Rectangle {
anchors.fill: parent
radius: 5
color: "transparent"
border.color: Qt.rgba(root.iconColor.r,
root.iconColor.g,
root.iconColor.b, 0.2)
border.width: 1
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.t.batPopupSeparator
opacity: 0.6
}
component DetailRow: RowLayout {
required property string label
required property string value
Layout.fillWidth: true
Text {
text: label
color: root.t.batPopupDim
font.pixelSize: 12
Layout.fillWidth: true
}
Text {
text: value
color: root.t.batPopupText
font.pixelSize: 12
font.bold: true
font.family: "monospace"
}
}
DetailRow {
label: root.charging ? "Time to full" : "Time left"
value: {
if (!root.batReady || root.fullCharge) return "—"
let secs = root.charging ? bat.timeToFull : bat.timeToEmpty
if (secs <= 0) return "—"
let h = Math.floor(secs / 3600)
let m = Math.floor((secs % 3600) / 60)
return h > 0 ? h + "h " + m + "m" : m + "m"
}
}
DetailRow {
label: "Rate"
value: {
if (!root.batReady) return "—"
let r = bat.changeRate
return (r >= 0 ? "+" : "") + r.toFixed(1) + " W"
}
}
DetailRow {
label: "Health"
value: {
if (!root.batReady) return "—"
if (!bat.healthSupported) return "Undetected"
return Math.round(bat.healthPercentage) + "%"
}
}
DetailRow {
label: "Energy"
value: {
if (!root.batReady) return "—"
return bat.energy.toFixed(1) + " / "
+ bat.energyCapacity.toFixed(1) + " Wh"
}
}
Item {
Layout.fillWidth: true
height: 16
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 1
color: root.t.batPopupSeparator
opacity: 0.6
}
Rectangle {
anchors.centerIn: parent
width: sectionLabel.implicitWidth + 14
height: sectionLabel.implicitHeight + 4
radius: 4
color: root.t.batPopupBg
Text {
id: sectionLabel
anchors.centerIn: parent
text: "Power Profile"
color: root.t.batPopupDim
font.pixelSize: 10
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.8
}
}
}
component ProfileButton: Rectangle {
required property string text
required property var profileValue
property bool interactive: true
property bool isActive: interactive && (root.pp.profile === profileValue)
height: 38
radius: 10
color: isActive ? root.iconColor
: Qt.rgba(root.iconColor.r, root.iconColor.g, root.iconColor.b,
(profileHover.containsMouse && interactive) ? 0.14 : 0.06)
border.color: isActive
? Qt.lighter(root.iconColor, 1.2)
: Qt.rgba(root.iconColor.r, root.iconColor.g, root.iconColor.b, 0.12)
border.width: 1
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
Text {
anchors.centerIn: parent
text: parent.text
font.pixelSize: 10
font.bold: parent.isActive
font.letterSpacing: 0.4
color: parent.isActive ? root.t.batProfileActiveText : root.t.batPopupText
}
HoverHandler { id: profileHover; enabled: parent.interactive }
MouseArea {
anchors.fill: parent
cursorShape: parent.interactive ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (parent.interactive) root.pp.profile = parent.profileValue
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 6
ProfileButton {
text: "Power Saver"
profileValue: PowerProfile.PowerSaver
Layout.fillWidth: true
}
ProfileButton {
text: "Balanced"
profileValue: PowerProfile.Balanced
Layout.fillWidth: true
}
ProfileButton {
text: "Performance"
profileValue: PowerProfile.Performance
interactive: root.pp.hasPerformanceProfile
Layout.fillWidth: true
}
}
DetailRow {
label: "Degradation"
value: root.degradationText || "None"
}
DetailRow {
label: "Holds"
value: root.profileHolds.length > 0 ? root.profileHolds[0] : "—"
visible: root.profileHolds.length > 0
}
Item {
Layout.fillWidth: true
height: 16
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 1
color: root.t.batPopupSeparator
opacity: 0.6
}
Rectangle {
anchors.centerIn: parent
width: devLabel.implicitWidth + 14
height: devLabel.implicitHeight + 4
radius: 4
color: root.t.batPopupBg
Text {
id: devLabel
anchors.centerIn: parent
text: "Device"
color: root.t.batPopupDim
font.pixelSize: 10
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.8
}
}
}
DetailRow { label: "Type"; value: root.typeText }
DetailRow { label: "Model"; value: root.batReady ? (bat.model || "Undetected") : "—" }
DetailRow { label: "Power supply"; value: root.batReady ? (bat.powerSupply ? "Yes" : "No") : "—" }
DetailRow { label: "On battery"; value: root.up.onBattery ? "Yes" : "No" }
DetailRow { label: "Devices"; value: root.up.devices.values.length + " total" }
Item { height: 2 }
}
}
}
}
}

View File

@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import "../../config" as Cfg
@@ -17,8 +18,44 @@ Rectangle {
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
readonly property string tz: Cfg.Config.timezone || ""
readonly property int tzOffsetHours: -timeManager.now.getTimezoneOffset() / 60
property int tzOffsetMin: 0
Process {
id: tzProc
command: ["bash", "-c", "TZ=" + (root.tz || "UTC") + " date +%z"]
running: root.tz !== ""
stdout: SplitParser {
onRead: data => {
var s = data.trim()
if (s.length < 5) return
var sign = (s[0] === "-") ? -1 : 1
var h = parseInt(s.substring(1, 3), 10)
var m = parseInt(s.substring(3, 5), 10)
root.tzOffsetMin = sign * (h * 60 + m)
}
}
}
Timer {
interval: 1800000; running: root.tz !== ""; repeat: true
onTriggered: tzProc.running = true
}
function toTz(date) {
if (!tz) return date
return new Date(date.getTime()
+ date.getTimezoneOffset() * 60000
+ tzOffsetMin * 60000)
}
function tzOffsetHours() {
if (!tz) return -timeManager.now.getTimezoneOffset() / 60
return tzOffsetMin / 60
}
width: clockLayout.implicitWidth + 24
height: Cfg.Config.moduleHeight
@@ -66,7 +103,7 @@ Rectangle {
}
Text {
text: timeManager.now.toLocaleTimeString(Qt.locale(), "HH:mm")
text: root.toTz(timeManager.now).toLocaleTimeString(Qt.locale(), "HH:mm")
color: root.t.clockTime
font.pixelSize: 13
font.bold: true
@@ -75,7 +112,7 @@ Rectangle {
Text {
id: secondsText
text: ":" + timeManager.now.toLocaleTimeString(Qt.locale(), "ss")
text: ":" + root.toTz(timeManager.now).toLocaleTimeString(Qt.locale(), "ss")
color: root.t.clockSeconds
font.pixelSize: 13
font.bold: true
@@ -230,7 +267,7 @@ Rectangle {
Layout.fillWidth: true
Text {
text: timeManager.now.toLocaleDateString(Qt.locale(), "dddd")
text: root.toTz(timeManager.now).toLocaleDateString(Qt.locale(), "dddd")
color: root.t.clockPopupDim
font.pixelSize: 12
font.capitalization: Font.AllUppercase
@@ -239,7 +276,7 @@ Rectangle {
}
Text {
text: timeManager.now.toLocaleDateString(Qt.locale(), "d MMMM yyyy")
text: root.toTz(timeManager.now).toLocaleDateString(Qt.locale(), "d MMMM yyyy")
color: root.t.clockPopupHeader
font.pixelSize: 22
font.bold: true
@@ -269,8 +306,8 @@ Rectangle {
id: cal
Layout.fillWidth: true
implicitHeight: gridCol.implicitHeight
property int displayYear: timeManager.now.getFullYear()
property int displayMonth: timeManager.now.getMonth()
property int displayYear: root.toTz(timeManager.now).getFullYear()
property int displayMonth: root.toTz(timeManager.now).getMonth()
property int pendingYear: displayYear
property int pendingMonth: displayMonth
property int slideDir: 1
@@ -279,8 +316,9 @@ Rectangle {
target: calendarPopup
function onPopupOpenChanged() {
if (calendarPopup.popupOpen) {
cal.displayYear = timeManager.now.getFullYear()
cal.displayMonth = timeManager.now.getMonth()
let tzd = root.toTz(timeManager.now)
cal.displayYear = tzd.getFullYear()
cal.displayMonth = tzd.getMonth()
}
}
}
@@ -339,7 +377,12 @@ Rectangle {
width: (card.width - 32) / 7; height: 26
readonly property int dayNum: (weekRow.weekIndex * 7 + index) - ((new Date(cal.displayYear, cal.displayMonth, 1).getDay() + 6) % 7) + 1
readonly property bool inMonth: dayNum >= 1 && dayNum <= new Date(cal.displayYear, cal.displayMonth + 1, 0).getDate()
readonly property bool isToday: inMonth && dayNum === timeManager.now.getDate() && cal.displayYear === timeManager.now.getFullYear() && cal.displayMonth === timeManager.now.getMonth()
readonly property bool isToday: {
let tzd = root.toTz(timeManager.now)
return inMonth && dayNum === tzd.getDate()
&& cal.displayYear === tzd.getFullYear()
&& cal.displayMonth === tzd.getMonth()
}
Rectangle {
anchors.centerIn: parent; width: 22; height: 22; radius: 11
@@ -371,7 +414,7 @@ Rectangle {
Layout.rightMargin: 6
Text {
text: "UTC " + (root.tzOffsetHours >= 0 ? "+" : "") + root.tzOffsetHours
text: "UTC " + (root.tzOffsetHours() >= 0 ? "+" : "") + root.tzOffsetHours()
color: root.t.clockPopupUtc
font.pixelSize: 13
font.bold: true
@@ -382,11 +425,11 @@ Rectangle {
Text {
text: {
let d = timeManager.now
let d = root.toTz(timeManager.now)
let month = d.getMonth() + 1
let day = d.getDate()
let year = d.getFullYear()
if (Cfg.Config.clockDateFormat === "DMY")
if (Cfg.Config.dateFormat === "DMY")
return day + "/" + month + "/" + year
return month + "/" + day + "/" + year
}

1200
bar/modules/Crypto.qml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,4 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "../../config" as Cfg
@@ -37,10 +35,10 @@ Rectangle {
width: Cfg.Config.logoIconSize * 2 + 4
height: parent.height
color: isHovered ? t.archLogoBgHover : t.archLogoBg
color: isHovered ? t.logoBgHover : t.logoBg
radius: panelRadius
border.color: launcherActive ? t.archLogoBorderActive : t.archLogoBorder
border.color: launcherActive ? t.logoBorderActive : t.logoBorder
border.width: borderWidth
Behavior on color { ColorAnimation { duration: 250 } }
@@ -69,7 +67,7 @@ Rectangle {
anchors.centerIn: parent
text: Cfg.Config.logoIcon
color: launcherActive ? t.archLogoIconActive : t.archLogoIcon
color: launcherActive ? t.logoIconActive : t.logoIcon
font.pixelSize: Cfg.Config.logoIconSize
Behavior on color { ColorAnimation { duration: 250 } }

728
bar/modules/Network.qml Normal file
View File

@@ -0,0 +1,728 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Networking
import Quickshell.Services.Polkit
import "../../config" as Cfg
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
readonly property var wifiDevice: {
for (const dev of (Networking.devices?.values ?? []))
if (dev.type === DeviceType.Wifi) return dev
return null
}
readonly property var wiredDevice: {
for (const dev of (Networking.devices?.values ?? []))
if (dev.type === DeviceType.Wired) return dev
return null
}
readonly property bool hasWifi: wifiDevice !== null
readonly property bool hasWired: wiredDevice !== null
readonly property bool wifiUp: hasWifi && wifiDevice.connected
readonly property bool wiredUp: hasWired && wiredDevice.connected
readonly property string connectivityLabel: {
switch (Networking.connectivity) {
case NetworkConnectivity.Portal: return "Captive Portal"
case NetworkConnectivity.Limited: return "Limited"
case NetworkConnectivity.None: return "No Internet"
default: return ""
}
}
readonly property string wifiModeLabel: {
if (!wifiUp || !wifiDevice) return ""
switch (wifiDevice.mode) {
case WifiDeviceMode.Infra: return "Infra"
case WifiDeviceMode.Ap: return "AP"
case WifiDeviceMode.Adhoc: return "Ad-Hoc"
case WifiDeviceMode.Mesh: return "Mesh"
default: return ""
}
}
readonly property var activeAP: {
for (const n of (wifiDevice?.networks?.values ?? []))
if (n.connected) return n
return null
}
readonly property string ssid: activeAP ? (activeAP.name || "") : ""
readonly property int strength: activeAP ? Math.round((activeAP.signalStrength || 0) * 100) : 0
readonly property string wifiIcon: {
if (!wifiUp) return "󰤭"
if (strength >= 80) return "󰤨"
if (strength >= 60) return "󰤥"
if (strength >= 40) return "󰤢"
if (strength >= 20) return "󰤟"
return "󰤯"
}
readonly property string displayIcon: hasWifi ? wifiIcon : ""
readonly property color iconColor: {
if (hasWifi) return wifiUp ? t.statusOk : t.statusErr
if (hasWired) return wiredUp ? t.statusOk : t.statusErr
return t.textDim
}
readonly property string chipLabel: {
if (hasWifi) return (wifiUp && ssid !== "") ? ssid : "Wi-Fi"
if (hasWired) return wiredUp ? "Ethernet" : "Offline"
return "No NIC"
}
property var pendingAP: null
property string pendingSSID: ""
property bool showPwdPrompt: false
property string enteredPwd: ""
function attemptConnect() {
if (!pendingAP) return
if (enteredPwd.length > 0)
pendingAP.connectWithPsk(enteredPwd)
else
pendingAP.connect()
pendingAP = null
pendingSSID = ""
enteredPwd = ""
showPwdPrompt = false
}
function cancelConnect() {
pendingAP = null
pendingSSID = ""
enteredPwd = ""
showPwdPrompt = false
}
implicitWidth: chipRow.implicitWidth + 24
height: Cfg.Config.moduleHeight
radius: panelRadius
color: chipHover.hovered ? t.netBgHover : t.netBg
border.color: chipHover.hovered ? t.netBorderHover : t.netBorder
border.width: borderWidth
Behavior on implicitWidth { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } }
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
HoverHandler { id: chipHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: netPopup.toggle() }
RowLayout {
id: chipRow
anchors.centerIn: parent
spacing: 6
Text {
text: root.displayIcon
color: root.iconColor
font.pixelSize: 16
Layout.alignment: Qt.AlignVCenter
SequentialAnimation on opacity {
running: root.hasWifi && root.wifiDevice
&& (root.wifiDevice.scannerEnabled || false)
loops: Animation.Infinite
NumberAnimation { to: 0.4; duration: 600; easing.type: Easing.InOutSine }
NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutSine }
}
opacity: 1.0
}
Text {
text: root.chipLabel
color: root.t.netText
font.pixelSize: 12
font.bold: true
verticalAlignment: Text.AlignVCenter
Layout.maximumWidth: 110
elide: Text.ElideRight
}
}
PanelWindow {
id: netPopup
screen: root.targetScreen
visible: false
color: "transparent"
property bool popupOpen: false
function open() {
root.cancelConnect()
hideTimer.stop()
visible = true
popupOpen = true
if (root.hasWifi && root.wifiDevice)
root.wifiDevice.scannerEnabled = true
}
function close() { popupOpen = false; hideTimer.restart() }
function toggle() { popupOpen ? close() : open() }
Timer {
id: hideTimer
interval: Cfg.Config.popupAnimDuration + 70
onTriggered: netPopup.visible = false
}
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "main-shell-network"
WlrLayershell.exclusiveZone: -1
mask: netPopup.popupOpen ? null : _noInput
Region { id: _noInput }
TapHandler { onTapped: netPopup.close() }
Item {
id: popupClip
readonly property int cardW: 310
readonly property int screenPad: root.barMargin + 10
x: {
var cx = root.mapToItem(null, 0, 0).x + root.width / 2
var dx = cx - cardW / 2
return Math.max(screenPad, Math.min(dx, parent.width - cardW - screenPad))
}
width: cardW
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
height: parent.height - root.barHeight - 10
clip: true
Rectangle {
id: card
width: parent.width
height: cardBody.implicitHeight + 24
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
transform: Translate {
y: netPopup.popupOpen ? 0 : (root.isTop ? -card.height : card.height)
Behavior on y {
NumberAnimation {
duration: Cfg.Config.popupAnimDuration
easing.type: Easing.OutExpo
}
enabled: netPopup.visible
}
}
opacity: netPopup.popupOpen ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 200 } }
color: root.t.netPopupBg
radius: Cfg.Config.popupRadius
border.color: root.t.netPopupBorder
border.width: Cfg.Config.popupBorderWidth
layer.enabled: true
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
ColumnLayout {
id: cardBody
anchors {
left: parent.left
right: parent.right
top: parent.top
margins: 16
topMargin: 18
}
spacing: 12
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
text: root.displayIcon
color: root.iconColor
font.pixelSize: 36
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
Text {
text: {
if (root.hasWifi)
return root.wifiUp
? (root.ssid !== "" ? root.ssid : "Connected")
: "Wi-Fi"
if (root.hasWired)
return root.wiredUp ? "Ethernet" : "Disconnected"
return "No Network"
}
color: root.t.netPopupHeader
font.pixelSize: 17
font.bold: true
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: {
if (root.hasWifi && root.wifiDevice) {
var st = root.wifiDevice.state
if (st === ConnectionState.Activating) return "Connecting..."
if (st === ConnectionState.Deactivating) return "Disconnecting..."
if (root.wifiUp && root.activeAP) {
var s = root.strength + "% signal"
if (root.wifiModeLabel !== "") s += " · " + root.wifiModeLabel
return s
}
if (st === ConnectionState.Deactivated)
return "Disconnected"
return "Not connected"
}
if (root.hasWired && root.wiredDevice)
return root.wiredDevice.name || "—"
return "No hardware found"
}
color: root.t.netPopupDim
font.pixelSize: 12
}
Text {
visible: root.connectivityLabel !== ""
text: root.connectivityLabel
color: root.t.statusErr
font.pixelSize: 11
}
}
Rectangle {
visible: root.hasWifi
width: 28; height: 28
radius: 8
color: scanHover.hovered ? root.t.netApHoverBg : "transparent"
border.color: root.t.netPopupBorder
border.width: 1
Behavior on color { ColorAnimation { duration: 120 } }
Text {
anchors.centerIn: parent
text: "\uf0450"
font.pixelSize: 15
color: scanHover.hovered ? root.t.accent : root.t.netPopupDim
RotationAnimator on rotation {
id: scanSpin; running: false
from: 0; to: 360; duration: 700; loops: 2
easing.type: Easing.InOutCubic
}
}
HoverHandler { id: scanHover; cursorShape: Qt.PointingHandCursor }
TapHandler {
onTapped: {
scanSpin.restart()
if (root.wifiDevice) root.wifiDevice.scannerEnabled = true
}
}
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.netPopupSeparator }
ColumnLayout {
visible: root.hasWifi
Layout.fillWidth: true
spacing: 4
Text {
text: "AVAILABLE NETWORKS"
color: root.t.netPopupDim
font.pixelSize: 10
font.letterSpacing: 1.2
Layout.fillWidth: true
}
Text {
visible: root.hasWifi && root.wifiDevice
&& root.wifiDevice.networks.values.length === 0
text: (root.wifiDevice && (root.wifiDevice.scannerEnabled || false))
? "Scanning..." : "No networks found"
color: root.t.netPopupDim
font.pixelSize: 12
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 4
}
Repeater {
model: (root.hasWifi && root.wifiDevice) ? root.wifiDevice.networks.values : []
delegate: Rectangle {
required property var modelData
Layout.fillWidth: true
height: 50
radius: 10
color: {
var active = modelData.connected || false
if (active) return root.t.netApActiveBg
if (apHover.hovered) return root.t.netApHoverBg
return "transparent"
}
border.color: (modelData.connected || false) ? root.t.accent : "transparent"
border.width: 1
Behavior on color { ColorAnimation { duration: 120 } }
Behavior on border.color { ColorAnimation { duration: 120 } }
HoverHandler { id: apHover; cursorShape: Qt.PointingHandCursor }
TapHandler {
onTapped: {
if (modelData.connected || false) return
if (modelData.security !== WifiSecurityType.None) {
root.pendingAP = modelData
root.pendingSSID = modelData.name || ""
root.enteredPwd = ""
root.showPwdPrompt = true
} else {
modelData.connect()
}
}
}
RowLayout {
anchors { fill: parent; leftMargin: 12; rightMargin: 12 }
spacing: 10
Text {
text: {
var s = Math.round((modelData.signalStrength || 0) * 100)
if (s >= 80) return "\uf0ec8"
if (s >= 60) return "\uf0ec5"
if (s >= 40) return "\uf0ec2"
if (s >= 20) return "\uf0ebf"
return "\uf0eef"
}
color: (modelData.connected || false)
? root.t.accent : root.t.netPopupDim
font.pixelSize: 18
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
Text {
text: modelData.name || "(hidden)"
color: (modelData.connected || false)
? root.t.netPopupHeader
: root.t.netPopupText
font.pixelSize: 13
font.bold: (modelData.connected || false)
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: {
if (modelData.stateChanging)
return modelData.state === ConnectionState.Activating
? "Connecting..." : "Disconnecting..."
var parts = []
if (modelData.known) parts.push("Saved")
if (modelData.security !== WifiSecurityType.None) parts.push("Secured")
else parts.push("Open")
parts.push(Math.round((modelData.signalStrength || 0) * 100) + "%")
return parts.join(" · ")
}
color: root.t.netPopupDim
font.pixelSize: 11
}
}
Text {
visible: (modelData.security !== WifiSecurityType.None) && !(modelData.connected || false)
text: "\uf033e"
color: root.t.netPopupDim
font.pixelSize: 13
Layout.alignment: Qt.AlignVCenter
}
Text {
visible: (modelData.connected || false)
text: "\uf012c"
color: root.t.statusOk
font.pixelSize: 15
Layout.alignment: Qt.AlignVCenter
}
}
}
}
Rectangle {
visible: root.wifiUp
Layout.fillWidth: true
Layout.topMargin: 4
height: 36; radius: 10
color: discoHover.hovered
? root.t.netDisconnectBg : "transparent"
border.color: root.t.netDisconnectBorder
border.width: 1
Behavior on color { ColorAnimation { duration: 120 } }
HoverHandler { id: discoHover; cursorShape: Qt.PointingHandCursor }
TapHandler {
onTapped: {
if (root.activeAP)
root.activeAP.disconnect()
}
}
Text {
anchors.centerIn: parent
text: "Disconnect"
color: root.t.statusErr
font.pixelSize: 12
font.bold: true
}
}
}
ColumnLayout {
visible: !root.hasWifi && root.hasWired
Layout.fillWidth: true
spacing: 10
Text {
text: "CONNECTION DETAILS"
color: root.t.netPopupDim
font.pixelSize: 10
font.letterSpacing: 1.2
Layout.fillWidth: true
}
Rectangle {
Layout.fillWidth: true
height: 38; radius: 10
color: root.wiredUp
? root.t.netWiredOkBg
: root.t.netWiredErrBg
border.color: root.wiredUp
? root.t.netWiredOkBorder
: root.t.netWiredErrBorder
border.width: 1
RowLayout {
anchors { fill: parent; leftMargin: 14; rightMargin: 14 }
spacing: 10
Text {
text: root.wiredUp ? "\uf0601" : "\uf0602"
color: root.wiredUp ? root.t.statusOk : root.t.statusErr
font.pixelSize: 18
Layout.alignment: Qt.AlignVCenter
}
Text {
text: root.wiredUp ? "Connected" : "Cable unplugged"
color: root.wiredUp ? root.t.statusOk : root.t.statusErr
font.pixelSize: 13
font.bold: true
Layout.fillWidth: true
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 6
Repeater {
model: [
{
label: "Interface",
value: (root.wiredDevice && root.wiredDevice.name)
? root.wiredDevice.name : "—"
},
{
label: "MAC",
value: (root.wiredDevice && root.wiredDevice.address)
? root.wiredDevice.address : "—"
},
{
label: "Link",
value: (root.wiredDevice)
? (root.wiredDevice.hasLink ? "Up" : "Down") : "—"
},
{
label: "Speed",
value: (root.wiredDevice && root.wiredUp &&
root.wiredDevice.linkSpeed > 0)
? (root.wiredDevice.linkSpeed + " Mbps") : "—"
}
]
delegate: RowLayout {
required property var modelData
Layout.fillWidth: true
Text {
text: modelData.label
color: root.t.netPopupDim
font.pixelSize: 12
Layout.fillWidth: true
}
Text {
text: modelData.value
color: root.t.netPopupText
font.pixelSize: 12
font.bold: true
font.family: "monospace"
}
}
}
}
}
Text {
visible: !root.hasWifi && !root.hasWired
text: "No network hardware detected."
color: root.t.netPopupDim
font.pixelSize: 12
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
Layout.topMargin: 4
}
ColumnLayout {
visible: root.showPwdPrompt
Layout.fillWidth: true
spacing: 8
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.netPopupSeparator }
Text {
text: "Connect to \"" + root.pendingSSID + "\""
color: root.t.netPopupHeader
font.pixelSize: 13
font.bold: true
elide: Text.ElideRight
Layout.fillWidth: true
}
Rectangle {
Layout.fillWidth: true
height: 36; radius: 8
color: root.t.netFieldBg
border.color: pwdField.activeFocus ? root.t.accent : root.t.netPopupBorder
border.width: 1
Behavior on border.color { ColorAnimation { duration: 120 } }
RowLayout {
anchors { fill: parent; leftMargin: 12; rightMargin: 10 }
spacing: 6
TextInput {
id: pwdField
Layout.fillWidth: true
verticalAlignment: TextInput.AlignVCenter
echoMode: eyeToggle.showClear
? TextInput.Normal : TextInput.Password
color: root.t.netPopupText
font.pixelSize: 13
selectionColor: Qt.alpha(root.t.accent, 0.4)
clip: true
text: root.enteredPwd
onTextChanged: root.enteredPwd = text
Keys.onReturnPressed: root.attemptConnect()
Keys.onEscapePressed: root.cancelConnect()
onVisibleChanged: if (visible) forceActiveFocus()
Text {
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
text: "Password"
color: root.t.netPopupDim
font.pixelSize: 13
visible: pwdField.text === "" && !pwdField.activeFocus
}
}
Text {
id: eyeToggle
property bool showClear: false
text: showClear ? "\uf0349" : "\uf0354"
color: eyeHover.hovered ? root.t.accent : root.t.netPopupDim
font.pixelSize: 15
Layout.alignment: Qt.AlignVCenter
HoverHandler { id: eyeHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: eyeToggle.showClear = !eyeToggle.showClear }
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Rectangle {
Layout.fillWidth: true
height: 34; radius: 8
color: cancelHover.hovered ? root.t.netCancelBgHover : "transparent"
border.color: root.t.netPopupBorder
border.width: 1
Behavior on color { ColorAnimation { duration: 120 } }
HoverHandler { id: cancelHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: root.cancelConnect() }
Text {
anchors.centerIn: parent
text: "Cancel"
color: root.t.netPopupDim
font.pixelSize: 12
}
}
Rectangle {
Layout.fillWidth: true
height: 34; radius: 8
color: connHover.hovered ? root.t.accent : Qt.alpha(root.t.accent, 0.75)
Behavior on color { ColorAnimation { duration: 120 } }
HoverHandler { id: connHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: root.attemptConnect() }
Text {
anchors.centerIn: parent
text: "Connect"
color: root.t.netConnectText
font.pixelSize: 12
font.bold: true
}
}
}
}
Item { height: 4 }
}
}
}
}
}

View File

@@ -1,6 +1,5 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import "../../config" as Cfg
@@ -25,7 +24,19 @@ Rectangle {
NumberAnimation { duration: 250; easing.type: Easing.OutCubic }
}
HoverHandler { cursorShape: Qt.PointingHandCursor }
property bool expanded: Cfg.Config.statsStartExpanded
property bool hovered: false
HoverHandler {
onHoveredChanged: root.hovered = hovered
}
TapHandler {
onTapped: {
root.expanded = !root.expanded
if (!root.expanded) root.hovered = false
}
}
property real cpuVal: 0
property real memVal: 0
@@ -36,166 +47,162 @@ Rectangle {
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
cpuProc.running = true
memProc.running = true
diskProc.running = true
}
onTriggered: { statsProc.running = true }
}
Process {
id: cpuProc
command: ["sh", "-c", "top -bn1 | awk '/^%Cpu/ {print 100-$8}'"]
id: statsProc
command: ["sh", "-c", "top -bn1 | awk '/^%Cpu/ {print 100-$8}'; free | awk '/Mem:/ {print ($2-$7)/$2 * 100}'; df / --output=pcent | tail -1 | tr -dc '0-9'"]
property string rawOutput: ""
stdout: SplitParser {
onRead: data => {
let val = parseFloat(data.trim())
root.cpuVal = isNaN(val) ? 0 : val
onRead: data => { statsProc.rawOutput += data + "\n" }
}
onRunningChanged: {
if (!statsProc.running && statsProc.rawOutput) {
const lines = statsProc.rawOutput.trim().split("\n")
if (lines.length >= 3) {
let v = parseFloat(lines[0]); root.cpuVal = isNaN(v) ? 0 : v
v = parseFloat(lines[1]); root.memVal = isNaN(v) ? 0 : v
v = parseFloat(lines[2]); root.diskVal = isNaN(v) ? 0 : v
}
statsProc.rawOutput = ""
}
}
}
Process {
id: memProc
command: ["sh", "-c", "free | awk '/Mem:/ {print ($2-$7)/$2 * 100}'"]
stdout: SplitParser {
onRead: data => {
let val = parseFloat(data.trim())
root.memVal = isNaN(val) ? 0 : val
}
}
}
Process {
id: diskProc
command: ["sh", "-c", "df / --output=pcent | tail -1 | tr -dc '0-9'"]
stdout: SplitParser {
onRead: data => {
let val = parseFloat(data.trim())
root.diskVal = isNaN(val) ? 0 : val
}
}
}
component StatRing: Item {
component StatRing: RowLayout {
id: ring
spacing: 5
property real value: 0.0
property string label: ""
property string icon: ""
property real iconXOff: 0
property color ringColor: Cfg.Config.theme.accent
property color trackColor: Cfg.Config.theme.statsTrackColor
property color labelColor: Cfg.Config.theme.textMain
Behavior on value {
NumberAnimation {
duration: 800
easing.type: Easing.OutCubic
}
NumberAnimation { duration: 800; easing.type: Easing.OutCubic }
}
readonly property real fraction: Math.max(0, Math.min(value, 100)) / 100
width: Cfg.Config.statsRingSize
height: Cfg.Config.statsRingSize
Text {
id: leftPct
text: Math.round(ring.value) + "%"
color: ring.ringColor
font.pixelSize: Cfg.Config.statsRingFontSize
font.bold: true
font.family: "monospace"
Layout.alignment: Qt.AlignVCenter
clip: true
onFractionChanged: arc.requestPaint()
onRingColorChanged: arc.requestPaint()
readonly property bool active: Cfg.Config.statsLabelPosition === "left" && (root.expanded || root.hovered)
opacity: active ? 1 : 0
Layout.preferredWidth: active ? implicitWidth : 0
Canvas {
id: arc
anchors.fill: parent
antialiasing: true
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
}
onPaint: {
var ctx = getContext("2d")
ctx.reset()
Item {
id: ringCore
Layout.preferredWidth: Cfg.Config.statsRingSize
Layout.preferredHeight: Cfg.Config.statsRingSize
var cx = width / 2
var cy = height / 2
var lw = 2.8
var r = Math.min(cx, cy) - lw / 2 - 0.5
onVisibleChanged: if (visible) arc.requestPaint()
ctx.beginPath()
ctx.arc(cx, cy, r, 0, Math.PI * 2)
ctx.strokeStyle = ring.trackColor
ctx.lineWidth = lw
ctx.stroke()
Canvas {
id: arc
anchors.fill: parent
antialiasing: true
Connections {
target: ring
function onFractionChanged() { arc.requestPaint() }
function onRingColorChanged() { arc.requestPaint() }
}
onPaint: {
var ctx = getContext("2d")
ctx.reset()
var cx = width / 2
var cy = height / 2
var lw = 2.8
var r = Math.min(cx, cy) - lw / 2 - 0.5
if (ring.fraction > 0) {
var start = -Math.PI / 2
var end = start + (ring.fraction * Math.PI * 2)
ctx.beginPath()
ctx.arc(cx, cy, r, start, end)
ctx.strokeStyle = ring.ringColor
ctx.arc(cx, cy, r, 0, Math.PI * 2)
ctx.strokeStyle = ring.trackColor
ctx.lineWidth = lw
ctx.lineCap = "round"
ctx.stroke()
if (ring.fraction > 0) {
var start = -Math.PI / 2
var end = start + (ring.fraction * Math.PI * 2)
ctx.beginPath()
ctx.arc(cx, cy, r, start, end)
ctx.strokeStyle = ring.ringColor
ctx.lineWidth = lw
ctx.lineCap = "round"
ctx.stroke()
}
}
}
}
HoverHandler {
id: hov
onHoveredChanged: if (!hovered) ring.showAlt = false
}
property bool showAlt: false
onShowAltChanged: arc.requestPaint()
TapHandler {
enabled: !Cfg.Config.statsHideTextUntilHover || hov.hovered
onTapped: ring.showAlt = !ring.showAlt
Text {
anchors.centerIn: parent
anchors.horizontalCenterOffset: ring.iconXOff
text: ring.icon
color: ring.ringColor
font.pixelSize: Cfg.Config.statsIconSize
}
}
Text {
anchors.centerIn: parent
id: rightPct
text: Math.round(ring.value) + "%"
color: ring.ringColor
font.pixelSize: Cfg.Config.statsRingFontSize
font.bold: true
font.family: "monospace"
Layout.alignment: Qt.AlignVCenter
clip: true
text: {
const hide = Cfg.Config.statsHideTextUntilHover
const byDefault = Cfg.Config.statsShowPercentByDefault
if (hide && !hov.hovered) return ""
const showPct = ring.showAlt ? !byDefault : byDefault
return showPct ? Math.round(ring.value) + "%" : ring.label
}
readonly property bool active: Cfg.Config.statsLabelPosition === "right" && (root.expanded || root.hovered)
opacity: active ? 1 : 0
Layout.preferredWidth: active ? implicitWidth : 0
color: ring.labelColor
font.pixelSize: Cfg.Config.statsRingFontSize
font.bold: true
font.family: "monospace"
opacity: (Cfg.Config.statsHideTextUntilHover && !hov.hovered) ? 0 : 1
Behavior on opacity {
NumberAnimation { duration: 180; easing.type: Easing.OutCubic }
}
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
}
}
RowLayout {
id: statsLayout
anchors.centerIn: parent
spacing: 10
spacing: 8
StatRing {
value: root.cpuVal
label: "cpu"
icon: "󰍛"
iconXOff: 0.3
ringColor: root.t.statsCpuColor
trackColor: root.t.statsTrackColor
labelColor: root.t.statsText
}
StatRing {
value: root.memVal
label: "ram"
icon: "󰑭"
ringColor: root.t.statsMemColor
trackColor: root.t.statsTrackColor
labelColor: root.t.statsText
}
StatRing {
value: root.diskVal
label: "disk"
icon: "󰒋"
ringColor: root.t.statsDiskColor
trackColor: root.t.statsTrackColor
labelColor: root.t.statsText
}
}
}

View File

@@ -13,9 +13,9 @@ Item {
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property string barSide: "right"
property bool isTop: true
readonly property var t: Cfg.Config.theme
@@ -23,14 +23,24 @@ Item {
property bool manuallyExpanded: false
property var menuWindow: null
readonly property bool effectiveExpanded: expanded || trayMenuPopup.popupOpen
implicitWidth: bgRect.width
implicitHeight: bgRect.height
readonly property color tmBg: t.trayMenuBg
readonly property color tmBorder: t.trayMenuBorder
readonly property color tmText: t.trayMenuText
readonly property color tmDim: t.trayMenuDim
readonly property color tmHover: t.trayMenuHover
readonly property color tmCheck: t.trayMenuCheck
readonly property color tmSeparator: t.trayMenuSeparator
Rectangle {
id: bgRect
anchors.verticalCenter: parent.verticalCenter
height: Cfg.Config.trayHeight
height: parent.height
width: mainRow.width
radius: root.panelRadius
@@ -41,8 +51,8 @@ Item {
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
HoverHandler {
id: bgHover
HoverHandler {
id: bgHover
onHoveredChanged: {
if (!root.manuallyExpanded) {
root.expanded = hovered;
@@ -54,21 +64,21 @@ Item {
id: mainRow
anchors.verticalCenter: parent.verticalCenter
leftPadding: root.expanded && root.barSide === "right" ? 8 : 0
rightPadding: root.expanded && root.barSide === "left" ? 8 : 0
leftPadding: root.effectiveExpanded && root.barSide === "right" ? 8 : 0
rightPadding: root.effectiveExpanded && root.barSide === "left" ? 8 : 0
Behavior on leftPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
Behavior on rightPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
layoutDirection: root.barSide === "left" ? Qt.LeftToRight : Qt.RightToLeft
spacing: root.expanded ? 4 : 0
spacing: root.effectiveExpanded ? 4 : 0
Behavior on spacing { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } }
Item {
id: toggleArea
width: Cfg.Config.trayHeight
height: Cfg.Config.trayHeight
width: Cfg.Config.trayToggleSize
height: Cfg.Config.trayToggleSize
z: 10
TapHandler {
@@ -80,11 +90,11 @@ Item {
Text {
anchors.centerIn: parent
text: root.expanded
text: root.effectiveExpanded
? (root.barSide === "left" ? "󰅁" : "󰅂")
: (root.barSide === "left" ? "󰅂" : "󰅁")
color: root.t.trayIcon
font.pixelSize: Cfg.Config.trayHeight * 0.55
font.pixelSize: Cfg.Config.trayToggleSize * 0.55
}
}
@@ -94,9 +104,9 @@ Item {
clip: true
anchors.verticalCenter: parent.verticalCenter
width: root.expanded ? implicitWidth : 0
opacity: root.expanded ? 1 : 0
width: root.effectiveExpanded ? implicitWidth : 0
opacity: root.effectiveExpanded ? 1 : 0
Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 150 } }
@@ -122,7 +132,9 @@ Item {
} else if (mouse.button === Qt.MiddleButton) {
item.secondaryActivate()
} else if (mouse.button === Qt.RightButton) {
menuAnchor.open()
if (item.hasMenu) {
trayMenuPopup.open(item, iconDelegate)
}
}
}
@@ -135,22 +147,462 @@ Item {
anchors.fill: parent
source: item.icon
}
}
}
}
}
}
QsMenuAnchor {
id: menuAnchor
menu: item.menu
anchor.window: root.menuWindow
anchor.adjustment: PopupAdjustment.Flip
anchor.onAnchoring: {
if (!root.menuWindow) return
const global = iconDelegate.mapToGlobal(0, iconDelegate.height)
const local = root.menuWindow.contentItem.mapFromGlobal(
global.x, global.y
)
menuAnchor.anchor.rect = Qt.rect(
local.x, local.y,
iconDelegate.width, 0
)
PanelWindow {
id: trayMenuPopup
screen: root.targetScreen
visible: false
color: "transparent"
property bool popupOpen: false
property bool _pendingOpen: false
property var activeTrayItem: null
property var activeDelegate: null
function open(item, delegate) {
if (popupOpen) close()
hideTimer.stop()
_pendingOpen = false
activeTrayItem = item
activeDelegate = delegate
rootMenuOpener.menu = item.menu
visible = true
if (menuCol.height > 0) {
popupOpen = true
} else {
_pendingOpen = true
openFallbackTimer.restart()
}
}
function close() {
_pendingOpen = false
openFallbackTimer.stop()
popupOpen = false
hideTimer.restart()
}
Connections {
target: menuCol
function onHeightChanged() {
if (trayMenuPopup._pendingOpen && menuCol.height > 0) {
trayMenuPopup._pendingOpen = false
openFallbackTimer.stop()
trayMenuPopup.popupOpen = true
}
}
}
Timer {
id: openFallbackTimer
interval: 400
onTriggered: {
if (trayMenuPopup._pendingOpen) {
trayMenuPopup._pendingOpen = false
trayMenuPopup.popupOpen = true
}
}
}
Timer {
id: hideTimer
interval: Cfg.Config.popupAnimDuration + 70
onTriggered: {
trayMenuPopup.visible = false
rootMenuOpener.menu = null
trayMenuPopup.activeTrayItem = null
trayMenuPopup.activeDelegate = null
}
}
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "main-shell-tray-menu"
WlrLayershell.exclusiveZone: -1
mask: popupOpen ? null : _trayMenuNoInput
Region { id: _trayMenuNoInput }
TapHandler { onTapped: trayMenuPopup.close() }
QsMenuOpener { id: rootMenuOpener }
Item {
id: menuCardWrapper
readonly property int cardW: 220
readonly property int pad: 10
x: {
if (!trayMenuPopup.activeDelegate)
return (parent.width - cardW) / 2
var gp = trayMenuPopup.activeDelegate.mapToGlobal(0, 0)
var cx = gp.x + trayMenuPopup.activeDelegate.width / 2
return Math.max(pad,
Math.min(cx - cardW / 2, parent.width - cardW - pad))
}
width: cardW
clip: true
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
anchors.topMargin: root.isTop ? (Cfg.Config.barHeight + 8) : 0
anchors.bottomMargin: root.isTop ? 0 : (Cfg.Config.barHeight + 8)
height: parent.height - Cfg.Config.barHeight - 8
Rectangle {
id: menuCard
width: parent.width
height: menuCol.height + 12
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
transform: Translate {
y: trayMenuPopup.popupOpen ? 0
: (root.isTop ? -menuCard.height : menuCard.height)
Behavior on y {
NumberAnimation {
duration: Cfg.Config.popupAnimDuration
easing.type: Easing.OutExpo
}
}
}
opacity: trayMenuPopup.popupOpen ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 200 } }
color: root.tmBg
radius: Cfg.Config.popupRadius
border.color: root.tmBorder
border.width: Cfg.Config.popupBorderWidth
Column {
id: menuCol
anchors { left: parent.left; right: parent.right; top: parent.top }
anchors.margins: 6
spacing: 0
Repeater {
model: rootMenuOpener.children
delegate: Item {
id: menuDelegate
required property var modelData
property bool isSep: modelData.isSeparator
width: menuCol.width
height: isSep ? 5 : 32
Rectangle {
visible: isSep
anchors {
left: parent.left; right: parent.right
verticalCenter: parent.verticalCenter
}
anchors.leftMargin: 8
anchors.rightMargin: 8
height: 1
color: root.tmSeparator
}
Rectangle {
id: itemBg
visible: !isSep
anchors.fill: parent
anchors.margins: 2
radius: 10
property bool active: itemMouse.containsMouse
|| (submenuCard.visible
&& trayMenuPopup._submenuParentRef === menuDelegate)
color: active ? root.tmHover : "transparent"
Behavior on color { ColorAnimation { duration: 120 } }
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 8
Item {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
visible: !isSep
Text {
anchors.centerIn: parent
visible: modelData.buttonType === QsMenuButtonType.CheckBox
&& modelData.checkState === Qt.Checked
text: "✓"
color: root.tmCheck
font.pixelSize: 14
}
Text {
anchors.centerIn: parent
visible: modelData.buttonType === QsMenuButtonType.RadioButton
&& modelData.checkState === Qt.Checked
text: "●"
color: root.tmCheck
font.pixelSize: 9
}
Text {
anchors.centerIn: parent
visible: modelData.buttonType === QsMenuButtonType.RadioButton
&& modelData.checkState !== Qt.Checked
text: "○"
color: root.tmDim
font.pixelSize: 11
}
}
Image {
visible: !isSep && modelData.icon !== ""
source: modelData.icon
width: 16; height: 16
sourceSize: Qt.size(16, 16)
fillMode: Image.PreserveAspectFit
}
Text {
text: modelData.text
color: modelData.enabled ? root.tmText : root.tmDim
font.pixelSize: 13
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
visible: !isSep && modelData.hasChildren
text: "▶"
color: root.tmDim
font.pixelSize: 9
}
}
MouseArea {
id: itemMouse
anchors.fill: parent
hoverEnabled: true
enabled: !isSep && modelData.enabled
onClicked: {
if (modelData.hasChildren) {
closeSubmenuTimer.stop()
submenuDelayTimer.itemRef = menuDelegate
submenuDelayTimer.restart()
} else {
modelData.triggered()
trayMenuPopup.close()
}
}
onEntered: {
if (modelData.hasChildren) {
closeSubmenuTimer.stop()
submenuDelayTimer.itemRef = menuDelegate
submenuDelayTimer.restart()
} else {
closeSubmenuTimer.stop()
submenuCard.visible = false
}
}
onExited: {
if (modelData.hasChildren)
closeSubmenuTimer.restart()
}
}
}
}
}
}
}
}
property var _submenuParentRef: null
QsMenuOpener { id: subMenuOpener }
Timer {
id: submenuDelayTimer
interval: 250
property var itemRef: null
onTriggered: {
if (!itemRef) return
var entry = itemRef.modelData
if (!entry || !entry.hasChildren) return
subMenuOpener.menu = entry
trayMenuPopup._submenuParentRef = itemRef
submenuCard.visible = true
}
}
Timer {
id: closeSubmenuTimer
interval: 300
onTriggered: {
if (!submenuCard.containsMouse) {
submenuCard.visible = false
subMenuOpener.menu = null
trayMenuPopup._submenuParentRef = null
}
}
}
Rectangle {
id: submenuCard
visible: false
property bool containsMouse: false
property int subW: Math.max(submenuCol.width, 180)
x: {
if (!trayMenuPopup._submenuParentRef) return 0
var pItem = trayMenuPopup._submenuParentRef
var gp = pItem.mapToGlobal(0, 0)
var lp = trayMenuPopup.contentItem.mapFromGlobal(gp.x, gp.y)
var fromLeft = lp.x + pItem.width + 4
if (fromLeft + subW + 10 > trayMenuPopup.contentItem.width)
return Math.max(2, lp.x - subW - 4)
return fromLeft
}
y: {
if (!trayMenuPopup._submenuParentRef) return 0
var pItem = trayMenuPopup._submenuParentRef
var gp = pItem.mapToGlobal(0, 0)
var lp = trayMenuPopup.contentItem.mapFromGlobal(gp.x, gp.y)
return Math.max(2, lp.y - 2)
}
width: subW
height: submenuCol.height + 12
color: root.tmBg
radius: Cfg.Config.popupRadius
border.color: root.tmBorder
border.width: Cfg.Config.popupBorderWidth
HoverHandler {
onHoveredChanged: {
submenuCard.containsMouse = hovered
if (!hovered)
closeSubmenuTimer.restart()
}
}
Column {
id: submenuCol
anchors { left: parent.left; right: parent.right; top: parent.top }
anchors.margins: 6
spacing: 0
Repeater {
model: subMenuOpener.children
delegate: Item {
required property var modelData
property bool isSep: modelData.isSeparator
width: submenuCol.width
height: isSep ? 5 : 32
Rectangle {
visible: isSep
anchors {
left: parent.left; right: parent.right
verticalCenter: parent.verticalCenter
}
anchors.leftMargin: 8; anchors.rightMargin: 8
height: 1
color: root.tmSeparator
}
Rectangle {
id: subBg
visible: !isSep
anchors.fill: parent
anchors.margins: 2
radius: 10
color: subMouse.containsMouse ? root.tmHover : "transparent"
Behavior on color { ColorAnimation { duration: 120 } }
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8; anchors.rightMargin: 8
spacing: 8
Item {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
visible: !isSep
Text {
anchors.centerIn: parent
visible: modelData.buttonType === QsMenuButtonType.CheckBox
&& modelData.checkState === Qt.Checked
text: "✓"; color: root.tmCheck
font.pixelSize: 14
}
Text {
anchors.centerIn: parent
visible: modelData.buttonType === QsMenuButtonType.RadioButton
&& modelData.checkState === Qt.Checked
text: "●"; color: root.tmCheck
font.pixelSize: 9
}
Text {
anchors.centerIn: parent
visible: modelData.buttonType === QsMenuButtonType.RadioButton
&& modelData.checkState !== Qt.Checked
text: "○"; color: root.tmDim
font.pixelSize: 11
}
}
Image {
visible: !isSep && modelData.icon !== ""
source: modelData.icon
width: 16; height: 16
sourceSize: Qt.size(16, 16)
fillMode: Image.PreserveAspectFit
}
Text {
text: modelData.text
color: modelData.enabled ? root.tmText : root.tmDim
font.pixelSize: 13
Layout.fillWidth: true
elide: Text.ElideRight
}
}
MouseArea {
id: subMouse
anchors.fill: parent
hoverEnabled: true
enabled: !isSep && modelData.enabled
onClicked: {
modelData.triggered()
trayMenuPopup.close()
}
}
}
}

View File

@@ -22,6 +22,42 @@ Rectangle {
property int outputMaxVolume: Cfg.Config.volumeOutputMax
property int inputMaxVolume: Cfg.Config.volumeInputMax
// Path to config.qml — using HOME to avoid Qt.resolvedUrl ambiguity
readonly property string configPath:
Quickshell.env("HOME") + "/.config/quickshell/config/config.qml"
// Process that patches volumeOutputMax / volumeInputMax in config.qml
Process {
id: volConfigWriter
running: false
onExited: (code) => {
if (code !== 0)
console.warn("[Volume] Failed to patch config.qml (exit " + code + ")")
}
}
// Debounce: write to disk 800 ms after the last limit change
Timer {
id: saveDebounce
interval: 800
repeat: false
onTriggered: {
const outMax = Math.round(root.outputMaxVolume).toString()
const inMax = Math.round(root.inputMaxVolume).toString()
const cfgPath = root.configPath
const sh =
"sed -i" +
" -e 's/\\(property int[ ]\\+volumeOutputMax:[ ]\\+\\)[0-9.]\\+/\\1" + outMax + "/'" +
" -e 's/\\(property int[ ]\\+volumeInputMax:[ ]\\+\\)[0-9.]\\+/\\1" + inMax + "/'" +
" " + JSON.stringify(cfgPath)
volConfigWriter.command = ["/bin/sh", "-c", sh]
volConfigWriter.running = true
}
}
onOutputMaxVolumeChanged: { if (root.outputMaxVolume >= 10) saveDebounce.restart() }
onInputMaxVolumeChanged: { if (root.inputMaxVolume >= 10) saveDebounce.restart() }
readonly property var sink: Pipewire.defaultAudioSink
readonly property var source: Pipewire.defaultAudioSource