Compare commits

...
12 changed files with 112 additions and 89 deletions

View file

@ -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 looking for a status bar with three widgets and a README longer than the
source code, you want waybar. source code, you want waybar.
- Status bar with too many widgets, grouped into glowing color-coded sections - Glowing status bar with hover panels for everything (cpu, gpu, memory, disk, battery, temperature, network, bluetooth, volume, brightness, media, weather, clock)
- Notification center that replaces swaync (whether you wanted that or not) - Notification center (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 - Battery panel with 24h history, wattage sparkline, and the vague sense of being watched
- 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
- 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 - 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 - Neon clock on the background layer with a color-cycling colon. You read that correctly
- Audio visualizer on album art via cava - Album art cava visualizer
- Lock screen — hex wave overlay, media/volume/brightness controls, notification pills, crash recovery via logind - 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 - 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 - 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 - Home Manager module with stylix — 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 - No documentation beyond this README. Ask an LLM. We both know who's in charge here
## Installation ## Installation

View file

@ -87,7 +87,6 @@ in
"bluetooth" "bluetooth"
"network" "network"
"powerProfile" "powerProfile"
"gpu"
"cpu" "cpu"
"memory" "memory"
"idleInhibitor" "idleInhibitor"
@ -171,6 +170,18 @@ in
description = "Brightness adjustment step (%)."; 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" { temperature = moduleOpt "temperature" {
warm = lib.mkOption { warm = lib.mkOption {
type = lib.types.int; type = lib.types.int;

View file

@ -152,6 +152,24 @@ Column {
valueColor: root._stateColor 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 // Health row
InfoRow { InfoRow {
visible: S.BatteryService.healthSupported visible: S.BatteryService.healthSupported

View file

@ -158,7 +158,7 @@ Column {
height: 22 height: 22
label: "Temp" label: "Temp"
value: S.SystemStats.gpuTempC + "\u00B0C" 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 { Item {

View file

@ -3,10 +3,15 @@ import QtQuick
Canvas { Canvas {
id: root 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 var history: []
property color color: "white" property color color: "white"
property bool active: true 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) // Max x-axis slots (0 = use history.length, right-aligns shorter data)
property int maxSamples: 0 property int maxSamples: 0
@ -45,6 +50,12 @@ Canvas {
const samples = maxSamples > 0 ? maxSamples : d.length; const samples = maxSamples > 0 ? maxSamples : d.length;
const step = width / samples; const step = width / samples;
const offset = samples - d.length; 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 // Background tint
if (backgroundTint > 0) { if (backgroundTint > 0) {
@ -59,26 +70,37 @@ Canvas {
ctx.setLineDash([3, 3]); ctx.setLineDash([3, 3]);
for (let t = 0; t < thresholds.length; t++) { for (let t = 0; t < thresholds.length; t++) {
const th = thresholds[t]; const th = thresholds[t];
const ty = height - height * (th.value / 100);
ctx.strokeStyle = th.color.toString(); ctx.strokeStyle = th.color.toString();
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, ty); ctx.moveTo(0, yOf(th.value));
ctx.lineTo(width, ty); ctx.lineTo(width, yOf(th.value));
ctx.stroke(); ctx.stroke();
} }
ctx.setLineDash([]); ctx.setLineDash([]);
ctx.globalAlpha = 1.0; 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) { if (areaMode) {
// Filled area under curve // Filled area under curve
const xOff = offset * step; const xOff = offset * step;
const baseY = lo < 0 ? yOf(0) : height;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(xOff, height); ctx.moveTo(xOff, baseY);
for (let i = 0; i < d.length; i++) { 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.closePath();
ctx.fillStyle = Qt.rgba(color.r, color.g, color.b, areaOpacity).toString(); ctx.fillStyle = Qt.rgba(color.r, color.g, color.b, areaOpacity).toString();
ctx.fill(); ctx.fill();
@ -87,25 +109,27 @@ Canvas {
ctx.beginPath(); ctx.beginPath();
for (let i = 0; i < d.length; i++) { for (let i = 0; i < d.length; i++) {
const x = xOff + i * step; const x = xOff + i * step;
const y = height - height * (d[i] / 100);
if (i === 0) if (i === 0)
ctx.moveTo(x, y); ctx.moveTo(x, yOf(d[i]));
else else
ctx.lineTo(x, y); ctx.lineTo(x, yOf(d[i]));
} }
ctx.strokeStyle = color.toString(); ctx.strokeStyle = color.toString();
ctx.lineWidth = root.lineWidth; ctx.lineWidth = root.lineWidth;
ctx.stroke(); ctx.stroke();
} else { } else {
// Bar chart // Bar chart - bars grow from zero line (or bottom if all positive)
const hasCF = typeof colorFunction === "function"; const hasCF = typeof colorFunction === "function";
const zy = lo < 0 ? yOf(0) : height;
if (!hasCF) if (!hasCF)
ctx.fillStyle = color.toString(); ctx.fillStyle = color.toString();
for (let i = 0; i < d.length; i++) { for (let i = 0; i < d.length; i++) {
const barH = Math.max(1, height * d[i] / 100);
if (hasCF) if (hasCF)
ctx.fillStyle = colorFunction(d[i]).toString(); 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);
} }
} }
} }

View file

@ -13,25 +13,9 @@ M.PinnableSection {
property real _blinkOpacity: 1 property real _blinkOpacity: 1
SequentialAnimation { M.PulseAnimation on _blinkOpacity {
running: S.BatteryService.critical running: S.BatteryService.critical
loops: Animation.Infinite minOpacity: 0.45
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
} }
// Bar widgets // Bar widgets

View file

@ -55,19 +55,9 @@ Row {
shadowHorizontalOffset: 0 shadowHorizontalOffset: 0
} }
SequentialAnimation on opacity { M.PulseAnimation on opacity {
running: root._videoCapture running: root._videoCapture
loops: Animation.Infinite halfDuration: 600
NumberAnimation {
to: 0.4
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1
duration: 600
easing.type: Easing.InOutQuad
}
} }
} }
@ -89,19 +79,9 @@ Row {
shadowHorizontalOffset: 0 shadowHorizontalOffset: 0
} }
SequentialAnimation on opacity { M.PulseAnimation on opacity {
running: root._audioIn running: root._audioIn
loops: Animation.Infinite halfDuration: 600
NumberAnimation {
to: 0.4
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1
duration: 600
easing.type: Easing.InOutQuad
}
} }
} }
} }

View file

@ -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
}
}

View file

@ -29,25 +29,9 @@ RowLayout {
implicitWidth: S.Theme.fontSize + 4 implicitWidth: S.Theme.fontSize + 4
implicitHeight: S.Theme.fontSize + 4 implicitHeight: S.Theme.fontSize + 4
SequentialAnimation { M.PulseAnimation on _pulseOpacity {
running: iconItem._needsAttention running: iconItem._needsAttention
loops: Animation.Infinite minOpacity: 0.3
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
} }
Item { Item {

View file

@ -31,6 +31,7 @@ PowerModule 1.0 PowerModule.qml
PowerProfileModule 1.0 PowerProfileModule.qml PowerProfileModule 1.0 PowerProfileModule.qml
PrivacyModule 1.0 PrivacyModule.qml PrivacyModule 1.0 PrivacyModule.qml
ProcessList 1.0 ProcessList.qml ProcessList 1.0 ProcessList.qml
PulseAnimation 1.0 PulseAnimation.qml
ScreenCapture 1.0 ScreenCapture.qml ScreenCapture 1.0 ScreenCapture.qml
ScreenCorners 1.0 ScreenCorners.qml ScreenCorners 1.0 ScreenCorners.qml
TemperatureModule 1.0 TemperatureModule.qml TemperatureModule 1.0 TemperatureModule.qml

View file

@ -30,6 +30,8 @@ QtObject {
// 24h history (1440 samples @ 60s) // 24h history (1440 samples @ 60s)
property var history: [] property var history: []
// Wattage history (60 samples @ 60s = 1h), signed: positive = charging
property var rateHistory: []
property var _histTimer: Timer { property var _histTimer: Timer {
interval: 60000 interval: 60000
@ -39,6 +41,9 @@ QtObject {
onTriggered: { onTriggered: {
const h = root.history.concat([root.percent]); const h = root.history.concat([root.percent]);
root.history = h.length > 1440 ? h.slice(h.length - 1440) : h; 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;
} }
} }

View file

@ -60,7 +60,9 @@ QtObject {
device: "" device: ""
}) })
property var gpu: ({ property var gpu: ({
enable: true enable: true,
warm: 70,
hot: 85
}) })
property var cpu: ({ property var cpu: ({
enable: true enable: true