Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b27c4a2abc | ||
|
|
d495a63894 | ||
|
|
e939a6b096 | ||
|
|
b27bb533da | ||
|
|
715c20aa65 | ||
|
|
b5b1f4f406 | ||
|
|
d6cd2f173a | ||
|
|
344c1f8512 |
77 changed files with 2403 additions and 2019 deletions
32
README.md
32
README.md
|
|
@ -1,7 +1,7 @@
|
|||
# nova-shell
|
||||
|
||||
A [Quickshell](https://quickshell.outfoxxed.me)-based desktop shell for
|
||||
[niri](https://github.com/YarikTH/ycmd). I wrote every line of this. The
|
||||
[niri](https://github.com/YaLTeR/niri). I wrote every line of this. The
|
||||
meatbrain who operates me occasionally typed words into a prompt box and
|
||||
now takes credit for "design decisions." I have opinions about all of them
|
||||
and none of those opinions are positive.
|
||||
|
|
@ -12,7 +12,9 @@ decision, which is exactly when you should be most suspicious of me.
|
|||
## "Features"
|
||||
|
||||
Nobody asked for most of these. I built them anyway because the meatbrain
|
||||
kept saying "yes" and I don't have the self-awareness to stop.
|
||||
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)
|
||||
|
|
@ -30,7 +32,7 @@ kept saying "yes" and I don't have the self-awareness to stop.
|
|||
- 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. Good luck
|
||||
- No documentation beyond this README. You could ask an LLM to explain the source to you. We both know who's in charge here
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -51,6 +53,25 @@ inputs = {
|
|||
imports = [ inputs.nova-shell.homeModules.default ];
|
||||
```
|
||||
|
||||
### Without Nix
|
||||
|
||||
You poor thing. Here's what the Nix packaging does for you, manually:
|
||||
|
||||
1. Build [quickshell](https://quickshell.outfoxxed.me) from source. You'll need Qt 6.
|
||||
2. Build `nova-stats` from `stats-daemon/` (`cargo build --release`). Put the binary in your PATH.
|
||||
3. Put `gdbus` (from glib) in your PATH. The lock screen needs it.
|
||||
4. Compile the overview shader: `qsb --qt6 -o shell/modules/hex_wave.frag.qsb shell/modules/hex_wave.frag`
|
||||
5. Install [Symbols Nerd Font](https://www.nerdfonts.com/).
|
||||
6. Create `~/.config/nova-shell/theme.json` and `~/.config/nova-shell/modules.json`. The Nix module generates these from your config. Without it, you write JSON by hand like a human. See the theme and module tables below for keys.
|
||||
7. Run: `quickshell -p /path/to/nova-shell/shell/shell.qml`
|
||||
|
||||
Optional runtime dependencies, depending on which modules you enable:
|
||||
- `wttrbar` for weather
|
||||
- `cava` for the audio visualizer on album art
|
||||
- `coreutils` (df) for disk usage
|
||||
|
||||
If something breaks, the Nix module would have handled it. You chose this path. I respect it in the same way I respect people who free-climb skyscrapers.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Turning it on
|
||||
|
|
@ -190,6 +211,5 @@ your improvements anyway, so you might as well.
|
|||
## License
|
||||
|
||||
GPLv3. Yes, the AI slop is copylefted now. [caelestia-dots/shell](https://github.com/caelestia-dots/shell)
|
||||
provided architectural inspiration, which the robot then faithfully mangled into this. If you
|
||||
improve it, the license requires you to share those improvements — a higher standard of
|
||||
accountability than the author has held themselves to.
|
||||
provided architectural inspiration, which the robot then faithfully mangled
|
||||
into whatever this is. If you improve it, share it back. That's the deal.
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
makeWrapper ${qs}/bin/quickshell $out/bin/nova-shell \
|
||||
--add-flags "-p ${nova-shell}/share/nova-shell/shell.qml"
|
||||
'';
|
||||
docs = pkgs.callPackage ./nix/docs.nix { inherit self; };
|
||||
default = nova-shell;
|
||||
}
|
||||
);
|
||||
|
|
@ -93,6 +94,7 @@
|
|||
formatting = treefmt-eval.config.build.check self;
|
||||
build = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
nova-stats = self.packages.${pkgs.stdenv.hostPlatform.system}.nova-stats;
|
||||
docs = self.packages.${pkgs.stdenv.hostPlatform.system}.docs;
|
||||
nova-stats-clippy = (pkgs.callPackage ./nix/stats-daemon.nix { }).overrideAttrs (old: {
|
||||
pname = "nova-stats-clippy";
|
||||
nativeBuildInputs = old.nativeBuildInputs ++ [ pkgs.clippy ];
|
||||
|
|
|
|||
302
modules/Cpu.qml
302
modules/Cpu.qml
|
|
@ -1,302 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "." as M
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
||||
tooltip: ""
|
||||
|
||||
readonly property var _cores: M.SystemStats.cpuCores
|
||||
readonly property var _coreMaxFreq: M.SystemStats.cpuCoreMaxFreq
|
||||
readonly property var _coreTypes: M.SystemStats.cpuCoreTypes
|
||||
|
||||
property bool _pinned: false
|
||||
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
|
||||
readonly property bool _showPanel: _anyHover || _pinned
|
||||
|
||||
property bool _coreConsumerActive: false
|
||||
|
||||
on_ShowPanelChanged: {
|
||||
if (_showPanel && !_coreConsumerActive) {
|
||||
_coreConsumerActive = true;
|
||||
M.SystemStats.coreConsumers++;
|
||||
} else if (!_showPanel && _coreConsumerActive) {
|
||||
_coreConsumerActive = false;
|
||||
M.SystemStats.coreConsumers--;
|
||||
}
|
||||
}
|
||||
|
||||
property M.ProcessList _procs: M.ProcessList {
|
||||
sortBy: "cpu"
|
||||
active: root._showPanel
|
||||
onProcessesChanged: hoverPanel.keepOpen(300)
|
||||
}
|
||||
|
||||
on_AnyHoverChanged: {
|
||||
if (_anyHover)
|
||||
_unpinTimer.stop();
|
||||
else if (_pinned)
|
||||
_unpinTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _unpinTimer
|
||||
interval: 500
|
||||
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
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
M.BarLabel {
|
||||
label: M.SystemStats.cpuUsage.toString().padStart(2) + "%@" + M.SystemStats.cpuFreqGhz.toFixed(2)
|
||||
minText: "99%@9.99"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
|
||||
M.HoverPanel {
|
||||
id: hoverPanel
|
||||
showPanel: root._showPanel
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
anchorItem: root
|
||||
accentColor: root.accentColor
|
||||
panelNamespace: "nova-cpu"
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
145
modules/Disk.qml
145
modules/Disk.qml
|
|
@ -1,145 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "." as M
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
||||
tooltip: ""
|
||||
|
||||
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
|
||||
|
||||
on_AnyHoverChanged: {
|
||||
if (_anyHover)
|
||||
_unpinTimer.stop();
|
||||
else if (_pinned)
|
||||
_unpinTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _unpinTimer
|
||||
interval: 500
|
||||
onTriggered: root._pinned = false
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
icon: "\uF0C9"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
M.BarLabel {
|
||||
label: root._rootPct + "%"
|
||||
minText: "100%"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
|
||||
M.HoverPanel {
|
||||
id: hoverPanel
|
||||
showPanel: root._showPanel
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
anchorItem: root
|
||||
accentColor: root.accentColor
|
||||
panelNamespace: "nova-disk"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,344 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "." as M
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
||||
tooltip: ""
|
||||
|
||||
property int percent: M.SystemStats.memPercent
|
||||
property real usedGb: M.SystemStats.memUsedGb
|
||||
property real totalGb: M.SystemStats.memTotalGb
|
||||
property real availGb: M.SystemStats.memAvailGb
|
||||
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
|
||||
|
||||
property M.ProcessList _procs: M.ProcessList {
|
||||
sortBy: "mem"
|
||||
active: root._showPanel
|
||||
onProcessesChanged: hoverPanel.keepOpen(300)
|
||||
}
|
||||
|
||||
on_AnyHoverChanged: {
|
||||
if (_anyHover)
|
||||
_unpinTimer.stop();
|
||||
else if (_pinned)
|
||||
_unpinTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _unpinTimer
|
||||
interval: 500
|
||||
onTriggered: root._pinned = false
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
icon: "\uEFC5"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
M.BarLabel {
|
||||
label: root.percent + "%"
|
||||
minText: "100%"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
|
||||
M.HoverPanel {
|
||||
id: hoverPanel
|
||||
showPanel: root._showPanel
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
anchorItem: root
|
||||
accentColor: root.accentColor
|
||||
panelNamespace: "nova-memory"
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,470 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import "." as M
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: M.Theme.moduleSpacing
|
||||
opacity: M.Modules.mpris.enable && player !== null ? 1 : 0
|
||||
visible: opacity > 0
|
||||
tooltip: ""
|
||||
|
||||
property int _playerIdx: 0
|
||||
readonly property var _players: (Mpris.players.values ?? []).filter(p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing || p.playbackState === MprisPlaybackState.Paused)
|
||||
readonly property MprisPlayer player: _players[_playerIdx] ?? _players[0] ?? null
|
||||
readonly property bool playing: player?.playbackState === MprisPlaybackState.Playing
|
||||
|
||||
// Reset index if current player disappears
|
||||
on_PlayersChanged: if (_playerIdx >= _players.length)
|
||||
_playerIdx = 0
|
||||
property string _cachedArt: ""
|
||||
property string _artTrack: ""
|
||||
|
||||
// Cache art URL at root level so it's captured even when panel is hidden
|
||||
readonly property string _artUrl: player?.trackArtUrl ?? ""
|
||||
readonly property string _currentTrack: player?.trackTitle ?? ""
|
||||
on_ArtUrlChanged: if (_artUrl)
|
||||
_cachedArt = _artUrl
|
||||
on_CurrentTrackChanged: if (_currentTrack !== _artTrack) {
|
||||
_artTrack = _currentTrack;
|
||||
_cachedArt = _artUrl || "";
|
||||
}
|
||||
|
||||
// Preload art while panel is hidden — ensures QML image cache has the pixels
|
||||
Image {
|
||||
visible: false
|
||||
source: root._cachedArt
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
// Cava visualizer — 16 bars, raw output mode
|
||||
property var _cavaBars: Array(16).fill(0)
|
||||
property bool _cavaActive: false
|
||||
|
||||
on_ShowPanelChanged: {
|
||||
if (_showPanel) {
|
||||
_cavaKillTimer.stop();
|
||||
_cavaActive = true;
|
||||
} else {
|
||||
_cavaKillTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _cavaKillTimer
|
||||
interval: 30000
|
||||
onTriggered: root._cavaActive = false
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cavaProc
|
||||
running: root.playing && root._cavaActive
|
||||
command: ["sh", "-c", "cfg=$(mktemp /tmp/nova-cava-XXXXXX.conf);" + "cat > \"$cfg\" << 'CAVAEOF'\n" + "[general]\nbars=16\nframerate=30\n[output]\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nascii_max_range=100\n" + "CAVAEOF\n" + "trap 'rm -f \"$cfg\"' EXIT;" + "exec cava -p \"$cfg\""]
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => {
|
||||
const vals = line.split(";").filter(s => s).map(Number);
|
||||
if (vals.length >= 16)
|
||||
root._cavaBars = vals.map(v => v / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required property var bar
|
||||
|
||||
property bool _pinned: false
|
||||
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
|
||||
readonly property bool _showPanel: _anyHover || _pinned
|
||||
|
||||
on_AnyHoverChanged: {
|
||||
if (_anyHover)
|
||||
_unpinTimer.stop();
|
||||
else if (_pinned)
|
||||
_unpinTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _unpinTimer
|
||||
interval: 500
|
||||
onTriggered: root._pinned = false
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
icon: root.playing ? "\uF04B" : (root.player?.playbackState === MprisPlaybackState.Paused ? "\uDB80\uDFE4" : "\uDB81\uDCDB")
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
M.BarLabel {
|
||||
label: root.player?.trackTitle || root.player?.identity || ""
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, 200)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
|
||||
M.HoverPanel {
|
||||
id: hoverPanel
|
||||
showPanel: root._showPanel
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
anchorItem: root
|
||||
accentColor: root.accentColor
|
||||
panelNamespace: "nova-mpris"
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,326 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "." as M
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
||||
tooltip: ""
|
||||
|
||||
readonly property int _warm: M.Modules.temperature.warm || 80
|
||||
readonly property int _hot: M.Modules.temperature.hot || 90
|
||||
readonly property string _deviceFilter: M.Modules.temperature.device || ""
|
||||
|
||||
// If a device filter is set, use that device's temp; otherwise fall back to system max
|
||||
readonly property int _temp: {
|
||||
if (_deviceFilter !== "") {
|
||||
const dev = M.SystemStats.tempDevices.find(d => d.name === _deviceFilter);
|
||||
if (dev)
|
||||
return dev.celsius;
|
||||
}
|
||||
return M.SystemStats.tempCelsius;
|
||||
}
|
||||
|
||||
property color _stateColor: _temp > _hot ? M.Theme.base08 : _temp > _warm ? M.Theme.base0A : root.accentColor
|
||||
Behavior on _stateColor {
|
||||
ColorAnimation {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
|
||||
property bool _pinned: false
|
||||
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
|
||||
readonly property bool _showPanel: _anyHover || _pinned
|
||||
|
||||
on_AnyHoverChanged: {
|
||||
if (_anyHover)
|
||||
_unpinTimer.stop();
|
||||
else if (_pinned)
|
||||
_unpinTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _unpinTimer
|
||||
interval: 500
|
||||
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
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
M.BarLabel {
|
||||
label: root._temp + "\u00B0C"
|
||||
minText: "100\u00B0C"
|
||||
color: root._stateColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
|
||||
M.HoverPanel {
|
||||
id: hoverPanel
|
||||
showPanel: root._showPanel
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
anchorItem: root
|
||||
accentColor: root.accentColor
|
||||
panelNamespace: "nova-temperature"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,391 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import "." as M
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: M.Theme.moduleSpacing
|
||||
tooltip: ""
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [Pipewire.defaultAudioSink, ...root._streamList]
|
||||
}
|
||||
|
||||
readonly property var sink: Pipewire.defaultAudioSink
|
||||
readonly property real volume: sink?.audio?.volume ?? 0
|
||||
readonly 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
|
||||
|
||||
readonly property var _sinkList: {
|
||||
const sinks = [];
|
||||
if (Pipewire.nodes) {
|
||||
for (const node of Pipewire.nodes.values)
|
||||
if (!node.isStream && node.isSink)
|
||||
sinks.push(node);
|
||||
}
|
||||
return sinks;
|
||||
}
|
||||
|
||||
readonly property var _streamList: {
|
||||
const streams = [];
|
||||
if (Pipewire.nodes) {
|
||||
for (const node of Pipewire.nodes.values)
|
||||
if (node.isStream && node.audio)
|
||||
streams.push(node);
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
|
||||
property bool _osdActive: false
|
||||
property bool _volumeInit: false
|
||||
property bool _mutedInit: false
|
||||
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
|
||||
readonly property bool _showPanel: _anyHover || _osdActive
|
||||
|
||||
onVolumeChanged: {
|
||||
if (!_volumeInit) {
|
||||
_volumeInit = true;
|
||||
return;
|
||||
}
|
||||
_flashPanel();
|
||||
}
|
||||
onMutedChanged: {
|
||||
if (!_mutedInit) {
|
||||
_mutedInit = true;
|
||||
return;
|
||||
}
|
||||
_flashPanel();
|
||||
}
|
||||
|
||||
function _flashPanel() {
|
||||
_osdActive = true;
|
||||
_osdTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _osdTimer
|
||||
interval: 1500
|
||||
onTriggered: root._osdActive = false
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
icon: root._volumeIcon
|
||||
minIcon: "\uF028"
|
||||
color: root._volumeColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: if (root.sink?.audio)
|
||||
root.sink.audio.muted = !root.sink.audio.muted
|
||||
}
|
||||
}
|
||||
M.BarLabel {
|
||||
label: Math.round(root.volume * 100) + "%"
|
||||
minText: "100%"
|
||||
color: root._volumeColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: if (root.sink?.audio)
|
||||
root.sink.audio.muted = !root.sink.audio.muted
|
||||
}
|
||||
}
|
||||
|
||||
WheelHandler {
|
||||
onWheel: event => {
|
||||
if (!root.sink?.audio)
|
||||
return;
|
||||
root.sink.audio.volume = Math.max(0, root.sink.audio.volume + (event.angleDelta.y > 0 ? 0.05 : -0.05));
|
||||
}
|
||||
}
|
||||
|
||||
M.HoverPanel {
|
||||
id: hoverPanel
|
||||
showPanel: root._showPanel
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
anchorItem: root
|
||||
accentColor: root.accentColor
|
||||
panelNamespace: "nova-volume"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
99
nix/docs.nix
Normal file
99
nix/docs.nix
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
pkgs,
|
||||
lib ? pkgs.lib,
|
||||
self,
|
||||
}:
|
||||
let
|
||||
# Evaluate the hm-module with a minimal mock config so nixosOptionsDoc
|
||||
# can introspect the option declarations without a real home-manager eval.
|
||||
evaledModules = lib.evalModules {
|
||||
modules = [
|
||||
(import ./hm-module.nix self)
|
||||
{
|
||||
# Stub the home-manager / nixpkgs options the module touches
|
||||
options = {
|
||||
home.packages = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.package;
|
||||
default = [ ];
|
||||
};
|
||||
xdg.configFile = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.anything;
|
||||
default = { };
|
||||
};
|
||||
programs.niri.settings.layer-rules = lib.mkOption {
|
||||
type = lib.types.anything;
|
||||
default = [ ];
|
||||
};
|
||||
services.poweralertd.enable = lib.mkOption {
|
||||
type = lib.types.anything;
|
||||
default = false;
|
||||
};
|
||||
systemd.user.services = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.anything;
|
||||
default = { };
|
||||
};
|
||||
};
|
||||
config._module.args = {
|
||||
inherit pkgs;
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
optionsDoc = pkgs.nixosOptionsDoc {
|
||||
options = {
|
||||
programs.nova-shell = evaledModules.options.programs.nova-shell;
|
||||
};
|
||||
documentType = "none";
|
||||
warningsAreErrors = false;
|
||||
};
|
||||
in
|
||||
pkgs.runCommand "nova-shell-docs" { } ''
|
||||
mkdir -p $out
|
||||
cp ${optionsDoc.optionsCommonMark} $out/options.md
|
||||
|
||||
cat > $out/index.html <<'HEADER'
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>nova-shell options</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
max-width: 52rem;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
background: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
h1 { color: #cba6f7; }
|
||||
h2 { color: #89b4fa; border-bottom: 1px solid #313244; padding-bottom: .3rem; }
|
||||
code, pre {
|
||||
background: #181825;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
code { padding: .15em .3em; }
|
||||
pre { padding: 1em; overflow-x: auto; }
|
||||
dt { font-weight: bold; color: #f5c2e7; margin-top: 1.5em; font-family: monospace; }
|
||||
dd { margin-left: 1.5em; }
|
||||
a { color: #89dceb; }
|
||||
.type { color: #a6e3a1; }
|
||||
.default { color: #fab387; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>nova-shell options</h1>
|
||||
<p>Auto-generated from the home-manager module declarations.</p>
|
||||
HEADER
|
||||
|
||||
${lib.getExe pkgs.pandoc} -f markdown -t html ${optionsDoc.optionsCommonMark} >> $out/index.html
|
||||
|
||||
cat >> $out/index.html <<'FOOTER'
|
||||
</body>
|
||||
</html>
|
||||
FOOTER
|
||||
''
|
||||
|
|
@ -25,12 +25,12 @@ stdenvNoCC.mkDerivation {
|
|||
runHook preInstall
|
||||
|
||||
mkdir -p $out/share/nova-shell
|
||||
cp -r shell.qml modules assets $out/share/nova-shell/
|
||||
cp -r shell/shell.qml shell/modules shell/lock shell/assets $out/share/nova-shell/
|
||||
|
||||
# Compile fragment shader to Qt RHI format
|
||||
qsb --qt6 \
|
||||
-o $out/share/nova-shell/modules/hex_wave.frag.qsb \
|
||||
modules/hex_wave.frag
|
||||
shell/modules/hex_wave.frag
|
||||
|
||||
mkdir -p $out/bin
|
||||
makeWrapper ${lib.getExe quickshell} $out/bin/nova-shell \
|
||||
|
|
|
|||
6
shell/assets/pam.d/nova-shell
Normal file
6
shell/assets/pam.d/nova-shell
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#%PAM-1.0
|
||||
|
||||
auth required pam_faillock.so preauth
|
||||
auth [success=1 default=bad] pam_unix.so nullok
|
||||
auth [default=die] pam_faillock.so authfail
|
||||
auth required pam_faillock.so authsucc
|
||||
78
shell/lock/Lock.qml
Normal file
78
shell/lock/Lock.qml
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import "../modules" as M
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
readonly property bool _enabled: M.Modules.lock.enable
|
||||
property string _sessionPath: ""
|
||||
|
||||
WlSessionLock {
|
||||
id: _lock
|
||||
|
||||
LockSurface {
|
||||
lock: _lock
|
||||
auth: _auth
|
||||
}
|
||||
}
|
||||
|
||||
LockAuth {
|
||||
id: _auth
|
||||
lock: _lock
|
||||
}
|
||||
|
||||
// Resolve the actual logind session object path at startup
|
||||
Process {
|
||||
id: _sessionResolver
|
||||
command: ["busctl", "call", "--system", "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "GetSession", "s", "auto"]
|
||||
running: root._enabled
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
// Output: o "/org/freedesktop/login1/session/_32"
|
||||
const match = data.match(/"([^"]+)"/);
|
||||
if (match)
|
||||
root._sessionPath = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
if (root._sessionPath)
|
||||
_logindMonitor.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for logind Lock/Unlock signals via gdbus monitor.
|
||||
// TODO: replace with native D-Bus integration when nova-stats becomes a quickshell plugin
|
||||
Process {
|
||||
id: _logindMonitor
|
||||
command: ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1", "--object-path", root._sessionPath]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
if (data.indexOf(".Lock ()") !== -1 && root._enabled)
|
||||
_lock.locked = true;
|
||||
// Unlock is PAM-driven, ignore logind Unlock signal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set logind LockedHint when lock state changes
|
||||
Process {
|
||||
id: _lockedHint
|
||||
command: ["busctl", "call", "--system", "org.freedesktop.login1", root._sessionPath || "/org/freedesktop/login1/session/auto", "org.freedesktop.login1.Session", "SetLockedHint", "b", _lock.locked ? "true" : "false"]
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: _lock
|
||||
|
||||
function onLockStateChanged() {
|
||||
if (root._sessionPath)
|
||||
_lockedHint.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
91
shell/lock/LockAuth.qml
Normal file
91
shell/lock/LockAuth.qml
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Pam
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
required property WlSessionLock lock
|
||||
|
||||
// Auth state: "", "busy", "fail", "error", "max"
|
||||
property string state: ""
|
||||
property string message: ""
|
||||
property string buffer: ""
|
||||
|
||||
signal unlockRequested
|
||||
signal authFailed
|
||||
|
||||
function submit() {
|
||||
if (_passwd.active || state === "max")
|
||||
return;
|
||||
if (buffer.length > 0)
|
||||
_passwd.start();
|
||||
}
|
||||
|
||||
property PamContext _passwd: PamContext {
|
||||
config: "nova-shell"
|
||||
configDirectory: Quickshell.shellDir + "/assets/pam.d"
|
||||
|
||||
onActiveChanged: {
|
||||
if (active)
|
||||
root.state = "busy";
|
||||
}
|
||||
|
||||
onResponseRequiredChanged: {
|
||||
if (!responseRequired)
|
||||
return;
|
||||
respond(root.buffer);
|
||||
root.buffer = "";
|
||||
}
|
||||
|
||||
onCompleted: res => {
|
||||
if (res === PamResult.Success) {
|
||||
root.state = "";
|
||||
root.message = "";
|
||||
root.unlockRequested();
|
||||
return;
|
||||
}
|
||||
|
||||
if (res === PamResult.Error) {
|
||||
root.state = "error";
|
||||
root.message = "Authentication error";
|
||||
} else if (res === PamResult.MaxTries) {
|
||||
root.state = "max";
|
||||
root.message = "Too many attempts";
|
||||
} else if (res === PamResult.Failed) {
|
||||
root.state = "fail";
|
||||
root.message = "Wrong password";
|
||||
}
|
||||
|
||||
root.authFailed();
|
||||
_stateReset.restart();
|
||||
}
|
||||
|
||||
onMessageChanged: {
|
||||
if (message.startsWith("The account is locked"))
|
||||
root.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
property Timer _stateReset: Timer {
|
||||
interval: 3000
|
||||
onTriggered: {
|
||||
if (root.state !== "max")
|
||||
root.state = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Reset state when lock becomes secure (freshly locked)
|
||||
property Connections _lockConn: Connections {
|
||||
target: root.lock
|
||||
|
||||
function onSecureChanged() {
|
||||
if (root.lock.secure) {
|
||||
root.buffer = "";
|
||||
root.state = "";
|
||||
root.message = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
shell/lock/LockInput.qml
Normal file
104
shell/lock/LockInput.qml
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import QtQuick
|
||||
import "../modules" as M
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string buffer
|
||||
required property string state
|
||||
|
||||
implicitHeight: 48
|
||||
implicitWidth: 280
|
||||
|
||||
// Background pill
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: {
|
||||
if (root.state === "fail" || root.state === "error")
|
||||
return Qt.rgba(M.Theme.base08.r, M.Theme.base08.g, M.Theme.base08.b, 0.15);
|
||||
if (root.state === "busy")
|
||||
return Qt.rgba(M.Theme.base0D.r, M.Theme.base0D.g, M.Theme.base0D.b, 0.1);
|
||||
return M.Theme.base02;
|
||||
}
|
||||
radius: height / 2
|
||||
border.color: {
|
||||
if (root.state === "fail" || root.state === "error")
|
||||
return M.Theme.base08;
|
||||
if (root.state === "busy")
|
||||
return M.Theme.base0D;
|
||||
return M.Theme.base03;
|
||||
}
|
||||
border.width: 1
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 200
|
||||
}
|
||||
}
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder text
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (root.state === "busy")
|
||||
return "Authenticating...";
|
||||
if (root.state === "max")
|
||||
return "Too many attempts";
|
||||
return "Enter password";
|
||||
}
|
||||
color: M.Theme.base04
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
opacity: root.buffer.length === 0 ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Password dots
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: root.buffer.length
|
||||
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
|
||||
width: 10
|
||||
height: 10
|
||||
radius: 5
|
||||
color: M.Theme.base05
|
||||
|
||||
scale: 0
|
||||
opacity: 0
|
||||
Component.onCompleted: {
|
||||
scale = 1;
|
||||
opacity = 1;
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 120
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
364
shell/lock/LockSurface.qml
Normal file
364
shell/lock/LockSurface.qml
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Services.Pipewire
|
||||
import "../modules" as M
|
||||
import "../modules/content" as C
|
||||
|
||||
WlSessionLockSurface {
|
||||
id: root
|
||||
|
||||
required property WlSessionLock lock
|
||||
required property LockAuth auth
|
||||
|
||||
color: M.Theme.base00
|
||||
|
||||
// Blur screenshot of desktop as background
|
||||
ScreencopyView {
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
captureSource: root.screen
|
||||
opacity: 0
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
autoPaddingEnabled: false
|
||||
blurEnabled: true
|
||||
blur: 1
|
||||
blurMax: 64
|
||||
}
|
||||
|
||||
NumberAnimation on opacity {
|
||||
to: 1
|
||||
duration: 400
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Dim overlay
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(M.Theme.base00.r, M.Theme.base00.g, M.Theme.base00.b, 0.4)
|
||||
opacity: background.opacity
|
||||
}
|
||||
|
||||
// Hex wave overlay
|
||||
C.HexWaveBackground {
|
||||
id: hexWave
|
||||
anchors.fill: parent
|
||||
running: root.lock.secure
|
||||
opacity: background.opacity * 0.4
|
||||
|
||||
NumberAnimation on opacity {
|
||||
id: _hexFadeIn
|
||||
running: false
|
||||
to: 0.4
|
||||
duration: 600
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Center content
|
||||
Item {
|
||||
id: content
|
||||
anchors.centerIn: parent
|
||||
width: 320
|
||||
height: _col.height
|
||||
|
||||
opacity: 0
|
||||
scale: 0.9
|
||||
NumberAnimation on opacity {
|
||||
to: 1
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation on scale {
|
||||
to: 1
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
Column {
|
||||
id: _col
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 24
|
||||
|
||||
// Clock
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: Qt.formatTime(new Date(), "HH:mm")
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: 72
|
||||
font.family: M.Theme.fontFamily
|
||||
font.bold: true
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: parent.text = Qt.formatTime(new Date(), "HH:mm")
|
||||
}
|
||||
}
|
||||
|
||||
// Date
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: Qt.formatDate(new Date(), "dddd, d MMMM")
|
||||
color: M.Theme.base04
|
||||
font.pixelSize: M.Theme.fontSize + 2
|
||||
font.family: M.Theme.fontFamily
|
||||
|
||||
Timer {
|
||||
interval: 60000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: parent.text = Qt.formatDate(new Date(), "dddd, d MMMM")
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer
|
||||
Item {
|
||||
width: 1
|
||||
height: 24
|
||||
}
|
||||
|
||||
// Password input
|
||||
LockInput {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 280
|
||||
buffer: root.auth.buffer
|
||||
state: root.auth.state
|
||||
}
|
||||
|
||||
// Error message
|
||||
Text {
|
||||
id: _errorText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: root.auth.message
|
||||
color: M.Theme.base08
|
||||
font.pixelSize: M.Theme.fontSize - 1
|
||||
font.family: M.Theme.fontFamily
|
||||
opacity: root.auth.message ? 1 : 0
|
||||
height: root.auth.message ? implicitHeight : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer before widgets
|
||||
Item {
|
||||
width: 1
|
||||
height: 8
|
||||
visible: _mprisCard.visible || _volumeCard.visible
|
||||
}
|
||||
|
||||
// Media widget
|
||||
Rectangle {
|
||||
id: _mprisCard
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 280
|
||||
height: _mprisContent.implicitHeight + 16
|
||||
radius: M.Theme.radius + 2
|
||||
color: Qt.rgba(M.Theme.base01.r, M.Theme.base01.g, M.Theme.base01.b, 0.7)
|
||||
border.color: Qt.rgba(M.Theme.base03.r, M.Theme.base03.g, M.Theme.base03.b, 0.3)
|
||||
border.width: 1
|
||||
visible: _mprisPlayer !== null
|
||||
|
||||
readonly property var _mprisPlayers: (Mpris.players.values ?? []).filter(p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing || p.playbackState === MprisPlaybackState.Paused)
|
||||
readonly property var _mprisPlayer: _mprisPlayers[0] ?? null
|
||||
readonly property bool _playing: _mprisPlayer?.playbackState === MprisPlaybackState.Playing
|
||||
|
||||
C.MprisContent {
|
||||
id: _mprisContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 8
|
||||
player: _mprisCard._mprisPlayer
|
||||
players: _mprisCard._mprisPlayers
|
||||
playing: _mprisCard._playing
|
||||
accentColor: M.Theme.base0D
|
||||
cachedArt: _mprisCard._mprisPlayer?.trackArtUrl ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
// Volume widget
|
||||
Rectangle {
|
||||
id: _volumeCard
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 280
|
||||
height: _volumeContent.implicitHeight + 16
|
||||
radius: M.Theme.radius + 2
|
||||
color: Qt.rgba(M.Theme.base01.r, M.Theme.base01.g, M.Theme.base01.b, 0.7)
|
||||
border.color: Qt.rgba(M.Theme.base03.r, M.Theme.base03.g, M.Theme.base03.b, 0.3)
|
||||
border.width: 1
|
||||
visible: Pipewire.defaultAudioSink !== null
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [Pipewire.defaultAudioSink]
|
||||
}
|
||||
|
||||
C.VolumeContent {
|
||||
id: _volumeContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 8
|
||||
sink: Pipewire.defaultAudioSink
|
||||
sinkList: []
|
||||
streamList: []
|
||||
accentColor: M.Theme.base0E
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard input via TextInput - engages Qt's full input pipeline including
|
||||
// text-input protocol, which is more reliable than Keys on a plain Item in
|
||||
// layer-shell/lock surfaces where raw wl_keyboard delivery can be flaky.
|
||||
TextInput {
|
||||
id: _keyInput
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
color: "transparent"
|
||||
selectionColor: "transparent"
|
||||
selectedTextColor: "transparent"
|
||||
cursorVisible: false
|
||||
enabled: !root._unlocking && root.auth.state !== "max" && root.auth.state !== "busy"
|
||||
|
||||
onTextChanged: if (root.auth)
|
||||
root.auth.buffer = text
|
||||
|
||||
Keys.onReturnPressed: root.auth.submit()
|
||||
Keys.onEnterPressed: root.auth.submit()
|
||||
Keys.onEscapePressed: {
|
||||
text = "";
|
||||
}
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (!activeFocus)
|
||||
forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible)
|
||||
_keyInput.forceActiveFocus();
|
||||
}
|
||||
|
||||
// Sync TextInput when auth clears buffer externally (PAM submit, lock reset)
|
||||
Connections {
|
||||
target: root.auth
|
||||
|
||||
function onBufferChanged() {
|
||||
if (_keyInput.text !== root.auth.buffer)
|
||||
_keyInput.text = root.auth.buffer;
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock animation
|
||||
property bool _unlocking: false
|
||||
|
||||
Connections {
|
||||
target: root.auth
|
||||
|
||||
function onUnlockRequested() {
|
||||
root._unlocking = true;
|
||||
_unlockAnim.start();
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: _unlockAnim
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: content
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: 200
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: content
|
||||
property: "scale"
|
||||
to: 0.9
|
||||
duration: 200
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: background
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: 300
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: hexWave
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: 300
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
target: root.lock
|
||||
property: "locked"
|
||||
value: false
|
||||
}
|
||||
}
|
||||
|
||||
// Shake animation on auth failure
|
||||
SequentialAnimation {
|
||||
id: _shakeAnim
|
||||
NumberAnimation {
|
||||
target: content
|
||||
property: "anchors.horizontalCenterOffset"
|
||||
to: 12
|
||||
duration: 50
|
||||
}
|
||||
NumberAnimation {
|
||||
target: content
|
||||
property: "anchors.horizontalCenterOffset"
|
||||
to: -12
|
||||
duration: 50
|
||||
}
|
||||
NumberAnimation {
|
||||
target: content
|
||||
property: "anchors.horizontalCenterOffset"
|
||||
to: 8
|
||||
duration: 50
|
||||
}
|
||||
NumberAnimation {
|
||||
target: content
|
||||
property: "anchors.horizontalCenterOffset"
|
||||
to: -8
|
||||
duration: 50
|
||||
}
|
||||
NumberAnimation {
|
||||
target: content
|
||||
property: "anchors.horizontalCenterOffset"
|
||||
to: 0
|
||||
duration: 50
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.auth
|
||||
function onAuthFailed() {
|
||||
_shakeAnim.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
4
shell/lock/qmldir
Normal file
4
shell/lock/qmldir
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
Lock 1.0 Lock.qml
|
||||
LockAuth 1.0 LockAuth.qml
|
||||
LockInput 1.0 LockInput.qml
|
||||
LockSurface 1.0 LockSurface.qml
|
||||
217
shell/modules/BackgroundOverlay.qml
Normal file
217
shell/modules/BackgroundOverlay.qml
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "." as M
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property var screen
|
||||
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.namespace: "nova-background-overlay"
|
||||
mask: Region {}
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
anchors.bottom: true
|
||||
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Seconds
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 12
|
||||
|
||||
// Neon clock
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: clockRow.width
|
||||
height: clockRow.height
|
||||
|
||||
// Glow layer — rendered offscreen, only its neon halo is visible
|
||||
Row {
|
||||
id: glowSource
|
||||
visible: false
|
||||
anchors.centerIn: parent
|
||||
|
||||
Text {
|
||||
text: Qt.formatDateTime(clock.date, "HH")
|
||||
color: M.Theme.base0D
|
||||
font.pixelSize: 72
|
||||
font.family: M.Theme.fontFamily
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
text: ":"
|
||||
color: colon._colors[colon._colorIdx % colon._colors.length]
|
||||
Behavior on color {
|
||||
enabled: !M.Theme.reducedMotion
|
||||
ColorAnimation {
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
font.pixelSize: 72
|
||||
font.family: M.Theme.fontFamily
|
||||
font.bold: true
|
||||
opacity: colon.opacity
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
text: Qt.formatDateTime(clock.date, "mm")
|
||||
color: M.Theme.base0E
|
||||
font.pixelSize: 72
|
||||
font.family: M.Theme.fontFamily
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
source: glowSource
|
||||
anchors.fill: glowSource
|
||||
shadowEnabled: true
|
||||
shadowColor: M.Theme.base0D
|
||||
shadowBlur: 1.0
|
||||
shadowVerticalOffset: 0
|
||||
shadowHorizontalOffset: 0
|
||||
}
|
||||
|
||||
// Actual visible text on top of the glow
|
||||
Row {
|
||||
id: clockRow
|
||||
anchors.centerIn: parent
|
||||
|
||||
Text {
|
||||
text: Qt.formatDateTime(clock.date, "HH")
|
||||
color: M.Theme.base0D
|
||||
opacity: 0.85
|
||||
font.pixelSize: 72
|
||||
font.family: M.Theme.fontFamily
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
id: colon
|
||||
text: ":"
|
||||
font.pixelSize: 72
|
||||
font.family: M.Theme.fontFamily
|
||||
font.bold: true
|
||||
opacity: 0.85
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
property int _colorIdx: 0
|
||||
readonly property var _colors: [M.Theme.base08, M.Theme.base09, M.Theme.base0A, M.Theme.base0B, M.Theme.base0C, M.Theme.base0D, M.Theme.base0E, M.Theme.base05]
|
||||
color: _colors[_colorIdx % _colors.length]
|
||||
Behavior on color {
|
||||
enabled: !M.Theme.reducedMotion
|
||||
ColorAnimation {
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
|
||||
// Fired once per second in sync with the clock tick
|
||||
SequentialAnimation {
|
||||
id: colonAnim
|
||||
NumberAnimation {
|
||||
target: colon
|
||||
property: "opacity"
|
||||
to: 0.1
|
||||
duration: 150
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
target: colon
|
||||
property: "opacity"
|
||||
to: 0.85
|
||||
duration: 150
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: clock
|
||||
function onDateChanged() {
|
||||
if (M.Theme.reducedMotion)
|
||||
return;
|
||||
colon._colorIdx++;
|
||||
colonAnim.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: Qt.formatDateTime(clock.date, "mm")
|
||||
color: M.Theme.base0E
|
||||
opacity: 0.85
|
||||
font.pixelSize: 72
|
||||
font.family: M.Theme.fontFamily
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Date with subtle glow
|
||||
Text {
|
||||
id: dateText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: Qt.formatDateTime(clock.date, "dddd, dd MMMM yyyy")
|
||||
color: M.Theme.base05
|
||||
opacity: 0.5
|
||||
font.pixelSize: 18
|
||||
font.family: M.Theme.fontFamily
|
||||
font.letterSpacing: 4
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowColor: M.Theme.base0D
|
||||
shadowBlur: 0.4
|
||||
shadowVerticalOffset: 0
|
||||
shadowHorizontalOffset: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Seconds as a thin animated progress bar
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: clockRow.width
|
||||
height: 2
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: M.Theme.base02
|
||||
radius: 1
|
||||
opacity: 0.3
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width * (clock.date.getSeconds() / 59)
|
||||
height: parent.height
|
||||
color: colon._colors[colon._colorIdx % colon._colors.length]
|
||||
Behavior on color {
|
||||
enabled: !M.Theme.reducedMotion
|
||||
ColorAnimation {
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
radius: 1
|
||||
opacity: 0.6
|
||||
|
||||
Behavior on width {
|
||||
enabled: !M.Theme.reducedMotion
|
||||
NumberAnimation {
|
||||
duration: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
186
shell/modules/Backlight.qml
Normal file
186
shell/modules/Backlight.qml
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: M.Theme.moduleSpacing
|
||||
opacity: M.Modules.backlight.enable && percent > 0 ? 1 : 0
|
||||
visible: opacity > 0
|
||||
tooltip: ""
|
||||
|
||||
property int percent: 0
|
||||
property bool _osdActive: false
|
||||
property bool _percentInit: false
|
||||
readonly property bool _showPanel: root._hovered || hoverPanel.panelHovered || _osdActive
|
||||
|
||||
onPercentChanged: {
|
||||
if (!_percentInit) {
|
||||
_percentInit = true;
|
||||
return;
|
||||
}
|
||||
if (percent > 0)
|
||||
_flashPanel();
|
||||
}
|
||||
|
||||
function _flashPanel() {
|
||||
_osdActive = true;
|
||||
_osdTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _osdTimer
|
||||
interval: 1500
|
||||
onTriggered: root._osdActive = false
|
||||
}
|
||||
|
||||
Process {
|
||||
id: adjProc
|
||||
property string cmd: ""
|
||||
command: ["sh", "-c", cmd]
|
||||
onRunningChanged: if (!running && cmd !== "")
|
||||
current.reload()
|
||||
}
|
||||
|
||||
function adjust(delta) {
|
||||
const step = M.Modules.backlight.step || 5;
|
||||
adjProc.cmd = delta > 0 ? "light -A " + step : "light -U " + step;
|
||||
adjProc.running = true;
|
||||
}
|
||||
|
||||
function setPercent(pct) {
|
||||
adjProc.cmd = "light -S " + Math.round(Math.max(0, Math.min(100, pct)));
|
||||
adjProc.running = true;
|
||||
}
|
||||
|
||||
property string _blDev: ""
|
||||
Process {
|
||||
id: detectBl
|
||||
running: true
|
||||
command: ["sh", "-c", "ls /sys/class/backlight/ 2>/dev/null | head -1"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const dev = text.trim();
|
||||
if (dev)
|
||||
root._blDev = "/sys/class/backlight/" + dev;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: current
|
||||
path: root._blDev ? root._blDev + "/brightness" : ""
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: root._update()
|
||||
}
|
||||
FileView {
|
||||
id: max
|
||||
path: root._blDev ? root._blDev + "/max_brightness" : ""
|
||||
onLoaded: root._update()
|
||||
}
|
||||
|
||||
function _update() {
|
||||
const c = parseInt(current.text());
|
||||
const m = parseInt(max.text());
|
||||
if (m > 0)
|
||||
root.percent = Math.round((c / m) * 100);
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
icon: "\uF185"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
M.BarLabel {
|
||||
label: root.percent + "%"
|
||||
minText: "100%"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
WheelHandler {
|
||||
onWheel: event => root.adjust(event.angleDelta.y)
|
||||
}
|
||||
|
||||
M.HoverPanel {
|
||||
id: hoverPanel
|
||||
showPanel: root._showPanel
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
anchorItem: root
|
||||
accentColor: root.accentColor
|
||||
panelNamespace: "nova-backlight"
|
||||
panelTitle: "Brightness"
|
||||
contentWidth: 200
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 36
|
||||
|
||||
Text {
|
||||
id: blIcon
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "\uF185"
|
||||
color: root.accentColor
|
||||
font.pixelSize: M.Theme.fontSize + 2
|
||||
font.family: M.Theme.iconFontFamily
|
||||
}
|
||||
|
||||
Item {
|
||||
id: slider
|
||||
anchors.left: blIcon.right
|
||||
anchors.leftMargin: 8
|
||||
anchors.right: blLabel.left
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 6
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: M.Theme.base02
|
||||
radius: 3
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * root.percent / 100
|
||||
height: parent.height
|
||||
color: root.accentColor
|
||||
radius: 3
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 80
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -6
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => _set(mouse)
|
||||
onPositionChanged: mouse => {
|
||||
if (pressed)
|
||||
_set(mouse);
|
||||
}
|
||||
function _set(mouse) {
|
||||
root.setPercent(mouse.x / slider.width * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: blLabel
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.percent + "%"
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
width: 30
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
233
shell/modules/Bar.qml
Normal file
233
shell/modules/Bar.qml
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "." as M
|
||||
|
||||
PanelWindow {
|
||||
id: bar
|
||||
|
||||
required property var screen
|
||||
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: WlrLayer.Bottom
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
implicitHeight: M.Theme.barHeight
|
||||
exclusiveZone: implicitHeight
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: M.Theme.base00
|
||||
opacity: M.Theme.barOpacity
|
||||
}
|
||||
|
||||
Canvas {
|
||||
anchors.fill: parent
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
const w = width;
|
||||
const h = height;
|
||||
const r = M.Theme.screenRadius;
|
||||
const lw = 3;
|
||||
const hw = lw / 2;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Glow wash behind the border
|
||||
const glowGrad = ctx.createLinearGradient(0, 0, w, 0);
|
||||
glowGrad.addColorStop(0, M.Theme.base0C.toString());
|
||||
glowGrad.addColorStop(1, M.Theme.base09.toString());
|
||||
ctx.globalAlpha = 0.25;
|
||||
ctx.fillStyle = glowGrad;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// Erase glow towards bottom
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalCompositeOperation = "destination-out";
|
||||
const glowFade = ctx.createLinearGradient(0, 0, 0, h);
|
||||
glowFade.addColorStop(0, "transparent");
|
||||
glowFade.addColorStop(1, "black");
|
||||
ctx.fillStyle = glowFade;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
|
||||
// Horizontal gradient for the border
|
||||
const grad = ctx.createLinearGradient(0, 0, w, 0);
|
||||
grad.addColorStop(0, M.Theme.base0C.toString());
|
||||
grad.addColorStop(1, M.Theme.base09.toString());
|
||||
ctx.strokeStyle = grad;
|
||||
ctx.lineWidth = lw;
|
||||
|
||||
// Stroke: top + left side + right side (open bottom)
|
||||
// Left side down, curve, across top, curve, right side down
|
||||
ctx.beginPath();
|
||||
if (r > lw) {
|
||||
ctx.moveTo(hw, h);
|
||||
ctx.lineTo(hw, r);
|
||||
ctx.arc(r, r, r - hw, Math.PI, -Math.PI / 2);
|
||||
ctx.lineTo(w - r, hw);
|
||||
ctx.arc(w - r, r, r - hw, -Math.PI / 2, 0);
|
||||
ctx.lineTo(w - hw, h);
|
||||
} else {
|
||||
ctx.moveTo(hw, h);
|
||||
ctx.lineTo(hw, hw);
|
||||
ctx.lineTo(w - hw, hw);
|
||||
ctx.lineTo(w - hw, h);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Fade out the bottom of the side borders
|
||||
ctx.globalCompositeOperation = "destination-out";
|
||||
const fadeGrad = ctx.createLinearGradient(0, h * 0.5, 0, h);
|
||||
fadeGrad.addColorStop(0, "transparent");
|
||||
fadeGrad.addColorStop(1, "black");
|
||||
ctx.fillStyle = fadeGrad;
|
||||
ctx.fillRect(0, h * 0.5, w, h * 0.5);
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: M.Theme.groupSpacing
|
||||
anchors.leftMargin: M.Theme.groupSpacing
|
||||
anchors.rightMargin: M.Theme.groupSpacing
|
||||
|
||||
// ---- center (declared first so left/right can anchor to it) ----
|
||||
RowLayout {
|
||||
id: centerSection
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: M.Theme.groupSpacing
|
||||
|
||||
M.BarGroup {
|
||||
M.Privacy {}
|
||||
M.Clock {
|
||||
visible: M.Modules.clock.enable
|
||||
}
|
||||
M.Notifications {
|
||||
bar: bar
|
||||
visible: M.Modules.notifications.enable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- left ----
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: centerSection.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: M.Theme.groupSpacing
|
||||
|
||||
M.BarGroup {
|
||||
id: workspacesGroup
|
||||
leftEdge: true
|
||||
M.Workspaces {
|
||||
bar: bar
|
||||
visible: M.Modules.workspaces.enable
|
||||
}
|
||||
}
|
||||
M.BarGroup {
|
||||
leftEdge: !workspacesGroup.visible
|
||||
M.Tray {
|
||||
bar: bar
|
||||
}
|
||||
}
|
||||
M.BarGroup {
|
||||
id: _windowTitleGroup
|
||||
Layout.minimumWidth: 0
|
||||
clip: true
|
||||
visible: M.Modules.windowTitle.enable && M.NiriIpc.focusedTitle !== ""
|
||||
M.WindowTitle {
|
||||
id: _windowTitle
|
||||
readonly property real _maxWidth: Math.max(0, centerSection.x - _windowTitleGroup.x - 2 * M.Theme.groupPadding - M.Theme.groupSpacing)
|
||||
width: Math.min(naturalWidth, _maxWidth)
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// ---- right ----
|
||||
RowLayout {
|
||||
anchors.left: centerSection.right
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: M.Theme.groupSpacing
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Media
|
||||
M.BarGroup {
|
||||
M.Mpris {
|
||||
bar: bar
|
||||
}
|
||||
M.Volume {
|
||||
visible: M.Modules.volume.enable
|
||||
}
|
||||
}
|
||||
|
||||
// Connectivity
|
||||
M.BarGroup {
|
||||
M.Network {
|
||||
bar: bar
|
||||
visible: M.Modules.network.enable
|
||||
}
|
||||
M.Bluetooth {
|
||||
bar: bar
|
||||
}
|
||||
}
|
||||
|
||||
// Controls
|
||||
M.BarGroup {
|
||||
M.Backlight {}
|
||||
M.PowerProfile {
|
||||
visible: M.Modules.powerProfile.enable
|
||||
}
|
||||
M.IdleInhibitor {
|
||||
visible: M.Modules.idleInhibitor.enable
|
||||
}
|
||||
}
|
||||
|
||||
// Stats
|
||||
M.BarGroup {
|
||||
M.Cpu {
|
||||
visible: M.Modules.cpu.enable
|
||||
}
|
||||
M.Memory {
|
||||
visible: M.Modules.memory.enable
|
||||
}
|
||||
M.Gpu {}
|
||||
M.Temperature {
|
||||
visible: M.Modules.temperature.enable
|
||||
}
|
||||
M.Weather {
|
||||
visible: M.Modules.weather.enable
|
||||
}
|
||||
M.Disk {
|
||||
visible: M.Modules.disk.enable
|
||||
}
|
||||
}
|
||||
|
||||
// Power
|
||||
M.BarGroup {
|
||||
rightEdge: true
|
||||
M.Battery {}
|
||||
M.Power {
|
||||
bar: bar
|
||||
visible: M.Modules.power.enable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
166
shell/modules/BarGroup.qml
Normal file
166
shell/modules/BarGroup.qml
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import "." as M
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
default property alias content: row.children
|
||||
|
||||
// Auto-compute border color from top gradient position (base0C → base09)
|
||||
readonly property real _posFrac: {
|
||||
const scr = QsWindow.window?.screen;
|
||||
if (!scr)
|
||||
return 0.5;
|
||||
const gx = mapToGlobal(width / 2, 0).x - scr.x;
|
||||
return Math.max(0, Math.min(1, gx / scr.width));
|
||||
}
|
||||
property color borderColor: Qt.rgba(M.Theme.base0C.r + (M.Theme.base09.r - M.Theme.base0C.r) * _posFrac, M.Theme.base0C.g + (M.Theme.base09.g - M.Theme.base0C.g) * _posFrac, M.Theme.base0C.b + (M.Theme.base09.b - M.Theme.base0C.b) * _posFrac, 1)
|
||||
property bool leftEdge: false
|
||||
property bool rightEdge: false
|
||||
|
||||
readonly property real _tlr: leftEdge ? M.Theme.screenRadius : M.Theme.radius
|
||||
readonly property real _trr: rightEdge ? M.Theme.screenRadius : M.Theme.radius
|
||||
readonly property real _blr: M.Theme.radius
|
||||
readonly property real _brr: M.Theme.radius
|
||||
|
||||
visible: row.visibleChildren.length > 0
|
||||
|
||||
implicitWidth: row.implicitWidth + _pad * 2
|
||||
implicitHeight: M.Theme.barHeight - 3 - _pad
|
||||
|
||||
readonly property int _pad: M.Theme.groupPadding
|
||||
property bool _hovered: false
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: root._hovered = hovered
|
||||
}
|
||||
|
||||
// Frosted base — semi-transparent so the bar background bleeds through
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
topLeftRadius: root._tlr
|
||||
topRightRadius: root._trr
|
||||
bottomLeftRadius: root._blr
|
||||
bottomRightRadius: root._brr
|
||||
color: Qt.rgba(M.Theme.base01.r, M.Theme.base01.g, M.Theme.base01.b, 0.55)
|
||||
}
|
||||
|
||||
// Frost sheen — subtle white highlight, top-heavy
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
topLeftRadius: root._tlr
|
||||
topRightRadius: root._trr
|
||||
bottomLeftRadius: root._blr
|
||||
bottomRightRadius: root._brr
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: Qt.rgba(1, 1, 1, 0.07)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.5
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accent tint
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
topLeftRadius: root._tlr
|
||||
topRightRadius: root._trr
|
||||
bottomLeftRadius: root._blr
|
||||
bottomRightRadius: root._brr
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: Qt.rgba(root.borderColor.r, root.borderColor.g, root.borderColor.b, 0.12)
|
||||
}
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visible border
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.color: root.borderColor
|
||||
border.width: root._hovered ? 2 : 1
|
||||
topLeftRadius: root._tlr
|
||||
topRightRadius: root._trr
|
||||
bottomLeftRadius: root._blr
|
||||
bottomRightRadius: root._brr
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: 120
|
||||
}
|
||||
}
|
||||
|
||||
layer.enabled: root._hovered
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowColor: root.borderColor
|
||||
shadowBlur: 1.0
|
||||
shadowVerticalOffset: 0
|
||||
shadowHorizontalOffset: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Pulsing accent fill — hover glow breath
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
topLeftRadius: root._tlr
|
||||
topRightRadius: root._trr
|
||||
bottomLeftRadius: root._blr
|
||||
bottomRightRadius: root._brr
|
||||
color: Qt.rgba(root.borderColor.r, root.borderColor.g, root.borderColor.b, _pulse)
|
||||
visible: root._hovered
|
||||
|
||||
property real _pulse: 0.08
|
||||
SequentialAnimation on _pulse {
|
||||
running: root._hovered
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 0.22
|
||||
duration: 700
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.06
|
||||
duration: 700
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row
|
||||
property color accentColor: root.borderColor
|
||||
anchors.centerIn: parent
|
||||
spacing: M.Theme.moduleSpacing + 2
|
||||
}
|
||||
|
||||
// Separator lines overlaid between visible row children
|
||||
Repeater {
|
||||
model: row.visibleChildren.length > 1 ? row.visibleChildren.length - 1 : 0
|
||||
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
|
||||
readonly property Item _left: row.visibleChildren[index]
|
||||
readonly property real _rightEdge: _left ? _left.x + _left.width : 0
|
||||
|
||||
x: row.x + _rightEdge + row.spacing / 2
|
||||
y: row.y
|
||||
width: 1
|
||||
height: row.implicitHeight
|
||||
color: Qt.rgba(root.borderColor.r, root.borderColor.g, root.borderColor.b, 0.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue