Compare commits

...
17 changed files with 602 additions and 508 deletions

View file

@ -0,0 +1,79 @@
import QtQuick
import "../services" as S
Item {
id: root
required property int percent
required property color accentColor
signal setPercent(real pct)
implicitHeight: 36
Text {
id: _icon
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: _icon.right
anchors.leftMargin: 8
anchors.right: _label.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: _label
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

@ -6,3 +6,4 @@ CpuApplet 1.0 CpuApplet.qml
MemoryApplet 1.0 MemoryApplet.qml MemoryApplet 1.0 MemoryApplet.qml
TemperatureApplet 1.0 TemperatureApplet.qml TemperatureApplet 1.0 TemperatureApplet.qml
DiskApplet 1.0 DiskApplet.qml DiskApplet 1.0 DiskApplet.qml
BacklightApplet 1.0 BacklightApplet.qml

View file

@ -115,16 +115,6 @@ WlSessionLockSurface {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: 24 spacing: 24
LockNotifPills {
anchors.horizontalCenter: parent.horizontalCenter
}
// Spacer
Item {
width: 1
height: 24
}
// Password input // Password input
LockInput { LockInput {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter

View file

@ -1,5 +1,4 @@
import QtQuick import QtQuick
import Quickshell.Services.Mpris
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
@ -28,13 +27,19 @@ Item {
} }
implicitHeight: _widgetContent.implicitHeight implicitHeight: _widgetContent.implicitHeight
visible: _mprisCard.visible || _volumeCard.visible visible: _mprisCard.visible || _volumeCard.visible || _backlightCard.visible || _notifPills.visible
Column { Column {
id: _widgetContent id: _widgetContent
width: parent.width width: parent.width
spacing: 12 spacing: 12
// Notification pills
LockNotifPills {
id: _notifPills
anchors.horizontalCenter: parent.horizontalCenter
}
// Media widget // Media widget
Rectangle { Rectangle {
id: _mprisCard id: _mprisCard
@ -44,12 +49,7 @@ Item {
color: Qt.rgba(S.Theme.base01.r, S.Theme.base01.g, S.Theme.base01.b, 0.7) color: Qt.rgba(S.Theme.base01.r, S.Theme.base01.g, S.Theme.base01.b, 0.7)
border.color: Qt.rgba(S.Theme.base03.r, S.Theme.base03.g, S.Theme.base03.b, 0.3) border.color: Qt.rgba(S.Theme.base03.r, S.Theme.base03.g, S.Theme.base03.b, 0.3)
border.width: 1 border.width: 1
visible: (S.Modules.lock.mpris ?? true) && _mprisPlayer !== null visible: (S.Modules.lock.mpris ?? true) && S.MprisService.player !== null
readonly property var _mprisPlayers: (Mpris.players.values ?? []).filter(p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing || p.playbackState === MprisPlaybackState.Paused)
property int _playerIdx: 0
readonly property var _mprisPlayer: _mprisPlayers[_playerIdx] ?? _mprisPlayers[0] ?? null
readonly property bool _playing: _mprisPlayer?.playbackState === MprisPlaybackState.Playing
C.MprisApplet { C.MprisApplet {
id: _mprisContent id: _mprisContent
@ -57,15 +57,13 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 8 anchors.topMargin: 8
player: _mprisCard._mprisPlayer player: S.MprisService.player
players: _mprisCard._mprisPlayers players: S.MprisService.players
playing: _mprisCard._playing playing: S.MprisService.playing
playerIdx: _mprisCard._playerIdx playerIdx: S.MprisService.playerIdx
accentColor: S.Theme.base0D accentColor: S.Theme.base0D
cachedArt: _mprisCard._mprisPlayer?.trackArtUrl ?? "" cachedArt: S.MprisService.player?.trackArtUrl ?? ""
onPlayerSwitched: idx => { onPlayerSwitched: idx => S.MprisService.switchPlayer(idx)
_mprisCard._playerIdx = idx;
}
} }
} }
@ -96,5 +94,28 @@ Item {
accentColor: S.Theme.base0E accentColor: S.Theme.base0E
} }
} }
// Brightness widget
Rectangle {
id: _backlightCard
width: parent.width
height: _backlightContent.implicitHeight + 8
radius: S.Theme.radius + 2
color: Qt.rgba(S.Theme.base01.r, S.Theme.base01.g, S.Theme.base01.b, 0.7)
border.color: Qt.rgba(S.Theme.base03.r, S.Theme.base03.g, S.Theme.base03.b, 0.3)
border.width: 1
visible: S.BacklightService.available
C.BacklightApplet {
id: _backlightContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 4
percent: S.BacklightService.percent
accentColor: S.Theme.base0A
onSetPercent: pct => S.BacklightService.setPercent(pct)
}
}
} }
} }

