diff --git a/shell/applets/BacklightApplet.qml b/shell/applets/BacklightApplet.qml deleted file mode 100644 index c9a60f5..0000000 --- a/shell/applets/BacklightApplet.qml +++ /dev/null @@ -1,79 +0,0 @@ -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 - } -} diff --git a/shell/applets/qmldir b/shell/applets/qmldir index 8a5282f..78bc931 100644 --- a/shell/applets/qmldir +++ b/shell/applets/qmldir @@ -6,4 +6,3 @@ 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 diff --git a/shell/lock/LockSurface.qml b/shell/lock/LockSurface.qml index d79d576..757c6ee 100644 --- a/shell/lock/LockSurface.qml +++ b/shell/lock/LockSurface.qml @@ -115,6 +115,16 @@ 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 diff --git a/shell/lock/LockWidgets.qml b/shell/lock/LockWidgets.qml index 1d51ac3..1818cad 100644 --- a/shell/lock/LockWidgets.qml +++ b/shell/lock/LockWidgets.qml @@ -1,4 +1,5 @@ import QtQuick +import Quickshell.Services.Mpris import Quickshell.Services.Pipewire import "../services" as S import "../applets" as C @@ -27,19 +28,13 @@ Item { } implicitHeight: _widgetContent.implicitHeight - visible: _mprisCard.visible || _volumeCard.visible || _backlightCard.visible || _notifPills.visible + visible: _mprisCard.visible || _volumeCard.visible Column { id: _widgetContent width: parent.width spacing: 12 - // Notification pills - LockNotifPills { - id: _notifPills - anchors.horizontalCenter: parent.horizontalCenter - } - // Media widget Rectangle { id: _mprisCard @@ -49,7 +44,12 @@ 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) && S.MprisService.player !== null + 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 C.MprisApplet { id: _mprisContent @@ -57,13 +57,15 @@ Item { anchors.right: parent.right anchors.top: parent.top anchors.topMargin: 8 - player: S.MprisService.player - players: S.MprisService.players - playing: S.MprisService.playing - playerIdx: S.MprisService.playerIdx + player: _mprisCard._mprisPlayer + players: _mprisCard._mprisPlayers + playing: _mprisCard._playing + playerIdx: _mprisCard._playerIdx accentColor: S.Theme.base0D - cachedArt: S.MprisService.player?.trackArtUrl ?? "" - onPlayerSwitched: idx => S.MprisService.switchPlayer(idx) + cachedArt: _mprisCard._mprisPlayer?.trackArtUrl ?? "" + onPlayerSwitched: idx => { + _mprisCard._playerIdx = idx; + } } } @@ -94,28 +96,5 @@ 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) - } - } } } diff --git a/shell/modules/BacklightModule.qml b/shell/modules/BacklightModule.qml index 67ab22f..3ca36dd 100644 --- a/shell/modules/BacklightModule.qml +++ b/shell/modules/BacklightModule.qml @@ -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 && S.BacklightService.available ? 1 : 0 + opacity: S.Modules.backlight.enable && percent > 0 ? 1 : 0 visible: opacity > 0 tooltip: "" - property int percent: S.BacklightService.percent + property int percent: 0 property bool _osdActive: false property bool _percentInit: false readonly property bool _showPanel: root._hovered || hoverPanel.panelHovered || _osdActive @@ -36,8 +36,71 @@ 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 => S.BacklightService.adjust(event.angleDelta.y) + onWheel: event => root.adjust(event.angleDelta.y) } M.HoverPanel { @@ -50,21 +113,75 @@ M.BarSection { panelTitle: "Brightness" contentWidth: 200 - C.BacklightApplet { + Item { width: parent.width - percent: root.percent - accentColor: root.accentColor - onSetPercent: pct => S.BacklightService.setPercent(pct) + 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 + } } } - - M.BarIcon { - icon: "\uF185" - anchors.verticalCenter: parent.verticalCenter - } - M.BarLabel { - label: root.percent + "%" - minText: "100%" - anchors.verticalCenter: parent.verticalCenter - } } diff --git a/shell/modules/BluetoothMenu.qml b/shell/modules/BluetoothMenu.qml index f348e90..67a2478 100644 --- a/shell/modules/BluetoothMenu.qml +++ b/shell/modules/BluetoothMenu.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Io import "." as M import "../services" as S @@ -18,7 +19,7 @@ M.HoverPanel { Text { anchors.centerIn: parent text: "\uF011" - color: S.BluetoothService.enabled ? menuWindow.accentColor : S.Theme.base04 + color: menuWindow._btEnabled ? menuWindow.accentColor : S.Theme.base04 font.pixelSize: S.Theme.fontSize font.family: S.Theme.iconFontFamily @@ -34,16 +35,78 @@ M.HoverPanel { } TapHandler { - onTapped: S.BluetoothService.setPower(!S.BluetoothService.enabled) + onTapped: { + powerProc._action = menuWindow._btEnabled ? "off" : "on"; + powerProc.running = true; + } } } } onVisibleChanged: if (visible) - S.BluetoothService.refresh() + scanner.running = true + + property var _devices: [] + property bool _btEnabled: true + + property Process _scanner: Process { + id: scanner + running: false + command: ["sh", "-c", "bluetoothctl show 2>/dev/null | awk '/Powered:/{print $2; exit}';" + "echo '---DEVICES---';" + "bluetoothctl devices Paired 2>/dev/null | while read -r _ mac name; do " + "info=$(bluetoothctl info \"$mac\" 2>/dev/null); " + "conn=$(echo \"$info\" | grep -c 'Connected: yes'); " + "bat=$(echo \"$info\" | awk -F'[(): ]' '/Battery Percentage/{for(i=1;i<=NF;i++) if($i+0==$i && $i!=\"\") print $i}'); " + "echo \"$mac:$conn:${bat:-}:$name\"; " + "done"] + stdout: StdioCollector { + onStreamFinished: { + const sections = text.split("---DEVICES---"); + menuWindow._btEnabled = (sections[0] || "").trim() === "yes"; + + const devs = []; + for (const line of (sections[1] || "").trim().split("\n")) { + if (!line) + continue; + const i1 = line.indexOf(":"); + const i2 = line.indexOf(":", i1 + 1); + const i3 = line.indexOf(":", i2 + 1); + if (i3 < 0) + continue; + devs.push({ + "mac": line.slice(0, i1), + "connected": line.slice(i1 + 1, i2) === "1", + "battery": parseInt(line.slice(i2 + 1, i3)) || -1, + "name": line.slice(i3 + 1) + }); + } + devs.sort((a, b) => { + if (a.connected !== b.connected) + return a.connected ? -1 : 1; + return a.name.localeCompare(b.name); + }); + menuWindow._devices = devs; + } + } + } + + property Process _powerProc: Process { + id: powerProc + property string _action: "" + command: ["bluetoothctl", "power", _action] + onRunningChanged: if (!running) { + scanner.running = true; + menuWindow.keepOpen(500); + } + } + + property Process _toggleProc: Process { + id: toggleProc + property string action: "" + property string mac: "" + command: ["bluetoothctl", action, mac] + onRunningChanged: if (!running) { + scanner.running = true; + menuWindow.keepOpen(500); + } + } Repeater { - model: S.BluetoothService.devices + model: menuWindow._devices delegate: Item { id: entry @@ -104,20 +167,21 @@ M.HoverPanel { } TapHandler { onTapped: { - S.BluetoothService.toggleDevice(entry.modelData.mac, !entry.modelData.connected); - menuWindow.keepOpen(500); + toggleProc.action = entry.modelData.connected ? "disconnect" : "connect"; + toggleProc.mac = entry.modelData.mac; + toggleProc.running = true; } } } } Text { - visible: S.BluetoothService.devices.length === 0 + visible: menuWindow._devices.length === 0 width: menuWindow.contentWidth height: 32 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - text: S.BluetoothService.enabled ? "No paired devices" : "Bluetooth is off" + text: menuWindow._btEnabled ? "No paired devices" : "Bluetooth is off" color: S.Theme.base04 font.pixelSize: S.Theme.fontSize font.family: S.Theme.fontFamily diff --git a/shell/modules/BluetoothModule.qml b/shell/modules/BluetoothModule.qml index 81e715e..03ecf5c 100644 --- a/shell/modules/BluetoothModule.qml +++ b/shell/modules/BluetoothModule.qml @@ -1,24 +1,72 @@ 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 && S.BluetoothService.state !== "unavailable" ? 1 : 0 + opacity: S.Modules.bluetooth.enable && root.state !== "unavailable" ? 1 : 0 visible: opacity > 0 tooltip: { - if (S.BluetoothService.state === "off") + if (root.state === "off") return "Bluetooth: off"; - if (S.BluetoothService.state === "connected") - return "Bluetooth: " + S.BluetoothService.device + (S.BluetoothService.batteryPct >= 0 ? "\nBattery: " + S.BluetoothService.batteryPct + "%" : ""); + 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: S.BluetoothService.state === "off" ? S.Theme.base04 : root.accentColor + color: root.state === "off" ? S.Theme.base04 : root.accentColor anchors.verticalCenter: parent.verticalCenter TapHandler { onTapped: { @@ -28,8 +76,8 @@ M.BarSection { } } M.BarLabel { - visible: S.BluetoothService.state === "connected" - label: S.BluetoothService.device + (S.BluetoothService.batteryPct >= 0 ? " " + S.BluetoothService.batteryPct + "%" : "") + visible: root.state === "connected" + label: root.device + (root.batteryPct >= 0 ? " " + root.batteryPct + "%" : "") anchors.verticalCenter: parent.verticalCenter TapHandler { onTapped: { diff --git a/shell/modules/IdleInhibitorModule.qml b/shell/modules/IdleInhibitorModule.qml index 4cda17d..81508a9 100644 --- a/shell/modules/IdleInhibitorModule.qml +++ b/shell/modules/IdleInhibitorModule.qml @@ -1,31 +1,51 @@ import QtQuick import Quickshell +import Quickshell.Io import "." as M import "../services" as S M.BarIcon { id: root - color: S.IdleInhibitService.active ? S.Theme.base09 : root.accentColor + color: root.active ? S.Theme.base09 : root.accentColor tooltip: { - const parts = ["Idle inhibition: " + (S.IdleInhibitService.active ? "active" : "inactive")]; - if (S.IdleInhibitService.inhibitors) - parts.push(S.IdleInhibitService.inhibitors); + const parts = ["Idle inhibition: " + (root.active ? "active" : "inactive")]; + if (root._inhibitors) + parts.push(root._inhibitors); return parts.join("\n"); } - icon: S.IdleInhibitService.active ? "\uF06E" : "\uF070" + 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: S.IdleInhibitService.refreshInhibitors() + onTriggered: listProc.running = true } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor - onClicked: S.IdleInhibitService.toggle() + onClicked: root.active = !root.active } } diff --git a/shell/modules/MprisModule.qml b/shell/modules/MprisModule.qml index 0edbf0e..2072bce 100644 --- a/shell/modules/MprisModule.qml +++ b/shell/modules/MprisModule.qml @@ -13,9 +13,14 @@ M.BarSection { visible: opacity > 0 tooltip: "" - readonly property var _players: S.MprisService.players - readonly property MprisPlayer player: S.MprisService.player - readonly property bool playing: S.MprisService.playing + 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: "" @@ -127,9 +132,9 @@ M.BarSection { accentColor: root.accentColor cachedArt: root._cachedArt cavaBars: root._cavaBars - playerIdx: S.MprisService.playerIdx + playerIdx: root._playerIdx onPlayerSwitched: idx => { - S.MprisService.switchPlayer(idx); + root._playerIdx = idx; hoverPanel.keepOpen(400); } } diff --git a/shell/modules/NetworkMenu.qml b/shell/modules/NetworkMenu.qml index f9c656f..36e41dd 100644 --- a/shell/modules/NetworkMenu.qml +++ b/shell/modules/NetworkMenu.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Io import "." as M import "../services" as S @@ -17,7 +18,7 @@ M.HoverPanel { Text { anchors.centerIn: parent text: "\uF011" - color: S.NetworkService.wifiEnabled ? menuWindow.accentColor : S.Theme.base04 + color: menuWindow._wifiEnabled ? menuWindow.accentColor : S.Theme.base04 font.pixelSize: S.Theme.fontSize font.family: S.Theme.iconFontFamily @@ -33,16 +34,116 @@ M.HoverPanel { } TapHandler { - onTapped: S.NetworkService.setWifi(!S.NetworkService.wifiEnabled) + onTapped: { + radioProc._state = menuWindow._wifiEnabled ? "off" : "on"; + radioProc.running = true; + } } } } onVisibleChanged: if (visible) - S.NetworkService.refresh() + 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); + } + } Repeater { - model: S.NetworkService.networks + model: menuWindow._networks delegate: Item { id: entry @@ -103,23 +204,25 @@ M.HoverPanel { } TapHandler { onTapped: { - if (entry.modelData.active) - S.NetworkService.disconnectNetwork(entry.modelData.uuid); - else - S.NetworkService.connectNetwork(entry.modelData.uuid); - menuWindow.keepOpen(500); + if (entry.modelData.active) { + disconnectProc.uuid = entry.modelData.uuid; + disconnectProc.running = true; + } else { + connectProc.uuid = entry.modelData.uuid; + connectProc.running = true; + } } } } } Text { - visible: S.NetworkService.networks.length === 0 + visible: menuWindow._networks.length === 0 width: menuWindow.contentWidth height: 32 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - text: S.NetworkService.wifiEnabled ? "No networks available" : "Wi-Fi is off" + text: menuWindow._wifiEnabled ? "No networks available" : "Wi-Fi is off" color: S.Theme.base04 font.pixelSize: S.Theme.fontSize font.family: S.Theme.fontFamily diff --git a/shell/modules/NetworkModule.qml b/shell/modules/NetworkModule.qml index d37eff8..0de5532 100644 --- a/shell/modules/NetworkModule.qml +++ b/shell/modules/NetworkModule.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Io import "." as M import "../services" as S @@ -8,7 +9,73 @@ M.BarSection { spacing: S.Theme.moduleSpacing tooltip: "" - readonly property string state: S.NetworkService.state + 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: { @@ -25,7 +92,7 @@ M.BarSection { } M.BarLabel { visible: root.state === "wifi" - label: S.NetworkService.essid + label: root.essid color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor anchors.verticalCenter: parent.verticalCenter } diff --git a/shell/services/BacklightService.qml b/shell/services/BacklightService.qml deleted file mode 100644 index caa4cd8..0000000 --- a/shell/services/BacklightService.qml +++ /dev/null @@ -1,64 +0,0 @@ -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() - } -} diff --git a/shell/services/BluetoothService.qml b/shell/services/BluetoothService.qml deleted file mode 100644 index e648538..0000000 --- a/shell/services/BluetoothService.qml +++ /dev/null @@ -1,126 +0,0 @@ -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() - } -} diff --git a/shell/services/IdleInhibitService.qml b/shell/services/IdleInhibitService.qml deleted file mode 100644 index f74878e..0000000 --- a/shell/services/IdleInhibitService.qml +++ /dev/null @@ -1,31 +0,0 @@ -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() : "" - } - } -} diff --git a/shell/services/MprisService.qml b/shell/services/MprisService.qml deleted file mode 100644 index 9f06ead..0000000 --- a/shell/services/MprisService.qml +++ /dev/null @@ -1,21 +0,0 @@ -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; - } -} diff --git a/shell/services/NetworkService.qml b/shell/services/NetworkService.qml deleted file mode 100644 index 2c2b531..0000000 --- a/shell/services/NetworkService.qml +++ /dev/null @@ -1,180 +0,0 @@ -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() - } -} diff --git a/shell/services/qmldir b/shell/services/qmldir index 57dad10..d23b48f 100644 --- a/shell/services/qmldir +++ b/shell/services/qmldir @@ -7,8 +7,3 @@ 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