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
TemperatureApplet 1.0 TemperatureApplet.qml
DiskApplet 1.0 DiskApplet.qml
BacklightApplet 1.0 BacklightApplet.qml

View file

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

View file

@ -1,5 +1,4 @@
import QtQuick
import Quickshell.Services.Mpris
import Quickshell.Services.Pipewire
import "../services" as S
import "../applets" as C
@ -28,13 +27,19 @@ Item {
}
implicitHeight: _widgetContent.implicitHeight
visible: _mprisCard.visible || _volumeCard.visible
visible: _mprisCard.visible || _volumeCard.visible || _backlightCard.visible || _notifPills.visible
Column {
id: _widgetContent
width: parent.width
spacing: 12
// Notification pills
LockNotifPills {
id: _notifPills
anchors.horizontalCenter: parent.horizontalCenter
}
// Media widget
Rectangle {
id: _mprisCard
@ -44,12 +49,7 @@ Item {
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.Modules.lock.mpris ?? true) && _mprisPlayer !== 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
visible: (S.Modules.lock.mpris ?? true) && S.MprisService.player !== null
C.MprisApplet {
id: _mprisContent
@ -57,15 +57,13 @@ Item {
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 8
player: _mprisCard._mprisPlayer
players: _mprisCard._mprisPlayers
playing: _mprisCard._playing
playerIdx: _mprisCard._playerIdx
player: S.MprisService.player
players: S.MprisService.players
playing: S.MprisService.playing
playerIdx: S.MprisService.playerIdx
accentColor: S.Theme.base0D
cachedArt: _mprisCard._mprisPlayer?.trackArtUrl ?? ""
onPlayerSwitched: idx => {
_mprisCard._playerIdx = idx;
}
cachedArt: S.MprisService.player?.trackArtUrl ?? ""
onPlayerSwitched: idx => S.MprisService.switchPlayer(idx)
}
}
@ -96,5 +94,28 @@ Item {
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 Quickshell
import Quickshell.Io
import "." as M
import "../services" as S
import "../applets" as C
M.BarSection {
id: root
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
tooltip: ""
property int percent: 0
property int percent: S.BacklightService.percent
property bool _osdActive: false
property bool _percentInit: false
readonly property bool _showPanel: root._hovered || hoverPanel.panelHovered || _osdActive
@ -36,71 +36,8 @@ M.BarSection {
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)
onWheel: event => S.BacklightService.adjust(event.angleDelta.y)
}
M.HoverPanel {
@ -113,75 +50,21 @@ M.BarSection {
panelTitle: "Brightness"
contentWidth: 200
Item {
C.BacklightApplet {
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
}
percent: root.percent
accentColor: root.accentColor
onSetPercent: pct => S.BacklightService.setPercent(pct)
}
}
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 Quickshell
import Quickshell.Io
import "." as M
import "../services" as S
@ -19,7 +18,7 @@ M.HoverPanel {
Text {
anchors.centerIn: parent
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.family: S.Theme.iconFontFamily
@ -35,78 +34,16 @@ M.HoverPanel {
}
TapHandler {
onTapped: {
powerProc._action = menuWindow._btEnabled ? "off" : "on";
powerProc.running = true;
}
onTapped: S.BluetoothService.setPower(!S.BluetoothService.enabled)
}
}
}
onVisibleChanged: if (visible)
scanner.running = true
property var _devices: []
property bool _btEnabled: true
property Process _scanner: Process {
id: scanner
running: false
command: ["sh", "-c", "bluetoothctl show 2>/dev/null | awk '/Powered:/{print $2; exit}';" + "echo '---DEVICES---';" + "bluetoothctl devices Paired 2>/dev/null | while read -r _ mac name; do " + "info=$(bluetoothctl info \"$mac\" 2>/dev/null); " + "conn=$(echo \"$info\" | grep -c 'Connected: yes'); " + "bat=$(echo \"$info\" | awk -F'[(): ]' '/Battery Percentage/{for(i=1;i<=NF;i++) if($i+0==$i && $i!=\"\") print $i}'); " + "echo \"$mac:$conn:${bat:-}:$name\"; " + "done"]
stdout: StdioCollector {
onStreamFinished: {
const sections = text.split("---DEVICES---");
menuWindow._btEnabled = (sections[0] || "").trim() === "yes";
const devs = [];
for (const line of (sections[1] || "").trim().split("\n")) {
if (!line)
continue;
const i1 = line.indexOf(":");
const i2 = line.indexOf(":", i1 + 1);
const i3 = line.indexOf(":", i2 + 1);
if (i3 < 0)
continue;
devs.push({
"mac": line.slice(0, i1),
"connected": line.slice(i1 + 1, i2) === "1",
"battery": parseInt(line.slice(i2 + 1, i3)) || -1,
"name": line.slice(i3 + 1)
});
}
devs.sort((a, b) => {
if (a.connected !== b.connected)
return a.connected ? -1 : 1;
return a.name.localeCompare(b.name);
});
menuWindow._devices = devs;
}
}
}
property Process _powerProc: Process {
id: powerProc
property string _action: ""
command: ["bluetoothctl", "power", _action]
onRunningChanged: if (!running) {
scanner.running = true;
menuWindow.keepOpen(500);
}
}
property Process _toggleProc: Process {
id: toggleProc
property string action: ""
property string mac: ""
command: ["bluetoothctl", action, mac]
onRunningChanged: if (!running) {
scanner.running = true;
menuWindow.keepOpen(500);
}
}
S.BluetoothService.refresh()
Repeater {
model: menuWindow._devices
model: S.BluetoothService.devices
delegate: Item {
id: entry
@ -167,21 +104,20 @@ M.HoverPanel {
}
TapHandler {
onTapped: {
toggleProc.action = entry.modelData.connected ? "disconnect" : "connect";
toggleProc.mac = entry.modelData.mac;
toggleProc.running = true;
S.BluetoothService.toggleDevice(entry.modelData.mac, !entry.modelData.connected);
menuWindow.keepOpen(500);
}
}
}
}
Text {
visible: menuWindow._devices.length === 0
visible: S.BluetoothService.devices.length === 0
width: menuWindow.contentWidth
height: 32
horizontalAlignment: Text.AlignHCenter
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
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.fontFamily

View file

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

View file

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

View file

@ -13,14 +13,9 @@ M.BarSection {
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
readonly property var _players: S.MprisService.players
readonly property MprisPlayer player: S.MprisService.player
readonly property bool playing: S.MprisService.playing
property string _cachedArt: ""
property string _artTrack: ""
@ -132,9 +127,9 @@ M.BarSection {
accentColor: root.accentColor
cachedArt: root._cachedArt
cavaBars: root._cavaBars
playerIdx: root._playerIdx
playerIdx: S.MprisService.playerIdx
onPlayerSwitched: idx => {
root._playerIdx = idx;
S.MprisService.switchPlayer(idx);
hoverPanel.keepOpen(400);
}
}

View file

@ -1,6 +1,5 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "." as M
import "../services" as S
@ -18,7 +17,7 @@ M.HoverPanel {
Text {
anchors.centerIn: parent
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.family: S.Theme.iconFontFamily
@ -34,116 +33,16 @@ M.HoverPanel {
}
TapHandler {
onTapped: {
radioProc._state = menuWindow._wifiEnabled ? "off" : "on";
radioProc.running = true;
}
onTapped: S.NetworkService.setWifi(!S.NetworkService.wifiEnabled)
}
}
}
onVisibleChanged: if (visible)
scanner.running = true
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);
}
}
S.NetworkService.refresh()
Repeater {
model: menuWindow._networks
model: S.NetworkService.networks
delegate: Item {
id: entry
@ -204,25 +103,23 @@ M.HoverPanel {
}
TapHandler {
onTapped: {
if (entry.modelData.active) {
disconnectProc.uuid = entry.modelData.uuid;
disconnectProc.running = true;
} else {
connectProc.uuid = entry.modelData.uuid;
connectProc.running = true;
}
if (entry.modelData.active)
S.NetworkService.disconnectNetwork(entry.modelData.uuid);
else
S.NetworkService.connectNetwork(entry.modelData.uuid);
menuWindow.keepOpen(500);
}
}
}
}
Text {
visible: menuWindow._networks.length === 0
visible: S.NetworkService.networks.length === 0
width: menuWindow.contentWidth
height: 32
horizontalAlignment: Text.AlignHCenter
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
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.fontFamily

View file

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