Compare commits
17 changed files with 508 additions and 602 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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() : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Reference in a new issue