From 299bf0bae8be6ab505f97d6aba4f82e4e6f96f62 Mon Sep 17 00:00:00 2001 From: littleblack111 Date: Sat, 12 Jul 2025 18:15:32 +0800 Subject: lot of minor improvement added utils and working networkusage --- services/Mpris.qml | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++ services/Network.qml | 86 +++++++++++++++++++++++ services/Resource.qml | 105 ++++++++++++++++++++++++++++ services/SMpris.qml | 188 -------------------------------------------------- 4 files changed, 379 insertions(+), 188 deletions(-) create mode 100644 services/Mpris.qml create mode 100644 services/Network.qml create mode 100644 services/Resource.qml delete mode 100644 services/SMpris.qml (limited to 'services') diff --git a/services/Mpris.qml b/services/Mpris.qml new file mode 100644 index 0000000..9618459 --- /dev/null +++ b/services/Mpris.qml @@ -0,0 +1,188 @@ +pragma Singleton +// pragma ComponentBehavior: Bound + +// From https://git.outfoxxed.me/outfoxxed/nixnew +// It does not have a license, but the author is okay with redistribution. + +import QtQml.Models +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Mpris + +/** + * A service that provides easy access to the active Mpris player. + */ +Singleton { + id: root + property MprisPlayer trackedPlayer: null + property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null + signal trackChanged(reverse: bool) + + property bool __reverse: false + + property var activeTrack + + Timer { + running: root.activePlayer?.playbackState == MprisPlaybackState.Playing && root.activePlayer?.positionSupported + interval: 1000 + repeat: true + onTriggered: root.activePlayer.positionChanged() + } + + Instantiator { + model: Mpris.players + + Connections { + required property MprisPlayer modelData + target: modelData + + Component.onCompleted: { + if (root.trackedPlayer == null || modelData.isPlaying) { + root.trackedPlayer = modelData; + } + } + + Component.onDestruction: { + if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) { + for (const player of Mpris.players.values) { + if (player.playbackState.isPlaying) { + root.trackedPlayer = player; + break; + } + } + + if (trackedPlayer == null && Mpris.players.values.length != 0) { + trackedPlayer = Mpris.players.values[0]; + } + } + } + + function onPlaybackStateChanged() { + if (root.trackedPlayer !== modelData) + root.trackedPlayer = modelData; + } + } + } + + Connections { + target: activePlayer + + function onPostTrackChanged() { + root.updateTrack(); + } + + function onTrackArtUrlChanged() { + // console.log("arturl:", activePlayer.trackArtUrl) + // root.updateTrack(); + if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) { + // cantata likes to send cover updates *BEFORE* updating the track info. + // as such, art url changes shouldn't be able to break the reverse animation + const r = root.__reverse; + root.updateTrack(); + root.__reverse = r; + } + } + // not working https://github.com/quickshell-mirror/quickshell/issues/111 + // function onPositionChanged() { + // } + } + + onActivePlayerChanged: this.updateTrack() + + function updateTrack() { + // console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`); + this.activeTrack = { + uniqueId: this.activePlayer?.uniqueId ?? 0, + artUrl: this.activePlayer?.trackArtUrl ?? "", + title: this.activePlayer?.trackTitle || qsTr("Unknown Title"), + artist: this.activePlayer?.trackArtist || qsTr("Unknown Artist"), + album: this.activePlayer?.trackAlbum || qsTr("Unknown Album"), + position: this.activePlayer?.position ?? 0, + length: this.activePlayer?.length ?? 0 + }; + + this.trackChanged(__reverse); + this.__reverse = false; + } + + function updateProgress() { + this.activePlayer.position; + } + + property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying + property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false + function togglePlaying() { + if (this.canTogglePlaying) + this.activePlayer.togglePlaying(); + } + + property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false + function previous() { + if (this.canGoPrevious) { + this.__reverse = true; + this.activePlayer.previous(); + } + } + + property bool canGoNext: this.activePlayer?.canGoNext ?? false + function next() { + if (this.canGoNext) { + this.__reverse = false; + this.activePlayer.next(); + } + } + + property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl + + property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl + property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None + function setLoopState(loopState: var) { + if (this.loopSupported) { + this.activePlayer.loopState = loopState; + } + } + + property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl + property bool hasShuffle: this.activePlayer?.shuffle ?? false + function setShuffle(shuffle: bool) { + if (this.shuffleSupported) { + this.activePlayer.shuffle = shuffle; + } + } + + function setActivePlayer(player: MprisPlayer) { + const targetPlayer = player ?? Mpris.players[0]; + console.log(`[Mpris] Active player ${targetPlayer} << ${activePlayer}`); + + if (targetPlayer && this.activePlayer) { + this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer); + } else { + // always animate forward if going to null + this.__reverse = false; + } + + this.trackedPlayer = targetPlayer; + } + + IpcHandler { + target: "mpris" + + function pauseAll(): void { + for (const player of Mpris.players.values) { + if (player.canPause) + player.pause(); + } + } + + function playPause(): void { + root.togglePlaying(); + } + function previous(): void { + root.previous(); + } + function next(): void { + root.next(); + } + } +} diff --git a/services/Network.qml b/services/Network.qml new file mode 100644 index 0000000..52f15fa --- /dev/null +++ b/services/Network.qml @@ -0,0 +1,86 @@ +pragma Singleton +// pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import QtQuick + +import qs.config + +Singleton { + id: root + + property bool wifi: true + property bool ethernet: false + property string networkName: "" + property int networkStrength + // property string materialSymbol: ethernet ? "lan" : (Network.networkName.length > 0 && Network.networkName != "lo") ? (Network.networkStrength > 80 ? "signal_wifi_4_bar" : Network.networkStrength > 60 ? "network_wifi_3_bar" : Network.networkStrength > 40 ? "network_wifi_2_bar" : Network.networkStrength > 20 ? "network_wifi_1_bar" : "signal_wifi_0_bar") : "signal_wifi_off" + property string state: ethernet ? Icons.resource.network.wifi : (networkName.length >= 0 && networkName !== "lo") ? (networkStrength >= 90 ? Icons.resource.network.wifi.max : networkStrength >= 80 ? Icons.resource.network.wifi.high : networkStrength >= 60 ? Icons.resource.network.wifi.mid : networkStrength >= 40 ? Icons.resource.network.wifi.low : networkStrength >= 20 ? Icons.resource.network.wifi.min : Icons.resource.network.disconnected) : Icons.resource.network.disconnected + + function update() { + updateConnectionType.startCheck(); + updateNetworkName.running = true; + updateNetworkStrength.running = true; + } + + Timer { + interval: General.resourceUpdateInterval + running: true + repeat: true + onTriggered: { + root.update(); + interval = General.resourceUpdateInterval * 1000; + } + } + + Process { + id: updateConnectionType + property string buffer + command: ["nmcli", "-t", "-f", "NAME,TYPE,DEVICE", "c", "show", "--active"] + running: true + function startCheck() { + buffer = ""; + updateConnectionType.running = true; + } + stdout: SplitParser { + onRead: data => { + updateConnectionType.buffer += data + "\n"; + } + } + onExited: (exitCode, exitStatus) => { + const lines = updateConnectionType.buffer.trim().split('\n'); + let hasEthernet = false; + let hasWifi = false; + lines.forEach(line => { + if (line.includes("ethernet")) + hasEthernet = true; + else if (line.includes("wireless")) + hasWifi = true; + }); + root.ethernet = hasEthernet; + root.wifi = hasWifi; + } + } + + Process { + id: updateNetworkName + command: ["sh", "-c", "nmcli -t -f NAME c show --active | head -1"] + running: true + stdout: SplitParser { + onRead: data => { + root.networkName = data; + } + } + } + + Process { + id: updateNetworkStrength + running: true + command: ["sh", "-c", "nmcli -f IN-USE,SIGNAL,SSID device wifi | awk '/^\*/{if (NR!=1) {print $2}}'"] + stdout: SplitParser { + onRead: data => { + root.networkStrength = parseInt(data); + } + } + } +} diff --git a/services/Resource.qml b/services/Resource.qml new file mode 100644 index 0000000..b45538b --- /dev/null +++ b/services/Resource.qml @@ -0,0 +1,105 @@ +pragma Singleton +// pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.config + +Singleton { + id: root + + property double memoryTotal: 1 + property double memoryFree: 1 + property double memoryUsed: memoryTotal - memoryFree + property double memoryUsedPercentage: memoryUsed / memoryTotal + property double swapTotal: 1 + property double swapFree: 1 + property double swapUsed: swapTotal - swapFree + property double swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0 + + property double cpuUsage: 0 + property double cpuTemp: 0 + property var previousCpuStats + + property int gpuUsage: 0 + property int gpuTemp: 0 + + Timer { + interval: General.resourceUpdateInterval + running: true + repeat: true + onTriggered: { + fileMeminfo.reload(); + fileStat.reload(); + fileCpuThermal.reload(); + + const textMeminfo = fileMeminfo.text(); + memoryTotal = Number(textMeminfo.match(/MemTotal: *(\d+)/)[1] ?? 1); + memoryFree = Number(textMeminfo.match(/MemAvailable: *(\d+)/)[1] ?? 0); + memoryUsed = memoryTotal - memoryFree; + memoryUsedPercentage = memoryUsed / memoryTotal; + swapTotal = Number(textMeminfo.match(/SwapTotal: *(\d+)/)[1] ?? 1); + swapFree = Number(textMeminfo.match(/SwapFree: *(\d+)/)[1] ?? 0); + + const textStat = fileStat.text(); + const cpuLine = textStat.match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/); + if (cpuLine) { + const stats = cpuLine.slice(1).map(Number); + const total = stats.reduce((a, b) => a + b, 0); + const idle = stats[3]; + + if (root.previousCpuStats) { + const totalDiff = total - root.previousCpuStats.total; + const idleDiff = idle - root.previousCpuStats.idle; + cpuUsage = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; + } + + root.previousCpuStats = { + total, + idle + }; + } + const cpuTempText = fileCpuThermal.text(); + cpuTemp = cpuTempText ? parseInt(cpuTempText.trim()) / 100000 : 0; + + gpuUsageProcess.running = true; + gpuTempProcess.running = true; + interval = General.resourceUpdateInterval * 1000; + } + } + + FileView { + id: fileMeminfo + path: "/proc/meminfo" + } + FileView { + id: fileStat + path: "/proc/stat" + } + FileView { + id: fileCpuThermal + path: General.cpuThermalPath + } + // nvidia-smi may be dead if firmware update so ABI break etc. + Process { + id: gpuUsageProcess + running: true + command: ["nvidia-smi", "--query-gpu=utilization.gpu", "--format=csv,noheader,nounits"] + stdout: StdioCollector { + onStreamFinished: { + root.gpuUsage = parseInt(this.text.trim()); + } + } + } + Process { + id: gpuTempProcess + running: true + command: ["nvidia-smi", "--query-gpu=temperature.gpu", "--format=csv,noheader,nounits"] + stdout: StdioCollector { + onStreamFinished: { + root.gpuTemp = parseInt(this.text.trim()); + } + } + } +} diff --git a/services/SMpris.qml b/services/SMpris.qml deleted file mode 100644 index 012a354..0000000 --- a/services/SMpris.qml +++ /dev/null @@ -1,188 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -// From https://git.outfoxxed.me/outfoxxed/nixnew -// It does not have a license, but the author is okay with redistribution. - -import QtQml.Models -import QtQuick -import Quickshell -import Quickshell.Io -import Quickshell.Services.Mpris - -/** - * A service that provides easy access to the active Mpris player. - */ -Singleton { - id: root - property MprisPlayer trackedPlayer: null - property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null - signal trackChanged(reverse: bool) - - property bool __reverse: false - - property var activeTrack - - Timer { - running: root.activePlayer?.playbackState == MprisPlaybackState.Playing && root.activePlayer?.positionSupported - interval: 1000 - repeat: true - onTriggered: root.activePlayer.positionChanged() - } - - Instantiator { - model: Mpris.players - - Connections { - required property MprisPlayer modelData - target: modelData - - Component.onCompleted: { - if (root.trackedPlayer == null || modelData.isPlaying) { - root.trackedPlayer = modelData; - } - } - - Component.onDestruction: { - if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) { - for (const player of Mpris.players.values) { - if (player.playbackState.isPlaying) { - root.trackedPlayer = player; - break; - } - } - - if (trackedPlayer == null && Mpris.players.values.length != 0) { - trackedPlayer = Mpris.players.values[0]; - } - } - } - - function onPlaybackStateChanged() { - if (root.trackedPlayer !== modelData) - root.trackedPlayer = modelData; - } - } - } - - Connections { - target: activePlayer - - function onPostTrackChanged() { - root.updateTrack(); - } - - function onTrackArtUrlChanged() { - // console.log("arturl:", activePlayer.trackArtUrl) - // root.updateTrack(); - if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) { - // cantata likes to send cover updates *BEFORE* updating the track info. - // as such, art url changes shouldn't be able to break the reverse animation - const r = root.__reverse; - root.updateTrack(); - root.__reverse = r; - } - } - // not working https://github.com/quickshell-mirror/quickshell/issues/111 - // function onPositionChanged() { - // } - } - - onActivePlayerChanged: this.updateTrack() - - function updateTrack() { - // console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`); - this.activeTrack = { - uniqueId: this.activePlayer?.uniqueId ?? 0, - artUrl: this.activePlayer?.trackArtUrl ?? "", - title: this.activePlayer?.trackTitle || qsTr("Unknown Title"), - artist: this.activePlayer?.trackArtist || qsTr("Unknown Artist"), - album: this.activePlayer?.trackAlbum || qsTr("Unknown Album"), - position: this.activePlayer?.position ?? 0, - length: this.activePlayer?.length ?? 0 - }; - - this.trackChanged(__reverse); - this.__reverse = false; - } - - function updateProgress() { - this.activePlayer.position; - } - - property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying - property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false - function togglePlaying() { - if (this.canTogglePlaying) - this.activePlayer.togglePlaying(); - } - - property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false - function previous() { - if (this.canGoPrevious) { - this.__reverse = true; - this.activePlayer.previous(); - } - } - - property bool canGoNext: this.activePlayer?.canGoNext ?? false - function next() { - if (this.canGoNext) { - this.__reverse = false; - this.activePlayer.next(); - } - } - - property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl - - property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl - property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None - function setLoopState(loopState: var) { - if (this.loopSupported) { - this.activePlayer.loopState = loopState; - } - } - - property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl - property bool hasShuffle: this.activePlayer?.shuffle ?? false - function setShuffle(shuffle: bool) { - if (this.shuffleSupported) { - this.activePlayer.shuffle = shuffle; - } - } - - function setActivePlayer(player: MprisPlayer) { - const targetPlayer = player ?? Mpris.players[0]; - console.log(`[Mpris] Active player ${targetPlayer} << ${activePlayer}`); - - if (targetPlayer && this.activePlayer) { - this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer); - } else { - // always animate forward if going to null - this.__reverse = false; - } - - this.trackedPlayer = targetPlayer; - } - - IpcHandler { - target: "mpris" - - function pauseAll(): void { - for (const player of Mpris.players.values) { - if (player.canPause) - player.pause(); - } - } - - function playPause(): void { - root.togglePlaying(); - } - function previous(): void { - root.previous(); - } - function next(): void { - root.next(); - } - } -} -- cgit v1.2.3