rename bar module files and qmldir types to *Module

This commit is contained in:
Damocles 2026-04-17 22:12:16 +02:00
commit c3d7fa0bc5
23 changed files with 46 additions and 41 deletions

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

View file

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

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

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

View 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")
}

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

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

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

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

View 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);
}
}
}
}

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

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

View 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;
}
}
}
}

View 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]);
}
}
}

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

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

View 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;
}
}
}
}
}
}

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

View 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