Compare commits

...
Author SHA1 Message Date
Damocles
a0cb1d991d extract BatteryApplet from BatteryModule 2026-04-22 21:17:34 +02:00
Damocles
57d42d7ac3 add BatteryService, refactor BatteryModule to use it 2026-04-22 21:13:29 +02:00
Damocles
d814ee041f extract GpuApplet from GpuModule 2026-04-22 21:06:56 +02:00
Damocles
fcecfc84f2 gpu module: swap icon to eb4c 2026-04-22 21:02:26 +02:00
7 changed files with 600 additions and 551 deletions

View file

@ -0,0 +1,276 @@
import QtQuick
import "../services" as S
Column {
id: root
required property color accentColor
property bool active: true
readonly property color _stateColor: S.BatteryService.charging ? S.Theme.base0B : S.BatteryService.critical ? S.Theme.base09 : S.BatteryService.percent < S.BatteryService.warnThresh ? S.Theme.base0A : root.accentColor
// Header - pct + time
Item {
width: parent.width
height: 28
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: {
const t = S.BatteryService.charging ? S.BatteryService.timeToFull : S.BatteryService.timeToEmpty;
const ts = S.BatteryService.fmtTime(t);
return Math.round(S.BatteryService.percent) + "%" + (ts ? " " + ts : "");
}
color: root._stateColor
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.fontFamily
font.bold: true
}
}
// Progress bar
Item {
width: parent.width
height: 14
Item {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
height: 6
Rectangle {
anchors.fill: parent
color: S.Theme.base02
radius: 3
}
Rectangle {
width: parent.width * Math.min(1, S.BatteryService.percent / 100)
height: parent.height
color: root._stateColor
radius: 3
Behavior on width {
enabled: root.active
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
// Warning threshold marker
Rectangle {
x: parent.width * (S.BatteryService.warnThresh / 100) - 1
width: 1
height: parent.height + 4
anchors.verticalCenter: parent.verticalCenter
color: S.Theme.base0A
opacity: 0.6
}
// Critical threshold marker
Rectangle {
x: parent.width * (S.BatteryService.critThresh / 100) - 1
width: 1
height: parent.height + 4
anchors.verticalCenter: parent.verticalCenter
color: S.Theme.base08
opacity: 0.6
}
}
}
// 24h history sparkline (area chart)
Canvas {
id: _sparkline
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
height: 44
property var _hist: S.BatteryService.history
property color _col: root._stateColor
on_HistChanged: if (root.active)
requestPaint()
on_ColChanged: if (root.active)
requestPaint()
onVisibleChanged: if (visible)
requestPaint()
onPaint: {
const ctx = getContext("2d");
if (!ctx)
return;
ctx.clearRect(0, 0, width, height);
const d = _hist;
if (d.length < 2)
return;
const maxSamples = 1440;
const xScale = width / maxSamples;
const xOffset = (maxSamples - d.length) * xScale;
// Background tint
ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.07).toString();
ctx.fillRect(0, 0, width, height);
// Warning threshold line
const warnY = height - height * (S.BatteryService.warnThresh / 100);
ctx.strokeStyle = S.Theme.base0A.toString();
ctx.globalAlpha = 0.3;
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.beginPath();
ctx.moveTo(0, warnY);
ctx.lineTo(width, warnY);
ctx.stroke();
// Critical threshold line
const critY = height - height * (S.BatteryService.critThresh / 100);
ctx.strokeStyle = S.Theme.base08.toString();
ctx.beginPath();
ctx.moveTo(0, critY);
ctx.lineTo(width, critY);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1.0;
// Filled area under the curve
ctx.beginPath();
ctx.moveTo(xOffset, height);
for (let i = 0; i < d.length; i++) {
const x = xOffset + i * xScale;
const y = height - height * (d[i] / 100);
ctx.lineTo(x, y);
}
ctx.lineTo(xOffset + (d.length - 1) * xScale, height);
ctx.closePath();
ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.18).toString();
ctx.fill();
// Top line
ctx.beginPath();
for (let i = 0; i < d.length; i++) {
const x = xOffset + i * xScale;
const y = height - height * (d[i] / 100);
if (i === 0)
ctx.moveTo(x, y);
else
ctx.lineTo(x, y);
}
ctx.strokeStyle = _col.toString();
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
// Footer: thresholds + time label
Item {
width: parent.width
height: 16
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "warn " + S.BatteryService.warnThresh + "% crit " + S.BatteryService.critThresh + "%"
color: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
font.letterSpacing: 0.5
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "24h"
color: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
}
}
// Separator
Rectangle {
width: parent.width - 16
height: 1
anchors.horizontalCenter: parent.horizontalCenter
color: S.Theme.base03
}
// Rate row
Item {
width: parent.width
height: 20
visible: S.BatteryService.changeRate !== 0
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: S.BatteryService.charging ? "Charging" : "Discharging"
color: S.Theme.base04
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: {
const r = Math.abs(S.BatteryService.changeRate);
return r > 0 ? r.toFixed(1) + " W" : "";
}
color: root._stateColor
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
}
// Health row
Item {
width: parent.width
height: 20
visible: S.BatteryService.healthSupported
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "Health"
color: S.Theme.base04
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: Math.round(S.BatteryService.healthPercent) + "%"
color: {
const h = S.BatteryService.healthPercent;
return h < 50 ? S.Theme.base08 : h < 75 ? S.Theme.base0A : S.Theme.base0B;
}
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
}
Item {
width: 1
height: 4
}
}

223
shell/applets/GpuApplet.qml Normal file
View file

@ -0,0 +1,223 @@
import QtQuick
import "../services" as S
Column {
id: root
required property color accentColor
property bool active: true
function _loadColor(pct) {
const t = Math.max(0, Math.min(100, pct)) / 100;
const a = t < 0.5 ? S.Theme.base0B : S.Theme.base0A;
const b = t < 0.5 ? S.Theme.base0A : S.Theme.base08;
const u = t < 0.5 ? t * 2 : (t - 0.5) * 2;
return Qt.rgba(a.r + (b.r - a.r) * u, a.g + (b.g - a.g) * u, a.b + (b.b - a.b) * u, 1);
}
function _fmt(gb) {
return gb >= 10 ? gb.toFixed(1) + "G" : gb.toFixed(2) + "G";
}
// Header - vendor + usage%
Item {
width: parent.width
height: 28
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: S.SystemStats.gpuVendor.toUpperCase()
color: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
font.letterSpacing: 1
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: S.SystemStats.gpuUsage + "%"
color: root._loadColor(S.SystemStats.gpuUsage)
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.fontFamily
font.bold: true
}
}
// Usage bar
Item {
width: parent.width
height: 14
Item {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
height: 6
Rectangle {
anchors.fill: parent
color: S.Theme.base02
radius: 3
}
Rectangle {
width: parent.width * Math.min(1, S.SystemStats.gpuUsage / 100)
height: parent.height
color: root._loadColor(S.SystemStats.gpuUsage)
radius: 3
Behavior on width {
enabled: root.active
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
}
}
// Usage history sparkline
Canvas {
id: _sparkline
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
height: 36
property var _hist: S.SystemStats.gpuHistory
on_HistChanged: if (root.active)
requestPaint()
onVisibleChanged: if (visible)
requestPaint()
onPaint: {
const ctx = getContext("2d");
if (!ctx)
return;
ctx.clearRect(0, 0, width, height);
const d = _hist;
if (!d.length)
return;
const maxSamples = 60;
const bw = width / maxSamples;
const offset = maxSamples - d.length;
for (let i = 0; i < d.length; i++) {
const barH = Math.max(1, height * d[i] / 100);
const col = root._loadColor(d[i]);
ctx.fillStyle = col.toString();
ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH);
}
}
}
// VRAM section
Rectangle {
width: parent.width - 16
height: 1
anchors.horizontalCenter: parent.horizontalCenter
color: S.Theme.base03
}
Item {
width: parent.width
height: 22
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "VRAM"
color: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
font.letterSpacing: 1
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root._fmt(S.SystemStats.gpuVramUsedGb) + " / " + root._fmt(S.SystemStats.gpuVramTotalGb)
color: root.accentColor
font.pixelSize: S.Theme.fontSize - 1
font.family: S.Theme.fontFamily
font.bold: true
}
}
Item {
width: parent.width
height: 12
Item {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
height: 5
Rectangle {
anchors.fill: parent
color: S.Theme.base02
radius: 2
}
Rectangle {
width: S.SystemStats.gpuVramTotalGb > 0 ? parent.width * Math.min(1, S.SystemStats.gpuVramUsedGb / S.SystemStats.gpuVramTotalGb) : 0
height: parent.height
color: root.accentColor
radius: 2
Behavior on width {
enabled: root.active
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
}
}
// Temperature row
Item {
width: parent.width
height: 22
visible: S.SystemStats.gpuTempC > 0
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "Temp"
color: S.Theme.base04
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: S.SystemStats.gpuTempC + "\u00B0C"
color: S.SystemStats.gpuTempC > 85 ? S.Theme.base08 : S.SystemStats.gpuTempC > 70 ? S.Theme.base0A : S.Theme.base05
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
}
Item {
width: 1
height: 4
}
}

View file

@ -1,8 +1,10 @@
module applets module applets
# keep-sorted start # keep-sorted start
BacklightApplet 1.0 BacklightApplet.qml BacklightApplet 1.0 BacklightApplet.qml
BatteryApplet 1.0 BatteryApplet.qml
CpuApplet 1.0 CpuApplet.qml CpuApplet 1.0 CpuApplet.qml
DiskApplet 1.0 DiskApplet.qml DiskApplet 1.0 DiskApplet.qml
GpuApplet 1.0 GpuApplet.qml
HexWaveBackground 1.0 HexWaveBackground.qml HexWaveBackground 1.0 HexWaveBackground.qml
MemoryApplet 1.0 MemoryApplet.qml MemoryApplet 1.0 MemoryApplet.qml
MprisApplet 1.0 MprisApplet.qml MprisApplet 1.0 MprisApplet.qml

View file

@ -1,28 +1,20 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower
import "." as M import "." as M
import "../services" as S import "../services" as S
import "../applets" as C
M.BarSection { M.BarSection {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
opacity: S.Modules.battery.enable && (UPower.displayDevice?.isLaptopBattery ?? false) ? 1 : 0 opacity: S.Modules.battery.enable && S.BatteryService.available ? 1 : 0
visible: opacity > 0 visible: opacity > 0
tooltip: "" tooltip: ""
readonly property var dev: UPower.displayDevice
readonly property real pct: (dev?.percentage ?? 0) * 100
readonly property bool charging: dev?.state === UPowerDeviceState.Charging
readonly property int _critThresh: S.Modules.battery.critical || 15
readonly property int _warnThresh: S.Modules.battery.warning || 25
readonly property bool _critical: pct < _critThresh && !charging
property color _stateColor: charging ? S.Theme.base0B : _critical ? S.Theme.base09 : pct < _warnThresh ? S.Theme.base0A : root.accentColor
property real _blinkOpacity: 1 property real _blinkOpacity: 1
SequentialAnimation { SequentialAnimation {
running: root._critical running: S.BatteryService.critical
loops: Animation.Infinite loops: Animation.Infinite
NumberAnimation { NumberAnimation {
target: root target: root
@ -42,48 +34,7 @@ M.BarSection {
root._blinkOpacity = 1 root._blinkOpacity = 1
} }
// Notifications // Panel state
property bool _warnSent: false
property bool _critSent: false
onChargingChanged: {
_warnSent = false;
_critSent = false;
}
onPctChanged: {
if (charging)
return;
if (pct < _critThresh && !_critSent) {
_critSent = true;
_warnSent = true;
_notif.command = ["notify-send", "--urgency=critical", "--icon=battery-low", "--category=device", "Very Low Battery", "Connect to power now!"];
_notif.running = true;
} else if (pct < _warnThresh && !_warnSent) {
_warnSent = true;
_notif.command = ["notify-send", "--icon=battery-caution", "--category=device", "Low Battery"];
_notif.running = true;
}
}
Process {
id: _notif
}
// History (always-running, 1440 samples @ 60s = 24h)
property var _history: []
Timer {
interval: 60000
running: root.visible
repeat: true
triggeredOnStart: true
onTriggered: {
const h = root._history.concat([root.pct]);
root._history = h.length > 1440 ? h.slice(h.length - 1440) : h;
}
}
// Panel state
property bool _pinned: false property bool _pinned: false
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
readonly property bool _showPanel: _anyHover || _pinned readonly property bool _showPanel: _anyHover || _pinned
@ -101,23 +52,15 @@ M.BarSection {
onTriggered: root._pinned = false onTriggered: root._pinned = false
} }
function _fmtTime(secs) { // Bar widgets
if (!secs || secs <= 0)
return "";
const h = Math.floor(secs / 3600);
const m = Math.floor((secs % 3600) / 60);
return h > 0 ? h + "h " + m + "m" : m + "m";
}
// Bar widgets
M.BarIcon { M.BarIcon {
icon: { icon: {
if (root.charging) if (S.BatteryService.charging)
return "\uDB80\uDC84"; return "\uDB80\uDC84";
const icons = ["\uDB80\uDC8E", "\uDB80\uDC7A", "\uDB80\uDC7B", "\uDB80\uDC7C", "\uDB80\uDC7D", "\uDB80\uDC7E", "\uDB80\uDC7F", "\uDB80\uDC80", "\uDB80\uDC81", "\uDB80\uDC82", "\uDB85\uDFE2"]; const icons = ["\uDB80\uDC8E", "\uDB80\uDC7A", "\uDB80\uDC7B", "\uDB80\uDC7C", "\uDB80\uDC7D", "\uDB80\uDC7E", "\uDB80\uDC7F", "\uDB80\uDC80", "\uDB80\uDC81", "\uDB80\uDC82", "\uDB85\uDFE2"];
return icons[Math.min(10, Math.floor(root.pct / 10))]; return icons[Math.min(10, Math.floor(S.BatteryService.percent / 10))];
} }
color: root._stateColor color: S.BatteryService.stateColor
opacity: root._blinkOpacity opacity: root._blinkOpacity
font.pixelSize: S.Theme.fontSize + 2 font.pixelSize: S.Theme.fontSize + 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -126,9 +69,9 @@ M.BarSection {
} }
} }
M.BarLabel { M.BarLabel {
label: Math.round(root.pct) + "%" label: Math.round(S.BatteryService.percent) + "%"
minText: "100%" minText: "100%"
color: root._stateColor color: S.BatteryService.stateColor
opacity: root._blinkOpacity opacity: root._blinkOpacity
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler { TapHandler {
@ -136,7 +79,7 @@ M.BarSection {
} }
} }
// Hover panel // Hover panel
M.HoverPanel { M.HoverPanel {
id: hoverPanel id: hoverPanel
showPanel: root._showPanel showPanel: root._showPanel
@ -147,272 +90,10 @@ M.BarSection {
panelTitle: "Battery" panelTitle: "Battery"
contentWidth: 240 contentWidth: 240
// Header pct + time C.BatteryApplet {
Item { width: hoverPanel.contentWidth
width: parent.width active: root._showPanel
height: 28 accentColor: root.accentColor
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: {
const t = root.charging ? root.dev?.timeToFull : root.dev?.timeToEmpty;
const ts = root._fmtTime(t);
return Math.round(root.pct) + "%" + (ts ? " " + ts : "");
}
color: root._stateColor
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.fontFamily
font.bold: true
}
}
// Progress bar
Item {
width: parent.width
height: 14
Item {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
height: 6
Rectangle {
anchors.fill: parent
color: S.Theme.base02
radius: 3
}
Rectangle {
width: parent.width * Math.min(1, root.pct / 100)
height: parent.height
color: root._stateColor
radius: 3
Behavior on width {
enabled: root._showPanel
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
// Warning threshold marker
Rectangle {
x: parent.width * (root._warnThresh / 100) - 1
width: 1
height: parent.height + 4
anchors.verticalCenter: parent.verticalCenter
color: S.Theme.base0A
opacity: 0.6
}
// Critical threshold marker
Rectangle {
x: parent.width * (root._critThresh / 100) - 1
width: 1
height: parent.height + 4
anchors.verticalCenter: parent.verticalCenter
color: S.Theme.base08
opacity: 0.6
}
}
}
// 24h history sparkline (area chart)
Canvas {
id: _sparkline
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
height: 44
property var _hist: root._history
property color _col: root._stateColor
on_HistChanged: if (root._showPanel)
requestPaint()
on_ColChanged: if (root._showPanel)
requestPaint()
Connections {
target: root
function on_ShowPanelChanged() {
if (root._showPanel)
_sparkline.requestPaint();
}
}
onPaint: {
const ctx = getContext("2d");
if (!ctx)
return;
ctx.clearRect(0, 0, width, height);
const d = _hist;
if (d.length < 2)
return;
const maxSamples = 1440;
const xScale = width / maxSamples;
const xOffset = (maxSamples - d.length) * xScale;
// Background tint
ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.07).toString();
ctx.fillRect(0, 0, width, height);
// Warning threshold line
const warnY = height - height * (root._warnThresh / 100);
ctx.strokeStyle = S.Theme.base0A.toString();
ctx.globalAlpha = 0.3;
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.beginPath();
ctx.moveTo(0, warnY);
ctx.lineTo(width, warnY);
ctx.stroke();
// Critical threshold line
const critY = height - height * (root._critThresh / 100);
ctx.strokeStyle = S.Theme.base08.toString();
ctx.beginPath();
ctx.moveTo(0, critY);
ctx.lineTo(width, critY);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1.0;
// Filled area under the curve
ctx.beginPath();
ctx.moveTo(xOffset, height);
for (let i = 0; i < d.length; i++) {
const x = xOffset + i * xScale;
const y = height - height * (d[i] / 100);
ctx.lineTo(x, y);
}
ctx.lineTo(xOffset + (d.length - 1) * xScale, height);
ctx.closePath();
ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.18).toString();
ctx.fill();
// Top line
ctx.beginPath();
for (let i = 0; i < d.length; i++) {
const x = xOffset + i * xScale;
const y = height - height * (d[i] / 100);
if (i === 0)
ctx.moveTo(x, y);
else
ctx.lineTo(x, y);
}
ctx.strokeStyle = _col.toString();
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
// Footer: thresholds + time label
Item {
width: parent.width
height: 16
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "warn " + root._warnThresh + "% crit " + root._critThresh + "%"
color: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
font.letterSpacing: 0.5
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "24h"
color: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
}
}
// Separator
Rectangle {
width: parent.width - 16
height: 1
anchors.horizontalCenter: parent.horizontalCenter
color: S.Theme.base03
}
// Rate + health rows
Item {
width: parent.width
height: 20
visible: (root.dev?.changeRate ?? 0) !== 0
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root.charging ? "Charging" : "Discharging"
color: S.Theme.base04
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: {
const r = Math.abs(root.dev?.changeRate ?? 0);
return r > 0 ? r.toFixed(1) + " W" : "";
}
color: root._stateColor
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
}
Item {
width: parent.width
height: 20
visible: root.dev?.healthSupported ?? false
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "Health"
color: S.Theme.base04
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: Math.round((root.dev?.healthPercentage ?? 0) * 100) + "%"
color: {
const h = (root.dev?.healthPercentage ?? 1) * 100;
return h < 50 ? S.Theme.base08 : h < 75 ? S.Theme.base0A : S.Theme.base0B;
}
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
}
Item {
width: 1
height: 4
} }
} }
} }

View file

@ -2,6 +2,7 @@ import QtQuick
import Quickshell import Quickshell
import "." as M import "." as M
import "../services" as S import "../services" as S
import "../applets" as C
M.BarSection { M.BarSection {
id: root id: root
@ -26,20 +27,8 @@ M.BarSection {
onTriggered: root._pinned = false onTriggered: root._pinned = false
} }
function _loadColor(pct) {
const t = Math.max(0, Math.min(100, pct)) / 100;
const a = t < 0.5 ? S.Theme.base0B : S.Theme.base0A;
const b = t < 0.5 ? S.Theme.base0A : S.Theme.base08;
const u = t < 0.5 ? t * 2 : (t - 0.5) * 2;
return Qt.rgba(a.r + (b.r - a.r) * u, a.g + (b.g - a.g) * u, a.b + (b.b - a.b) * u, 1);
}
function _fmt(gb) {
return gb >= 10 ? gb.toFixed(1) + "G" : gb.toFixed(2) + "G";
}
M.BarIcon { M.BarIcon {
icon: "\uDB84\uDCB0" icon: "\uEB4C"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler { TapHandler {
onTapped: root._pinned = !root._pinned onTapped: root._pinned = !root._pinned
@ -64,210 +53,10 @@ M.BarSection {
panelTitle: "GPU" panelTitle: "GPU"
contentWidth: 240 contentWidth: 240
// Header vendor + usage% C.GpuApplet {
Item { width: hoverPanel.contentWidth
width: parent.width active: root._showPanel
height: 28 accentColor: root.accentColor
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: S.SystemStats.gpuVendor.toUpperCase()
color: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
font.letterSpacing: 1
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: S.SystemStats.gpuUsage + "%"
color: root._loadColor(S.SystemStats.gpuUsage)
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.fontFamily
font.bold: true
}
}
// Usage bar
Item {
width: parent.width
height: 14
Item {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
height: 6
Rectangle {
anchors.fill: parent
color: S.Theme.base02
radius: 3
}
Rectangle {
width: parent.width * Math.min(1, S.SystemStats.gpuUsage / 100)
height: parent.height
color: root._loadColor(S.SystemStats.gpuUsage)
radius: 3
Behavior on width {
enabled: root._showPanel
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
}
}
// Usage history sparkline
Canvas {
id: _sparkline
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
height: 36
property var _hist: S.SystemStats.gpuHistory
on_HistChanged: if (root._showPanel)
requestPaint()
Connections {
target: root
function on_ShowPanelChanged() {
if (root._showPanel)
_sparkline.requestPaint();
}
}
onPaint: {
const ctx = getContext("2d");
if (!ctx)
return;
ctx.clearRect(0, 0, width, height);
const d = _hist;
if (!d.length)
return;
const maxSamples = 60;
const bw = width / maxSamples;
const offset = maxSamples - d.length;
for (let i = 0; i < d.length; i++) {
const barH = Math.max(1, height * d[i] / 100);
const col = root._loadColor(d[i]);
ctx.fillStyle = col.toString();
ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH);
}
}
}
// VRAM section
Rectangle {
width: parent.width - 16
height: 1
anchors.horizontalCenter: parent.horizontalCenter
color: S.Theme.base03
}
Item {
width: parent.width
height: 22
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "VRAM"
color: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
font.letterSpacing: 1
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root._fmt(S.SystemStats.gpuVramUsedGb) + " / " + root._fmt(S.SystemStats.gpuVramTotalGb)
color: root.accentColor
font.pixelSize: S.Theme.fontSize - 1
font.family: S.Theme.fontFamily
font.bold: true
}
}
Item {
width: parent.width
height: 12
Item {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
height: 5
Rectangle {
anchors.fill: parent
color: S.Theme.base02
radius: 2
}
Rectangle {
width: S.SystemStats.gpuVramTotalGb > 0 ? parent.width * Math.min(1, S.SystemStats.gpuVramUsedGb / S.SystemStats.gpuVramTotalGb) : 0
height: parent.height
color: root.accentColor
radius: 2
Behavior on width {
enabled: root._showPanel
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
}
}
// Temperature row
Item {
width: parent.width
height: 22
visible: S.SystemStats.gpuTempC > 0
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "Temp"
color: S.Theme.base04
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: S.SystemStats.gpuTempC + "\u00B0C"
color: S.SystemStats.gpuTempC > 85 ? S.Theme.base08 : S.SystemStats.gpuTempC > 70 ? S.Theme.base0A : S.Theme.base05
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
}
Item {
width: 1
height: 4
} }
} }
} }

View file

@ -0,0 +1,77 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower
import "." as S
QtObject {
id: root
readonly property var dev: UPower.displayDevice
readonly property bool available: dev?.isLaptopBattery ?? false
readonly property real pct: (dev?.percentage ?? 0) * 100
readonly property bool charging: dev?.state === UPowerDeviceState.Charging
readonly property real changeRate: dev?.changeRate ?? 0
readonly property int timeToFull: dev?.timeToFull ?? 0
readonly property int timeToEmpty: dev?.timeToEmpty ?? 0
readonly property bool healthSupported: dev?.healthSupported ?? false
readonly property real healthPct: (dev?.healthPercentage ?? 1) * 100
readonly property int critThresh: S.Modules.battery.critical || 15
readonly property int warnThresh: S.Modules.battery.warning || 25
readonly property bool critical: pct < critThresh && !charging
readonly property color stateColor: charging ? S.Theme.base0B : critical ? S.Theme.base09 : pct < warnThresh ? S.Theme.base0A : S.Theme.base05
// 24h history (1440 samples @ 60s)
property var history: []
property var _histTimer: Timer {
interval: 60000
running: root.available
repeat: true
triggeredOnStart: true
onTriggered: {
const h = root.history.concat([root.pct]);
root.history = h.length > 1440 ? h.slice(h.length - 1440) : h;
}
}
// Low battery notifications
property bool _warnSent: false
property bool _critSent: false
property var _chargingWatcher: Connections {
target: root
function onChargingChanged() {
root._warnSent = false;
root._critSent = false;
}
function onPctChanged() {
if (root.charging)
return;
if (root.pct < root.critThresh && !root._critSent) {
root._critSent = true;
root._warnSent = true;
_notifProc.command = ["notify-send", "--urgency=critical", "--icon=battery-low", "--category=device", "Very Low Battery", "Connect to power now!"];
_notifProc.running = true;
} else if (root.pct < root.warnThresh && !root._warnSent) {
root._warnSent = true;
_notifProc.command = ["notify-send", "--icon=battery-caution", "--category=device", "Low Battery"];
_notifProc.running = true;
}
}
}
property var _notifProc: Process {}
function fmtTime(secs) {
if (!secs || secs <= 0)
return "";
const h = Math.floor(secs / 3600);
const m = Math.floor((secs % 3600) / 60);
return h > 0 ? h + "h " + m + "m" : m + "m";
}
}

View file

@ -2,6 +2,7 @@ module services
# keep-sorted start # keep-sorted start
NotifItem 1.0 NotifItem.qml NotifItem 1.0 NotifItem.qml
singleton BacklightService 1.0 BacklightService.qml singleton BacklightService 1.0 BacklightService.qml
singleton BatteryService 1.0 BatteryService.qml
singleton BluetoothService 1.0 BluetoothService.qml singleton BluetoothService 1.0 BluetoothService.qml
singleton IdleInhibitService 1.0 IdleInhibitService.qml singleton IdleInhibitService 1.0 IdleInhibitService.qml
singleton LockService 1.0 LockService.qml singleton LockService 1.0 LockService.qml