View file

@ -1,17 +1,17 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import "." as M import "." as M
import "../services" as S import "../services" as S
import "../applets" as C
M.BarSection { M.BarSection {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
opacity: S.Modules.backlight.enable && percent > 0 ? 1 : 0 opacity: S.Modules.backlight.enable && S.BacklightService.available ? 1 : 0
visible: opacity > 0 visible: opacity > 0
tooltip: "" tooltip: ""
property int percent: 0 property int percent: S.BacklightService.percent
property bool _osdActive: false property bool _osdActive: false
property bool _percentInit: false property bool _percentInit: false
readonly property bool _showPanel: root._hovered || hoverPanel.panelHovered || _osdActive readonly property bool _showPanel: root._hovered || hoverPanel.panelHovered || _osdActive
@ -36,71 +36,8 @@ M.BarSection {
onTriggered: root._osdActive = false 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 { WheelHandler {
onWheel: event => root.adjust(event.angleDelta.y) onWheel: event => S.BacklightService.adjust(event.angleDelta.y)
} }
M.HoverPanel { M.HoverPanel {
@ -113,75 +50,21 @@ M.BarSection {
panelTitle: "Brightness" panelTitle: "Brightness"
contentWidth: 200 contentWidth: 200
Item { C.BacklightApplet {
width: parent.width width: parent.width
height: 36 percent: root.percent
accentColor: root.accentColor
Text { onSetPercent: pct => S.BacklightService.setPercent(pct)
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
}
} }
} }
M.BarIcon {
icon: "\uF185"
anchors.verticalCenter: parent.verticalCenter
}
M.BarLabel {
label: root.percent + "%"
minText: "100%"
anchors.verticalCenter: parent.verticalCenter
}
} }

View file

@ -1,6 +1,5 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import "." as M import "." as M
import "../services" as S import "../services" as S
@ -19,7 +18,7 @@ M.HoverPanel {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "\uF011" text: "\uF011"
color: menuWindow._btEnabled ? menuWindow.accentColor : S.Theme.base04 color: S.BluetoothService.enabled ? menuWindow.accentColor : S.Theme.base04
font.pixelSize: S.Theme.fontSize font.pixelSize: S.Theme.fontSize
font.family: S.Theme.iconFontFamily font.family: S.Theme.iconFontFamily
@ -35,78 +34,16 @@ M.HoverPanel {
} }
TapHandler { TapHandler {
onTapped: { onTapped: S.BluetoothService.setPower(!S.BluetoothService.enabled)
powerProc._action = menuWindow._btEnabled ? "off" : "on";
powerProc.running = true;
}
} }
} }
} }
onVisibleChanged: if (visible) onVisibleChanged: if (visible)
scanner.running = true S.BluetoothService.refresh()
property var _devices: []
property bool _btEnabled: true
property Process _scanner: Process {
id: scanner
running: false
command: ["sh", "-c", "bluetoothctl show 2>/dev/null | awk '/Powered:/{print $2; exit}';" + "echo '---DEVICES---';" + "bluetoothctl devices Paired 2>/dev/null | while read -r _ mac name; do " + "info=$(bluetoothctl info \"$mac\" 2>/dev/null); " + "conn=$(echo \"$info\" | grep -c 'Connected: yes'); " + "bat=$(echo \"$info\" | awk -F'[(): ]' '/Battery Percentage/{for(i=1;i<=NF;i++) if($i+0==$i && $i!=\"\") print $i}'); " + "echo \"$mac:$conn:${bat:-}:$name\"; " + "done"]
stdout: StdioCollector {
onStreamFinished: {
const sections = text.split("---DEVICES---");
menuWindow._btEnabled = (sections[0] || "").trim() === "yes";
const devs = [];
for (const line of (sections[1] || "").trim().split("\n")) {
if (!line)
continue;
const i1 = line.indexOf(":");
const i2 = line.indexOf(":", i1 + 1);
const i3 = line.indexOf(":", i2 + 1);
if (i3 < 0)
continue;
devs.push({
"mac": line.slice(0, i1),
"connected": line.slice(i1 + 1, i2) === "1",
"battery": parseInt(line.slice(i2 + 1, i3)) || -1,
"name": line.slice(i3 + 1)
});
}
devs.sort((a, b) => {
if (a.connected !== b.connected)
return a.connected ? -1 : 1;
return a.name.localeCompare(b.name);
});
menuWindow._devices = devs;
}
}
}
property Process _powerProc: Process {
id: powerProc
property string _action: ""
command: ["bluetoothctl", "power", _action]
onRunningChanged: if (!running) {
scanner.running = true;
menuWindow.keepOpen(500);
}
}
property Process _toggleProc: Process {
id: toggleProc
property string action: ""
property string mac: ""
command: ["bluetoothctl", action, mac]
onRunningChanged: if (!running) {
scanner.running = true;
menuWindow.keepOpen(500);
}
}
Repeater { Repeater {
model: menuWindow._devices model: S.BluetoothService.devices
delegate: Item { delegate: Item {
id: entry id: entry
@ -167,21 +104,20 @@ M.HoverPanel {
} }
TapHandler { TapHandler {
onTapped: { onTapped: {
toggleProc.action = entry.modelData.connected ? "disconnect" : "connect"; S.BluetoothService.toggleDevice(entry.modelData.mac, !entry.modelData.connected);
toggleProc.mac = entry.modelData.mac; menuWindow.keepOpen(500);
toggleProc.running = true;
} }
} }
} }
} }
Text { Text {
visible: menuWindow._devices.length === 0 visible: S.BluetoothService.devices.length === 0
width: menuWindow.contentWidth width: menuWindow.contentWidth
height: 32 height: 32
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
text: menuWindow._btEnabled ? "No paired devices" : "Bluetooth is off" text: S.BluetoothService.enabled ? "No paired devices" : "Bluetooth is off"
color: S.Theme.base04 color: S.Theme.base04
font.pixelSize: S.Theme.fontSize font.pixelSize: S.Theme.fontSize
font.family: S.Theme.fontFamily font.family: S.Theme.fontFamily

View file

@ -1,72 +1,24 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import "." as M import "." as M
import "../services" as S import "../services" as S
M.BarSection { M.BarSection {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
opacity: S.Modules.bluetooth.enable && root.state !== "unavailable" ? 1 : 0 opacity: S.Modules.bluetooth.enable && S.BluetoothService.state !== "unavailable" ? 1 : 0
visible: opacity > 0 visible: opacity > 0
tooltip: { tooltip: {
if (root.state === "off") if (S.BluetoothService.state === "off")
return "Bluetooth: off"; return "Bluetooth: off";
if (root.state === "connected") if (S.BluetoothService.state === "connected")
return "Bluetooth: " + root.device + (root.batteryPct >= 0 ? "\nBattery: " + root.batteryPct + "%" : ""); return "Bluetooth: " + S.BluetoothService.device + (S.BluetoothService.batteryPct >= 0 ? "\nBattery: " + S.BluetoothService.batteryPct + "%" : "");
return "Bluetooth: on"; 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 { M.BarIcon {
icon: "\uF294" icon: "\uF294"
color: root.state === "off" ? S.Theme.base04 : root.accentColor color: S.BluetoothService.state === "off" ? S.Theme.base04 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler { TapHandler {
onTapped: { onTapped: {
@ -76,8 +28,8 @@ M.BarSection {
} }
} }
M.BarLabel { M.BarLabel {
visible: root.state === "connected" visible: S.BluetoothService.state === "connected"
label: root.device + (root.batteryPct >= 0 ? " " + root.batteryPct + "%" : "") label: S.BluetoothService.device + (S.BluetoothService.batteryPct >= 0 ? " " + S.BluetoothService.batteryPct + "%" : "")
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler { TapHandler {
onTapped: { onTapped: {

View file

@ -1,51 +1,31 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import "." as M import "." as M
import "../services" as S import "../services" as S
M.BarIcon { M.BarIcon {
id: root id: root
color: root.active ? S.Theme.base09 : root.accentColor color: S.IdleInhibitService.active ? S.Theme.base09 : root.accentColor
tooltip: { tooltip: {
const parts = ["Idle inhibition: " + (root.active ? "active" : "inactive")]; const parts = ["Idle inhibition: " + (S.IdleInhibitService.active ? "active" : "inactive")];
if (root._inhibitors) if (S.IdleInhibitService.inhibitors)
parts.push(root._inhibitors); parts.push(S.IdleInhibitService.inhibitors);
return parts.join("\n"); return parts.join("\n");
} }
property bool active: false icon: S.IdleInhibitService.active ? "\uF06E" : "\uF070"
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 { Timer {
interval: 5000 interval: 5000
running: root._hovered running: root._hovered
repeat: true repeat: true
triggeredOnStart: true triggeredOnStart: true
onTriggered: listProc.running = true onTriggered: S.IdleInhibitService.refreshInhibitors()
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: root.active = !root.active onClicked: S.IdleInhibitService.toggle()
} }
} }

View file

@ -13,14 +13,9 @@ M.BarSection {
visible: opacity > 0 visible: opacity > 0
tooltip: "" tooltip: ""
property int _playerIdx: 0 readonly property var _players: S.MprisService.players
readonly property var _players: (Mpris.players.values ?? []).filter(p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing || p.playbackState === MprisPlaybackState.Paused) readonly property MprisPlayer player: S.MprisService.player
readonly property MprisPlayer player: _players[_playerIdx] ?? _players[0] ?? null readonly property bool playing: S.MprisService.playing
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 _cachedArt: ""
property string _artTrack: "" property string _artTrack: ""
@ -132,9 +127,9 @@ M.BarSection {
accentColor: root.accentColor accentColor: root.accentColor
cachedArt: root._cachedArt cachedArt: root._cachedArt
cavaBars: root._cavaBars cavaBars: root._cavaBars
playerIdx: root._playerIdx playerIdx: S.MprisService.playerIdx
onPlayerSwitched: idx => { onPlayerSwitched: idx => {
root._playerIdx = idx; S.MprisService.switchPlayer(idx);
hoverPanel.keepOpen(400); hoverPanel.keepOpen(400);
} }
} }

View file

@ -1,6 +1,5 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import "." as M import "." as M
import "../services" as S import "../services" as S
@ -18,7 +17,7 @@ M.HoverPanel {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "\uF011" text: "\uF011"
color: menuWindow._wifiEnabled ? menuWindow.accentColor : S.Theme.base04 color: S.NetworkService.wifiEnabled ? menuWindow.accentColor : S.Theme.base04
font.pixelSize: S.Theme.fontSize font.pixelSize: S.Theme.fontSize
font.family: S.Theme.iconFontFamily font.family: S.Theme.iconFontFamily
@ -34,116 +33,16 @@ M.HoverPanel {
} }
TapHandler { TapHandler {
onTapped: { onTapped: S.NetworkService.setWifi(!S.NetworkService.wifiEnabled)
radioProc._state = menuWindow._wifiEnabled ? "off" : "on";
radioProc.running = true;
}
} }
} }
} }
onVisibleChanged: if (visible) onVisibleChanged: if (visible)
scanner.running = true S.NetworkService.refresh()
function triggerRefresh() {
if (visible)
scanner.running = true;
}
property var _networks: []
property bool _wifiEnabled: true
property Process _scanner: Process {
id: scanner
running: true
command: ["sh", "-c", "echo '---RADIO---';" + "nmcli radio wifi 2>/dev/null;" + "echo '---CONNS---';" + "nmcli -t -f NAME,UUID,TYPE,ACTIVE connection show 2>/dev/null;" + "echo '---WIFI---';" + "nmcli -t -f SSID,SIGNAL device wifi list --rescan no 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: {
const radioSection = text.split("---CONNS---")[0].split("---RADIO---")[1] || "";
menuWindow._wifiEnabled = radioSection.trim() === "enabled";
const sections = text.split("---WIFI---");
const connLines = (sections[0] || "").split("---CONNS---")[1] || "";
const wifiLines = sections[1] || "";
const visible = {};
for (const l of wifiLines.trim().split("\n")) {
if (!l)
continue;
const parts = l.split(":");
const ssid = parts[0];
if (ssid)
visible[ssid] = parseInt(parts[1]) || 0;
}
const nets = [];
for (const l of connLines.trim().split("\n")) {
if (!l)
continue;
const parts = l.split(":");
const name = parts[0];
const uuid = parts[1];
const type = parts[2] || "";
const active = parts[3] === "yes";
const isWifi = type.includes("wireless");
if (isWifi && !(name in visible))
continue;
nets.push({
"name": name,
"uuid": uuid,
"isWifi": isWifi,
"active": active,
"signal": isWifi ? (visible[name] || 0) : -1
});
}
nets.sort((a, b) => {
if (a.active !== b.active)
return a.active ? -1 : 1;
if (a.signal >= 0 && b.signal >= 0)
return b.signal - a.signal;
return a.name.localeCompare(b.name);
});
menuWindow._networks = nets;
}
}
}
property Process _radioProc: Process {
id: radioProc
property string _state: ""
command: ["nmcli", "radio", "wifi", _state]
onRunningChanged: if (!running) {
scanner.running = true;
menuWindow.keepOpen(500);
}
}
property Process _connectProc: Process {
id: connectProc
property string uuid: ""
command: ["nmcli", "connection", "up", uuid]
onRunningChanged: if (!running) {
scanner.running = true;
menuWindow.keepOpen(500);
}
}
property Process _disconnectProc: Process {
id: disconnectProc
property string uuid: ""
command: ["nmcli", "connection", "down", uuid]
onRunningChanged: if (!running) {
scanner.running = true;
menuWindow.keepOpen(500);
}
}
Repeater { Repeater {
model: menuWindow._networks model: S.NetworkService.networks
delegate: Item { delegate: Item {
id: entry id: entry
@ -204,25 +103,23 @@ M.HoverPanel {
} }
TapHandler { TapHandler {
onTapped: { onTapped: {
if (entry.modelData.active) { if (entry.modelData.active)
disconnectProc.uuid = entry.modelData.uuid; S.NetworkService.disconnectNetwork(entry.modelData.uuid);
disconnectProc.running = true; else
} else { S.NetworkService.connectNetwork(entry.modelData.uuid);
connectProc.uuid = entry.modelData.uuid; menuWindow.keepOpen(500);
connectProc.running = true;
}
} }
} }
} }
} }
Text { Text {
visible: menuWindow._networks.length === 0 visible: S.NetworkService.networks.length === 0
width: menuWindow.contentWidth width: menuWindow.contentWidth
height: 32 height: 32
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
text: menuWindow._wifiEnabled ? "No networks available" : "Wi-Fi is off" text: S.NetworkService.wifiEnabled ? "No networks available" : "Wi-Fi is off"
color: S.Theme.base04 color: S.Theme.base04
font.pixelSize: S.Theme.fontSize font.pixelSize: S.Theme.fontSize
font.family: S.Theme.fontFamily font.family: S.Theme.fontFamily

View file

@ -1,6 +1,5 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import "." as M import "." as M
import "../services" as S import "../services" as S
@ -9,73 +8,7 @@ M.BarSection {
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
tooltip: "" tooltip: ""
property string ifname: "" readonly property string state: S.NetworkService.state
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 { M.BarIcon {
icon: { icon: {
@ -92,7 +25,7 @@ M.BarSection {
} }
M.BarLabel { M.BarLabel {
visible: root.state === "wifi" visible: root.state === "wifi"
label: root.essid label: S.NetworkService.essid
color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }

View file

@ -0,0 +1,64 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import "." as S
QtObject {
id: root
property int percent: 0
readonly property bool available: _blDev !== ""
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: ""
property Process _detectProc: Process {
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;
}
}
}
property FileView _current: FileView {
path: root._blDev ? root._blDev + "/brightness" : ""
watchChanges: true
onFileChanged: reload()
onLoaded: root._update()
}
property FileView _max: FileView {
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)
percent = Math.round((c / m) * 100);
}
property Process _adjProc: Process {
property string cmd: ""
command: ["sh", "-c", cmd]
onRunningChanged: if (!running && cmd !== "")
root._current.reload()
}
}

View file

@ -0,0 +1,126 @@
pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
QtObject {
id: root
// Adapter state: "unavailable" | "off" | "on" | "connected"
property string state: "unavailable"
property string device: ""
property int batteryPct: -1
// Paired device list and power state (for menu)
property var devices: []
property bool enabled: true
function refresh() {
_statusProc.running = true;
_scannerProc.running = true;
}
function setPower(on) {
_powerProc._action = on ? "on" : "off";
_powerProc.running = true;
}
function toggleDevice(mac, connect) {
_toggleProc._action = connect ? "connect" : "disconnect";
_toggleProc._mac = mac;
_toggleProc.running = true;
}
// Status polling (bar icon state)
property Process _statusProc: Process {
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: {
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;
}
}
}
}
// Event-driven: watch BlueZ DBus property changes
property Process _monitor: Process {
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()
}
}
property Timer _debounce: Timer {
interval: 500
onTriggered: root.refresh()
}
property Timer _fallbackPoll: Timer {
interval: 60000
running: S.Modules.bluetooth.enable
repeat: true
onTriggered: root.refresh()
}
// Paired device scanner (for menu)
property Process _scannerProc: Process {
command: ["sh", "-c", "bluetoothctl show 2>/dev/null | awk '/Powered:/{print $2; exit}';" + "echo '---DEVICES---';" + "bluetoothctl devices Paired 2>/dev/null | while read -r _ mac name; do " + "info=$(bluetoothctl info \"$mac\" 2>/dev/null); " + "conn=$(echo \"$info\" | grep -c 'Connected: yes'); " + "bat=$(echo \"$info\" | awk -F'[(): ]' '/Battery Percentage/{for(i=1;i<=NF;i++) if($i+0==$i && $i!=\"\") print $i}'); " + "echo \"$mac:$conn:${bat:-}:$name\"; " + "done"]
stdout: StdioCollector {
onStreamFinished: {
const sections = text.split("---DEVICES---");
root.enabled = (sections[0] || "").trim() === "yes";
const devs = [];
for (const line of (sections[1] || "").trim().split("\n")) {
if (!line)
continue;
const i1 = line.indexOf(":");
const i2 = line.indexOf(":", i1 + 1);
const i3 = line.indexOf(":", i2 + 1);
if (i3 < 0)
continue;
devs.push({
mac: line.slice(0, i1),
connected: line.slice(i1 + 1, i2) === "1",
battery: parseInt(line.slice(i2 + 1, i3)) || -1,
name: line.slice(i3 + 1)
});
}
devs.sort((a, b) => {
if (a.connected !== b.connected)
return a.connected ? -1 : 1;
return a.name.localeCompare(b.name);
});
root.devices = devs;
}
}
}
// Action processes
property Process _powerProc: Process {
property string _action: ""
command: ["bluetoothctl", "power", _action]
onRunningChanged: if (!running)
root.refresh()
}
property Process _toggleProc: Process {
property string _action: ""
property string _mac: ""
command: ["bluetoothctl", _action, _mac]
onRunningChanged: if (!running)
root.refresh()
}
}

View file

@ -0,0 +1,31 @@
pragma Singleton
import QtQuick
import Quickshell.Io
QtObject {
id: root
property bool active: false
property string inhibitors: ""
function toggle() {
active = !active;
}
function refreshInhibitors() {
_listProc.running = true;
}
property Process _inhibitor: Process {
command: ["systemd-inhibit", "--what=idle", "--who=nova-shell", "--why=user", "sleep", "infinity"]
running: root.active
}
property Process _listProc: Process {
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() : ""
}
}
}

View file

@ -0,0 +1,21 @@
pragma Singleton
import QtQuick
import Quickshell.Services.Mpris
QtObject {
id: root
readonly property var players: (Mpris.players.values ?? []).filter(p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing || p.playbackState === MprisPlaybackState.Paused)
property int playerIdx: 0
readonly property MprisPlayer player: players[playerIdx] ?? players[0] ?? null
readonly property bool playing: player?.playbackState === MprisPlaybackState.Playing
// Reset index if current player disappears
onPlayersChanged: if (playerIdx >= players.length)
playerIdx = 0
function switchPlayer(idx) {
playerIdx = idx;
}
}

View file

@ -0,0 +1,180 @@
pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
QtObject {
id: root
// Connection state
property string ifname: ""
property string essid: ""
property string state: "disconnected" // "disconnected" | "wifi" | "eth" | "linked"
property string ipAddr: ""
property string signal: ""
// Wi-Fi networks and radio state
property var networks: []
property bool wifiEnabled: true
function refresh() {
_statusProc.running = true;
_scannerProc.running = true;
}
function setWifi(enabled) {
_radioProc._state = enabled ? "on" : "off";
_radioProc.running = true;
}
function connectNetwork(uuid) {
_connectProc._uuid = uuid;
_connectProc.running = true;
}
function disconnectNetwork(uuid) {
_disconnectProc._uuid = uuid;
_disconnectProc.running = true;
}
// Status polling
property Process _statusProc: Process {
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";
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 monitor
property Process _monitor: Process {
running: S.Modules.network.enable
command: ["nmcli", "monitor"]
stdout: SplitParser {
splitMarker: "\n"
onRead: _debounce.restart()
}
}
property Timer _debounce: Timer {
interval: 300
onTriggered: root.refresh()
}
// Fallback poll
property Timer _fallbackPoll: Timer {
interval: 60000
running: S.Modules.network.enable
repeat: true
onTriggered: root.refresh()
}
// Wi-Fi scanner (connections + available SSIDs)
property Process _scannerProc: Process {
running: true
command: ["sh", "-c", "echo '---RADIO---';" + "nmcli radio wifi 2>/dev/null;" + "echo '---CONNS---';" + "nmcli -t -f NAME,UUID,TYPE,ACTIVE connection show 2>/dev/null;" + "echo '---WIFI---';" + "nmcli -t -f SSID,SIGNAL device wifi list --rescan no 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: {
const radioSection = text.split("---CONNS---")[0].split("---RADIO---")[1] || "";
root.wifiEnabled = radioSection.trim() === "enabled";
const sections = text.split("---WIFI---");
const connLines = (sections[0] || "").split("---CONNS---")[1] || "";
const wifiLines = sections[1] || "";
const visible = {};
for (const l of wifiLines.trim().split("\n")) {
if (!l)
continue;
const parts = l.split(":");
const ssid = parts[0];
if (ssid)
visible[ssid] = parseInt(parts[1]) || 0;
}
const nets = [];
for (const l of connLines.trim().split("\n")) {
if (!l)
continue;
const parts = l.split(":");
const name = parts[0];
const uuid = parts[1];
const type = parts[2] || "";
const active = parts[3] === "yes";
const isWifi = type.includes("wireless");
if (isWifi && !(name in visible))
continue;
nets.push({
name: name,
uuid: uuid,
isWifi: isWifi,
active: active,
signal: isWifi ? (visible[name] || 0) : -1
});
}
nets.sort((a, b) => {
if (a.active !== b.active)
return a.active ? -1 : 1;
if (a.signal >= 0 && b.signal >= 0)
return b.signal - a.signal;
return a.name.localeCompare(b.name);
});
root.networks = nets;
}
}
}
// Action processes
property Process _radioProc: Process {
property string _state: ""
command: ["nmcli", "radio", "wifi", _state]
onRunningChanged: if (!running)
root.refresh()
}
property Process _connectProc: Process {
property string _uuid: ""
command: ["nmcli", "connection", "up", _uuid]
onRunningChanged: if (!running)
root.refresh()
}
property Process _disconnectProc: Process {
property string _uuid: ""
command: ["nmcli", "connection", "down", _uuid]
onRunningChanged: if (!running)
root.refresh()
}
}

View file

@ -7,3 +7,8 @@ singleton PowerProfileService 1.0 PowerProfileService.qml
singleton NotifService 1.0 NotifService.qml singleton NotifService 1.0 NotifService.qml
NotifItem 1.0 NotifItem.qml NotifItem 1.0 NotifItem.qml
singleton LockService 1.0 LockService.qml singleton LockService 1.0 LockService.qml
singleton BacklightService 1.0 BacklightService.qml
singleton MprisService 1.0 MprisService.qml
singleton NetworkService 1.0 NetworkService.qml
singleton BluetoothService 1.0 BluetoothService.qml
singleton IdleInhibitService 1.0 IdleInhibitService.qml