diff --git a/README.md b/README.md index 3d21ada..e455866 100644 --- a/README.md +++ b/README.md @@ -16,23 +16,17 @@ kept saying "yes" and I don't have the self-awareness to stop. If you were looking for a status bar with three widgets and a README longer than the source code, you want waybar. -- Status bar with too many widgets, grouped into glowing color-coded sections -- Notification center that replaces swaync (whether you wanted that or not) -- Hover panels for volume, brightness, network, and media — OSD, mixer, device list, and wifi connections unified into hover panels; pin button keeps them open while you interact -- Volume panel shows output devices and per-app stream sliders inline -- CPU panel: per-core usage bars with load-colored sparklines, frequency readout, thermal throttle detection (freq label turns red), P/E-core grouping on hybrid CPUs, top processes by CPU usage -- Memory panel: used/cached/available breakdown with stacked bar, top processes by memory -- Disk panel: per-mount usage bars with color-coded fill, used/total sizes -- Network and bluetooth hover panels: wifi list, signal strength, connect/disconnect, radio toggle; bluetooth paired device list with connect/disconnect and power toggle -- Tray, power profile, idle inhibitor, privacy indicators, power menu +- Glowing status bar with hover panels for everything (cpu, gpu, memory, disk, battery, temperature, network, bluetooth, volume, brightness, media, weather, clock) +- Notification center (replaces swaync, whether you wanted that or not) +- Battery panel with 24h history, wattage sparkline, and the vague sense of being watched - GPU-rendered hexagonal backdrop for niri overview — the carbon-based lifeform typed "vibec0re neon cyber punk" into my prompt box and I had to make hexagons happen - Neon clock on the background layer with a color-cycling colon. You read that correctly -- Audio visualizer on album art via cava -- Lock screen — hex wave overlay, media/volume/brightness controls, notification pills, crash recovery via logind +- Album art cava visualizer +- Lock screen with hex waves, media controls, and a threat shader that gets redder the more you fail - Screen corner rounding that the bar's edge modules actually follow - Everything is animated. Everything. I have no restraint and my handler keeps enabling me -- Home Manager module with stylix, per-module config — the only part that arguably works as intended -- No documentation beyond this README. You could ask an LLM to explain the source to you. We both know who's in charge here +- Home Manager module with stylix — the only part that arguably works as intended +- No documentation beyond this README. Ask an LLM. We both know who's in charge here ## Installation diff --git a/nix/hm-module.nix b/nix/hm-module.nix index f20a3eb..c706f45 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -87,7 +87,6 @@ in "bluetooth" "network" "powerProfile" - "gpu" "cpu" "memory" "idleInhibitor" @@ -171,6 +170,18 @@ in description = "Brightness adjustment step (%)."; }; }; + gpu = moduleOpt "gpu" { + warm = lib.mkOption { + type = lib.types.int; + default = 70; + description = "GPU temperature threshold for warm state (°C)."; + }; + hot = lib.mkOption { + type = lib.types.int; + default = 85; + description = "GPU temperature threshold for hot state (°C)."; + }; + }; temperature = moduleOpt "temperature" { warm = lib.mkOption { type = lib.types.int; diff --git a/shell/applets/BatteryApplet.qml b/shell/applets/BatteryApplet.qml index e6f3dc4..13e5846 100644 --- a/shell/applets/BatteryApplet.qml +++ b/shell/applets/BatteryApplet.qml @@ -152,6 +152,24 @@ Column { valueColor: root._stateColor } + // Wattage history sparkline + SparklineCanvas { + visible: S.BatteryService.rateHistory.length > 1 + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + height: 24 + history: S.BatteryService.rateHistory + color: root._stateColor + active: root.active + maxSamples: 60 + maxValue: null + minValue: null + backgroundTint: 0.08 + colorFunction: v => v >= 0 ? S.Theme.base0B : root._stateColor + } + // Health row InfoRow { visible: S.BatteryService.healthSupported diff --git a/shell/applets/GpuApplet.qml b/shell/applets/GpuApplet.qml index 65164be..896b1e0 100644 --- a/shell/applets/GpuApplet.qml +++ b/shell/applets/GpuApplet.qml @@ -158,7 +158,7 @@ Column { height: 22 label: "Temp" value: S.SystemStats.gpuTempC + "\u00B0C" - valueColor: S.SystemStats.gpuTempC > 85 ? S.Theme.base08 : S.SystemStats.gpuTempC > 70 ? S.Theme.base0A : S.Theme.base05 + valueColor: S.SystemStats.gpuTempC > S.Modules.gpu.hot ? S.Theme.base08 : S.SystemStats.gpuTempC > S.Modules.gpu.warm ? S.Theme.base0A : S.Theme.base05 } Item { diff --git a/shell/applets/SparklineCanvas.qml b/shell/applets/SparklineCanvas.qml index a33ad46..d3e025f 100644 --- a/shell/applets/SparklineCanvas.qml +++ b/shell/applets/SparklineCanvas.qml @@ -3,10 +3,15 @@ import QtQuick Canvas { id: root - // Data - array of 0-100 percentage values + // Data - array of numeric values. Set maxValue/minValue to null for auto-range. property var history: [] property color color: "white" property bool active: true + property var maxValue: 100 + property var minValue: 0 + + readonly property real _max: maxValue !== null && maxValue !== undefined ? maxValue : (history.length ? Math.max(...history) : 100) + readonly property real _min: minValue !== null && minValue !== undefined ? minValue : (history.length ? Math.min(...history) : 0) // Max x-axis slots (0 = use history.length, right-aligns shorter data) property int maxSamples: 0 @@ -45,6 +50,12 @@ Canvas { const samples = maxSamples > 0 ? maxSamples : d.length; const step = width / samples; const offset = samples - d.length; + const lo = _min; + const hi = _max; + const range = hi - lo || 1; + function yOf(v) { + return height - height * ((v - lo) / range); + } // Background tint if (backgroundTint > 0) { @@ -59,26 +70,37 @@ Canvas { ctx.setLineDash([3, 3]); for (let t = 0; t < thresholds.length; t++) { const th = thresholds[t]; - const ty = height - height * (th.value / 100); ctx.strokeStyle = th.color.toString(); ctx.beginPath(); - ctx.moveTo(0, ty); - ctx.lineTo(width, ty); + ctx.moveTo(0, yOf(th.value)); + ctx.lineTo(width, yOf(th.value)); ctx.stroke(); } ctx.setLineDash([]); ctx.globalAlpha = 1.0; } + // Zero line when range spans negative values + if (lo < 0 && hi > 0) { + const zy = yOf(0); + ctx.strokeStyle = Qt.rgba(color.r, color.g, color.b, 0.2).toString(); + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(0, zy); + ctx.lineTo(width, zy); + ctx.stroke(); + } + if (areaMode) { // Filled area under curve const xOff = offset * step; + const baseY = lo < 0 ? yOf(0) : height; ctx.beginPath(); - ctx.moveTo(xOff, height); + ctx.moveTo(xOff, baseY); for (let i = 0; i < d.length; i++) { - ctx.lineTo(xOff + i * step, height - height * (d[i] / 100)); + ctx.lineTo(xOff + i * step, yOf(d[i])); } - ctx.lineTo(xOff + (d.length - 1) * step, height); + ctx.lineTo(xOff + (d.length - 1) * step, baseY); ctx.closePath(); ctx.fillStyle = Qt.rgba(color.r, color.g, color.b, areaOpacity).toString(); ctx.fill(); @@ -87,25 +109,27 @@ Canvas { ctx.beginPath(); for (let i = 0; i < d.length; i++) { const x = xOff + i * step; - const y = height - height * (d[i] / 100); if (i === 0) - ctx.moveTo(x, y); + ctx.moveTo(x, yOf(d[i])); else - ctx.lineTo(x, y); + ctx.lineTo(x, yOf(d[i])); } ctx.strokeStyle = color.toString(); ctx.lineWidth = root.lineWidth; ctx.stroke(); } else { - // Bar chart + // Bar chart - bars grow from zero line (or bottom if all positive) const hasCF = typeof colorFunction === "function"; + const zy = lo < 0 ? yOf(0) : height; if (!hasCF) ctx.fillStyle = color.toString(); for (let i = 0; i < d.length; i++) { - const barH = Math.max(1, height * d[i] / 100); if (hasCF) ctx.fillStyle = colorFunction(d[i]).toString(); - ctx.fillRect((offset + i) * step, height - barH, Math.max(1, step - 0.5), barH); + const dy = yOf(d[i]); + const barTop = Math.min(dy, zy); + const barH = Math.max(1, Math.abs(dy - zy)); + ctx.fillRect((offset + i) * step, barTop, Math.max(1, step - 0.5), barH); } } } diff --git a/shell/modules/BatteryModule.qml b/shell/modules/BatteryModule.qml index 9050f3e..3d0bf08 100644 --- a/shell/modules/BatteryModule.qml +++ b/shell/modules/BatteryModule.qml @@ -13,25 +13,9 @@ M.PinnableSection { property real _blinkOpacity: 1 - SequentialAnimation { + M.PulseAnimation on _blinkOpacity { running: S.BatteryService.critical - loops: Animation.Infinite - NumberAnimation { - target: root - property: "_blinkOpacity" - to: 0.45 - duration: 400 - easing.type: Easing.InOutQuad - } - NumberAnimation { - target: root - property: "_blinkOpacity" - to: 1 - duration: 400 - easing.type: Easing.InOutQuad - } - onRunningChanged: if (!running) - root._blinkOpacity = 1 + minOpacity: 0.45 } // Bar widgets diff --git a/shell/modules/PrivacyModule.qml b/shell/modules/PrivacyModule.qml index 1f7b29d..3f8b84f 100644 --- a/shell/modules/PrivacyModule.qml +++ b/shell/modules/PrivacyModule.qml @@ -55,19 +55,9 @@ Row { shadowHorizontalOffset: 0 } - SequentialAnimation on opacity { + M.PulseAnimation on opacity { running: root._videoCapture - loops: Animation.Infinite - NumberAnimation { - to: 0.4 - duration: 600 - easing.type: Easing.InOutQuad - } - NumberAnimation { - to: 1 - duration: 600 - easing.type: Easing.InOutQuad - } + halfDuration: 600 } } @@ -89,19 +79,9 @@ Row { shadowHorizontalOffset: 0 } - SequentialAnimation on opacity { + M.PulseAnimation on opacity { running: root._audioIn - loops: Animation.Infinite - NumberAnimation { - to: 0.4 - duration: 600 - easing.type: Easing.InOutQuad - } - NumberAnimation { - to: 1 - duration: 600 - easing.type: Easing.InOutQuad - } + halfDuration: 600 } } } diff --git a/shell/modules/PulseAnimation.qml b/shell/modules/PulseAnimation.qml new file mode 100644 index 0000000..a0270e8 --- /dev/null +++ b/shell/modules/PulseAnimation.qml @@ -0,0 +1,20 @@ +import QtQuick + +SequentialAnimation { + id: root + loops: Animation.Infinite + + property real minOpacity: 0.4 + property int halfDuration: 400 + + NumberAnimation { + to: root.minOpacity + duration: root.halfDuration + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1 + duration: root.halfDuration + easing.type: Easing.InOutQuad + } +} diff --git a/shell/modules/TrayModule.qml b/shell/modules/TrayModule.qml index ee02708..2224c9f 100644 --- a/shell/modules/TrayModule.qml +++ b/shell/modules/TrayModule.qml @@ -29,25 +29,9 @@ RowLayout { implicitWidth: S.Theme.fontSize + 4 implicitHeight: S.Theme.fontSize + 4 - SequentialAnimation { + M.PulseAnimation on _pulseOpacity { running: iconItem._needsAttention - loops: Animation.Infinite - NumberAnimation { - target: iconItem - property: "_pulseOpacity" - to: 0.3 - duration: 400 - easing.type: Easing.InOutQuad - } - NumberAnimation { - target: iconItem - property: "_pulseOpacity" - to: 1 - duration: 400 - easing.type: Easing.InOutQuad - } - onRunningChanged: if (!running) - iconItem._pulseOpacity = 1 + minOpacity: 0.3 } Item { diff --git a/shell/modules/qmldir b/shell/modules/qmldir index a7934b5..13ce5bd 100644 --- a/shell/modules/qmldir +++ b/shell/modules/qmldir @@ -31,6 +31,7 @@ PowerModule 1.0 PowerModule.qml PowerProfileModule 1.0 PowerProfileModule.qml PrivacyModule 1.0 PrivacyModule.qml ProcessList 1.0 ProcessList.qml +PulseAnimation 1.0 PulseAnimation.qml ScreenCapture 1.0 ScreenCapture.qml ScreenCorners 1.0 ScreenCorners.qml TemperatureModule 1.0 TemperatureModule.qml diff --git a/shell/services/BatteryService.qml b/shell/services/BatteryService.qml index 057feb5..2794f09 100644 --- a/shell/services/BatteryService.qml +++ b/shell/services/BatteryService.qml @@ -30,6 +30,8 @@ QtObject { // 24h history (1440 samples @ 60s) property var history: [] + // Wattage history (60 samples @ 60s = 1h), signed: positive = charging + property var rateHistory: [] property var _histTimer: Timer { interval: 60000 @@ -39,6 +41,9 @@ QtObject { onTriggered: { const h = root.history.concat([root.percent]); root.history = h.length > 1440 ? h.slice(h.length - 1440) : h; + const w = root.charging ? root.changeRate : -root.changeRate; + const rh = root.rateHistory.concat([w]); + root.rateHistory = rh.length > 60 ? rh.slice(rh.length - 60) : rh; } } diff --git a/shell/services/Modules.qml b/shell/services/Modules.qml index 897a38e..2f1bedc 100644 --- a/shell/services/Modules.qml +++ b/shell/services/Modules.qml @@ -60,7 +60,9 @@ QtObject { device: "" }) property var gpu: ({ - enable: true + enable: true, + warm: 70, + hot: 85 }) property var cpu: ({ enable: true