rename bar module files and qmldir types to *Module
This commit is contained in:
parent
6461b9a943
commit
c3d7fa0bc5
24 changed files with 46 additions and 41 deletions
187
shell/modules/BacklightModule.qml
Normal file
187
shell/modules/BacklightModule.qml
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: S.Theme.moduleSpacing
|
||||
opacity: S.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 = S.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: S.Theme.fontSize + 2
|
||||
font.family: S.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: S.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: S.Theme.base05
|
||||
font.pixelSize: S.Theme.fontSize
|
||||
font.family: S.Theme.fontFamily
|
||||
width: 30
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -109,11 +109,11 @@ PanelWindow {
|
|||
spacing: S.Theme.groupSpacing
|
||||
|
||||
M.BarGroup {
|
||||
M.Privacy {}
|
||||
M.Clock {
|
||||
M.PrivacyModule {}
|
||||
M.ClockModule {
|
||||
visible: S.Modules.clock.enable
|
||||
}
|
||||
M.Notifications {
|
||||
M.NotificationsModule {
|
||||
bar: bar
|
||||
visible: S.Modules.notifications.enable
|
||||
}
|
||||
|
|
@ -130,14 +130,14 @@ PanelWindow {
|
|||
M.BarGroup {
|
||||
id: workspacesGroup
|
||||
leftEdge: true
|
||||
M.Workspaces {
|
||||
M.WorkspacesModule {
|
||||
bar: bar
|
||||
visible: S.Modules.workspaces.enable
|
||||
}
|
||||
}
|
||||
M.BarGroup {
|
||||
leftEdge: !workspacesGroup.visible
|
||||
M.Tray {
|
||||
M.TrayModule {
|
||||
bar: bar
|
||||
}
|
||||
}
|
||||
|
|
@ -146,7 +146,7 @@ PanelWindow {
|
|||
Layout.minimumWidth: 0
|
||||
clip: true
|
||||
visible: S.Modules.windowTitle.enable && M.NiriIpc.focusedTitle !== ""
|
||||
M.WindowTitle {
|
||||
M.WindowTitleModule {
|
||||
id: _windowTitle
|
||||
readonly property real _maxWidth: Math.max(0, centerSection.x - _windowTitleGroup.x - 2 * S.Theme.groupPadding - S.Theme.groupSpacing)
|
||||
width: Math.min(naturalWidth, _maxWidth)
|
||||
|
|
@ -170,52 +170,52 @@ PanelWindow {
|
|||
|
||||
// Media
|
||||
M.BarGroup {
|
||||
M.Mpris {
|
||||
M.MprisModule {
|
||||
bar: bar
|
||||
}
|
||||
M.Volume {
|
||||
M.VolumeModule {
|
||||
visible: S.Modules.volume.enable
|
||||
}
|
||||
}
|
||||
|
||||
// Connectivity
|
||||
M.BarGroup {
|
||||
M.Network {
|
||||
M.NetworkModule {
|
||||
bar: bar
|
||||
visible: S.Modules.network.enable
|
||||
}
|
||||
M.Bluetooth {
|
||||
M.BluetoothModule {
|
||||
bar: bar
|
||||
}
|
||||
}
|
||||
|
||||
// Controls
|
||||
M.BarGroup {
|
||||
M.Backlight {}
|
||||
M.PowerProfile {
|
||||
M.BacklightModule {}
|
||||
M.PowerProfileModule {
|
||||
visible: S.Modules.powerProfile.enable
|
||||
}
|
||||
M.IdleInhibitor {
|
||||
M.IdleInhibitorModule {
|
||||
visible: S.Modules.idleInhibitor.enable
|
||||
}
|
||||
}
|
||||
|
||||
// Stats
|
||||
M.BarGroup {
|
||||
M.Cpu {
|
||||
M.CpuModule {
|
||||
visible: S.Modules.cpu.enable
|
||||
}
|
||||
M.Memory {
|
||||
M.MemoryModule {
|
||||
visible: S.Modules.memory.enable
|
||||
}
|
||||
M.Gpu {}
|
||||
M.Temperature {
|
||||
M.GpuModule {}
|
||||
M.TemperatureModule {
|
||||
visible: S.Modules.temperature.enable
|
||||
}
|
||||
M.Weather {
|
||||
M.WeatherModule {
|
||||
visible: S.Modules.weather.enable
|
||||
}
|
||||
M.Disk {
|
||||
M.DiskModule {
|
||||
visible: S.Modules.disk.enable
|
||||
}
|
||||
}
|
||||
|
|
@ -223,8 +223,8 @@ PanelWindow {
|
|||
// Power
|
||||
M.BarGroup {
|
||||
rightEdge: true
|
||||
M.Battery {}
|
||||
M.Power {
|
||||
M.BatteryModule {}
|
||||
M.PowerModule {
|
||||
bar: bar
|
||||
visible: S.Modules.power.enable
|
||||
}
|
||||
|
|
|
|||
420
shell/modules/BatteryModule.qml
Normal file
420
shell/modules/BatteryModule.qml
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.UPower
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: S.Theme.moduleSpacing
|
||||
opacity: S.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: S.Modules.battery.critical || 15
|
||||
readonly property int _warnThresh: S.Modules.battery.warning || 25
|
||||
readonly property bool _critical: pct < _critThresh && !charging
|
||||
property color _stateColor: charging ? S.Theme.base0B : _critical ? S.Theme.base09 : pct < _warnThresh ? S.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: S.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: S.Theme.fontSize
|
||||
font.family: S.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: S.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: S.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: S.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 = S.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 = S.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: S.Theme.base03
|
||||
font.pixelSize: S.Theme.fontSize - 3
|
||||
font.family: S.Theme.fontFamily
|
||||
font.letterSpacing: 0.5
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "24h"
|
||||
color: S.Theme.base03
|
||||
font.pixelSize: S.Theme.fontSize - 3
|
||||
font.family: S.Theme.fontFamily
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
width: parent.width - 16
|
||||
height: 1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: S.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: S.Theme.base04
|
||||
font.pixelSize: S.Theme.fontSize - 2
|
||||
font.family: S.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: S.Theme.fontSize - 2
|
||||
font.family: S.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: S.Theme.base04
|
||||
font.pixelSize: S.Theme.fontSize - 2
|
||||
font.family: S.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 ? S.Theme.base08 : h < 75 ? S.Theme.base0A : S.Theme.base0B;
|
||||
}
|
||||
font.pixelSize: S.Theme.fontSize - 2
|
||||
font.family: S.Theme.fontFamily
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
104
shell/modules/BluetoothModule.qml
Normal file
104
shell/modules/BluetoothModule.qml
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: S.Theme.moduleSpacing
|
||||
opacity: S.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: S.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: S.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: S.Modules.bluetooth.enable
|
||||
repeat: true
|
||||
onTriggered: proc.running = true
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
icon: "\uF294"
|
||||
color: root.state === "off" ? S.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
|
||||
}
|
||||
}
|
||||
}
|
||||
15
shell/modules/ClockModule.qml
Normal file
15
shell/modules/ClockModule.qml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
M.BarLabel {
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Seconds
|
||||
}
|
||||
|
||||
font.pixelSize: S.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")
|
||||
}
|
||||
89
shell/modules/CpuModule.qml
Normal file
89
shell/modules/CpuModule.qml
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
import "../applets" as C
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: Math.max(1, S.Theme.moduleSpacing - 2)
|
||||
tooltip: ""
|
||||
|
||||
readonly property var _cores: S.SystemStats.cpuCores
|
||||
readonly property var _coreMaxFreq: S.SystemStats.cpuCoreMaxFreq
|
||||
readonly property var _coreTypes: S.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;
|
||||
S.SystemStats.coreConsumers++;
|
||||
} else if (!_showPanel && _coreConsumerActive) {
|
||||
_coreConsumerActive = false;
|
||||
S.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
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
icon: "\uF2DB"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
M.BarLabel {
|
||||
label: S.SystemStats.cpuUsage.toString().padStart(2) + "%@" + S.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
|
||||
|
||||
C.CpuApplet {
|
||||
width: hoverPanel.contentWidth
|
||||
cores: root._cores
|
||||
coreMaxFreq: root._coreMaxFreq
|
||||
coreTypes: root._coreTypes
|
||||
processes: root._procs.processes
|
||||
accentColor: root.accentColor
|
||||
active: root._showPanel
|
||||
}
|
||||
}
|
||||
}
|
||||
66
shell/modules/DiskModule.qml
Normal file
66
shell/modules/DiskModule.qml
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
import "../applets" as C
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: Math.max(1, S.Theme.moduleSpacing - 2)
|
||||
tooltip: ""
|
||||
|
||||
property var _mounts: S.SystemStats.diskMounts
|
||||
property int _rootPct: S.SystemStats.diskRootPct
|
||||
|
||||
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
|
||||
|
||||
C.DiskApplet {
|
||||
width: hoverPanel.contentWidth
|
||||
mounts: root._mounts
|
||||
accentColor: root.accentColor
|
||||
}
|
||||
}
|
||||
}
|
||||
277
shell/modules/GpuModule.qml
Normal file
277
shell/modules/GpuModule.qml
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: Math.max(1, S.Theme.moduleSpacing - 2)
|
||||
tooltip: ""
|
||||
visible: S.Modules.gpu.enable && S.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 ? S.Theme.base0B : S.Theme.base0A;
|
||||
const b = t < 0.5 ? S.Theme.base0A : S.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(S.SystemStats.gpuUsage)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
M.BarLabel {
|
||||
label: S.SystemStats.gpuUsage + "%"
|
||||
minText: "100%"
|
||||
color: root._loadColor(S.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: S.SystemStats.gpuVendor.toUpperCase()
|
||||
color: S.Theme.base03
|
||||
font.pixelSize: S.Theme.fontSize - 3
|
||||
font.family: S.Theme.fontFamily
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: S.SystemStats.gpuUsage + "%"
|
||||
color: root._loadColor(S.SystemStats.gpuUsage)
|
||||
font.pixelSize: S.Theme.fontSize
|
||||
font.family: S.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: S.Theme.base02
|
||||
radius: 3
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * Math.min(1, S.SystemStats.gpuUsage / 100)
|
||||
height: parent.height
|
||||
color: root._loadColor(S.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: S.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: S.Theme.base03
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 22
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "VRAM"
|
||||
color: S.Theme.base03
|
||||
font.pixelSize: S.Theme.fontSize - 3
|
||||
font.family: S.Theme.fontFamily
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root._fmt(S.SystemStats.gpuVramUsedGb) + " / " + root._fmt(S.SystemStats.gpuVramTotalGb)
|
||||
color: root.accentColor
|
||||
font.pixelSize: S.Theme.fontSize - 1
|
||||
font.family: S.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: S.Theme.base02
|
||||
radius: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: S.SystemStats.gpuVramTotalGb > 0 ? parent.width * Math.min(1, S.SystemStats.gpuVramUsedGb / S.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: S.SystemStats.gpuTempC > 0
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Temp"
|
||||
color: S.Theme.base04
|
||||
font.pixelSize: S.Theme.fontSize - 2
|
||||
font.family: S.Theme.fontFamily
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: S.SystemStats.gpuTempC + "\u00B0C"
|
||||
color: S.SystemStats.gpuTempC > 85 ? S.Theme.base08 : S.SystemStats.gpuTempC > 70 ? S.Theme.base0A : S.Theme.base05
|
||||
font.pixelSize: S.Theme.fontSize - 2
|
||||
font.family: S.Theme.fontFamily
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
51
shell/modules/IdleInhibitorModule.qml
Normal file
51
shell/modules/IdleInhibitorModule.qml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
M.BarIcon {
|
||||
id: root
|
||||
color: root.active ? S.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
|
||||
}
|
||||
}
|
||||
83
shell/modules/MemoryModule.qml
Normal file
83
shell/modules/MemoryModule.qml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
import "../applets" as C
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: Math.max(1, S.Theme.moduleSpacing - 2)
|
||||
tooltip: ""
|
||||
|
||||
property int percent: S.SystemStats.memPercent
|
||||
property real usedGb: S.SystemStats.memUsedGb
|
||||
property real totalGb: S.SystemStats.memTotalGb
|
||||
property real availGb: S.SystemStats.memAvailGb
|
||||
property real cachedGb: S.SystemStats.memCachedGb
|
||||
property real buffersGb: S.SystemStats.memBuffersGb
|
||||
|
||||
property bool _pinned: false
|
||||
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
|
||||
readonly property bool _showPanel: _anyHover || _pinned
|
||||
|
||||
property M.ProcessList _procs: M.ProcessList {
|
||||
sortBy: "mem"
|
||||
active: root._showPanel
|
||||
onProcessesChanged: hoverPanel.keepOpen(300)
|
||||
}
|
||||
|
||||
on_AnyHoverChanged: {
|
||||
if (_anyHover)
|
||||
_unpinTimer.stop();
|
||||
else if (_pinned)
|
||||
_unpinTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _unpinTimer
|
||||
interval: 500
|
||||
onTriggered: root._pinned = false
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
icon: "\uEFC5"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
M.BarLabel {
|
||||
label: root.percent + "%"
|
||||
minText: "100%"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
|
||||
M.HoverPanel {
|
||||
id: hoverPanel
|
||||
showPanel: root._showPanel
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
anchorItem: root
|
||||
accentColor: root.accentColor
|
||||
panelNamespace: "nova-memory"
|
||||
panelTitle: "Memory"
|
||||
contentWidth: 240
|
||||
|
||||
C.MemoryApplet {
|
||||
width: hoverPanel.contentWidth
|
||||
percent: root.percent
|
||||
usedGb: root.usedGb
|
||||
totalGb: root.totalGb
|
||||
availGb: root.availGb
|
||||
cachedGb: root.cachedGb
|
||||
buffersGb: root.buffersGb
|
||||
processes: root._procs.processes
|
||||
accentColor: root.accentColor
|
||||
active: root._showPanel
|
||||
}
|
||||
}
|
||||
}
|
||||
142
shell/modules/MprisModule.qml
Normal file
142
shell/modules/MprisModule.qml
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
import "../applets" as C
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: S.Theme.moduleSpacing
|
||||
opacity: S.Modules.mpris.enable && player !== null ? 1 : 0
|
||||
visible: opacity > 0
|
||||
tooltip: ""
|
||||
|
||||
property int _playerIdx: 0
|
||||
readonly property var _players: (Mpris.players.values ?? []).filter(p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing || p.playbackState === MprisPlaybackState.Paused)
|
||||
readonly property MprisPlayer player: _players[_playerIdx] ?? _players[0] ?? null
|
||||
readonly property bool playing: player?.playbackState === MprisPlaybackState.Playing
|
||||
|
||||
// Reset index if current player disappears
|
||||
on_PlayersChanged: if (_playerIdx >= _players.length)
|
||||
_playerIdx = 0
|
||||
property string _cachedArt: ""
|
||||
property string _artTrack: ""
|
||||
|
||||
// Cache art URL at root level so it's captured even when panel is hidden
|
||||
readonly property string _artUrl: player?.trackArtUrl ?? ""
|
||||
readonly property string _currentTrack: player?.trackTitle ?? ""
|
||||
on_ArtUrlChanged: if (_artUrl)
|
||||
_cachedArt = _artUrl
|
||||
on_CurrentTrackChanged: if (_currentTrack !== _artTrack) {
|
||||
_artTrack = _currentTrack;
|
||||
_cachedArt = _artUrl || "";
|
||||
}
|
||||
|
||||
// Preload art while panel is hidden
|
||||
Image {
|
||||
visible: false
|
||||
source: root._cachedArt
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
// Cava visualizer - 16 bars, raw output mode
|
||||
property var _cavaBars: Array(16).fill(0)
|
||||
property bool _cavaActive: false
|
||||
|
||||
on_ShowPanelChanged: {
|
||||
if (_showPanel) {
|
||||
_cavaKillTimer.stop();
|
||||
_cavaActive = true;
|
||||
} else {
|
||||
_cavaKillTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _cavaKillTimer
|
||||
interval: 30000
|
||||
onTriggered: root._cavaActive = false
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cavaProc
|
||||
running: root.playing && root._cavaActive
|
||||
command: ["sh", "-c", "cfg=$(mktemp /tmp/nova-cava-XXXXXX.conf);" + "cat > \"$cfg\" << 'CAVAEOF'\n" + "[general]\nbars=16\nframerate=30\n[output]\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nascii_max_range=100\n" + "CAVAEOF\n" + "trap 'rm -f \"$cfg\"' EXIT;" + "exec cava -p \"$cfg\""]
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => {
|
||||
const vals = line.split(";").filter(s => s).map(Number);
|
||||
if (vals.length >= 16)
|
||||
root._cavaBars = vals.map(v => v / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required property var bar
|
||||
|
||||
property bool _pinned: false
|
||||
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
|
||||
readonly property bool _showPanel: _anyHover || _pinned
|
||||
|
||||
on_AnyHoverChanged: {
|
||||
if (_anyHover)
|
||||
_unpinTimer.stop();
|
||||
else if (_pinned)
|
||||
_unpinTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _unpinTimer
|
||||
interval: 500
|
||||
onTriggered: root._pinned = false
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
icon: root.playing ? "\uF04B" : (root.player?.playbackState === MprisPlaybackState.Paused ? "\uDB80\uDFE4" : "\uDB81\uDCDB")
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
M.BarLabel {
|
||||
label: root.player?.trackTitle || root.player?.identity || ""
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, 200)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
|
||||
M.HoverPanel {
|
||||
id: hoverPanel
|
||||
showPanel: root._showPanel
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
anchorItem: root
|
||||
accentColor: root.accentColor
|
||||
panelNamespace: "nova-mpris"
|
||||
panelTitle: "Now Playing"
|
||||
contentWidth: 280
|
||||
|
||||
C.MprisApplet {
|
||||
width: hoverPanel.contentWidth
|
||||
player: root.player
|
||||
players: root._players
|
||||
playing: root.playing
|
||||
accentColor: root.accentColor
|
||||
cachedArt: root._cachedArt
|
||||
cavaBars: root._cavaBars
|
||||
playerIdx: root._playerIdx
|
||||
onPlayerSwitched: idx => {
|
||||
root._playerIdx = idx;
|
||||
hoverPanel.keepOpen(400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
shell/modules/NetworkModule.qml
Normal file
111
shell/modules/NetworkModule.qml
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: S.Theme.moduleSpacing
|
||||
tooltip: ""
|
||||
|
||||
property string ifname: ""
|
||||
property string essid: ""
|
||||
property string state: "disconnected"
|
||||
property string ipAddr: ""
|
||||
property string signal: ""
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
running: S.Modules.network.enable
|
||||
command: ["sh", "-c", "line=$(nmcli -t -f NAME,TYPE,DEVICE connection show --active 2>/dev/null | head -1); if [ -z \"$line\" ]; then dev=$(nmcli -t -f DEVICE,STATE device 2>/dev/null | grep ':connected' | grep -v ':unmanaged\\|:unavailable\\|:disconnected\\|:connecting' | head -1 | cut -d: -f1); [ -n \"$dev\" ] && line=\"linked:linked:$dev\"; fi; [ -z \"$line\" ] && exit 0; echo \"$line\"; dev=$(echo \"$line\" | cut -d: -f3); ip=$(nmcli -t -f IP4.ADDRESS device show \"$dev\" 2>/dev/null | head -1 | cut -d: -f2); echo \"ip:${ip:-}\"; sig=$(nmcli -t -f GENERAL.SIGNAL device show \"$dev\" 2>/dev/null | head -1 | cut -d: -f2); echo \"sig:${sig:-}\""]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const lines = text.trim().split("\n");
|
||||
if (!lines[0]) {
|
||||
root.state = "disconnected";
|
||||
root.essid = "";
|
||||
root.ifname = "";
|
||||
root.ipAddr = "";
|
||||
root.signal = "";
|
||||
return;
|
||||
}
|
||||
const parts = lines[0].split(":");
|
||||
root.essid = parts[0] || "";
|
||||
root.ifname = parts[2] || "";
|
||||
if ((parts[1] || "").includes("wireless"))
|
||||
root.state = "wifi";
|
||||
else if (parts[0] === "linked")
|
||||
root.state = "linked";
|
||||
else
|
||||
root.state = "eth";
|
||||
// Parse extra info lines
|
||||
root.ipAddr = "";
|
||||
root.signal = "";
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
if (lines[i].startsWith("ip:"))
|
||||
root.ipAddr = lines[i].slice(3);
|
||||
else if (lines[i].startsWith("sig:"))
|
||||
root.signal = lines[i].slice(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Event-driven: re-poll on any network change
|
||||
Process {
|
||||
id: monitor
|
||||
running: S.Modules.network.enable
|
||||
command: ["nmcli", "monitor"]
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: _debounce.restart()
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: _debounce
|
||||
interval: 300
|
||||
onTriggered: {
|
||||
proc.running = true;
|
||||
networkMenu.triggerRefresh();
|
||||
}
|
||||
}
|
||||
// Fallback poll
|
||||
Timer {
|
||||
interval: 60000
|
||||
running: S.Modules.network.enable
|
||||
repeat: true
|
||||
onTriggered: proc.running = true
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
icon: {
|
||||
if (root.state === "wifi")
|
||||
return "\uF1EB";
|
||||
if (root.state === "eth")
|
||||
return "\uDB80\uDE00";
|
||||
if (root.state === "linked")
|
||||
return "\uDB85\uDE16";
|
||||
return "\uDB82\uDCFD";
|
||||
}
|
||||
color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
M.BarLabel {
|
||||
visible: root.state === "wifi"
|
||||
label: root.essid
|
||||
color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
required property var bar
|
||||
|
||||
readonly property bool _anyHover: root._hovered || networkMenu.panelHovered
|
||||
|
||||
M.NetworkMenu {
|
||||
id: networkMenu
|
||||
showPanel: root._anyHover
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
anchorItem: root
|
||||
accentColor: root.accentColor
|
||||
}
|
||||
}
|
||||
95
shell/modules/NotificationsModule.qml
Normal file
95
shell/modules/NotificationsModule.qml
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: S.Theme.moduleSpacing
|
||||
tooltip: {
|
||||
const parts = [M.NotifService.count + " notification" + (M.NotifService.count !== 1 ? "s" : "")];
|
||||
if (M.NotifService.dnd)
|
||||
parts.push("Do not disturb");
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
required property var bar
|
||||
|
||||
readonly property bool hasUrgent: M.NotifService.list.some(n => n.urgency === NotificationUrgency.Critical && n.state !== "dismissed")
|
||||
|
||||
M.BarIcon {
|
||||
icon: {
|
||||
if (M.NotifService.dnd)
|
||||
return M.NotifService.count > 0 ? "\uDB80\uDCA0" : "\uDB82\uDE93";
|
||||
return M.NotifService.count > 0 ? "\uDB84\uDD6B" : "\uDB80\uDC9C";
|
||||
}
|
||||
color: M.NotifService.dnd ? S.Theme.base04 : root.accentColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
M.BarLabel {
|
||||
id: countLabel
|
||||
label: M.NotifService.count > 0 ? String(M.NotifService.count) + (root.hasUrgent ? "!" : "") : ""
|
||||
color: root.hasUrgent ? S.Theme.base08 : root.accentColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
transform: Scale {
|
||||
id: countScale
|
||||
origin.x: countLabel.width / 2
|
||||
origin.y: countLabel.height / 2
|
||||
xScale: 1
|
||||
yScale: 1
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: popAnim
|
||||
NumberAnimation {
|
||||
target: countScale
|
||||
properties: "xScale,yScale"
|
||||
to: 1.4
|
||||
duration: 100
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
target: countScale
|
||||
properties: "xScale,yScale"
|
||||
to: 1.0
|
||||
duration: 200
|
||||
easing.type: Easing.OutElastic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: M.NotifService
|
||||
function onCountChanged() {
|
||||
if (M.NotifService.count > 0)
|
||||
popAnim.start();
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: {
|
||||
centerLoader.active = !centerLoader.active;
|
||||
M.FlyoutState.visible = false;
|
||||
}
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: M.NotifService.toggleDnd()
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: centerLoader
|
||||
active: false
|
||||
M.NotifCenter {
|
||||
accentColor: root.accentColor
|
||||
screen: root.bar.screen
|
||||
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
|
||||
onDismissed: centerLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
41
shell/modules/PowerModule.qml
Normal file
41
shell/modules/PowerModule.qml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
M.BarIcon {
|
||||
id: root
|
||||
icon: "\uF011"
|
||||
tooltip: "Power menu"
|
||||
|
||||
required property var bar
|
||||
|
||||
Process {
|
||||
id: runner
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
menuLoader.active = !menuLoader.active;
|
||||
M.FlyoutState.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: menuLoader
|
||||
active: false
|
||||
M.PowerMenu {
|
||||
accentColor: root.accentColor
|
||||
screen: root.bar.screen
|
||||
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
|
||||
onDismissed: menuLoader.active = false
|
||||
onRunCommand: cmd => {
|
||||
runner.command = cmd;
|
||||
runner.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
shell/modules/PowerProfileModule.qml
Normal file
30
shell/modules/PowerProfileModule.qml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import QtQuick
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
M.BarIcon {
|
||||
id: root
|
||||
tooltip: "Power profile: " + (M.PowerProfileService.profile || "unknown")
|
||||
|
||||
color: M.PowerProfileService.profile === "performance" ? S.Theme.base09 : M.PowerProfileService.profile === "power-saver" ? S.Theme.base0B : root.accentColor
|
||||
|
||||
icon: {
|
||||
if (M.PowerProfileService.profile === "performance")
|
||||
return "\uF0E7";
|
||||
if (M.PowerProfileService.profile === "power-saver")
|
||||
return "\uF06C";
|
||||
if (M.PowerProfileService.profile === "balanced")
|
||||
return "\uF24E";
|
||||
return "\uF0E7";
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const cycle = ["performance", "balanced", "power-saver"];
|
||||
const idx = cycle.indexOf(M.PowerProfileService.profile);
|
||||
M.PowerProfileService.set(cycle[(idx + 1) % cycle.length]);
|
||||
}
|
||||
}
|
||||
}
|
||||
107
shell/modules/PrivacyModule.qml
Normal file
107
shell/modules/PrivacyModule.qml
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Services.Pipewire
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
Row {
|
||||
id: root
|
||||
spacing: S.Theme.moduleSpacing
|
||||
|
||||
// Only detect active client streams, not hardware sources/devices
|
||||
readonly property bool _videoCapture: {
|
||||
if (!Pipewire.nodes)
|
||||
return false;
|
||||
for (const node of Pipewire.nodes.values) {
|
||||
if (!node.isStream)
|
||||
continue;
|
||||
const mc = node.properties?.["media.class"] ?? "";
|
||||
if (mc === "Stream/Input/Video" || mc === "Stream/Output/Video")
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
readonly property bool _audioIn: {
|
||||
if (!Pipewire.nodes)
|
||||
return false;
|
||||
for (const node of Pipewire.nodes.values) {
|
||||
if (!node.isStream)
|
||||
continue;
|
||||
const mc = node.properties?.["media.class"] ?? "";
|
||||
if (mc === "Stream/Input/Audio")
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
visible: S.Modules.privacy.enable && (root._videoCapture || root._audioIn)
|
||||
|
||||
// Screenshare indicator
|
||||
Text {
|
||||
visible: root._videoCapture
|
||||
text: "\uF03D"
|
||||
color: S.Theme.base08
|
||||
font.pixelSize: S.Theme.fontSize + 2
|
||||
font.family: S.Theme.iconFontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowColor: S.Theme.base08
|
||||
shadowBlur: 0.8
|
||||
shadowVerticalOffset: 0
|
||||
shadowHorizontalOffset: 0
|
||||
}
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: root._videoCapture
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 0.4
|
||||
duration: 600
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1
|
||||
duration: 600
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Microphone indicator
|
||||
Text {
|
||||
visible: root._audioIn
|
||||
text: "\uF130"
|
||||
color: S.Theme.base0B
|
||||
font.pixelSize: S.Theme.fontSize + 2
|
||||
font.family: S.Theme.iconFontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowColor: S.Theme.base0B
|
||||
shadowBlur: 0.8
|
||||
shadowVerticalOffset: 0
|
||||
shadowHorizontalOffset: 0
|
||||
}
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: root._audioIn
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 0.4
|
||||
duration: 600
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1
|
||||
duration: 600
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
shell/modules/TemperatureModule.qml
Normal file
92
shell/modules/TemperatureModule.qml
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
import "../applets" as C
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: Math.max(1, S.Theme.moduleSpacing - 2)
|
||||
tooltip: ""
|
||||
|
||||
readonly property int _warm: S.Modules.temperature.warm || 80
|
||||
readonly property int _hot: S.Modules.temperature.hot || 90
|
||||
readonly property string _deviceFilter: S.Modules.temperature.device || ""
|
||||
|
||||
// If a device filter is set, use that device's temp; otherwise fall back to system max
|
||||
readonly property int _temp: {
|
||||
if (_deviceFilter !== "") {
|
||||
const dev = S.SystemStats.tempDevices.find(d => d.name === _deviceFilter);
|
||||
if (dev)
|
||||
return dev.celsius;
|
||||
}
|
||||
return S.SystemStats.tempCelsius;
|
||||
}
|
||||
|
||||
property color _stateColor: _temp > _hot ? S.Theme.base08 : _temp > _warm ? S.Theme.base0A : root.accentColor
|
||||
Behavior on _stateColor {
|
||||
ColorAnimation {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
|
||||
property bool _pinned: false
|
||||
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
|
||||
readonly property bool _showPanel: _anyHover || _pinned
|
||||
|
||||
on_AnyHoverChanged: {
|
||||
if (_anyHover)
|
||||
_unpinTimer.stop();
|
||||
else if (_pinned)
|
||||
_unpinTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _unpinTimer
|
||||
interval: 500
|
||||
onTriggered: root._pinned = false
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
icon: "\uF2C9"
|
||||
color: root._stateColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
M.BarLabel {
|
||||
label: root._temp + "\u00B0C"
|
||||
minText: "100\u00B0C"
|
||||
color: root._stateColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
TapHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onTapped: root._pinned = !root._pinned
|
||||
}
|
||||
}
|
||||
|
||||
M.HoverPanel {
|
||||
id: hoverPanel
|
||||
showPanel: root._showPanel
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
anchorItem: root
|
||||
accentColor: root.accentColor
|
||||
panelNamespace: "nova-temperature"
|
||||
panelTitle: "Temperature"
|
||||
contentWidth: 220
|
||||
|
||||
C.TemperatureApplet {
|
||||
width: hoverPanel.contentWidth
|
||||
temp: root._temp
|
||||
warm: root._warm
|
||||
hot: root._hot
|
||||
history: S.SystemStats.tempHistory
|
||||
devices: S.SystemStats.tempDevices
|
||||
accentColor: root.accentColor
|
||||
deviceFilter: root._deviceFilter
|
||||
active: root._showPanel
|
||||
}
|
||||
}
|
||||
}
|
||||
126
shell/modules/TrayModule.qml
Normal file
126
shell/modules/TrayModule.qml
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
spacing: S.Theme.moduleSpacing + 2
|
||||
visible: S.Modules.tray.enable && SystemTray.items.length > 0
|
||||
|
||||
required property var bar
|
||||
property var _activeMenu: null
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: Item {
|
||||
id: iconItem
|
||||
required property SystemTrayItem modelData
|
||||
|
||||
readonly property bool _needsAttention: modelData.status === 2
|
||||
property bool _hovered: false
|
||||
property real _pulseOpacity: 1
|
||||
|
||||
implicitWidth: S.Theme.fontSize + 4
|
||||
implicitHeight: S.Theme.fontSize + 4
|
||||
|
||||
SequentialAnimation {
|
||||
running: iconItem._needsAttention
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
target: iconItem
|
||||
property: "_pulseOpacity"
|
||||
to: 0.3
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
target: iconItem
|
||||
property: "_pulseOpacity"
|
||||
to: 1
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
onRunningChanged: if (!running)
|
||||
iconItem._pulseOpacity = 1
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
opacity: iconItem._pulseOpacity
|
||||
|
||||
layer.enabled: iconItem._needsAttention || iconItem._hovered
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowColor: iconItem._needsAttention ? S.Theme.base08 : S.Theme.base05
|
||||
shadowBlur: iconItem._needsAttention ? 0.8 : 0.5
|
||||
shadowVerticalOffset: 0
|
||||
shadowHorizontalOffset: 0
|
||||
}
|
||||
|
||||
S.ThemedIcon {
|
||||
anchors.fill: parent
|
||||
source: iconItem.modelData.icon
|
||||
tint: iconItem._needsAttention ? S.Theme.base08 : (root.parent?.accentColor ?? S.Theme.base05)
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: {
|
||||
iconItem._hovered = hovered;
|
||||
const tip = [iconItem.modelData.tooltipTitle, iconItem.modelData.tooltipDescription].filter(s => s).join("\n") || iconItem.modelData.title;
|
||||
if (hovered && tip) {
|
||||
M.FlyoutState.text = tip;
|
||||
M.FlyoutState.itemX = iconItem.mapToGlobal(iconItem.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
|
||||
M.FlyoutState.screen = QsWindow.window?.screen ?? null;
|
||||
M.FlyoutState.accentColor = root.parent?.accentColor ?? S.Theme.base05;
|
||||
M.FlyoutState.visible = true;
|
||||
} else if (!hovered) {
|
||||
M.FlyoutState.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
iconItem.modelData.activate();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (iconItem.modelData.menu) {
|
||||
if (root._activeMenu && root._activeMenu !== menuLoader)
|
||||
root._activeMenu.active = false;
|
||||
menuLoader.active = true;
|
||||
M.FlyoutState.visible = false;
|
||||
root._activeMenu = menuLoader;
|
||||
} else {
|
||||
iconItem.modelData.secondaryActivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Per-icon context menu window, created on demand
|
||||
LazyLoader {
|
||||
id: menuLoader
|
||||
active: false
|
||||
M.TrayMenu {
|
||||
accentColor: root.parent?.accentColor ?? S.Theme.base05
|
||||
handle: iconItem.modelData.menu
|
||||
screen: root.bar.screen
|
||||
anchorX: iconItem.mapToGlobal(iconItem.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
|
||||
onDismissed: {
|
||||
menuLoader.active = false;
|
||||
root._activeMenu = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
shell/modules/VolumeModule.qml
Normal file
126
shell/modules/VolumeModule.qml
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
import "../applets" as C
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: S.Theme.moduleSpacing
|
||||
tooltip: ""
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [Pipewire.defaultAudioSink, ...root._streamList]
|
||||
}
|
||||
|
||||
readonly property var sink: Pipewire.defaultAudioSink
|
||||
readonly property real volume: sink?.audio?.volume ?? 0
|
||||
readonly property bool muted: sink?.audio?.muted ?? false
|
||||
readonly property string _volumeIcon: muted ? "\uF026" : (volume > 0.5 ? "\uF028" : (volume > 0 ? "\uF027" : "\uF026"))
|
||||
readonly property color _volumeColor: muted ? S.Theme.base04 : root.accentColor
|
||||
|
||||
readonly property var _sinkList: {
|
||||
const sinks = [];
|
||||
if (Pipewire.nodes) {
|
||||
for (const node of Pipewire.nodes.values)
|
||||
if (!node.isStream && node.isSink)
|
||||
sinks.push(node);
|
||||
}
|
||||
return sinks;
|
||||
}
|
||||
|
||||
readonly property var _streamList: {
|
||||
const streams = [];
|
||||
if (Pipewire.nodes) {
|
||||
for (const node of Pipewire.nodes.values)
|
||||
if (node.isStream && node.audio)
|
||||
streams.push(node);
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
|
||||
property bool _osdActive: false
|
||||
property bool _volumeInit: false
|
||||
property bool _mutedInit: false
|
||||
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
|
||||
readonly property bool _showPanel: _anyHover || _osdActive
|
||||
|
||||
onVolumeChanged: {
|
||||
if (!_volumeInit) {
|
||||
_volumeInit = true;
|
||||
return;
|
||||
}
|
||||
_flashPanel();
|
||||
}
|
||||
onMutedChanged: {
|
||||
if (!_mutedInit) {
|
||||
_mutedInit = true;
|
||||
return;
|
||||
}
|
||||
_flashPanel();
|
||||
}
|
||||
|
||||
function _flashPanel() {
|
||||
_osdActive = true;
|
||||
_osdTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _osdTimer
|
||||
interval: 1500
|
||||
onTriggered: root._osdActive = false
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
icon: root._volumeIcon
|
||||
minIcon: "\uF028"
|
||||
color: root._volumeColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: if (root.sink?.audio)
|
||||
root.sink.audio.muted = !root.sink.audio.muted
|
||||
}
|
||||
}
|
||||
M.BarLabel {
|
||||
label: Math.round(root.volume * 100) + "%"
|
||||
minText: "100%"
|
||||
color: root._volumeColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: if (root.sink?.audio)
|
||||
root.sink.audio.muted = !root.sink.audio.muted
|
||||
}
|
||||
}
|
||||
|
||||
WheelHandler {
|
||||
onWheel: event => {
|
||||
if (!root.sink?.audio)
|
||||
return;
|
||||
root.sink.audio.volume = Math.max(0, root.sink.audio.volume + (event.angleDelta.y > 0 ? 0.05 : -0.05));
|
||||
}
|
||||
}
|
||||
|
||||
M.HoverPanel {
|
||||
id: hoverPanel
|
||||
showPanel: root._showPanel
|
||||
screen: QsWindow.window?.screen ?? null
|
||||
anchorItem: root
|
||||
accentColor: root.accentColor
|
||||
panelNamespace: "nova-volume"
|
||||
panelTitle: "Sound"
|
||||
contentWidth: 220
|
||||
|
||||
C.VolumeApplet {
|
||||
width: hoverPanel.contentWidth
|
||||
sink: root.sink
|
||||
sinkList: root._sinkList
|
||||
streamList: root._streamList
|
||||
accentColor: root.accentColor
|
||||
}
|
||||
}
|
||||
}
|
||||
41
shell/modules/WeatherModule.qml
Normal file
41
shell/modules/WeatherModule.qml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
M.BarSection {
|
||||
id: root
|
||||
spacing: S.Theme.moduleSpacing
|
||||
tooltip: root.weatherTooltip
|
||||
|
||||
property string weatherTooltip: ""
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
running: true
|
||||
command: ["wttrbar"].concat(S.Modules.weather.args)
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const data = JSON.parse(text);
|
||||
label.icon = data.text ?? "";
|
||||
root.weatherTooltip = data.tooltip ?? "";
|
||||
} catch (e) {
|
||||
label.icon = "";
|
||||
root.weatherTooltip = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: S.Modules.weather.interval || 3600000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: proc.running = true
|
||||
}
|
||||
|
||||
M.BarIcon {
|
||||
id: label
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue