reorganize repo: move shell sources into shell/, test scripts into test/
This commit is contained in:
parent
344c1f8512
commit
d6cd2f173a
60 changed files with 2 additions and 2 deletions
|
|
@ -25,12 +25,12 @@ stdenvNoCC.mkDerivation {
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
|
|
||||||
mkdir -p $out/share/nova-shell
|
mkdir -p $out/share/nova-shell
|
||||||
cp -r shell.qml modules assets $out/share/nova-shell/
|
cp -r shell/shell.qml shell/modules shell/assets $out/share/nova-shell/
|
||||||
|
|
||||||
# Compile fragment shader to Qt RHI format
|
# Compile fragment shader to Qt RHI format
|
||||||
qsb --qt6 \
|
qsb --qt6 \
|
||||||
-o $out/share/nova-shell/modules/hex_wave.frag.qsb \
|
-o $out/share/nova-shell/modules/hex_wave.frag.qsb \
|
||||||
modules/hex_wave.frag
|
shell/modules/hex_wave.frag
|
||||||
|
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
makeWrapper ${lib.getExe quickshell} $out/bin/nova-shell \
|
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
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
shell/modules/BarIcon.qml
Normal file
74
shell/modules/BarIcon.qml
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: root
|
||||||
|
property string icon: ""
|
||||||
|
property string tooltip: ""
|
||||||
|
property string minIcon: ""
|
||||||
|
property color accentColor: parent?.accentColor ?? M.Theme.base05
|
||||||
|
property bool _hovered: false
|
||||||
|
property string _displayIcon: icon
|
||||||
|
property string _pendingIcon: ""
|
||||||
|
|
||||||
|
text: _displayIcon
|
||||||
|
|
||||||
|
onIconChanged: {
|
||||||
|
_pendingIcon = icon;
|
||||||
|
if (!_crossfade.running)
|
||||||
|
_crossfade.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: _crossfade
|
||||||
|
NumberAnimation {
|
||||||
|
target: root
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
duration: 60
|
||||||
|
easing.type: Easing.InQuad
|
||||||
|
}
|
||||||
|
ScriptAction {
|
||||||
|
script: root._displayIcon = root._pendingIcon
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: root
|
||||||
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
duration: 100
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
width: minIcon ? Math.max(implicitWidth, _minIconMetrics.width) : implicitWidth
|
||||||
|
horizontalAlignment: minIcon ? Text.AlignHCenter : Text.AlignLeft
|
||||||
|
color: root.accentColor
|
||||||
|
font.pixelSize: M.Theme.fontSize + 1
|
||||||
|
font.family: M.Theme.iconFontFamily
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: _minIconMetrics
|
||||||
|
text: root.minIcon
|
||||||
|
font.pixelSize: root.font.pixelSize
|
||||||
|
font.family: root.font.family
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
onHoveredChanged: {
|
||||||
|
root._hovered = hovered;
|
||||||
|
if (hovered && root.tooltip !== "") {
|
||||||
|
M.FlyoutState.text = root.tooltip;
|
||||||
|
M.FlyoutState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
|
||||||
|
M.FlyoutState.screen = QsWindow.window?.screen ?? null;
|
||||||
|
M.FlyoutState.accentColor = root.accentColor;
|
||||||
|
M.FlyoutState.visible = true;
|
||||||
|
} else if (!hovered && root.tooltip !== "") {
|
||||||
|
M.FlyoutState.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTooltipChanged: if (_hovered && tooltip !== "")
|
||||||
|
M.FlyoutState.text = tooltip
|
||||||
|
}
|
||||||
45
shell/modules/BarLabel.qml
Normal file
45
shell/modules/BarLabel.qml
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: root
|
||||||
|
property string label: ""
|
||||||
|
property string tooltip: ""
|
||||||
|
property string minText: ""
|
||||||
|
property color accentColor: parent?.accentColor ?? M.Theme.base05
|
||||||
|
property bool _hovered: false
|
||||||
|
|
||||||
|
text: label
|
||||||
|
width: minText ? Math.max(implicitWidth, _minMetrics.width) : implicitWidth
|
||||||
|
horizontalAlignment: minText ? Text.AlignHCenter : Text.AlignLeft
|
||||||
|
color: root.accentColor
|
||||||
|
font.pixelSize: M.Theme.fontSize
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: _minMetrics
|
||||||
|
text: root.minText
|
||||||
|
font.pixelSize: root.font.pixelSize
|
||||||
|
font.family: root.font.family
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
onHoveredChanged: {
|
||||||
|
root._hovered = hovered;
|
||||||
|
if (hovered && root.tooltip !== "") {
|
||||||
|
M.FlyoutState.text = root.tooltip;
|
||||||
|
M.FlyoutState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
|
||||||
|
M.FlyoutState.screen = QsWindow.window?.screen ?? null;
|
||||||
|
M.FlyoutState.accentColor = root.accentColor;
|
||||||
|
M.FlyoutState.visible = true;
|
||||||
|
} else if (!hovered && root.tooltip !== "") {
|
||||||
|
M.FlyoutState.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTooltipChanged: if (_hovered && tooltip !== "")
|
||||||
|
M.FlyoutState.text = tooltip
|
||||||
|
}
|
||||||
34
shell/modules/BarSection.qml
Normal file
34
shell/modules/BarSection.qml
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
property string tooltip: ""
|
||||||
|
property bool _hovered: false
|
||||||
|
property color accentColor: parent?.accentColor ?? M.Theme.base05
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 150
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
onHoveredChanged: {
|
||||||
|
root._hovered = hovered;
|
||||||
|
if (hovered && root.tooltip !== "") {
|
||||||
|
M.FlyoutState.text = root.tooltip;
|
||||||
|
M.FlyoutState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
|
||||||
|
M.FlyoutState.screen = QsWindow.window?.screen ?? null;
|
||||||
|
M.FlyoutState.accentColor = root.accentColor;
|
||||||
|
M.FlyoutState.visible = true;
|
||||||
|
} else if (!hovered && root.tooltip !== "") {
|
||||||
|
M.FlyoutState.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTooltipChanged: if (_hovered && tooltip !== "")
|
||||||
|
M.FlyoutState.text = tooltip
|
||||||
|
}
|
||||||
419
shell/modules/Battery.qml
Normal file
419
shell/modules/Battery.qml
Normal file
|
|
@ -0,0 +1,419 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.UPower
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
M.BarSection {
|
||||||
|
id: root
|
||||||
|
spacing: M.Theme.moduleSpacing
|
||||||
|
opacity: M.Modules.battery.enable && (UPower.displayDevice?.isLaptopBattery ?? false) ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
tooltip: ""
|
||||||
|
|
||||||
|
readonly property var dev: UPower.displayDevice
|
||||||
|
readonly property real pct: (dev?.percentage ?? 0) * 100
|
||||||
|
readonly property bool charging: dev?.state === UPowerDeviceState.Charging
|
||||||
|
readonly property int _critThresh: M.Modules.battery.critical || 15
|
||||||
|
readonly property int _warnThresh: M.Modules.battery.warning || 25
|
||||||
|
readonly property bool _critical: pct < _critThresh && !charging
|
||||||
|
property color _stateColor: charging ? M.Theme.base0B : _critical ? M.Theme.base09 : pct < _warnThresh ? M.Theme.base0A : root.accentColor
|
||||||
|
property real _blinkOpacity: 1
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
running: root._critical
|
||||||
|
loops: Animation.Infinite
|
||||||
|
NumberAnimation {
|
||||||
|
target: root
|
||||||
|
property: "_blinkOpacity"
|
||||||
|
to: 0.45
|
||||||
|
duration: 400
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: root
|
||||||
|
property: "_blinkOpacity"
|
||||||
|
to: 1
|
||||||
|
duration: 400
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
onRunningChanged: if (!running)
|
||||||
|
root._blinkOpacity = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Notifications ────────────────────────────────────────────────────
|
||||||
|
property bool _warnSent: false
|
||||||
|
property bool _critSent: false
|
||||||
|
|
||||||
|
onChargingChanged: {
|
||||||
|
_warnSent = false;
|
||||||
|
_critSent = false;
|
||||||
|
}
|
||||||
|
onPctChanged: {
|
||||||
|
if (charging)
|
||||||
|
return;
|
||||||
|
if (pct < _critThresh && !_critSent) {
|
||||||
|
_critSent = true;
|
||||||
|
_warnSent = true;
|
||||||
|
_notif.command = ["notify-send", "--urgency=critical", "--icon=battery-low", "--category=device", "Very Low Battery", "Connect to power now!"];
|
||||||
|
_notif.running = true;
|
||||||
|
} else if (pct < _warnThresh && !_warnSent) {
|
||||||
|
_warnSent = true;
|
||||||
|
_notif.command = ["notify-send", "--icon=battery-caution", "--category=device", "Low Battery"];
|
||||||
|
_notif.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: _notif
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── History (always-running, 1440 samples @ 60s = 24h) ───────────────
|
||||||
|
property var _history: []
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 60000
|
||||||
|
running: root.visible
|
||||||
|
repeat: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: {
|
||||||
|
const h = root._history.concat([root.pct]);
|
||||||
|
root._history = h.length > 1440 ? h.slice(h.length - 1440) : h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Panel state ──────────────────────────────────────────────────────
|
||||||
|
property bool _pinned: false
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
function _fmtTime(secs) {
|
||||||
|
if (!secs || secs <= 0)
|
||||||
|
return "";
|
||||||
|
const h = Math.floor(secs / 3600);
|
||||||
|
const m = Math.floor((secs % 3600) / 60);
|
||||||
|
return h > 0 ? h + "h " + m + "m" : m + "m";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bar widgets ──────────────────────────────────────────────────────
|
||||||
|
M.BarIcon {
|
||||||
|
icon: {
|
||||||
|
if (root.charging)
|
||||||
|
return "\uDB80\uDC84";
|
||||||
|
const icons = ["\uDB80\uDC8E", "\uDB80\uDC7A", "\uDB80\uDC7B", "\uDB80\uDC7C", "\uDB80\uDC7D", "\uDB80\uDC7E", "\uDB80\uDC7F", "\uDB80\uDC80", "\uDB80\uDC81", "\uDB80\uDC82", "\uDB85\uDFE2"];
|
||||||
|
return icons[Math.min(10, Math.floor(root.pct / 10))];
|
||||||
|
}
|
||||||
|
color: root._stateColor
|
||||||
|
opacity: root._blinkOpacity
|
||||||
|
font.pixelSize: M.Theme.fontSize + 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
TapHandler {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onTapped: root._pinned = !root._pinned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
M.BarLabel {
|
||||||
|
label: Math.round(root.pct) + "%"
|
||||||
|
minText: "100%"
|
||||||
|
color: root._stateColor
|
||||||
|
opacity: root._blinkOpacity
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
TapHandler {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onTapped: root._pinned = !root._pinned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hover panel ──────────────────────────────────────────────────────
|
||||||
|
M.HoverPanel {
|
||||||
|
id: hoverPanel
|
||||||
|
showPanel: root._showPanel
|
||||||
|
screen: QsWindow.window?.screen ?? null
|
||||||
|
anchorItem: root
|
||||||
|
accentColor: root.accentColor
|
||||||
|
panelNamespace: "nova-battery"
|
||||||
|
panelTitle: "Battery"
|
||||||
|
contentWidth: 240
|
||||||
|
|
||||||
|
// Header — pct + time
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 28
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: {
|
||||||
|
const t = root.charging ? root.dev?.timeToFull : root.dev?.timeToEmpty;
|
||||||
|
const ts = root._fmtTime(t);
|
||||||
|
return Math.round(root.pct) + "%" + (ts ? " " + ts : "");
|
||||||
|
}
|
||||||
|
color: root._stateColor
|
||||||
|
font.pixelSize: M.Theme.fontSize
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 14
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
height: 6
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: M.Theme.base02
|
||||||
|
radius: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width * Math.min(1, root.pct / 100)
|
||||||
|
height: parent.height
|
||||||
|
color: root._stateColor
|
||||||
|
radius: 3
|
||||||
|
Behavior on width {
|
||||||
|
enabled: root._showPanel
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 300
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning threshold marker
|
||||||
|
Rectangle {
|
||||||
|
x: parent.width * (root._warnThresh / 100) - 1
|
||||||
|
width: 1
|
||||||
|
height: parent.height + 4
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: M.Theme.base0A
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
|
|
||||||
|
// Critical threshold marker
|
||||||
|
Rectangle {
|
||||||
|
x: parent.width * (root._critThresh / 100) - 1
|
||||||
|
width: 1
|
||||||
|
height: parent.height + 4
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: M.Theme.base08
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 24h history sparkline (area chart)
|
||||||
|
Canvas {
|
||||||
|
id: _sparkline
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
height: 44
|
||||||
|
|
||||||
|
property var _hist: root._history
|
||||||
|
property color _col: root._stateColor
|
||||||
|
|
||||||
|
on_HistChanged: if (root._showPanel)
|
||||||
|
requestPaint()
|
||||||
|
on_ColChanged: if (root._showPanel)
|
||||||
|
requestPaint()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function on_ShowPanelChanged() {
|
||||||
|
if (root._showPanel)
|
||||||
|
_sparkline.requestPaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPaint: {
|
||||||
|
const ctx = getContext("2d");
|
||||||
|
if (!ctx)
|
||||||
|
return;
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
const d = _hist;
|
||||||
|
if (d.length < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const maxSamples = 1440;
|
||||||
|
const xScale = width / maxSamples;
|
||||||
|
const xOffset = (maxSamples - d.length) * xScale;
|
||||||
|
|
||||||
|
// Background tint
|
||||||
|
ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.07).toString();
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Warning threshold line
|
||||||
|
const warnY = height - height * (root._warnThresh / 100);
|
||||||
|
ctx.strokeStyle = M.Theme.base0A.toString();
|
||||||
|
ctx.globalAlpha = 0.3;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([3, 3]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, warnY);
|
||||||
|
ctx.lineTo(width, warnY);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Critical threshold line
|
||||||
|
const critY = height - height * (root._critThresh / 100);
|
||||||
|
ctx.strokeStyle = M.Theme.base08.toString();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, critY);
|
||||||
|
ctx.lineTo(width, critY);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.globalAlpha = 1.0;
|
||||||
|
|
||||||
|
// Filled area under the curve
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(xOffset, height);
|
||||||
|
for (let i = 0; i < d.length; i++) {
|
||||||
|
const x = xOffset + i * xScale;
|
||||||
|
const y = height - height * (d[i] / 100);
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.lineTo(xOffset + (d.length - 1) * xScale, height);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.18).toString();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Top line
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let i = 0; i < d.length; i++) {
|
||||||
|
const x = xOffset + i * xScale;
|
||||||
|
const y = height - height * (d[i] / 100);
|
||||||
|
if (i === 0)
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
else
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = _col.toString();
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer: thresholds + time label
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 16
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "warn " + root._warnThresh + "% crit " + root._critThresh + "%"
|
||||||
|
color: 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: "24h"
|
||||||
|
color: M.Theme.base03
|
||||||
|
font.pixelSize: M.Theme.fontSize - 3
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - 16
|
||||||
|
height: 1
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: M.Theme.base03
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate + health rows
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 20
|
||||||
|
visible: (root.dev?.changeRate ?? 0) !== 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.charging ? "Charging" : "Discharging"
|
||||||
|
color: 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: {
|
||||||
|
const r = Math.abs(root.dev?.changeRate ?? 0);
|
||||||
|
return r > 0 ? r.toFixed(1) + " W" : "";
|
||||||
|
}
|
||||||
|
color: root._stateColor
|
||||||
|
font.pixelSize: M.Theme.fontSize - 2
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 20
|
||||||
|
visible: root.dev?.healthSupported ?? false
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Health"
|
||||||
|
color: 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: Math.round((root.dev?.healthPercentage ?? 0) * 100) + "%"
|
||||||
|
color: {
|
||||||
|
const h = (root.dev?.healthPercentage ?? 1) * 100;
|
||||||
|
return h < 50 ? M.Theme.base08 : h < 75 ? M.Theme.base0A : M.Theme.base0B;
|
||||||
|
}
|
||||||
|
font.pixelSize: M.Theme.fontSize - 2
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
shell/modules/Bluetooth.qml
Normal file
103
shell/modules/Bluetooth.qml
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
M.BarSection {
|
||||||
|
id: root
|
||||||
|
spacing: M.Theme.moduleSpacing
|
||||||
|
opacity: M.Modules.bluetooth.enable && root.state !== "unavailable" ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
tooltip: {
|
||||||
|
if (root.state === "off")
|
||||||
|
return "Bluetooth: off";
|
||||||
|
if (root.state === "connected")
|
||||||
|
return "Bluetooth: " + root.device + (root.batteryPct >= 0 ? "\nBattery: " + root.batteryPct + "%" : "");
|
||||||
|
return "Bluetooth: on";
|
||||||
|
}
|
||||||
|
|
||||||
|
property string state: "unavailable"
|
||||||
|
property string device: ""
|
||||||
|
property int batteryPct: -1
|
||||||
|
|
||||||
|
function _parse(text) {
|
||||||
|
const lines = text.trim().split("\n");
|
||||||
|
const t = lines[0] || "";
|
||||||
|
const sep = t.indexOf(":");
|
||||||
|
root.state = sep === -1 ? t : t.slice(0, sep);
|
||||||
|
root.device = sep === -1 ? "" : t.slice(sep + 1);
|
||||||
|
root.batteryPct = -1;
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
if (lines[i].startsWith("bat:"))
|
||||||
|
root.batteryPct = parseInt(lines[i].slice(4)) || -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: proc
|
||||||
|
running: M.Modules.bluetooth.enable
|
||||||
|
command: ["sh", "-c", "s=$(bluetoothctl show 2>/dev/null); " + "[ -z \"$s\" ] && echo unavailable && exit; " + "echo \"$s\" | grep -q 'Powered: yes' || { echo off:; exit; }; " + "info=$(bluetoothctl info 2>/dev/null); " + "d=$(echo \"$info\" | awk -F': ' '/\\tName:/{n=$2}/Connected: yes/{c=1}END{if(c)print n}'); " + "[ -n \"$d\" ] && echo \"connected:$d\" || { echo on:; exit; }; " + "bat=$(echo \"$info\" | awk -F': ' '/Battery Percentage.*\\(/{gsub(/[^0-9]/,\"\",$2);print $2}'); " + "[ -n \"$bat\" ] && echo \"bat:$bat\""]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root._parse(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Event-driven: watch BlueZ DBus property changes
|
||||||
|
Process {
|
||||||
|
id: btMonitor
|
||||||
|
running: M.Modules.bluetooth.enable
|
||||||
|
command: ["sh", "-c", "dbus-monitor --system \"interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path_namespace='/org/bluez'\" 2>/dev/null"]
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: _debounce.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
id: _debounce
|
||||||
|
interval: 500
|
||||||
|
onTriggered: proc.running = true
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
interval: 60000
|
||||||
|
running: M.Modules.bluetooth.enable
|
||||||
|
repeat: true
|
||||||
|
onTriggered: proc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
M.BarIcon {
|
||||||
|
icon: "\uF294"
|
||||||
|
color: root.state === "off" ? M.Theme.base04 : root.accentColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
TapHandler {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onTapped: {
|
||||||
|
M.FlyoutState.visible = false;
|
||||||
|
btLoader.active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
M.BarLabel {
|
||||||
|
visible: root.state === "connected"
|
||||||
|
label: root.device + (root.batteryPct >= 0 ? " " + root.batteryPct + "%" : "")
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
TapHandler {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onTapped: {
|
||||||
|
M.FlyoutState.visible = false;
|
||||||
|
btLoader.active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required property var bar
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: btLoader
|
||||||
|
active: false
|
||||||
|
M.BluetoothMenu {
|
||||||
|
accentColor: root.accentColor
|
||||||
|
screen: QsWindow.window?.screen ?? null
|
||||||
|
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
|
||||||
|
onDismissed: btLoader.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
188
shell/modules/BluetoothMenu.qml
Normal file
188
shell/modules/BluetoothMenu.qml
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
M.HoverPanel {
|
||||||
|
id: menuWindow
|
||||||
|
|
||||||
|
contentWidth: 250
|
||||||
|
panelNamespace: "nova-bluetooth"
|
||||||
|
popupMode: true
|
||||||
|
panelTitle: "Bluetooth"
|
||||||
|
titleActionsComponent: Component {
|
||||||
|
Item {
|
||||||
|
width: 20
|
||||||
|
height: 20
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "\uF011"
|
||||||
|
color: menuWindow._btEnabled ? menuWindow.accentColor : M.Theme.base04
|
||||||
|
font.pixelSize: M.Theme.fontSize
|
||||||
|
font.family: M.Theme.iconFontFamily
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
onTapped: {
|
||||||
|
powerProc._action = menuWindow._btEnabled ? "off" : "on";
|
||||||
|
powerProc.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: if (visible)
|
||||||
|
scanner.running = true
|
||||||
|
|
||||||
|
property var _devices: []
|
||||||
|
property bool _btEnabled: true
|
||||||
|
|
||||||
|
property Process _scanner: Process {
|
||||||
|
id: scanner
|
||||||
|
running: false
|
||||||
|
command: ["sh", "-c", "bluetoothctl show 2>/dev/null | awk '/Powered:/{print $2; exit}';" + "echo '---DEVICES---';" + "bluetoothctl devices Paired 2>/dev/null | while read -r _ mac name; do " + "info=$(bluetoothctl info \"$mac\" 2>/dev/null); " + "conn=$(echo \"$info\" | grep -c 'Connected: yes'); " + "bat=$(echo \"$info\" | awk -F'[(): ]' '/Battery Percentage/{for(i=1;i<=NF;i++) if($i+0==$i && $i!=\"\") print $i}'); " + "echo \"$mac:$conn:${bat:-}:$name\"; " + "done"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const sections = text.split("---DEVICES---");
|
||||||
|
menuWindow._btEnabled = (sections[0] || "").trim() === "yes";
|
||||||
|
|
||||||
|
const devs = [];
|
||||||
|
for (const line of (sections[1] || "").trim().split("\n")) {
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
const i1 = line.indexOf(":");
|
||||||
|
const i2 = line.indexOf(":", i1 + 1);
|
||||||
|
const i3 = line.indexOf(":", i2 + 1);
|
||||||
|
if (i3 < 0)
|
||||||
|
continue;
|
||||||
|
devs.push({
|
||||||
|
"mac": line.slice(0, i1),
|
||||||
|
"connected": line.slice(i1 + 1, i2) === "1",
|
||||||
|
"battery": parseInt(line.slice(i2 + 1, i3)) || -1,
|
||||||
|
"name": line.slice(i3 + 1)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
devs.sort((a, b) => {
|
||||||
|
if (a.connected !== b.connected)
|
||||||
|
return a.connected ? -1 : 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
menuWindow._devices = devs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property Process _powerProc: Process {
|
||||||
|
id: powerProc
|
||||||
|
property string _action: ""
|
||||||
|
command: ["bluetoothctl", "power", _action]
|
||||||
|
onRunningChanged: if (!running) {
|
||||||
|
scanner.running = true;
|
||||||
|
menuWindow.keepOpen(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property Process _toggleProc: Process {
|
||||||
|
id: toggleProc
|
||||||
|
property string action: ""
|
||||||
|
property string mac: ""
|
||||||
|
command: ["bluetoothctl", action, mac]
|
||||||
|
onRunningChanged: if (!running) {
|
||||||
|
scanner.running = true;
|
||||||
|
menuWindow.keepOpen(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: menuWindow._devices
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: entry
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: menuWindow.contentWidth
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 4
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
color: entryHover.hovered ? M.Theme.base02 : "transparent"
|
||||||
|
radius: M.Theme.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: btIcon
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "\uF294"
|
||||||
|
color: entry.modelData.connected ? menuWindow.accentColor : M.Theme.base04
|
||||||
|
font.pixelSize: M.Theme.fontSize + 1
|
||||||
|
font.family: M.Theme.iconFontFamily
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: btIcon.right
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
anchors.right: batLabel.left
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: entry.modelData.name
|
||||||
|
color: entry.modelData.connected ? menuWindow.accentColor : M.Theme.base05
|
||||||
|
font.pixelSize: M.Theme.fontSize
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
font.bold: entry.modelData.connected
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: batLabel
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: entry.modelData.battery >= 0 ? entry.modelData.battery + "%" : ""
|
||||||
|
color: M.Theme.base04
|
||||||
|
font.pixelSize: M.Theme.fontSize - 1
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
width: entry.modelData.battery >= 0 ? implicitWidth : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: entryHover
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
TapHandler {
|
||||||
|
onTapped: {
|
||||||
|
toggleProc.action = entry.modelData.connected ? "disconnect" : "connect";
|
||||||
|
toggleProc.mac = entry.modelData.mac;
|
||||||
|
toggleProc.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: menuWindow._devices.length === 0
|
||||||
|
width: menuWindow.contentWidth
|
||||||
|
height: 32
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
text: menuWindow._btEnabled ? "No paired devices" : "Bluetooth is off"
|
||||||
|
color: M.Theme.base04
|
||||||
|
font.pixelSize: M.Theme.fontSize
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
}
|
||||||
|
}
|
||||||
14
shell/modules/Clock.qml
Normal file
14
shell/modules/Clock.qml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
M.BarLabel {
|
||||||
|
SystemClock {
|
||||||
|
id: clock
|
||||||
|
precision: SystemClock.Seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
font.pixelSize: M.Theme.fontSize + 1
|
||||||
|
label: Qt.formatDateTime(clock.date, "ddd, dd. MMM HH:mm")
|
||||||
|
tooltip: Qt.formatDateTime(clock.date, "dddd, dd. MMMM yyyy\nHH:mm:ss")
|
||||||
|
}
|
||||||
302
shell/modules/Cpu.qml
Normal file
302
shell/modules/Cpu.qml
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
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
shell/modules/Disk.qml
Normal file
145
shell/modules/Disk.qml
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
shell/modules/Flyout.qml
Normal file
101
shell/modules/Flyout.qml
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var screen
|
||||||
|
|
||||||
|
visible: _winVisible
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
property bool _winVisible: false
|
||||||
|
property bool _shown: M.FlyoutState.visible && M.FlyoutState.screen === root.screen
|
||||||
|
|
||||||
|
on_ShownChanged: {
|
||||||
|
if (_shown) {
|
||||||
|
_winVisible = true;
|
||||||
|
hideAnim.stop();
|
||||||
|
showAnim.start();
|
||||||
|
} else {
|
||||||
|
showAnim.stop();
|
||||||
|
hideAnim.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: 0
|
||||||
|
WlrLayershell.namespace: "nova-flyout"
|
||||||
|
|
||||||
|
anchors.top: true
|
||||||
|
anchors.left: true
|
||||||
|
|
||||||
|
margins.top: 0
|
||||||
|
margins.left: Math.max(0, Math.min(Math.round(M.FlyoutState.itemX - implicitWidth / 2), screen.width - implicitWidth))
|
||||||
|
|
||||||
|
implicitWidth: label.implicitWidth + M.Theme.barPadding * 2
|
||||||
|
implicitHeight: label.implicitHeight + M.Theme.barPadding * 2
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: showAnim
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
duration: 120
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "y"
|
||||||
|
to: 0
|
||||||
|
duration: 150
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: hideAnim
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
duration: 150
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "y"
|
||||||
|
to: -content.height
|
||||||
|
duration: 150
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
onFinished: root._winVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: content
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
height: root.implicitHeight
|
||||||
|
opacity: 0
|
||||||
|
y: -height
|
||||||
|
|
||||||
|
M.PopupBackground {
|
||||||
|
anchors.fill: parent
|
||||||
|
accentColor: M.FlyoutState.accentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: label
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: M.FlyoutState.text.replace(/\n/g, "<br>")
|
||||||
|
textFormat: Text.RichText
|
||||||
|
color: M.Theme.base05
|
||||||
|
font.pixelSize: M.Theme.fontSize
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
shell/modules/FlyoutState.qml
Normal file
11
shell/modules/FlyoutState.qml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
property bool visible: false
|
||||||
|
property string text: ""
|
||||||
|
property real itemX: 0
|
||||||
|
property var screen: null
|
||||||
|
property color accentColor: M.Theme.base05
|
||||||
|
}
|
||||||
276
shell/modules/Gpu.qml
Normal file
276
shell/modules/Gpu.qml
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
M.BarSection {
|
||||||
|
id: root
|
||||||
|
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
||||||
|
tooltip: ""
|
||||||
|
visible: M.Modules.gpu.enable && M.SystemStats.gpuAvailable
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _fmt(gb) {
|
||||||
|
return gb >= 10 ? gb.toFixed(1) + "G" : gb.toFixed(2) + "G";
|
||||||
|
}
|
||||||
|
|
||||||
|
M.BarIcon {
|
||||||
|
icon: "\uDB84\uDCB0"
|
||||||
|
color: root._loadColor(M.SystemStats.gpuUsage)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
TapHandler {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onTapped: root._pinned = !root._pinned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
M.BarLabel {
|
||||||
|
label: M.SystemStats.gpuUsage + "%"
|
||||||
|
minText: "100%"
|
||||||
|
color: root._loadColor(M.SystemStats.gpuUsage)
|
||||||
|
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-gpu"
|
||||||
|
panelTitle: "GPU"
|
||||||
|
contentWidth: 240
|
||||||
|
|
||||||
|
// Header — vendor + usage%
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 28
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: M.SystemStats.gpuVendor.toUpperCase()
|
||||||
|
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: M.SystemStats.gpuUsage + "%"
|
||||||
|
color: root._loadColor(M.SystemStats.gpuUsage)
|
||||||
|
font.pixelSize: M.Theme.fontSize
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage bar
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 14
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
height: 6
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: M.Theme.base02
|
||||||
|
radius: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width * Math.min(1, M.SystemStats.gpuUsage / 100)
|
||||||
|
height: parent.height
|
||||||
|
color: root._loadColor(M.SystemStats.gpuUsage)
|
||||||
|
radius: 3
|
||||||
|
Behavior on width {
|
||||||
|
enabled: root._showPanel
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage history sparkline
|
||||||
|
Canvas {
|
||||||
|
id: _sparkline
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
height: 36
|
||||||
|
|
||||||
|
property var _hist: M.SystemStats.gpuHistory
|
||||||
|
|
||||||
|
on_HistChanged: if (root._showPanel)
|
||||||
|
requestPaint()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function on_ShowPanelChanged() {
|
||||||
|
if (root._showPanel)
|
||||||
|
_sparkline.requestPaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPaint: {
|
||||||
|
const ctx = getContext("2d");
|
||||||
|
if (!ctx)
|
||||||
|
return;
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
const d = _hist;
|
||||||
|
if (!d.length)
|
||||||
|
return;
|
||||||
|
const maxSamples = 60;
|
||||||
|
const bw = width / maxSamples;
|
||||||
|
const offset = maxSamples - d.length;
|
||||||
|
for (let i = 0; i < d.length; i++) {
|
||||||
|
const barH = Math.max(1, height * d[i] / 100);
|
||||||
|
const col = root._loadColor(d[i]);
|
||||||
|
ctx.fillStyle = col.toString();
|
||||||
|
ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VRAM section
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width - 16
|
||||||
|
height: 1
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: M.Theme.base03
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 22
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "VRAM"
|
||||||
|
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: root._fmt(M.SystemStats.gpuVramUsedGb) + " / " + root._fmt(M.SystemStats.gpuVramTotalGb)
|
||||||
|
color: root.accentColor
|
||||||
|
font.pixelSize: M.Theme.fontSize - 1
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 12
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
height: 5
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: M.Theme.base02
|
||||||
|
radius: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: M.SystemStats.gpuVramTotalGb > 0 ? parent.width * Math.min(1, M.SystemStats.gpuVramUsedGb / M.SystemStats.gpuVramTotalGb) : 0
|
||||||
|
height: parent.height
|
||||||
|
color: root.accentColor
|
||||||
|
radius: 2
|
||||||
|
Behavior on width {
|
||||||
|
enabled: root._showPanel
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temperature row
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 22
|
||||||
|
visible: M.SystemStats.gpuTempC > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Temp"
|
||||||
|
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: M.SystemStats.gpuTempC + "\u00B0C"
|
||||||
|
color: M.SystemStats.gpuTempC > 85 ? M.Theme.base08 : M.SystemStats.gpuTempC > 70 ? M.Theme.base0A : M.Theme.base05
|
||||||
|
font.pixelSize: M.Theme.fontSize - 2
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
381
shell/modules/HoverPanel.qml
Normal file
381
shell/modules/HoverPanel.qml
Normal file
|
|
@ -0,0 +1,381 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
// Unified bar panel — fullscreen transparent window so content can resize
|
||||||
|
// freely without triggering Wayland surface resizes.
|
||||||
|
//
|
||||||
|
// Hover mode (popupMode: false, default):
|
||||||
|
// Parent drives visibility via showPanel. Panel auto-closes when showPanel
|
||||||
|
// drops, with a 50ms debounce. Reports panelHovered back to parent.
|
||||||
|
// Pass anchorItem for lazy position computation on each show.
|
||||||
|
//
|
||||||
|
// Popup mode (popupMode: true):
|
||||||
|
// Shows immediately on creation. Stays open until click-outside or
|
||||||
|
// dismiss() call. Emits dismissed() when closed — caller's LazyLoader
|
||||||
|
// sets active: false. Pass anchorX (screen-relative centre x).
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool popupMode: false
|
||||||
|
|
||||||
|
// Hover mode
|
||||||
|
property bool showPanel: true
|
||||||
|
property Item anchorItem: null
|
||||||
|
property bool panelHovered: false
|
||||||
|
|
||||||
|
// Popup mode
|
||||||
|
property real anchorX: -1
|
||||||
|
signal dismissed
|
||||||
|
|
||||||
|
// Shared
|
||||||
|
required property color accentColor
|
||||||
|
property string panelTitle: ""
|
||||||
|
property Component titleActionsComponent: null
|
||||||
|
property string panelNamespace: "nova-panel"
|
||||||
|
property real contentWidth: 220
|
||||||
|
|
||||||
|
default property alias content: panelContent.children
|
||||||
|
|
||||||
|
visible: _winVisible
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
property bool _winVisible: false
|
||||||
|
property bool _pinned: false
|
||||||
|
property real _dragStartX: 0
|
||||||
|
property real _dragStartY: 0
|
||||||
|
property bool _dragging: false
|
||||||
|
|
||||||
|
// When pinned: mask = full panel so content is interactive, desktop accessible outside.
|
||||||
|
// When dragging: mask = null (full screen) so Niri keeps delivering events when cursor
|
||||||
|
// leaves the panel bounds mid-drag.
|
||||||
|
mask: (_pinned && !_dragging) ? _pinMask : null
|
||||||
|
|
||||||
|
property Region _pinMask: Region {
|
||||||
|
x: panelContainer.x
|
||||||
|
y: panelContainer.y
|
||||||
|
width: panelContainer.width
|
||||||
|
height: panelContainer.height
|
||||||
|
}
|
||||||
|
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: 0
|
||||||
|
WlrLayershell.namespace: root.panelNamespace
|
||||||
|
|
||||||
|
property real topMargin: 0
|
||||||
|
|
||||||
|
anchors.top: true
|
||||||
|
anchors.left: true
|
||||||
|
anchors.right: true
|
||||||
|
anchors.bottom: true
|
||||||
|
|
||||||
|
margins.top: topMargin
|
||||||
|
|
||||||
|
function _updatePosition() {
|
||||||
|
const scr = screen;
|
||||||
|
const sw = scr?.width ?? 1920;
|
||||||
|
let cx;
|
||||||
|
if (root.anchorItem) {
|
||||||
|
const pt = root.anchorItem.mapToGlobal(root.anchorItem.width / 2, 0);
|
||||||
|
cx = pt.x - (scr?.x ?? 0);
|
||||||
|
} else {
|
||||||
|
cx = root.anchorX;
|
||||||
|
}
|
||||||
|
panelContainer.x = Math.max(0, Math.min(Math.round(cx - root.contentWidth / 2), sw - root.contentWidth));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grace period: after _show(), suppress auto-close briefly so Niri has time
|
||||||
|
// to route wl_pointer.enter to the new overlay surface (cursor may be stationary).
|
||||||
|
// Popup mode gets a longer window (1500ms) so the user can move the cursor to the
|
||||||
|
// panel content after clicking the bar without accidentally dismissing it.
|
||||||
|
property bool _grace: false
|
||||||
|
Timer {
|
||||||
|
id: _graceTimer
|
||||||
|
interval: root.popupMode ? 1500 : 400
|
||||||
|
onTriggered: {
|
||||||
|
root._grace = false;
|
||||||
|
if (!root.showPanel && !root._pinned)
|
||||||
|
root.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content-change grace: call keepOpen(ms) when panel content is about to
|
||||||
|
// resize/rebuild (session switch, device list change, etc.) to prevent the
|
||||||
|
// hover-drop-on-resize from closing the panel.
|
||||||
|
property bool _contentBusy: false
|
||||||
|
Timer {
|
||||||
|
id: _contentBusyTimer
|
||||||
|
onTriggered: {
|
||||||
|
root._contentBusy = false;
|
||||||
|
if (!root.showPanel && !root._grace && !root._pinned)
|
||||||
|
_hideTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function keepOpen(ms) {
|
||||||
|
_contentBusy = true;
|
||||||
|
_contentBusyTimer.interval = ms ?? 400;
|
||||||
|
_contentBusyTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _show() {
|
||||||
|
_updatePosition();
|
||||||
|
// Only snap to closed position when genuinely opening from scratch.
|
||||||
|
// If we are interrupting a hide animation, animate back from the current
|
||||||
|
// y/opacity so there's no visible jump — the showAnim NumberAnimations
|
||||||
|
// always run from the *current* value to their target.
|
||||||
|
if (!hideAnim.running) {
|
||||||
|
// Explicitly set y before animating — avoids the y:-height binding (live, depends on
|
||||||
|
// _panelColumn.height) surviving a 0→0 no-op animation when layout isn't done yet.
|
||||||
|
panelContainer.y = -(panelContainer.height > 0 ? panelContainer.height : 400);
|
||||||
|
panelContainer.opacity = 0;
|
||||||
|
}
|
||||||
|
_winVisible = true;
|
||||||
|
hideAnim.stop();
|
||||||
|
showAnim.start();
|
||||||
|
_grace = true;
|
||||||
|
_graceTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
_pinned = false;
|
||||||
|
showAnim.stop();
|
||||||
|
hideAnim.start();
|
||||||
|
_grace = false;
|
||||||
|
_graceTimer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: if (popupMode)
|
||||||
|
_show()
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: _hideTimer
|
||||||
|
interval: 150
|
||||||
|
onTriggered: if (!root.showPanel && !root._grace && !root._pinned && !root._contentBusy)
|
||||||
|
root.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
onShowPanelChanged: {
|
||||||
|
if (root.popupMode)
|
||||||
|
return;
|
||||||
|
if (showPanel) {
|
||||||
|
_hideTimer.stop();
|
||||||
|
// Only replay the open animation if the panel is actually closed or
|
||||||
|
// currently animating away. If it is already visible, stopping the
|
||||||
|
// hide timer is sufficient — calling _show() would reset y/opacity to
|
||||||
|
// 0 and cause a visible flash when the cursor crosses the gap between
|
||||||
|
// the bar module and the panel.
|
||||||
|
if (!_winVisible || hideAnim.running)
|
||||||
|
_show();
|
||||||
|
} else {
|
||||||
|
_hideTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: showAnim
|
||||||
|
NumberAnimation {
|
||||||
|
target: panelContainer
|
||||||
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
duration: 120
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: panelContainer
|
||||||
|
property: "y"
|
||||||
|
to: 0
|
||||||
|
duration: 150
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: hideAnim
|
||||||
|
NumberAnimation {
|
||||||
|
target: panelContainer
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
duration: 150
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: panelContainer
|
||||||
|
property: "y"
|
||||||
|
to: -panelContainer.height
|
||||||
|
duration: 150
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
onFinished: {
|
||||||
|
root._winVisible = false;
|
||||||
|
if (root.popupMode)
|
||||||
|
root.dismissed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popup mode: click-outside dismiss.
|
||||||
|
// TapHandler fires for all taps; position check skips taps inside panelContainer.
|
||||||
|
// Gated on !_grace so spurious events during the 400ms opening window don't dismiss.
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.popupMode
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
enabled: !root._grace
|
||||||
|
onTapped: {
|
||||||
|
const p = point.position;
|
||||||
|
const pad = 8;
|
||||||
|
if (p.x < panelContainer.x - pad || p.x > panelContainer.x + panelContainer.width + pad || p.y < panelContainer.y - pad || p.y > panelContainer.y + panelContainer.height + pad)
|
||||||
|
root.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
M.PopupBackground {
|
||||||
|
x: panelContainer.x
|
||||||
|
y: panelContainer.y
|
||||||
|
width: panelContainer.width
|
||||||
|
height: panelContainer.height
|
||||||
|
opacity: panelContainer.opacity * Math.max(M.Theme.barOpacity, 0.85)
|
||||||
|
accentColor: root.accentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: panelContainer
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: root.contentWidth
|
||||||
|
height: _panelColumn.height
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
enabled: !root.popupMode && !root._pinned
|
||||||
|
onHoveredChanged: if (!root.popupMode && !root._pinned)
|
||||||
|
root.panelHovered = hovered
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: _panelColumn
|
||||||
|
width: root.contentWidth
|
||||||
|
|
||||||
|
// Header row: title + action buttons + pin — shown in hover mode always,
|
||||||
|
// and in popup mode when a title or actions are provided.
|
||||||
|
Item {
|
||||||
|
id: _headerItem
|
||||||
|
visible: !root.popupMode || root.panelTitle !== "" || root.titleActionsComponent !== null
|
||||||
|
width: parent.width
|
||||||
|
height: 24
|
||||||
|
|
||||||
|
// Drag header to freely reposition panel while pinned (hover mode only).
|
||||||
|
// _dragging clears the input mask so Niri keeps delivering events when the
|
||||||
|
// cursor leaves the panel bounds during a fast drag.
|
||||||
|
DragHandler {
|
||||||
|
enabled: root._pinned && !root.popupMode
|
||||||
|
onActiveChanged: {
|
||||||
|
root._dragging = active;
|
||||||
|
if (active) {
|
||||||
|
root._dragStartX = panelContainer.x;
|
||||||
|
root._dragStartY = panelContainer.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onActiveTranslationChanged: {
|
||||||
|
if (active) {
|
||||||
|
const sw = root.screen?.width ?? 1920;
|
||||||
|
const sh = root.screen?.height ?? 1080;
|
||||||
|
panelContainer.x = Math.max(0, Math.min(root._dragStartX + activeTranslation.x, sw - root.contentWidth));
|
||||||
|
panelContainer.y = Math.max(0, Math.min(root._dragStartY + activeTranslation.y, sh - panelContainer.height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show move cursor on header when pinned
|
||||||
|
HoverHandler {
|
||||||
|
enabled: root._pinned && !root.popupMode
|
||||||
|
cursorShape: Qt.SizeAllCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: root.panelTitle !== ""
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.panelTitle
|
||||||
|
color: root.accentColor
|
||||||
|
font.pixelSize: M.Theme.fontSize - 1
|
||||||
|
font.bold: true
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action buttons — anchored left of pin button slot
|
||||||
|
Loader {
|
||||||
|
id: _titleActionsLoader
|
||||||
|
anchors.right: _pinBtn.left
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
sourceComponent: root.titleActionsComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pin button — zero-width in popup mode so actions anchor flush to right
|
||||||
|
Item {
|
||||||
|
id: _pinBtn
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: root.popupMode ? 0 : 4
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: root.popupMode ? 0 : 20
|
||||||
|
height: 20
|
||||||
|
visible: !root.popupMode
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onTapped: {
|
||||||
|
root._pinned = !root._pinned;
|
||||||
|
if (!root._pinned && !root.showPanel)
|
||||||
|
root.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root._pinned ? "\uDB81\uDC03" : "\uDB82\uDD31"
|
||||||
|
color: root._pinned ? root.accentColor : M.Theme.base04
|
||||||
|
font.pixelSize: M.Theme.fontSize - 1
|
||||||
|
font.family: M.Theme.iconFontFamily
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Divider at bottom of header
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: 1
|
||||||
|
color: M.Theme.base03
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: panelContent
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border overlay — on top of content so full-bleed items don't cover it
|
||||||
|
Rectangle {
|
||||||
|
x: panelContainer.x
|
||||||
|
y: panelContainer.y
|
||||||
|
width: panelContainer.width
|
||||||
|
height: panelContainer.height
|
||||||
|
color: "transparent"
|
||||||
|
border.color: root.accentColor
|
||||||
|
border.width: 1
|
||||||
|
radius: M.Theme.radius
|
||||||
|
opacity: panelContainer.opacity
|
||||||
|
}
|
||||||
|
}
|
||||||
50
shell/modules/IdleInhibitor.qml
Normal file
50
shell/modules/IdleInhibitor.qml
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
M.BarIcon {
|
||||||
|
id: root
|
||||||
|
color: root.active ? M.Theme.base09 : root.accentColor
|
||||||
|
tooltip: {
|
||||||
|
const parts = ["Idle inhibition: " + (root.active ? "active" : "inactive")];
|
||||||
|
if (root._inhibitors)
|
||||||
|
parts.push(root._inhibitors);
|
||||||
|
return parts.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool active: false
|
||||||
|
property string _inhibitors: ""
|
||||||
|
|
||||||
|
icon: root.active ? "\uF06E" : "\uF070"
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: inhibitor
|
||||||
|
command: ["systemd-inhibit", "--what=idle", "--who=nova-shell", "--why=user", "sleep", "infinity"]
|
||||||
|
running: root.active
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll current inhibitors
|
||||||
|
Process {
|
||||||
|
id: listProc
|
||||||
|
running: true
|
||||||
|
command: ["sh", "-c", "systemd-inhibit --list 2>/dev/null | grep -i idle | awk '{print $NF}' | sort -u | tr '\\n' ', ' | sed 's/, $//'"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root._inhibitors = text.trim() ? "Blocked by: " + text.trim() : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 5000
|
||||||
|
running: root._hovered
|
||||||
|
repeat: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: listProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.active = !root.active
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue