From e939a6b096cb3fa074160d37d7274ceff6333f43 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 21:44:44 +0200 Subject: [PATCH] extract panel contents into reusable content/ components --- shell/modules/Cpu.qml | 230 +----------- shell/modules/Disk.qml | 90 +---- shell/modules/Memory.qml | 284 +-------------- shell/modules/Mpris.qml | 359 +----------------- shell/modules/OverviewBackdrop.qml | 107 +----- shell/modules/Temperature.qml | 257 +------------ shell/modules/Volume.qml | 280 +------------- shell/modules/content/CpuContent.qml | 234 ++++++++++++ shell/modules/content/DiskContent.qml | 96 +++++ shell/modules/content/HexWaveBackground.qml | 115 ++++++ shell/modules/content/MemoryContent.qml | 287 +++++++++++++++ shell/modules/content/MprisContent.qml | 361 +++++++++++++++++++ shell/modules/content/TemperatureContent.qml | 265 ++++++++++++++ shell/modules/content/VolumeContent.qml | 292 +++++++++++++++ shell/modules/content/qmldir | 8 + 15 files changed, 1718 insertions(+), 1547 deletions(-) create mode 100644 shell/modules/content/CpuContent.qml create mode 100644 shell/modules/content/DiskContent.qml create mode 100644 shell/modules/content/HexWaveBackground.qml create mode 100644 shell/modules/content/MemoryContent.qml create mode 100644 shell/modules/content/MprisContent.qml create mode 100644 shell/modules/content/TemperatureContent.qml create mode 100644 shell/modules/content/VolumeContent.qml create mode 100644 shell/modules/content/qmldir diff --git a/shell/modules/Cpu.qml b/shell/modules/Cpu.qml index 3116195..389766e 100644 --- a/shell/modules/Cpu.qml +++ b/shell/modules/Cpu.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import "." as M +import "content" as C M.BarSection { id: root @@ -46,14 +47,6 @@ M.BarSection { onTriggered: root._pinned = false } - function _loadColor(pct) { - const t = Math.max(0, Math.min(100, pct)) / 100; - const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; - const b = t < 0.5 ? M.Theme.base0A : M.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); - } - M.BarIcon { icon: "\uF2DB" anchors.verticalCenter: parent.verticalCenter @@ -82,221 +75,14 @@ M.BarSection { panelTitle: "CPU" contentWidth: 260 - // Per-core rows - Repeater { - model: root._cores.length - - delegate: Item { - required property int index - width: hoverPanel.contentWidth - - readonly property int _u: root._cores[index]?.usage ?? 0 - readonly property real _f: root._cores[index]?.freq_ghz ?? 0 - readonly property color _barColor: root._loadColor(_u) - readonly property bool _throttled: { - const maxF = root._coreMaxFreq[index] ?? 0; - return maxF > 0 && _f < maxF * 0.85 && _u >= 60; - } - readonly property bool _isFirstECore: { - const types = root._coreTypes; - if (!types.length || index >= types.length) - return false; - if (types[index] !== "Efficiency") - return false; - return index === 0 || types[index - 1] !== "Efficiency"; - } - - height: _isFirstECore ? 28 : 20 - - // P/E-core divider - Rectangle { - visible: parent._isFirstECore - anchors.top: parent.top - anchors.topMargin: 3 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - 16 - height: 1 - color: M.Theme.base03 - } - - // Row content pinned to bottom of delegate - Item { - anchors.bottom: parent.bottom - width: parent.width - height: 20 - - Text { - id: coreLabel - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: index - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - width: 16 - } - - Item { - id: coreBar - anchors.left: coreLabel.right - anchors.leftMargin: 6 - anchors.right: sparkline.left - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - height: 4 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 2 - } - - Rectangle { - width: parent.width * (parent.parent.parent._u / 100) - height: parent.height - color: parent.parent.parent._barColor - radius: 2 - Behavior on width { - enabled: root._showPanel - NumberAnimation { - duration: 150 - } - } - } - } - - // Sparkline - Canvas { - id: sparkline - anchors.right: freqLabel.left - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - width: 32 - height: 10 - - property var _hist: root._cores[parent.parent.index]?.history ?? [] - property color _col: parent.parent._barColor - - 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) - return; - const bw = width / d.length; - ctx.fillStyle = _col.toString(); - for (let i = 0; i < d.length; i++) { - const h = Math.max(1, height * d[i] / 100); - ctx.fillRect(i * bw, height - h, Math.max(1, bw - 0.5), h); - } - } - } - - Text { - id: freqLabel - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: parent.parent._f.toFixed(2) - color: parent.parent._throttled ? M.Theme.base08 : M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - width: 34 - horizontalAlignment: Text.AlignRight - } - } - } - } - - // Process list separator - Rectangle { - width: parent.width - 16 - height: 1 - anchors.horizontalCenter: parent.horizontalCenter - color: M.Theme.base03 - } - - Item { + C.CpuContent { width: hoverPanel.contentWidth - height: 18 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "PROCESS" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 3 - font.family: M.Theme.fontFamily - font.letterSpacing: 1 - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "CPU" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 3 - font.family: M.Theme.fontFamily - font.letterSpacing: 1 - } - } - - // Top processes by CPU - Repeater { - model: root._procs.processes - - delegate: Item { - required property var modelData - width: hoverPanel.contentWidth - height: 20 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.cmd - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: parent.width - 80 - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.cpu.toFixed(1) + "%" - color: root._loadColor(modelData.cpu) - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - width: 36 - horizontalAlignment: Text.AlignRight - } - } - } - - Item { - width: 1 - height: 4 + cores: root._cores + coreMaxFreq: root._coreMaxFreq + coreTypes: root._coreTypes + processes: root._procs.processes + accentColor: root.accentColor + active: root._showPanel } } } diff --git a/shell/modules/Disk.qml b/shell/modules/Disk.qml index 01970dc..c679da9 100644 --- a/shell/modules/Disk.qml +++ b/shell/modules/Disk.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import "." as M +import "content" as C M.BarSection { id: root @@ -10,24 +11,6 @@ M.BarSection { property var _mounts: M.SystemStats.diskMounts property int _rootPct: M.SystemStats.diskRootPct - function _fmt(bytes) { - if (bytes >= 1e12) - return (bytes / 1e12).toFixed(1) + "T"; - if (bytes >= 1e9) - return Math.round(bytes / 1e9) + "G"; - if (bytes >= 1e6) - return Math.round(bytes / 1e6) + "M"; - return bytes + "B"; - } - - function _barColor(pct) { - const t = Math.max(0, Math.min(100, pct)) / 100; - const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; - const b = t < 0.5 ? M.Theme.base0A : M.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); - } - property bool _pinned: false readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered readonly property bool _showPanel: _anyHover || _pinned @@ -73,73 +56,10 @@ M.BarSection { panelTitle: "Disk" contentWidth: 260 - Repeater { - model: root._mounts - - delegate: Item { - required property var modelData - width: hoverPanel.contentWidth - height: 22 - - Text { - id: mountLabel - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.target - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: 72 - } - - Item { - id: mountBar - anchors.left: mountLabel.right - anchors.leftMargin: 6 - anchors.right: sizeLabel.left - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - height: 4 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 2 - } - - Rectangle { - width: parent.width * (modelData.pct / 100) - height: parent.height - color: root._barColor(modelData.pct) - radius: 2 - Behavior on width { - NumberAnimation { - duration: 200 - } - } - } - } - - Text { - id: sizeLabel - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._fmt(modelData.usedBytes) + "/" + root._fmt(modelData.totalBytes) - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - width: 72 - horizontalAlignment: Text.AlignRight - } - } - } - - Item { - width: 1 - height: 4 + C.DiskContent { + width: hoverPanel.contentWidth + mounts: root._mounts + accentColor: root.accentColor } } } diff --git a/shell/modules/Memory.qml b/shell/modules/Memory.qml index 4d8bb4c..ca745a8 100644 --- a/shell/modules/Memory.qml +++ b/shell/modules/Memory.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import "." as M +import "content" as C M.BarSection { id: root @@ -14,10 +15,6 @@ M.BarSection { property real cachedGb: M.SystemStats.memCachedGb property real buffersGb: M.SystemStats.memBuffersGb - function _fmt(gb) { - return gb >= 10 ? gb.toFixed(1) + "G" : gb.toFixed(2) + "G"; - } - property bool _pinned: false readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered readonly property bool _showPanel: _anyHover || _pinned @@ -69,276 +66,17 @@ M.BarSection { panelTitle: "Memory" contentWidth: 240 - // Usage bar - Item { - width: parent.width - height: 14 - - Item { - id: memBar - 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: M.Theme.base02 - radius: 3 - } - - // Cached (base0D, behind used) - Rectangle { - width: parent.width * Math.min(1, (root.usedGb + root.cachedGb) / Math.max(root.totalGb, 0.001)) - height: parent.height - color: M.Theme.base0D - opacity: 0.4 - radius: 3 - Behavior on width { - enabled: root._showPanel - NumberAnimation { - duration: 200 - } - } - } - - // Used (accentColor, on top) - Rectangle { - width: parent.width * Math.min(1, root.usedGb / Math.max(root.totalGb, 0.001)) - height: parent.height - color: root.accentColor - radius: 3 - Behavior on width { - enabled: root._showPanel - NumberAnimation { - duration: 200 - } - } - } - } - } - - // Memory history sparkline - Canvas { - id: memSparkline - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - height: 18 - - property var _hist: M.SystemStats.memHistory - property color _col: root.accentColor - - on_HistChanged: if (root._showPanel) - requestPaint() - on_ColChanged: if (root._showPanel) - requestPaint() - - Connections { - target: root - function on_ShowPanelChanged() { - if (root._showPanel) - memSparkline.requestPaint(); - } - } - - onPaint: { - const ctx = getContext("2d"); - if (!ctx) - return; - ctx.clearRect(0, 0, width, height); - const d = _hist; - if (!d.length) - return; - const bw = width / 30; - ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.15).toString(); - ctx.fillRect(0, 0, width, height); - ctx.fillStyle = _col.toString(); - for (let i = 0; i < d.length; i++) { - const h = Math.max(1, height * d[i] / 100); - ctx.fillRect((30 - d.length + i) * bw, height - h, Math.max(1, bw - 0.5), h); - } - } - } - - // Breakdown rows - Item { - width: parent.width - height: 18 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "Used" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._fmt(root.usedGb) - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - } - - Item { - width: parent.width - height: 18 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "Cached" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._fmt(root.cachedGb) - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - } - - Item { - width: parent.width - height: 18 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "Available" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._fmt(root.availGb) - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - } - - Item { - width: parent.width - height: 18 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "Total" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._fmt(root.totalGb) - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - } - - // Process list separator - Rectangle { - width: parent.width - 16 - height: 1 - anchors.horizontalCenter: parent.horizontalCenter - color: M.Theme.base03 - } - - Item { + C.MemoryContent { width: hoverPanel.contentWidth - height: 18 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "PROCESS" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 3 - font.family: M.Theme.fontFamily - font.letterSpacing: 1 - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "MEM" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 3 - font.family: M.Theme.fontFamily - font.letterSpacing: 1 - } - } - - // Top processes by memory - Repeater { - model: root._procs.processes - - delegate: Item { - required property var modelData - width: hoverPanel.contentWidth - height: 20 - - Text { - id: procCmd - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.cmd - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: parent.width - 80 - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.mem.toFixed(1) + "%" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - width: 36 - horizontalAlignment: Text.AlignRight - } - } - } - - Item { - width: 1 - height: 4 + percent: root.percent + usedGb: root.usedGb + totalGb: root.totalGb + availGb: root.availGb + cachedGb: root.cachedGb + buffersGb: root.buffersGb + processes: root._procs.processes + accentColor: root.accentColor + active: root._showPanel } } } diff --git a/shell/modules/Mpris.qml b/shell/modules/Mpris.qml index f783643..887105d 100644 --- a/shell/modules/Mpris.qml +++ b/shell/modules/Mpris.qml @@ -3,6 +3,7 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Mpris import "." as M +import "content" as C M.BarSection { id: root @@ -32,14 +33,14 @@ M.BarSection { _cachedArt = _artUrl || ""; } - // Preload art while panel is hidden — ensures QML image cache has the pixels + // Preload art while panel is hidden Image { visible: false source: root._cachedArt asynchronous: true } - // Cava visualizer — 16 bars, raw output mode + // Cava visualizer - 16 bars, raw output mode property var _cavaBars: Array(16).fill(0) property bool _cavaActive: false @@ -122,348 +123,18 @@ M.BarSection { panelTitle: "Now Playing" contentWidth: 280 - // Album art — always 1:1, crossfades on session switch - Item { - width: parent.width - height: width - clip: true - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - } - - // Outgoing art — snaps to current opacity, then fades out - Image { - id: _artImgPrev - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - asynchronous: true - opacity: 0 - - NumberAnimation { - id: _prevFadeOut - target: _artImgPrev - property: "opacity" - to: 0 - duration: 300 - easing.type: Easing.InOutCubic - } - } - - // Incoming art — fades in once loaded - Image { - id: _artImg - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - asynchronous: true - opacity: 0 - - property bool _hasArt: false - - onStatusChanged: { - if (status === Image.Ready && source !== "") { - _hasArt = true; - _artFadeIn.start(); - _prevFadeOut.start(); - } else if (status === Image.Error) { - _hasArt = false; - } - } - - NumberAnimation { - id: _artFadeIn - target: _artImg - property: "opacity" - to: 1 - duration: 300 - easing.type: Easing.InOutCubic - } - - Connections { - target: root - function on_CachedArtChanged() { - if (!root._cachedArt) { - _artFadeIn.stop(); - _prevFadeOut.stop(); - _artImg._hasArt = false; - _artImg.opacity = 0; - _artImgPrev.opacity = 0; - _artImg.source = ""; - } else if (root._cachedArt !== _artImg.source) { - _prevFadeOut.stop(); - _artFadeIn.stop(); - _artImgPrev.source = _artImg.source; - _artImgPrev.opacity = _artImg.opacity; - _artImg.opacity = 0; - _artImg.source = root._cachedArt; - } - } - } - } - - // Visualizer bars - Row { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: parent.height * 0.6 - spacing: 2 - visible: root.playing - opacity: 0.5 - - Repeater { - model: 16 - Rectangle { - required property int index - width: (parent.width - 15 * parent.spacing) / 16 - height: parent.height * (root._cavaBars[index] ?? 0) - anchors.bottom: parent.bottom - color: root.accentColor - radius: 1 - - Behavior on height { - NumberAnimation { - duration: 50 - } - } - } - } - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 40 - visible: _artImg._hasArt - gradient: Gradient { - GradientStop { - position: 0 - color: "transparent" - } - GradientStop { - position: 1 - color: M.Theme.base01 - } - } - } - - Text { - anchors.centerIn: parent - text: "\uF001" - color: M.Theme.base04 - font.pixelSize: 28 - font.family: M.Theme.iconFontFamily - visible: !_artImg._hasArt - } - } - - // Track info - Item { - width: parent.width - height: titleCol.implicitHeight + 8 - - Column { - id: titleCol - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 2 - - Text { - width: parent.width - text: root.player?.trackTitle || "No track" - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize + 1 - font.family: M.Theme.fontFamily - font.bold: true - elide: Text.ElideRight - } - - Text { - width: parent.width - text: { - const p = root.player; - if (!p) - return ""; - const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || ""); - return [artist, p.trackAlbum].filter(s => s).join(" \u2014 "); - } - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - visible: text !== "" - } - } - } - - // Progress - Item { - width: parent.width - height: 20 - - readonly property real pos: root.player?.position ?? 0 - readonly property real dur: root.player?.length ?? 0 - readonly property real frac: dur > 0 ? pos / dur : 0 - - function _fmtTime(ms) { - const s = Math.floor(ms / 1000); - const m = Math.floor(s / 60); - return m + ":" + String(s % 60).padStart(2, "0"); - } - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: parent._fmtTime(parent.pos) - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: parent._fmtTime(parent.dur) - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - - Item { - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 80 - height: 4 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 2 - } - Rectangle { - width: parent.width * Math.min(1, Math.max(0, parent.parent.frac)) - height: parent.height - color: root.accentColor - radius: 2 - } - } - } - - // Transport controls - Item { - width: parent.width - height: 36 - - Row { - anchors.centerIn: parent - spacing: 24 - - Text { - text: "\uF048" - color: root.player?.canGoPrevious ? M.Theme.base05 : M.Theme.base03 - font.pixelSize: M.Theme.fontSize + 4 - font.family: M.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - TapHandler { - cursorShape: Qt.PointingHandCursor - enabled: root.player?.canGoPrevious ?? false - onTapped: root.player.previous() - } - } - - Text { - text: root.playing ? "\uF04C" : "\uF04B" - color: root.accentColor - font.pixelSize: M.Theme.fontSize + 8 - font.family: M.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - TapHandler { - cursorShape: Qt.PointingHandCursor - onTapped: root.player?.togglePlaying() - } - } - - Text { - text: "\uF051" - color: root.player?.canGoNext ? M.Theme.base05 : M.Theme.base03 - font.pixelSize: M.Theme.fontSize + 4 - font.family: M.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - TapHandler { - cursorShape: Qt.PointingHandCursor - enabled: root.player?.canGoNext ?? false - onTapped: root.player.next() - } - } - } - } - - // Player switcher - Item { - width: parent.width - height: _players.length > 1 ? 28 : 0 - visible: _players.length > 1 - - Flickable { - id: _switcher - anchors.centerIn: parent - width: Math.min(_playerRow.implicitWidth, parent.width - 16) - height: 22 - contentWidth: _playerRow.implicitWidth - clip: true - - Row { - id: _playerRow - height: 22 - spacing: 6 - - Repeater { - model: root._players - - delegate: Rectangle { - required property var modelData - required property int index - - readonly property bool _active: index === root._playerIdx - - width: _pLabel.implicitWidth + 12 - height: 18 - radius: 9 - color: _active ? M.Theme.base02 : (pHover.hovered ? M.Theme.base02 : "transparent") - border.color: _active ? root.accentColor : M.Theme.base03 - border.width: _active ? 1 : 0 - anchors.verticalCenter: parent.verticalCenter - - Text { - id: _pLabel - anchors.centerIn: parent - text: modelData.identity ?? "Player" - color: _active ? root.accentColor : M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - font.bold: _active - } - - HoverHandler { - id: pHover - cursorShape: Qt.PointingHandCursor - } - - TapHandler { - onTapped: { - root._playerIdx = index; - hoverPanel.keepOpen(400); - } - } - } - } - } + C.MprisContent { + width: hoverPanel.contentWidth + player: root.player + players: root._players + playing: root.playing + accentColor: root.accentColor + cachedArt: root._cachedArt + cavaBars: root._cavaBars + playerIdx: root._playerIdx + onPlayerSwitched: idx => { + root._playerIdx = idx; + hoverPanel.keepOpen(400); } } } diff --git a/shell/modules/OverviewBackdrop.qml b/shell/modules/OverviewBackdrop.qml index 6abb7e8..0e0b050 100644 --- a/shell/modules/OverviewBackdrop.qml +++ b/shell/modules/OverviewBackdrop.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Wayland import "." as M +import "content" as C PanelWindow { id: root @@ -20,110 +21,8 @@ PanelWindow { anchors.right: true anchors.bottom: true - Rectangle { + C.HexWaveBackground { anchors.fill: parent - color: M.Theme.base01 - } - - ShaderEffect { - id: fx - anchors.fill: parent - fragmentShader: Quickshell.shellPath("modules/hex_wave.frag.qsb") - - property real uSize: 50.0 - property real uWavePhase: -200 - property real uBreath: 0 - property real uGlitch: 0 - property real uGlitchSeed: 0.0 - property vector4d uResolution: Qt.vector4d(width, height, 0, 0) - property color uC0: M.Theme.base0C - property color uC1: M.Theme.base0E - property color uC2: M.Theme.base09 - - Connections { - target: M.NiriIpc - function onOverviewOpenChanged() { - if (!M.NiriIpc.overviewOpen) { - fx.uWavePhase = -200; - fx.uBreath = 0; - } - } - } - - // Wave animation: 6s sweep + 8s pause, only while overview is open - SequentialAnimation on uWavePhase { - loops: Animation.Infinite - running: M.NiriIpc.overviewOpen && !M.Theme.reducedMotion - NumberAnimation { - from: -200 - to: fx.width + 200 - duration: 6000 - easing.type: Easing.InOutSine - } - PauseAnimation { - duration: 8000 - } - } - - // Breathing pulse while overview is open - SequentialAnimation on uBreath { - loops: Animation.Infinite - running: M.NiriIpc.overviewOpen && !M.Theme.reducedMotion - NumberAnimation { - from: 0 - to: 1 - duration: 2500 - easing.type: Easing.InOutSine - } - NumberAnimation { - from: 1 - to: 0 - duration: 2500 - easing.type: Easing.InOutSine - } - } - - // Random subtle glitches — fire every 12–37s, total ~250ms each - Timer { - interval: 20000 - repeat: true - running: !M.Theme.reducedMotion - onTriggered: { - interval = 12000 + Math.floor(Math.random() * 25000); - fx.uGlitchSeed = Math.random() * 1000.0; - _glitchAnim.start(); - } - } - - SequentialAnimation { - id: _glitchAnim - NumberAnimation { - target: fx - property: "uGlitch" - to: 0.7 - duration: 50 - easing.type: Easing.OutQuad - } - NumberAnimation { - target: fx - property: "uGlitch" - to: 0.15 - duration: 50 - } - NumberAnimation { - target: fx - property: "uGlitch" - to: 0.85 - duration: 60 - easing.type: Easing.OutQuad - } - NumberAnimation { - target: fx - property: "uGlitch" - to: 0 - duration: 100 - easing.type: Easing.InQuad - } - } + running: M.NiriIpc.overviewOpen } } diff --git a/shell/modules/Temperature.qml b/shell/modules/Temperature.qml index d7aa47c..dd69e3a 100644 --- a/shell/modules/Temperature.qml +++ b/shell/modules/Temperature.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import "." as M +import "content" as C M.BarSection { id: root @@ -45,15 +46,6 @@ M.BarSection { onTriggered: root._pinned = false } - // Returns a color interpolated green→yellow→red for a given celsius value - function _tempColor(celsius) { - const t = Math.max(0, Math.min(100, celsius)) / 100; - const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; - const b = t < 0.5 ? M.Theme.base0A : M.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); - } - M.BarIcon { icon: "\uF2C9" color: root._stateColor @@ -84,243 +76,16 @@ M.BarSection { panelTitle: "Temperature" contentWidth: 220 - // Header — current temp - Item { - width: parent.width - height: 28 - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._temp + "\u00B0C" - color: root._stateColor - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.fontFamily - font.bold: true - width: _tempSizer.implicitWidth - horizontalAlignment: Text.AlignRight - - Text { - id: _tempSizer - visible: false - text: "100\u00B0C" - font: parent.font - } - } - } - - // Gauge bar (0–100°C), with warm/hot threshold markers - Item { - width: parent.width - height: 16 - - Item { - id: _gaugeBar - 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: M.Theme.base02 - radius: 3 - } - - Rectangle { - width: parent.width * Math.min(1, root._temp / 100) - height: parent.height - color: root._stateColor - radius: 3 - Behavior on width { - enabled: root._showPanel - NumberAnimation { - duration: 300 - easing.type: Easing.OutCubic - } - } - } - - // Warm threshold marker - Rectangle { - x: parent.width * (root._warm / 100) - 1 - width: 1 - height: parent.height + 4 - anchors.verticalCenter: parent.verticalCenter - color: M.Theme.base0A - opacity: 0.6 - } - - // Hot threshold marker - Rectangle { - x: parent.width * (root._hot / 100) - 1 - width: 1 - height: parent.height + 4 - anchors.verticalCenter: parent.verticalCenter - color: M.Theme.base08 - opacity: 0.6 - } - } - } - - // History sparkline (~10 min @ 4s per sample) - Canvas { - id: _sparkline - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - height: 40 - - property var _hist: M.SystemStats.tempHistory - 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) - return; - - const maxSamples = 150; - const bw = width / maxSamples; - - // Background tint - ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.08).toString(); - ctx.fillRect(0, 0, width, height); - - // Warm threshold line - const warmY = height - height * (root._warm / 100); - ctx.strokeStyle = M.Theme.base0A.toString(); - ctx.globalAlpha = 0.3; - ctx.lineWidth = 1; - ctx.setLineDash([3, 3]); - ctx.beginPath(); - ctx.moveTo(0, warmY); - ctx.lineTo(width, warmY); - ctx.stroke(); - - // Hot threshold line - const hotY = height - height * (root._hot / 100); - ctx.strokeStyle = M.Theme.base08.toString(); - ctx.beginPath(); - ctx.moveTo(0, hotY); - ctx.lineTo(width, hotY); - ctx.stroke(); - - ctx.setLineDash([]); - ctx.globalAlpha = 1.0; - - // Bars - const offset = maxSamples - d.length; - for (let i = 0; i < d.length; i++) { - const barH = Math.max(1, height * d[i] / 100); - const barColor = d[i] > root._hot ? M.Theme.base08 : d[i] > root._warm ? M.Theme.base0A : _col; - ctx.fillStyle = barColor.toString(); - ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH); - } - } - } - - // Threshold labels - Item { - width: parent.width - height: 16 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "warm " + root._warm + "\u00B0 hot " + root._hot + "\u00B0" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 3 - font.family: M.Theme.fontFamily - font.letterSpacing: 0.5 - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "10 min" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 3 - font.family: M.Theme.fontFamily - } - } - - // Per-device breakdown - Rectangle { - width: parent.width - 16 - height: 1 - anchors.horizontalCenter: parent.horizontalCenter - color: M.Theme.base03 - visible: M.SystemStats.tempDevices.length > 0 - } - - Repeater { - model: M.SystemStats.tempDevices - delegate: Item { - required property var modelData - width: hoverPanel.contentWidth - height: 22 - - readonly property bool _isActive: root._deviceFilter === modelData.name - - Rectangle { - anchors.fill: parent - anchors.leftMargin: 8 - anchors.rightMargin: 8 - color: _isActive ? Qt.rgba(root.accentColor.r, root.accentColor.g, root.accentColor.b, 0.12) : "transparent" - radius: 3 - } - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.name - color: _isActive ? root.accentColor : M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: parent.width - 80 - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.celsius + "\u00B0C" - color: root._tempColor(modelData.celsius) - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - font.bold: _isActive - } - } - } - - Item { - width: 1 - height: 4 + C.TemperatureContent { + width: hoverPanel.contentWidth + temp: root._temp + warm: root._warm + hot: root._hot + history: M.SystemStats.tempHistory + devices: M.SystemStats.tempDevices + accentColor: root.accentColor + deviceFilter: root._deviceFilter + active: root._showPanel } } } diff --git a/shell/modules/Volume.qml b/shell/modules/Volume.qml index c4d5d33..97a93eb 100644 --- a/shell/modules/Volume.qml +++ b/shell/modules/Volume.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Services.Pipewire import "." as M +import "content" as C M.BarSection { id: root @@ -113,279 +114,12 @@ M.BarSection { panelTitle: "Sound" contentWidth: 220 - // Slider row - Item { - width: parent.width - height: 36 - - Text { - id: muteIcon - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._volumeIcon - color: root._volumeColor - font.pixelSize: M.Theme.fontSize + 2 - font.family: M.Theme.iconFontFamily - - TapHandler { - cursorShape: Qt.PointingHandCursor - onTapped: if (root.sink?.audio) - root.sink.audio.muted = !root.sink.audio.muted - } - } - - Item { - id: slider - anchors.left: muteIcon.right - anchors.leftMargin: 8 - anchors.right: volLabel.left - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - height: 6 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 3 - } - Rectangle { - width: parent.width * Math.min(1, Math.max(0, root.volume)) - height: parent.height - color: root._volumeColor - radius: 3 - Behavior on width { - NumberAnimation { - duration: 80 - } - } - } - - MouseArea { - anchors.fill: parent - anchors.margins: -6 - cursorShape: Qt.PointingHandCursor - onPressed: mouse => _setVol(mouse) - onPositionChanged: mouse => { - if (pressed) - _setVol(mouse); - } - function _setVol(mouse) { - if (!root.sink?.audio) - return; - root.sink.audio.volume = Math.max(0, Math.min(1, mouse.x / slider.width)); - } - } - } - - Text { - id: volLabel - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: Math.round(root.volume * 100) + "%" - color: root.muted ? M.Theme.base04 : M.Theme.base05 - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.fontFamily - width: 30 - } - } - - // Device + stream list - Column { - id: deviceList - width: parent.width - - // Output devices — only shown when more than one exists - Column { - visible: root._sinkList.length > 1 - width: parent.width - - Rectangle { - width: parent.width - 16 - height: 1 - anchors.horizontalCenter: parent.horizontalCenter - color: M.Theme.base03 - } - - Text { - width: parent.width - height: 24 - verticalAlignment: Text.AlignVCenter - leftPadding: 12 - text: "Output Devices" - color: root.accentColor - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - } - - Repeater { - model: root._sinkList - - delegate: Item { - required property var modelData - - width: deviceList.width - height: 28 - - readonly property bool _active: modelData === root.sink - - Rectangle { - anchors.fill: parent - anchors.leftMargin: 4 - anchors.rightMargin: 4 - color: deviceHover.hovered ? M.Theme.base02 : "transparent" - radius: M.Theme.radius - } - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.description || modelData.name || "Unknown" - color: parent._active ? root.accentColor : M.Theme.base05 - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.fontFamily - font.bold: parent._active - elide: Text.ElideRight - } - - HoverHandler { - id: deviceHover - cursorShape: Qt.PointingHandCursor - } - - TapHandler { - onTapped: Pipewire.preferredDefaultAudioSink = modelData - } - } - } - } - - // Streams section - Rectangle { - visible: root._streamList.length > 0 - width: parent.width - 16 - height: visible ? 1 : 0 - anchors.horizontalCenter: parent.horizontalCenter - color: M.Theme.base03 - } - - Text { - visible: root._streamList.length > 0 - width: parent.width - height: visible ? 24 : 0 - verticalAlignment: Text.AlignVCenter - leftPadding: 12 - text: "Applications" - color: root.accentColor - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - } - - Repeater { - model: root._streamList - - delegate: Item { - id: streamEntry - required property var modelData - - width: deviceList.width - height: 32 - - readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown" - readonly property real _vol: modelData.audio?.volume ?? 0 - readonly property bool _muted: modelData.audio?.muted ?? false - - Text { - id: streamIcon - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: streamEntry._muted ? "\uF026" : "\uF028" - color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.iconFontFamily - - TapHandler { - cursorShape: Qt.PointingHandCursor - onTapped: if (streamEntry.modelData.audio) - streamEntry.modelData.audio.muted = !streamEntry.modelData.audio.muted - } - } - - Text { - id: streamName - anchors.left: streamIcon.right - anchors.leftMargin: 6 - anchors.verticalCenter: parent.verticalCenter - text: streamEntry._appName - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: 70 - } - - Item { - id: streamSlider - anchors.left: streamName.right - anchors.leftMargin: 6 - anchors.right: streamVol.left - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - height: 4 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 2 - } - Rectangle { - width: parent.width * Math.min(1, Math.max(0, streamEntry._vol)) - height: parent.height - color: streamEntry._muted ? M.Theme.base04 : root.accentColor - radius: 2 - } - - MouseArea { - anchors.fill: parent - anchors.margins: -6 - cursorShape: Qt.PointingHandCursor - onPressed: mouse => _set(mouse) - onPositionChanged: mouse => { - if (pressed) - _set(mouse); - } - function _set(mouse) { - if (!streamEntry.modelData.audio) - return; - streamEntry.modelData.audio.volume = Math.max(0, Math.min(1, mouse.x / streamSlider.width)); - } - } - } - - Text { - id: streamVol - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: Math.round(streamEntry._vol * 100) + "%" - color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - width: 28 - } - } - } - - // Bottom padding - Item { - width: 1 - height: 4 - } + C.VolumeContent { + width: hoverPanel.contentWidth + sink: root.sink + sinkList: root._sinkList + streamList: root._streamList + accentColor: root.accentColor } } } diff --git a/shell/modules/content/CpuContent.qml b/shell/modules/content/CpuContent.qml new file mode 100644 index 0000000..9411538 --- /dev/null +++ b/shell/modules/content/CpuContent.qml @@ -0,0 +1,234 @@ +import QtQuick +import ".." as M + +Column { + id: root + + required property var cores + required property var coreMaxFreq + required property var coreTypes + required property var processes + 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 ? M.Theme.base0B : M.Theme.base0A; + const b = t < 0.5 ? M.Theme.base0A : M.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); + } + + // Per-core rows + Repeater { + model: root.cores.length + + delegate: Item { + required property int index + width: root.width + + readonly property int _u: root.cores[index]?.usage ?? 0 + readonly property real _f: root.cores[index]?.freq_ghz ?? 0 + readonly property color _barColor: root._loadColor(_u) + readonly property bool _throttled: { + const maxF = root.coreMaxFreq[index] ?? 0; + return maxF > 0 && _f < maxF * 0.85 && _u >= 60; + } + readonly property bool _isFirstECore: { + const types = root.coreTypes; + if (!types.length || index >= types.length) + return false; + if (types[index] !== "Efficiency") + return false; + return index === 0 || types[index - 1] !== "Efficiency"; + } + + height: _isFirstECore ? 28 : 20 + + // P/E-core divider + Rectangle { + visible: parent._isFirstECore + anchors.top: parent.top + anchors.topMargin: 3 + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - 16 + height: 1 + color: M.Theme.base03 + } + + // Row content pinned to bottom of delegate + Item { + anchors.bottom: parent.bottom + width: parent.width + height: 20 + + Text { + id: coreLabel + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: index + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 16 + } + + Item { + id: coreBar + anchors.left: coreLabel.right + anchors.leftMargin: 6 + anchors.right: sparkline.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + height: 4 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 2 + } + + Rectangle { + width: parent.width * (parent.parent.parent._u / 100) + height: parent.height + color: parent.parent.parent._barColor + radius: 2 + Behavior on width { + enabled: root.active + NumberAnimation { + duration: 150 + } + } + } + } + + // Sparkline + Canvas { + id: sparkline + anchors.right: freqLabel.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: 32 + height: 10 + + property var _hist: root.cores[parent.parent.index]?.history ?? [] + property color _col: parent.parent._barColor + + 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) + return; + const bw = width / d.length; + ctx.fillStyle = _col.toString(); + for (let i = 0; i < d.length; i++) { + const h = Math.max(1, height * d[i] / 100); + ctx.fillRect(i * bw, height - h, Math.max(1, bw - 0.5), h); + } + } + } + + Text { + id: freqLabel + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: parent.parent._f.toFixed(2) + color: parent.parent._throttled ? M.Theme.base08 : M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 34 + horizontalAlignment: Text.AlignRight + } + } + } + } + + // Process list separator + Rectangle { + width: root.width - 16 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + } + + Item { + width: root.width + height: 18 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "PROCESS" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + font.letterSpacing: 1 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "CPU" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + font.letterSpacing: 1 + } + } + + // Top processes by CPU + Repeater { + model: root.processes + + delegate: Item { + required property var modelData + width: root.width + height: 20 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.cmd + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: parent.width - 80 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.cpu.toFixed(1) + "%" + color: root._loadColor(modelData.cpu) + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 36 + horizontalAlignment: Text.AlignRight + } + } + } + + Item { + width: 1 + height: 4 + } +} diff --git a/shell/modules/content/DiskContent.qml b/shell/modules/content/DiskContent.qml new file mode 100644 index 0000000..4fe65f8 --- /dev/null +++ b/shell/modules/content/DiskContent.qml @@ -0,0 +1,96 @@ +import QtQuick +import ".." as M + +Column { + id: root + + required property var mounts + required property color accentColor + + function _fmt(bytes) { + if (bytes >= 1e12) + return (bytes / 1e12).toFixed(1) + "T"; + if (bytes >= 1e9) + return Math.round(bytes / 1e9) + "G"; + if (bytes >= 1e6) + return Math.round(bytes / 1e6) + "M"; + return bytes + "B"; + } + + function _barColor(pct) { + const t = Math.max(0, Math.min(100, pct)) / 100; + const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; + const b = t < 0.5 ? M.Theme.base0A : M.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); + } + + Repeater { + model: root.mounts + + delegate: Item { + required property var modelData + width: root.width + height: 22 + + Text { + id: mountLabel + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.target + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: 72 + } + + Item { + id: mountBar + anchors.left: mountLabel.right + anchors.leftMargin: 6 + anchors.right: sizeLabel.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + height: 4 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 2 + } + + Rectangle { + width: parent.width * (modelData.pct / 100) + height: parent.height + color: root._barColor(modelData.pct) + radius: 2 + Behavior on width { + NumberAnimation { + duration: 200 + } + } + } + } + + Text { + id: sizeLabel + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._fmt(modelData.usedBytes) + "/" + root._fmt(modelData.totalBytes) + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 72 + horizontalAlignment: Text.AlignRight + } + } + } + + Item { + width: 1 + height: 4 + } +} diff --git a/shell/modules/content/HexWaveBackground.qml b/shell/modules/content/HexWaveBackground.qml new file mode 100644 index 0000000..b8ccaed --- /dev/null +++ b/shell/modules/content/HexWaveBackground.qml @@ -0,0 +1,115 @@ +import QtQuick +import Quickshell +import ".." as M + +Item { + id: root + + property bool running: false + property bool reducedMotion: M.Theme.reducedMotion + + Rectangle { + anchors.fill: parent + color: M.Theme.base01 + } + + ShaderEffect { + id: fx + anchors.fill: parent + fragmentShader: Quickshell.shellPath("modules/hex_wave.frag.qsb") + + property real uSize: 50.0 + property real uWavePhase: -200 + property real uBreath: 0 + property real uGlitch: 0 + property real uGlitchSeed: 0.0 + property vector4d uResolution: Qt.vector4d(width, height, 0, 0) + property color uC0: M.Theme.base0C + property color uC1: M.Theme.base0E + property color uC2: M.Theme.base09 + + // Reset animations when stopped + onRunningChanged: { + if (!root.running) { + fx.uWavePhase = -200; + fx.uBreath = 0; + } + } + + // Wave animation: 6s sweep + 8s pause + SequentialAnimation on uWavePhase { + loops: Animation.Infinite + running: root.running && !root.reducedMotion + NumberAnimation { + from: -200 + to: fx.width + 200 + duration: 6000 + easing.type: Easing.InOutSine + } + PauseAnimation { + duration: 8000 + } + } + + // Breathing pulse + SequentialAnimation on uBreath { + loops: Animation.Infinite + running: root.running && !root.reducedMotion + NumberAnimation { + from: 0 + to: 1 + duration: 2500 + easing.type: Easing.InOutSine + } + NumberAnimation { + from: 1 + to: 0 + duration: 2500 + easing.type: Easing.InOutSine + } + } + + // Random subtle glitches - fire every 12-37s + Timer { + interval: 20000 + repeat: true + running: !root.reducedMotion + onTriggered: { + interval = 12000 + Math.floor(Math.random() * 25000); + fx.uGlitchSeed = Math.random() * 1000.0; + _glitchAnim.start(); + } + } + + SequentialAnimation { + id: _glitchAnim + NumberAnimation { + target: fx + property: "uGlitch" + to: 0.7 + duration: 50 + easing.type: Easing.OutQuad + } + NumberAnimation { + target: fx + property: "uGlitch" + to: 0.15 + duration: 50 + } + NumberAnimation { + target: fx + property: "uGlitch" + to: 0.85 + duration: 60 + easing.type: Easing.OutQuad + } + NumberAnimation { + target: fx + property: "uGlitch" + to: 0 + duration: 100 + easing.type: Easing.InQuad + } + } + } +} diff --git a/shell/modules/content/MemoryContent.qml b/shell/modules/content/MemoryContent.qml new file mode 100644 index 0000000..3ca568e --- /dev/null +++ b/shell/modules/content/MemoryContent.qml @@ -0,0 +1,287 @@ +import QtQuick +import ".." as M + +Column { + id: root + + required property int percent + required property real usedGb + required property real totalGb + required property real availGb + required property real cachedGb + required property real buffersGb + required property var processes + required property color accentColor + + property bool active: true + + function _fmt(gb) { + return gb >= 10 ? gb.toFixed(1) + "G" : gb.toFixed(2) + "G"; + } + + // Usage bar + Item { + width: root.width + height: 14 + + Item { + id: memBar + 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: M.Theme.base02 + radius: 3 + } + + // Cached (base0D, behind used) + Rectangle { + width: parent.width * Math.min(1, (root.usedGb + root.cachedGb) / Math.max(root.totalGb, 0.001)) + height: parent.height + color: M.Theme.base0D + opacity: 0.4 + radius: 3 + Behavior on width { + enabled: root.active + NumberAnimation { + duration: 200 + } + } + } + + // Used (accentColor, on top) + Rectangle { + width: parent.width * Math.min(1, root.usedGb / Math.max(root.totalGb, 0.001)) + height: parent.height + color: root.accentColor + radius: 3 + Behavior on width { + enabled: root.active + NumberAnimation { + duration: 200 + } + } + } + } + } + + // Memory history sparkline + Canvas { + id: memSparkline + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + height: 18 + + property var _hist: M.SystemStats.memHistory + property color _col: root.accentColor + + 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) + return; + const bw = width / 30; + ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.15).toString(); + ctx.fillRect(0, 0, width, height); + ctx.fillStyle = _col.toString(); + for (let i = 0; i < d.length; i++) { + const h = Math.max(1, height * d[i] / 100); + ctx.fillRect((30 - d.length + i) * bw, height - h, Math.max(1, bw - 0.5), h); + } + } + } + + // Breakdown rows + Item { + width: root.width + height: 18 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "Used" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._fmt(root.usedGb) + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + } + + Item { + width: root.width + height: 18 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "Cached" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._fmt(root.cachedGb) + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + } + + Item { + width: root.width + height: 18 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "Available" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._fmt(root.availGb) + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + } + + Item { + width: root.width + height: 18 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "Total" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._fmt(root.totalGb) + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + } + + // Process list separator + Rectangle { + width: root.width - 16 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + } + + Item { + width: root.width + height: 18 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "PROCESS" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + font.letterSpacing: 1 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "MEM" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + font.letterSpacing: 1 + } + } + + // Top processes by memory + Repeater { + model: root.processes + + delegate: Item { + required property var modelData + width: root.width + height: 20 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.cmd + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: parent.width - 80 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.mem.toFixed(1) + "%" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 36 + horizontalAlignment: Text.AlignRight + } + } + } + + Item { + width: 1 + height: 4 + } +} diff --git a/shell/modules/content/MprisContent.qml b/shell/modules/content/MprisContent.qml new file mode 100644 index 0000000..c916ae1 --- /dev/null +++ b/shell/modules/content/MprisContent.qml @@ -0,0 +1,361 @@ +import QtQuick +import Quickshell.Services.Mpris +import ".." as M + +Column { + id: root + + required property var player + required property var players + required property bool playing + required property color accentColor + property string cachedArt: "" + property var cavaBars: Array(16).fill(0) + property int playerIdx: 0 + + signal playerSwitched(int idx) + + // Album art - always 1:1, crossfades on session switch + Item { + width: root.width + height: width + clip: true + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + } + + // Outgoing art - snaps to current opacity, then fades out + Image { + id: _artImgPrev + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + asynchronous: true + opacity: 0 + + NumberAnimation { + id: _prevFadeOut + target: _artImgPrev + property: "opacity" + to: 0 + duration: 300 + easing.type: Easing.InOutCubic + } + } + + // Incoming art - fades in once loaded + Image { + id: _artImg + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + asynchronous: true + opacity: 0 + + property bool _hasArt: false + + onStatusChanged: { + if (status === Image.Ready && source !== "") { + _hasArt = true; + _artFadeIn.start(); + _prevFadeOut.start(); + } else if (status === Image.Error) { + _hasArt = false; + } + } + + NumberAnimation { + id: _artFadeIn + target: _artImg + property: "opacity" + to: 1 + duration: 300 + easing.type: Easing.InOutCubic + } + + Connections { + target: root + function onCachedArtChanged() { + if (!root.cachedArt) { + _artFadeIn.stop(); + _prevFadeOut.stop(); + _artImg._hasArt = false; + _artImg.opacity = 0; + _artImgPrev.opacity = 0; + _artImg.source = ""; + } else if (root.cachedArt !== _artImg.source) { + _prevFadeOut.stop(); + _artFadeIn.stop(); + _artImgPrev.source = _artImg.source; + _artImgPrev.opacity = _artImg.opacity; + _artImg.opacity = 0; + _artImg.source = root.cachedArt; + } + } + } + } + + // Visualizer bars + Row { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height * 0.6 + spacing: 2 + visible: root.playing + opacity: 0.5 + + Repeater { + model: 16 + Rectangle { + required property int index + width: (parent.width - 15 * parent.spacing) / 16 + height: parent.height * (root.cavaBars[index] ?? 0) + anchors.bottom: parent.bottom + color: root.accentColor + radius: 1 + + Behavior on height { + NumberAnimation { + duration: 50 + } + } + } + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 40 + visible: _artImg._hasArt + gradient: Gradient { + GradientStop { + position: 0 + color: "transparent" + } + GradientStop { + position: 1 + color: M.Theme.base01 + } + } + } + + Text { + anchors.centerIn: parent + text: "\uF001" + color: M.Theme.base04 + font.pixelSize: 28 + font.family: M.Theme.iconFontFamily + visible: !_artImg._hasArt + } + } + + // Track info + Item { + width: root.width + height: titleCol.implicitHeight + 8 + + Column { + id: titleCol + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 2 + + Text { + width: parent.width + text: root.player?.trackTitle || "No track" + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize + 1 + font.family: M.Theme.fontFamily + font.bold: true + elide: Text.ElideRight + } + + Text { + width: parent.width + text: { + const p = root.player; + if (!p) + return ""; + const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || ""); + return [artist, p.trackAlbum].filter(s => s).join(" \u2014 "); + } + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + visible: text !== "" + } + } + } + + // Progress + Item { + width: root.width + height: 20 + + readonly property real pos: root.player?.position ?? 0 + readonly property real dur: root.player?.length ?? 0 + readonly property real frac: dur > 0 ? pos / dur : 0 + + function _fmtTime(ms) { + const s = Math.floor(ms / 1000); + const m = Math.floor(s / 60); + return m + ":" + String(s % 60).padStart(2, "0"); + } + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: parent._fmtTime(parent.pos) + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: parent._fmtTime(parent.dur) + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + Item { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 80 + height: 4 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 2 + } + Rectangle { + width: parent.width * Math.min(1, Math.max(0, parent.parent.frac)) + height: parent.height + color: root.accentColor + radius: 2 + } + } + } + + // Transport controls + Item { + width: root.width + height: 36 + + Row { + anchors.centerIn: parent + spacing: 24 + + Text { + text: "\uF048" + color: root.player?.canGoPrevious ? M.Theme.base05 : M.Theme.base03 + font.pixelSize: M.Theme.fontSize + 4 + font.family: M.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + TapHandler { + cursorShape: Qt.PointingHandCursor + enabled: root.player?.canGoPrevious ?? false + onTapped: root.player.previous() + } + } + + Text { + text: root.playing ? "\uF04C" : "\uF04B" + color: root.accentColor + font.pixelSize: M.Theme.fontSize + 8 + font.family: M.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + TapHandler { + cursorShape: Qt.PointingHandCursor + onTapped: root.player?.togglePlaying() + } + } + + Text { + text: "\uF051" + color: root.player?.canGoNext ? M.Theme.base05 : M.Theme.base03 + font.pixelSize: M.Theme.fontSize + 4 + font.family: M.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + TapHandler { + cursorShape: Qt.PointingHandCursor + enabled: root.player?.canGoNext ?? false + onTapped: root.player.next() + } + } + } + } + + // Player switcher + Item { + width: root.width + height: root.players.length > 1 ? 28 : 0 + visible: root.players.length > 1 + + Flickable { + id: _switcher + anchors.centerIn: parent + width: Math.min(_playerRow.implicitWidth, parent.width - 16) + height: 22 + contentWidth: _playerRow.implicitWidth + clip: true + + Row { + id: _playerRow + height: 22 + spacing: 6 + + Repeater { + model: root.players + + delegate: Rectangle { + required property var modelData + required property int index + + readonly property bool _active: index === root.playerIdx + + width: _pLabel.implicitWidth + 12 + height: 18 + radius: 9 + color: _active ? M.Theme.base02 : (pHover.hovered ? M.Theme.base02 : "transparent") + border.color: _active ? root.accentColor : M.Theme.base03 + border.width: _active ? 1 : 0 + anchors.verticalCenter: parent.verticalCenter + + Text { + id: _pLabel + anchors.centerIn: parent + text: modelData.identity ?? "Player" + color: _active ? root.accentColor : M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + font.bold: _active + } + + HoverHandler { + id: pHover + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onTapped: { + root.playerSwitched(index); + } + } + } + } + } + } + } +} diff --git a/shell/modules/content/TemperatureContent.qml b/shell/modules/content/TemperatureContent.qml new file mode 100644 index 0000000..fe9ce70 --- /dev/null +++ b/shell/modules/content/TemperatureContent.qml @@ -0,0 +1,265 @@ +import QtQuick +import ".." as M + +Column { + id: root + + required property int temp + required property int warm + required property int hot + required property var history + required property var devices + required property color accentColor + + property bool active: true + property string deviceFilter: "" + + property color stateColor: temp > hot ? M.Theme.base08 : temp > warm ? M.Theme.base0A : root.accentColor + Behavior on stateColor { + ColorAnimation { + duration: 300 + } + } + + function _tempColor(celsius) { + const t = Math.max(0, Math.min(100, celsius)) / 100; + const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; + const b = t < 0.5 ? M.Theme.base0A : M.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); + } + + // Header - current temp + Item { + width: root.width + height: 28 + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root.temp + "\u00B0C" + color: root.stateColor + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.fontFamily + font.bold: true + width: _tempSizer.implicitWidth + horizontalAlignment: Text.AlignRight + + Text { + id: _tempSizer + visible: false + text: "100\u00B0C" + font: parent.font + } + } + } + + // Gauge bar (0-100C), with warm/hot threshold markers + Item { + width: root.width + height: 16 + + Item { + id: _gaugeBar + 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: M.Theme.base02 + radius: 3 + } + + Rectangle { + width: parent.width * Math.min(1, root.temp / 100) + height: parent.height + color: root.stateColor + radius: 3 + Behavior on width { + enabled: root.active + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } + } + } + + // Warm threshold marker + Rectangle { + x: parent.width * (root.warm / 100) - 1 + width: 1 + height: parent.height + 4 + anchors.verticalCenter: parent.verticalCenter + color: M.Theme.base0A + opacity: 0.6 + } + + // Hot threshold marker + Rectangle { + x: parent.width * (root.hot / 100) - 1 + width: 1 + height: parent.height + 4 + anchors.verticalCenter: parent.verticalCenter + color: M.Theme.base08 + opacity: 0.6 + } + } + } + + // History sparkline + Canvas { + id: _sparkline + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + height: 40 + + property var _hist: root.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) + return; + + const maxSamples = 150; + const bw = width / maxSamples; + + // Background tint + ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.08).toString(); + ctx.fillRect(0, 0, width, height); + + // Warm threshold line + const warmY = height - height * (root.warm / 100); + ctx.strokeStyle = M.Theme.base0A.toString(); + ctx.globalAlpha = 0.3; + ctx.lineWidth = 1; + ctx.setLineDash([3, 3]); + ctx.beginPath(); + ctx.moveTo(0, warmY); + ctx.lineTo(width, warmY); + ctx.stroke(); + + // Hot threshold line + const hotY = height - height * (root.hot / 100); + ctx.strokeStyle = M.Theme.base08.toString(); + ctx.beginPath(); + ctx.moveTo(0, hotY); + ctx.lineTo(width, hotY); + ctx.stroke(); + + ctx.setLineDash([]); + ctx.globalAlpha = 1.0; + + // Bars + const offset = maxSamples - d.length; + for (let i = 0; i < d.length; i++) { + const barH = Math.max(1, height * d[i] / 100); + const barColor = d[i] > root.hot ? M.Theme.base08 : d[i] > root.warm ? M.Theme.base0A : _col; + ctx.fillStyle = barColor.toString(); + ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH); + } + } + } + + // Threshold labels + Item { + width: root.width + height: 16 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "warm " + root.warm + "\u00B0 hot " + root.hot + "\u00B0" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + font.letterSpacing: 0.5 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "10 min" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + } + } + + // Per-device breakdown + Rectangle { + width: root.width - 16 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + visible: root.devices.length > 0 + } + + Repeater { + model: root.devices + delegate: Item { + required property var modelData + width: root.width + height: 22 + + readonly property bool _isActive: root.deviceFilter === modelData.name + + Rectangle { + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + color: _isActive ? Qt.rgba(root.accentColor.r, root.accentColor.g, root.accentColor.b, 0.12) : "transparent" + radius: 3 + } + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.name + color: _isActive ? root.accentColor : M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: parent.width - 80 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.celsius + "\u00B0C" + color: root._tempColor(modelData.celsius) + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + font.bold: _isActive + } + } + } + + Item { + width: 1 + height: 4 + } +} diff --git a/shell/modules/content/VolumeContent.qml b/shell/modules/content/VolumeContent.qml new file mode 100644 index 0000000..43363a9 --- /dev/null +++ b/shell/modules/content/VolumeContent.qml @@ -0,0 +1,292 @@ +import QtQuick +import Quickshell.Services.Pipewire +import ".." as M + +Column { + id: root + + required property var sink + required property var sinkList + required property var streamList + required property color accentColor + + property real volume: sink?.audio?.volume ?? 0 + property bool muted: sink?.audio?.muted ?? false + readonly property string volumeIcon: muted ? "\uF026" : (volume > 0.5 ? "\uF028" : (volume > 0 ? "\uF027" : "\uF026")) + readonly property color volumeColor: muted ? M.Theme.base04 : root.accentColor + + // Slider row + Item { + width: root.width + height: 36 + + Text { + id: muteIcon + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root.volumeIcon + color: root.volumeColor + font.pixelSize: M.Theme.fontSize + 2 + font.family: M.Theme.iconFontFamily + + TapHandler { + cursorShape: Qt.PointingHandCursor + onTapped: if (root.sink?.audio) + root.sink.audio.muted = !root.sink.audio.muted + } + } + + Item { + id: slider + anchors.left: muteIcon.right + anchors.leftMargin: 8 + anchors.right: volLabel.left + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + height: 6 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 3 + } + Rectangle { + width: parent.width * Math.min(1, Math.max(0, root.volume)) + height: parent.height + color: root.volumeColor + radius: 3 + Behavior on width { + NumberAnimation { + duration: 80 + } + } + } + + MouseArea { + anchors.fill: parent + anchors.margins: -6 + cursorShape: Qt.PointingHandCursor + onPressed: mouse => _setVol(mouse) + onPositionChanged: mouse => { + if (pressed) + _setVol(mouse); + } + function _setVol(mouse) { + if (!root.sink?.audio) + return; + root.sink.audio.volume = Math.max(0, Math.min(1, mouse.x / slider.width)); + } + } + } + + Text { + id: volLabel + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: Math.round(root.volume * 100) + "%" + color: root.muted ? M.Theme.base04 : M.Theme.base05 + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.fontFamily + width: 30 + } + } + + // Device + stream list + Column { + id: deviceList + width: root.width + + // Output devices - only shown when more than one exists + Column { + visible: root.sinkList.length > 1 + width: parent.width + + Rectangle { + width: parent.width - 16 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + } + + Text { + width: parent.width + height: 24 + verticalAlignment: Text.AlignVCenter + leftPadding: 12 + text: "Output Devices" + color: root.accentColor + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + } + + Repeater { + model: root.sinkList + + delegate: Item { + required property var modelData + + width: root.width + height: 28 + + readonly property bool _active: modelData === root.sink + + Rectangle { + anchors.fill: parent + anchors.leftMargin: 4 + anchors.rightMargin: 4 + color: deviceHover.hovered ? M.Theme.base02 : "transparent" + radius: M.Theme.radius + } + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.description || modelData.name || "Unknown" + color: parent._active ? root.accentColor : M.Theme.base05 + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.fontFamily + font.bold: parent._active + elide: Text.ElideRight + } + + HoverHandler { + id: deviceHover + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onTapped: Pipewire.preferredDefaultAudioSink = modelData + } + } + } + } + + // Streams section + Rectangle { + visible: root.streamList.length > 0 + width: parent.width - 16 + height: visible ? 1 : 0 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + } + + Text { + visible: root.streamList.length > 0 + width: parent.width + height: visible ? 24 : 0 + verticalAlignment: Text.AlignVCenter + leftPadding: 12 + text: "Applications" + color: root.accentColor + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + } + + Repeater { + model: root.streamList + + delegate: Item { + id: streamEntry + required property var modelData + + width: root.width + height: 32 + + readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown" + readonly property real _vol: modelData.audio?.volume ?? 0 + readonly property bool _muted: modelData.audio?.muted ?? false + + Text { + id: streamIcon + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: streamEntry._muted ? "\uF026" : "\uF028" + color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.iconFontFamily + + TapHandler { + cursorShape: Qt.PointingHandCursor + onTapped: if (streamEntry.modelData.audio) + streamEntry.modelData.audio.muted = !streamEntry.modelData.audio.muted + } + } + + Text { + id: streamName + anchors.left: streamIcon.right + anchors.leftMargin: 6 + anchors.verticalCenter: parent.verticalCenter + text: streamEntry._appName + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: 70 + } + + Item { + id: streamSlider + anchors.left: streamName.right + anchors.leftMargin: 6 + anchors.right: streamVol.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + height: 4 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 2 + } + Rectangle { + width: parent.width * Math.min(1, Math.max(0, streamEntry._vol)) + height: parent.height + color: streamEntry._muted ? M.Theme.base04 : root.accentColor + radius: 2 + } + + MouseArea { + anchors.fill: parent + anchors.margins: -6 + cursorShape: Qt.PointingHandCursor + onPressed: mouse => _set(mouse) + onPositionChanged: mouse => { + if (pressed) + _set(mouse); + } + function _set(mouse) { + if (!streamEntry.modelData.audio) + return; + streamEntry.modelData.audio.volume = Math.max(0, Math.min(1, mouse.x / streamSlider.width)); + } + } + } + + Text { + id: streamVol + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: Math.round(streamEntry._vol * 100) + "%" + color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + width: 28 + } + } + } + + // Bottom padding + Item { + width: 1 + height: 4 + } + } +} diff --git a/shell/modules/content/qmldir b/shell/modules/content/qmldir new file mode 100644 index 0000000..cb33d81 --- /dev/null +++ b/shell/modules/content/qmldir @@ -0,0 +1,8 @@ +module content +HexWaveBackground 1.0 HexWaveBackground.qml +VolumeContent 1.0 VolumeContent.qml +MprisContent 1.0 MprisContent.qml +CpuContent 1.0 CpuContent.qml +MemoryContent 1.0 MemoryContent.qml +TemperatureContent 1.0 TemperatureContent.qml +DiskContent 1.0 DiskContent.qml